├── .vscode
└── settings.json
├── assets
├── disc.png
├── home.png
├── icon.png
├── user.png
├── heart.png
├── reply.png
├── search.png
├── splash.png
├── favicon.png
├── message.png
├── new-video.png
├── music-note.png
├── plus-button.png
├── adaptive-icon.png
├── message-circle.png
└── floating-music-note.png
├── README.md
├── babel.config.js
├── clients
├── apollo.js
└── livepeer.js
├── .gitignore
├── App.js
├── routes.js
├── app.json
├── package.json
├── screens
├── Home.js
└── Login.js
├── queries.js
└── components
├── BottomTabs.js
└── VideoPlayer.js
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/assets/disc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/disc.png
--------------------------------------------------------------------------------
/assets/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/home.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/user.png
--------------------------------------------------------------------------------
/assets/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/heart.png
--------------------------------------------------------------------------------
/assets/reply.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/reply.png
--------------------------------------------------------------------------------
/assets/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/search.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/splash.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/favicon.png
--------------------------------------------------------------------------------
/assets/message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/message.png
--------------------------------------------------------------------------------
/assets/new-video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/new-video.png
--------------------------------------------------------------------------------
/assets/music-note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/music-note.png
--------------------------------------------------------------------------------
/assets/plus-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/plus-button.png
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/message-circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/message-circle.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # decentralized-tiktok
2 | A decentralized Tiktok built on top of Livepeer, Lens Protocol, and Bundlr Network
3 |
4 |
--------------------------------------------------------------------------------
/assets/floating-music-note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suhailkakar/decentralized-tiktok/HEAD/assets/floating-music-note.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/clients/apollo.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, InMemoryCache } from "@apollo/client";
2 |
3 | const APClient = new ApolloClient({
4 | uri: "https://api.lens.dev",
5 | cache: new InMemoryCache(),
6 | });
7 | export default APClient;
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # Temporary files created by Metro to check the health of the file watcher
17 | .metro-health-check*
18 |
--------------------------------------------------------------------------------
/clients/livepeer.js:
--------------------------------------------------------------------------------
1 | import { createReactClient } from "@livepeer/react-native";
2 | import { studioProvider } from "livepeer/providers/studio";
3 |
4 | const LPClient = createReactClient({
5 | provider: studioProvider({ apiKey: "2cedff44-a68e-4149-9345-e2b25b1cdbd2" }),
6 | });
7 |
8 | export default LPClient;
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Routes from './routes'
3 | import WalletConnectProvider from "react-native-walletconnect";
4 | import { LivepeerConfig } from '@livepeer/react-native';
5 | import LPClient from './clients/livepeer';
6 | import { ApolloProvider } from '@apollo/client';
7 | import APClient from './clients/apollo';
8 |
9 | export default function App() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
--------------------------------------------------------------------------------
/routes.js:
--------------------------------------------------------------------------------
1 | import { NavigationContainer } from "@react-navigation/native";
2 | import { createStackNavigator } from "@react-navigation/stack";
3 | import BottomTabs from "./components/BottomTabs";
4 | import Login from "./screens/Login";
5 |
6 | const Stack = createStackNavigator();
7 |
8 | function Routes() {
9 | return (
10 |
11 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | export default Routes;
23 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "web3-tiktok",
4 | "slug": "web3-tiktok",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "ios": {
18 | "supportsTablet": true
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | }
25 | },
26 | "web": {
27 | "favicon": "./assets/favicon.png"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web3-tiktok",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@apollo/client": "^3.7.9",
13 | "@livepeer/react-native": "^1.2.5",
14 | "@react-native-async-storage/async-storage": "^1.17.11",
15 | "@react-navigation/bottom-tabs": "^6.5.7",
16 | "@react-navigation/material-bottom-tabs": "^6.2.15",
17 | "@react-navigation/native": "^6.1.6",
18 | "@react-navigation/stack": "^6.3.16",
19 | "expo": "~48.0.4",
20 | "expo-av": "^13.2.1",
21 | "expo-media-library": "^15.2.2",
22 | "expo-status-bar": "~1.4.4",
23 | "graphql": "^16.6.0",
24 | "livepeer": "^2.2.3",
25 | "react": "18.2.0",
26 | "react-native": "0.71.3",
27 | "react-native-gesture-handler": "^2.9.0",
28 | "react-native-safe-area-context": "^4.5.0",
29 | "react-native-screens": "^3.20.0",
30 | "react-native-svg": "^13.8.0",
31 | "react-native-walletconnect": "^0.0.1-alpha.2",
32 | "react-native-webview": "^11.26.1"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.20.0"
36 | },
37 | "private": true
38 | }
39 |
--------------------------------------------------------------------------------
/screens/Home.js:
--------------------------------------------------------------------------------
1 | import { FlatList, StyleSheet, View, Text, Dimensions } from "react-native";
2 | import React, { useState } from "react";
3 | import { EXPLORE_POSTS } from "../queries";
4 | import { useQuery } from "@apollo/client";
5 | import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
6 | import VideoPlayer from "../components/VideoPlayer";
7 |
8 | export default function Home() {
9 | const [activeVideoIndex, setActiveVideoIndex] = useState(0);
10 |
11 | const bottomTabHeight = useBottomTabBarHeight();
12 | const { height: WINDOW_HEIGHT } = Dimensions.get("window");
13 | const { data } = useQuery(EXPLORE_POSTS, {
14 | variables: {
15 | request: {
16 | limit: 5,
17 | sources: ["lenstube-bytes"],
18 | publicationTypes: ["POST"],
19 | sortCriteria: "CURATED_PROFILES",
20 | },
21 | },
22 | });
23 |
24 | const pageInfo = data?.explorePublications?.pageInfo;
25 | const videos = data?.explorePublications?.items;
26 |
27 | return (
28 |
29 | (
33 |
34 | )}
35 | onScroll={(e) => {
36 | const index = Math.round(
37 | e.nativeEvent.contentOffset.y / (WINDOW_HEIGHT - bottomTabHeight)
38 | );
39 | setActiveVideoIndex(index);
40 | }}
41 | />
42 |
43 | );
44 | }
45 |
46 | const styles = StyleSheet.create({
47 | container: {
48 | flex: 1,
49 | justifyContent: "center",
50 | backgroundColor: "#fff",
51 | alignItems: "center",
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/queries.js:
--------------------------------------------------------------------------------
1 | import { gql } from "@apollo/client";
2 | export const EXPLORE_POSTS = gql`
3 | query ($request: ExplorePublicationRequest!) {
4 | explorePublications(request: $request) {
5 | items {
6 | __typename
7 | ... on Post {
8 | ...PostFields
9 | }
10 | }
11 | pageInfo {
12 | prev
13 | next
14 | totalCount
15 | }
16 | }
17 | }
18 | fragment MediaFields on Media {
19 | url
20 | width
21 | height
22 | mimeType
23 | }
24 | fragment ProfileFields on Profile {
25 | id
26 | name
27 | picture {
28 | ... on NftImage {
29 | contractAddress
30 | tokenId
31 | uri
32 | verified
33 | }
34 | ... on MediaSet {
35 | original {
36 | ...MediaFields
37 | }
38 | small {
39 | ...MediaFields
40 | }
41 | medium {
42 | ...MediaFields
43 | }
44 | }
45 | }
46 | }
47 | fragment PublicationStatsFields on PublicationStats {
48 | totalAmountOfMirrors
49 | totalUpvotes
50 | totalAmountOfCollects
51 | totalAmountOfComments
52 | }
53 | fragment MetadataOutputFields on MetadataOutput {
54 | name
55 | description
56 | content
57 | media {
58 | original {
59 | ...MediaFields
60 | }
61 | small {
62 | ...MediaFields
63 | }
64 | medium {
65 | ...MediaFields
66 | }
67 | }
68 | }
69 |
70 | fragment PostFields on Post {
71 | id
72 | profile {
73 | ...ProfileFields
74 | }
75 | stats {
76 | ...PublicationStatsFields
77 | }
78 | metadata {
79 | ...MetadataOutputFields
80 | }
81 | createdAt
82 | }
83 | `;
84 |
--------------------------------------------------------------------------------
/screens/Login.js:
--------------------------------------------------------------------------------
1 | import { Button, Pressable, StyleSheet, Text, TextBase, View } from 'react-native'
2 | import React from 'react'
3 | import { StatusBar } from 'expo-status-bar';
4 | import { useWalletConnect } from "react-native-walletconnect";
5 |
6 | export default function Login() {
7 | const {
8 | createSession,
9 | killSession,
10 | session,
11 | signTransaction,
12 | } = useWalletConnect();
13 |
14 | const hasWallet = !!session.length;
15 |
16 |
17 | return (
18 |
19 | {!hasWallet && (
20 |
21 | )}
22 | {!!hasWallet && (
23 |
43 | )
44 | }
45 |
46 | const styles = StyleSheet.create({
47 | container: {
48 | flex: 1,
49 | backgroundColor: "#fff",
50 | paddingTop: 50,
51 | paddingLeft: 30,
52 | },
53 | text: {
54 | fontSize: 40,
55 | fontWeight: "700",
56 | width: "50%",
57 | lineHeight: 50,
58 | marginTop: 50,
59 | },
60 | button: {
61 | borderWidth: 1,
62 | width: "90%",
63 | marginTop: 50,
64 | padding: 15,
65 | borderColor: "#ccc",
66 | alignItems: "center"
67 | },
68 | buttonText: {
69 | fontWeight: "600",
70 | },
71 | footer: {
72 | position: "absolute",
73 | bottom: 50,
74 | marginLeft: 30,
75 | textAlign: "center",
76 | color: "#aaa",
77 | }
78 | })
--------------------------------------------------------------------------------
/components/BottomTabs.js:
--------------------------------------------------------------------------------
1 | import { Image, StyleSheet } from "react-native";
2 | import React from "react";
3 |
4 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
5 | import Home from "../screens/Home";
6 |
7 | const BottomTab = createBottomTabNavigator();
8 |
9 | export default function BottomTabs() {
10 | return (
11 |
18 | (
23 |
30 | ),
31 | }}
32 | />
33 | (
38 |
45 | ),
46 | }}
47 | />
48 | null,
53 | tabBarIcon: ({ focused }) => (
54 |
61 | ),
62 | }}
63 | />
64 | (
69 |
76 | ),
77 | }}
78 | />
79 | (
84 |
91 | ),
92 | }}
93 | />
94 |
95 | );
96 | }
97 |
98 | const styles = StyleSheet.create({
99 | bottomTabIcon: {
100 | width: 20,
101 | height: 20,
102 | tintColor: "grey",
103 | },
104 | bottomTabIconFocused: {
105 | tintColor: "white",
106 | },
107 | newVideoButton: {
108 | width: 50,
109 | height: 25,
110 | },
111 | });
112 |
--------------------------------------------------------------------------------
/components/VideoPlayer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Dimensions, Image, StatusBar, StyleSheet, Text, View } from "react-native";
3 | import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
4 | import { Player } from "@livepeer/react-native";
5 |
6 | const { height: WINDOW_HEIGHT, width: WINDOW_WIDTH } = Dimensions.get("window");
7 |
8 |
9 | export default function VideoPlayer({ data, isActive }) {
10 | const bottomTabHeight = useBottomTabBarHeight();
11 | const statusBarHeight = StatusBar.currentHeight || 0;
12 |
13 | const getIPFSLink = (hash) => {
14 | const gateway = "https://lens.infura-ipfs.io/ipfs/";
15 |
16 | return hash
17 | .replace(/^Qm[1-9A-Za-z]{44}/gm, `${gateway}${hash}`)
18 | .replace("https://ipfs.io/ipfs/", gateway)
19 | .replace("ipfs://", gateway);
20 | };
21 |
22 | return (
23 |
29 |
30 |
37 |
38 |
39 |
40 |
41 | {data?.profile.name}
42 | {data?.metadata.name}
43 |
44 |
45 |
49 |
53 |
54 |
55 |
56 |
57 |
58 | {data?.profile?.picture?.original?.url && (
59 |
65 | )}
66 |
67 |
71 |
72 |
73 |
74 |
78 |
79 |
80 |
84 |
85 |
86 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | const styles = StyleSheet.create({
97 | container: {
98 | width: WINDOW_WIDTH,
99 | paddingTop: 70,
100 | backgroundColor: "#000",
101 | },
102 | video: {
103 | position: "absolute",
104 | width: "100%",
105 | height: "100%",
106 | },
107 | bottomSection: {
108 | position: "absolute",
109 | bottom: 0,
110 | flexDirection: "row",
111 | width: "100%",
112 | paddingHorizontal: 8,
113 | paddingBottom: 16,
114 | },
115 | bottomLeftSection: {
116 | flex: 4,
117 | },
118 | bottomRightSection: {
119 | flex: 1,
120 | justifyContent: "flex-end",
121 | alignItems: "flex-end",
122 | },
123 | channelName: {
124 | color: "white",
125 | fontWeight: "bold",
126 | },
127 | caption: {
128 | color: "white",
129 | marginVertical: 8,
130 | },
131 | musicNameContainer: {
132 | flexDirection: "row",
133 | alignItems: "center",
134 | },
135 | musicNameIcon: {
136 | width: 12,
137 | height: 12,
138 | marginRight: 8,
139 | },
140 | musicName: {
141 | color: "white",
142 | },
143 | musicDisc: {
144 | width: 40,
145 | height: 40,
146 | },
147 | verticalBar: {
148 | position: "absolute",
149 | right: 8,
150 | bottom: 72,
151 | },
152 | verticalBarItem: {
153 | marginBottom: 24,
154 | alignItems: "center",
155 | },
156 | verticalBarIcon: {
157 | width: 32,
158 | height: 32,
159 | },
160 | verticalBarText: {
161 | color: "white",
162 | marginTop: 4,
163 | },
164 | avatarContainer: {
165 | marginBottom: 48,
166 | },
167 | avatar: {
168 | width: 48,
169 | height: 48,
170 | borderRadius: 24,
171 | },
172 | followButton: {
173 | position: "absolute",
174 | bottom: -8,
175 | },
176 | followIcon: {
177 | width: 21,
178 | height: 21,
179 | },
180 | floatingMusicNote: {
181 | position: "absolute",
182 | right: 40,
183 | bottom: 16,
184 | width: 16,
185 | height: 16,
186 | tintColor: "white",
187 | },
188 | });
189 |
--------------------------------------------------------------------------------