[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Updates

C3B 196861e4 bbf9ca21

+418 -164
+1
app.json
··· 24 24 }, 25 25 "plugins": [ 26 26 "expo-router", 27 + "react-native-imglysdk", 27 28 [ 28 29 "expo-splash-screen", 29 30 {
+1 -1
app/(screens)/ProfileFeed.tsx
··· 51 51 viewAreaCoveragePercentThreshold: 95, 52 52 }); 53 53 54 - const { height: windowHeight } = Dimensions.get('window'); 54 + const { height: windowHeight } = Dimensions.get('screen'); 55 55 const TAB_BAR_HEIGHT = 0; 56 56 const availableHeight = windowHeight - TAB_BAR_HEIGHT; 57 57
+63 -63
app/(tabs)/NotificationsScreen.tsx
··· 17 17 timestamp: new Date(Date.now() - 1 * 3600 * 1000).toISOString(), // 1 hour ago 18 18 users: [ 19 19 { 20 - image: 'https://picsum.photos/200/300?random=12', 21 - name: 'Alice Johnson', 20 + avatar: 'https://picsum.photos/200/300?random=12', 21 + displayName: 'Alice Johnson', 22 22 did: 'did:plc:alice001', 23 - handler: 'alice.j', 23 + handle: 'alice.j', 24 24 }, 25 25 ], 26 26 }, ··· 35 35 timestamp: new Date(Date.now() - 2 * 3600 * 1000).toISOString(), // 2 hours ago 36 36 users: [ 37 37 { 38 - image: 'https://picsum.photos/200/300?random=13', 39 - name: 'Bob Smith', 38 + avatar: 'https://picsum.photos/200/300?random=13', 39 + displayName: 'Bob Smith', 40 40 did: 'did:plc:bob002', 41 - handler: 'bob.s', 41 + handle: 'bob.s', 42 42 }, 43 43 ], 44 44 }, ··· 53 53 timestamp: new Date(Date.now() - 3 * 3600 * 1000).toISOString(), // 3 hours ago 54 54 users: [ 55 55 { 56 - image: 'https://picsum.photos/200/300?random=14', 57 - name: 'Charlie Davis', 56 + avatar: 'https://picsum.photos/200/300?random=14', 57 + displayName: 'Charlie Davis', 58 58 did: 'did:plc:charlie003', 59 - handler: 'charlie.d', 59 + handle: 'charlie.d', 60 60 }, 61 61 ], 62 62 }, ··· 71 71 timestamp: new Date(Date.now() - 4 * 3600 * 1000).toISOString(), // 4 hours ago 72 72 users: [ 73 73 { 74 - image: 'https://picsum.photos/200/300?random=15', 75 - name: 'Diana Prince', 74 + avatar: 'https://picsum.photos/200/300?random=15', 75 + displayName: 'Diana Prince', 76 76 did: 'did:plc:diana004', 77 - handler: 'diana.p', 77 + handle: 'diana.p', 78 78 }, 79 79 { 80 - image: 'https://picsum.photos/200/300?random=16', 81 - name: 'Edward Norton', 80 + avatar: 'https://picsum.photos/200/300?random=16', 81 + displayName: 'Edward Norton', 82 82 did: 'did:plc:edward005', 83 - handler: 'edward.n', 83 + handle: 'edward.n', 84 84 }, 85 85 { 86 - image: 'https://picsum.photos/200/300?random=17', 87 - name: 'Fiona Gallagher', 86 + avatar: 'https://picsum.photos/200/300?random=17', 87 + displayName: 'Fiona Gallagher', 88 88 did: 'did:plc:fiona006', 89 - handler: 'fiona.g', 89 + handle: 'fiona.g', 90 90 }, 91 91 { 92 - image: 'https://picsum.photos/200/300?random=18', 93 - name: 'George Michael', 92 + avatar: 'https://picsum.photos/200/300?random=18', 93 + displayName: 'George Michael', 94 94 did: 'did:plc:george007', 95 - handler: 'george.m', 95 + handle: 'george.m', 96 96 }, 97 97 ], 98 98 }, ··· 107 107 timestamp: new Date(Date.now() - 5 * 3600 * 1000).toISOString(), // 5 hours ago 108 108 users: [ 109 109 { 110 - image: 'https://picsum.photos/200/300?random=19', 111 - name: 'Hannah Lee', 110 + avatar: 'https://picsum.photos/200/300?random=19', 111 + displayName: 'Hannah Lee', 112 112 did: 'did:plc:hannah008', 113 - handler: 'hannah.l', 113 + handle: 'hannah.l', 114 114 }, 115 115 { 116 - image: 'https://picsum.photos/200/300?random=20', 117 - name: 'Ian Somerhalder', 116 + avatar: 'https://picsum.photos/200/300?random=20', 117 + displayName: 'Ian Somerhalder', 118 118 did: 'did:plc:ian009', 119 - handler: 'ian.s', 119 + handle: 'ian.s', 120 120 }, 121 121 { 122 - image: 'https://picsum.photos/200/300?random=21', 123 - name: 'Jackie Chan', 122 + avatar: 'https://picsum.photos/200/300?random=21', 123 + displayName: 'Jackie Chan', 124 124 did: 'did:plc:jackie010', 125 - handler: 'jackie.c', 125 + handle: 'jackie.c', 126 126 }, 127 127 ], 128 128 }, ··· 137 137 timestamp: new Date(Date.now() - 6 * 3600 * 1000).toISOString(), // 6 hours ago 138 138 users: [ 139 139 { 140 - image: 'https://picsum.photos/200/300?random=22', 141 - name: 'Karen Williams', 140 + avatar: 'https://picsum.photos/200/300?random=22', 141 + displayName: 'Karen Williams', 142 142 did: 'did:plc:karen011', 143 - handler: 'karen.w', 143 + handle: 'karen.w', 144 144 }, 145 145 ], 146 146 }, ··· 155 155 timestamp: new Date(Date.now() - 7 * 3600 * 1000).toISOString(), // 7 hours ago 156 156 users: [ 157 157 { 158 - image: 'https://picsum.photos/200/300?random=23', 159 - name: 'Leo Messi', 158 + avatar: 'https://picsum.photos/200/300?random=23', 159 + displayName: 'Leo Messi', 160 160 did: 'did:plc:leo012', 161 - handler: 'leo.m', 161 + handle: 'leo.m', 162 162 }, 163 163 { 164 - image: 'https://picsum.photos/200/300?random=24', 165 - name: 'Mia Khalifa', 164 + avatar: 'https://picsum.photos/200/300?random=24', 165 + displayName: 'Mia Khalifa', 166 166 did: 'did:plc:mia013', 167 - handler: 'mia.k', 167 + handle: 'mia.k', 168 168 }, 169 169 { 170 - image: 'https://picsum.photos/200/300?random=25', 171 - name: 'Nina Dobrev', 170 + avatar: 'https://picsum.photos/200/300?random=25', 171 + displayName: 'Nina Dobrev', 172 172 did: 'did:plc:nina014', 173 - handler: 'nina.d', 173 + handle: 'nina.d', 174 174 }, 175 175 ], 176 176 }, ··· 185 185 timestamp: new Date(Date.now() - 8 * 3600 * 1000).toISOString(), // 8 hours ago 186 186 users: [ 187 187 { 188 - image: 'https://picsum.photos/200/300?random=26', 189 - name: 'Olivia Brown', 188 + avatar: 'https://picsum.photos/200/300?random=26', 189 + displayName: 'Olivia Brown', 190 190 did: 'did:plc:olivia015', 191 - handler: 'olivia.b', 191 + handle: 'olivia.b', 192 192 }, 193 193 ], 194 194 }, ··· 203 203 timestamp: new Date(Date.now() - 9 * 3600 * 1000).toISOString(), // 9 hours ago 204 204 users: [ 205 205 { 206 - image: 'https://picsum.photos/200/300?random=27', 207 - name: 'Peter Parker', 206 + avatar: 'https://picsum.photos/200/300?random=27', 207 + displayName: 'Peter Parker', 208 208 did: 'did:plc:peter016', 209 - handler: 'peter.p', 209 + handle: 'peter.p', 210 210 }, 211 211 ], 212 212 }, ··· 221 221 timestamp: new Date(Date.now() - 10 * 3600 * 1000).toISOString(), // 10 hours ago 222 222 users: [ 223 223 { 224 - image: 'https://picsum.photos/200/300?random=28', 225 - name: 'Quincy Adams', 224 + avatar: 'https://picsum.photos/200/300?random=28', 225 + displayName: 'Quincy Adams', 226 226 did: 'did:plc:quincy017', 227 - handler: 'quincy.a', 227 + handle: 'quincy.a', 228 228 }, 229 229 { 230 - image: 'https://picsum.photos/200/300?random=29', 231 - name: 'Rachel Green', 230 + avatar: 'https://picsum.photos/200/300?random=29', 231 + displayName: 'Rachel Green', 232 232 did: 'did:plc:rachel018', 233 - handler: 'rachel.g', 233 + handle: 'rachel.g', 234 234 }, 235 235 { 236 - image: 'https://picsum.photos/200/300?random=30', 237 - name: 'Steve Rogers', 236 + avatar: 'https://picsum.photos/200/300?random=30', 237 + displayName: 'Steve Rogers', 238 238 did: 'did:plc:steve019', 239 - handler: 'steve.r', 239 + handle: 'steve.r', 240 240 }, 241 241 { 242 - image: 'https://picsum.photos/200/300?random=31', 243 - name: 'Tony Stark', 242 + avatar: 'https://picsum.photos/200/300?random=31', 243 + displayName: 'Tony Stark', 244 244 did: 'did:plc:tony020', 245 - handler: 'tony.s', 245 + handle: 'tony.s', 246 246 }, 247 247 { 248 - image: 'https://picsum.photos/200/300?random=32', 249 - name: 'Ursula K. Le Guin', 248 + avatar: 'https://picsum.photos/200/300?random=32', 249 + displayName: 'Ursula K. Le Guin', 250 250 did: 'did:plc:ursula021', 251 - handler: 'ursula.k', 251 + handle: 'ursula.k', 252 252 }, 253 253 ], 254 254 },
+19 -2
app/(tabs)/ProfileScreen.tsx
··· 107 107 } 108 108 }; 109 109 110 - 111 110 loadProfileData(); 112 111 loadVideoPosts(); 113 112 }, []); ··· 124 123 }, 125 124 }); 126 125 } 126 + 127 + const isMine = true; 127 128 128 129 const styles = StyleSheet.create({ 129 130 container: { ··· 173 174 flexWrap: 'wrap', 174 175 justifyContent: 'center', 175 176 }, 177 + profileActionButtons: { 178 + flexDirection: 'row', 179 + justifyContent: 'center', 180 + gap: 10, 181 + width: '100%', 182 + }, 176 183 }); 177 184 178 185 return ( ··· 203 210 <View style={styles.profileHeader}> 204 211 {userData && <ProfilePicture userData={userData} />} 205 212 {userData && <ProfileInfo userData={userData} />} 206 - <ActionButton title="Follow" onPress={() => { }} width={250} /> 213 + {!isMine && ( 214 + <ActionButton title="Follow" onPress={() => { }} width={250} /> 215 + )} 216 + { 217 + isMine && ( 218 + <View style={styles.profileActionButtons}> 219 + <ActionButton type={'secondary'} title="Edit Profile" onPress={() => { }} width={140} icon='create' /> 220 + <ActionButton type={'secondary'} title="" onPress={() => { }} width={70} icon='share-social' /> 221 + </View> 222 + ) 223 + } 207 224 </View> 208 225 <View style={styles.profileContent}> 209 226 <View style={styles.profileTabs}>
-2
app/(tabs)/_layout.tsx
··· 64 64 tabBarStyle: Platform.select({ 65 65 ios: { 66 66 backgroundColor: Colors[colorScheme ?? 'light'].background, 67 - height: '10%', 68 67 alignItems: 'center', 69 68 justifyContent: 'space-between', 70 69 display: 'flex', ··· 73 72 }, 74 73 default: { 75 74 backgroundColor: Colors[colorScheme ?? 'light'].background, 76 - height: '10%', 77 75 alignItems: 'center', 78 76 justifyContent: 'space-between', 79 77 display: 'flex',
+7 -32
app/(tabs)/index.tsx
··· 11 11 import ImageScreen from '@/components/Image/ImageScreen'; 12 12 import { PostProps } from '@/types/Interfaces'; 13 13 import { fetchTrendingPosts } from '@/api/videoServices'; 14 + import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 14 15 15 16 export default function HomeScreen() { 16 17 const flatListRef = useRef<FlatList<PostProps>>(null); ··· 30 31 viewAreaCoveragePercentThreshold: 95, 31 32 }); 32 33 33 - const { height: windowHeight } = Dimensions.get('window'); 34 - const TAB_BAR_HEIGHT = windowHeight * 0.10017; 34 + const { height: windowHeight } = Dimensions.get('screen'); 35 + const TAB_BAR_HEIGHT = useBottomTabBarHeight(); 35 36 const availableHeight = windowHeight - TAB_BAR_HEIGHT; 36 37 37 - const getItemLayout = (_: any, index: number) => ({ 38 - length: availableHeight, 39 - offset: availableHeight * index, 40 - index, 41 - }); 42 - 43 38 const shuffleArray = (array: any[]) => { 44 39 return array.sort(() => Math.random() - 0.5); 45 40 }; 41 + 46 42 47 43 useEffect(() => { 48 44 const loadContent = async () => { ··· 60 56 }, []); 61 57 62 58 return ( 63 - <ThemedView style={styles.root}> 59 + <ThemedView > 64 60 <VideoTop /> 65 61 <FlatList 66 62 ref={flatListRef} ··· 71 67 const embedType = item.embed?.$type || ''; 72 68 73 69 return ( 74 - <View style={[styles.container, { height: availableHeight }]}> 70 + <View style={ { height: availableHeight }}> 75 71 {embedType === 'app.bsky.embed.video' || 76 72 embedType === 'app.bsky.embed.video#view' ? ( 77 73 <VideoScreen videoData={{ ...item, isActive }} /> ··· 89 85 decelerationRate="fast" 90 86 initialNumToRender={3} 91 87 maxToRenderPerBatch={2} 92 - windowSize={3} 93 88 removeClippedSubviews 94 89 scrollEventThrottle={16} 95 - style={[styles.flatList, { height: availableHeight }]} 96 - contentContainerStyle={styles.flatListContent} 97 - getItemLayout={getItemLayout} 90 + style={{ height: availableHeight }} 98 91 /> 99 92 </ThemedView> 100 93 ); 101 94 } 102 - 103 - const styles = StyleSheet.create({ 104 - root: { 105 - flex: 1, 106 - backgroundColor: '#000', 107 - }, 108 - flatList: { 109 - }, 110 - flatListContent: { 111 - paddingBottom: 0, 112 - }, 113 - container: { 114 - width: '100%', 115 - backgroundColor: '#000', 116 - justifyContent: 'center', 117 - alignItems: 'center', 118 - }, 119 - });
+65 -25
components/Video/VideoBottom.tsx
··· 1 - import React from 'react'; 2 - import { View, Text, StyleSheet, TouchableOpacity, useColorScheme } from 'react-native'; 1 + import React, { useState } from 'react'; 2 + import { View, StyleSheet, TouchableOpacity, useColorScheme } from 'react-native'; 3 3 import { ThemedText } from '../ThemedText'; 4 4 import { Colors } from '@/constants/Colors'; 5 5 import { VideoBottomProps } from '@/types/Interfaces'; 6 6 7 7 const VideoBottom: React.FC<VideoBottomProps> = ({ videoData }) => { 8 8 const colorScheme = useColorScheme(); 9 + const [expanded, setExpanded] = useState(false); 9 10 10 - // extract hashtags from the text 11 + // Hashtag extractor: supports one or more '#' characters. 11 12 const hashtagExtractor = (text: string) => { 12 - const regex = /#[a-zA-Z0-9_]+/g; 13 + const regex = /#+[a-zA-Z0-9_]+/g; 13 14 const hashtags = text.match(regex); 14 15 return hashtags || []; 15 16 }; 16 17 17 - const hashtags = hashtagExtractor(videoData.record?.text || ''); // extract hashtags from the text 18 - 18 + const textContent = videoData.record?.text || ''; 19 + // Remove hashtags from the text so they don't appear twice. 20 + const cleanedText = textContent.replace(/(#+[a-zA-Z0-9_]+\s*)/g, '').trim(); 21 + const hashtags = hashtagExtractor(textContent); 22 + const MAX_HASHTAGS_COLLAPSED = 3; 23 + 19 24 const styles = StyleSheet.create({ 20 25 container: { 21 - padding: 10, 22 26 borderRadius: 10, 23 - display: 'flex', 24 27 flexDirection: 'column', 25 28 justifyContent: 'flex-start', 26 29 alignItems: 'flex-start', ··· 41 44 fontSize: 14, 42 45 marginBottom: 4, 43 46 }, 47 + readMoreText: { 48 + color: '#888', 49 + fontSize: 12, 50 + marginTop: 4, 51 + }, 52 + hashtagsContainer: { 53 + marginTop: 6, 54 + }, 44 55 hashtags: { 45 56 flexDirection: 'row', 46 57 flexWrap: 'wrap', 47 58 gap: 4, 48 - marginTop: 6, 49 59 }, 50 60 hashtagText: { 51 61 color: '#fff', ··· 53 63 paddingVertical: 1, 54 64 paddingHorizontal: 8, 55 65 borderRadius: 50, 56 - display: 'flex', 57 - flexDirection: 'row', 58 - alignItems: 'center', 59 - justifyContent: 'center', 66 + marginRight: 4, 67 + marginBottom: 4, 60 68 }, 61 69 followButton: { 62 70 backgroundColor: '#ffffff40', ··· 67 75 display: 'none', 68 76 }, 69 77 }); 78 + 70 79 return ( 71 80 <View style={styles.container}> 72 - <ThemedText lightColor={Colors.dark.text} darkColor={Colors.dark.text} style={styles.userInfo}> 73 - {videoData.author?.displayName || 'Unknown'} • @{videoData.author?.handle || 'unknown'} 81 + <ThemedText 82 + lightColor={Colors.dark.text} 83 + darkColor={Colors.dark.text} 84 + style={styles.userInfo} 85 + > 86 + @{videoData.author?.handle || 'unknown'} 74 87 <TouchableOpacity style={styles.followButton}> 75 - <ThemedText type='subtitle' style={{color: "#fff"}}>Follow</ThemedText> 88 + <ThemedText type="subtitle" style={{ color: "#fff" }}> 89 + Follow 90 + </ThemedText> 76 91 </TouchableOpacity> 77 92 </ThemedText> 78 - <ThemedText type="description" lightColor={Colors.dark.text} darkColor={Colors.dark.text}>{videoData.record?.text || ''}</ThemedText> 79 - <View style={styles.hashtags}> 80 - {hashtags.map((hashtag, index) => ( 81 - <ThemedText type="description" key={index} style={styles.hashtagText}>#{hashtag} </ThemedText> 82 - ))} 83 - </View> 93 + <TouchableOpacity onPress={() => setExpanded(!expanded)}> 94 + <ThemedText 95 + type="description" 96 + lightColor={Colors.dark.text} 97 + darkColor={Colors.dark.text} 98 + style={styles.description} 99 + numberOfLines={!expanded ? 3 : undefined} 100 + > 101 + {cleanedText} 102 + </ThemedText> 103 + {hashtags.length > 0 && ( 104 + <View style={styles.hashtagsContainer}> 105 + <View style={styles.hashtags}> 106 + {(expanded || hashtags.length <= MAX_HASHTAGS_COLLAPSED 107 + ? hashtags 108 + : hashtags.slice(0, MAX_HASHTAGS_COLLAPSED) 109 + ).map((hashtag, index) => ( 110 + <ThemedText key={index} type="description" style={styles.hashtagText}> 111 + {hashtag} 112 + </ThemedText> 113 + ))} 114 + </View> 115 + </View> 116 + )} 117 + <ThemedText 118 + type="description" 119 + lightColor={Colors.dark.text} 120 + darkColor={Colors.dark.text} 121 + style={styles.readMoreText} 122 + > 123 + {!expanded ? 'Read More' : 'Read Less'} 124 + </ThemedText> 125 + </TouchableOpacity> 84 126 </View> 85 127 ); 86 128 }; 87 129 88 - 89 - 90 - export default VideoBottom; 130 + export default VideoBottom;
+18 -16
components/Video/VideoInfoOverlay.tsx
··· 22 22 setIsCommentsVisible(true); 23 23 }; 24 24 25 + 25 26 const comments = fetchPostThread(videoData.author.did, getVideoPostId(videoData.uri)); 26 27 27 28 const handleCloseComments = () => { 28 29 setIsCommentsVisible(false); 29 30 }; 30 31 32 + const screenHeight = Dimensions.get('window').height; 33 + 34 + const styles = StyleSheet.create({ 35 + container: { 36 + display: 'flex', 37 + flexDirection: 'row', 38 + zIndex: 1, 39 + width: '100%', 40 + height: screenHeight, 41 + position: 'absolute', 42 + justifyContent: 'space-between', 43 + alignItems: 'flex-end', 44 + paddingHorizontal: 15, 45 + paddingBottom: 55, 46 + }, 47 + }); 48 + 31 49 return ( 32 50 <View style={styles.container} pointerEvents="box-none"> 33 51 <VideoBottom videoData={videoData} /> ··· 45 63 ); 46 64 }; 47 65 48 - const screenHeight = Dimensions.get('window').height; 49 66 50 - const styles = StyleSheet.create({ 51 - container: { 52 - display: 'flex', 53 - flexDirection: 'row', 54 - zIndex: 1, 55 - width: '100%', 56 - height: screenHeight, 57 - position: 'absolute', 58 - justifyContent: 'space-between', 59 - alignItems: 'flex-end', 60 - paddingHorizontal: 20, 61 - paddingBottom: 60, 62 - marginBottom: 0, 63 - }, 64 - }); 65 67 66 68 export default VideoInfoOverlay;
+29 -5
components/Video/VideoScreen.tsx
··· 6 6 TouchableWithoutFeedback, 7 7 Animated, 8 8 PanResponder, 9 + Image, 9 10 } from 'react-native'; 10 11 import { useEvent } from 'expo'; 11 12 import { useVideoPlayer, VideoView, VideoSource } from 'expo-video'; ··· 14 15 import { VideoScreenProps } from '@/types/Interfaces'; 15 16 import { ThemedText } from '../ThemedText'; 16 17 import { LinearGradient } from 'expo-linear-gradient'; 18 + import { BlurView } from 'expo-blur'; 17 19 18 20 export default function VideoScreen({ 19 21 videoData, ··· 120 122 nativeControls={false} 121 123 allowsVideoFrameAnalysis={false} 122 124 /> 125 + <BlurView intensity={50} style={styles.blurOverlay} tint="dark" /> 126 + 127 + <Image 128 + style={styles.videoBackground} 129 + source={{ uri: videoData.embed?.thumbnail }} 130 + /> 123 131 <LinearGradient 124 132 colors={['transparent', 'rgba(0,0,0,0.8)']} 125 133 style={styles.background} ··· 151 159 152 160 const styles = StyleSheet.create({ 153 161 container: { 154 - flex: 1, 155 - backgroundColor: '#000', 156 162 justifyContent: 'center', 157 163 alignItems: 'center', 158 164 width: '100%', 159 - height: '100%', 165 + 160 166 }, 161 167 video: { 162 - flex: 1, 163 168 width: '100%', 164 169 height: '100%', 165 - backgroundColor: '#000', 170 + backgroundColor: 'transparent', 171 + }, 172 + videoBackground: { 173 + position: 'absolute', 174 + left: 0, 175 + right: 0, 176 + bottom: 0, 177 + height: '100%', 178 + width: '100%', 179 + zIndex: -2, 180 + }, 181 + blurOverlay: { 182 + position: 'absolute', 183 + width: '100%', 184 + height: '100%', 185 + top: 0, 186 + left: 0, 187 + right: 0, 188 + bottom: 0, 189 + zIndex: -1, 166 190 }, 167 191 progressContainer: { 168 192 position: 'absolute',
+3 -5
components/Video/VideoSide.tsx
··· 1 - // src/components/Video/VideoSide.tsx 2 - 3 1 import React from 'react'; 4 2 import { View, Image, TouchableOpacity, StyleSheet, useColorScheme } from 'react-native'; 5 3 import { Ionicons } from '@expo/vector-icons'; ··· 83 81 </TouchableOpacity> 84 82 85 83 <TouchableOpacity style={styles.iconContainer} onPress={() => console.log('Liked video')}> 86 - <Ionicons style={styles.icon} color="white" name="heart-outline" size={30} /> 84 + <Ionicons style={styles.icon} color="white" name="heart-outline" size={25} /> 87 85 <ThemedText 88 86 type="defaultBold" 89 87 style={styles.text} ··· 95 93 </TouchableOpacity> 96 94 97 95 <TouchableOpacity style={styles.iconContainer} onPress={onComments}> 98 - <Ionicons style={styles.icon} color="white" name="chatbubble-outline" size={30} /> 96 + <Ionicons style={styles.icon} color="white" name="chatbubble-outline" size={25} /> 99 97 <ThemedText 100 98 type="defaultBold" 101 99 style={styles.text} ··· 108 106 </TouchableOpacity> 109 107 110 108 <TouchableOpacity style={styles.iconContainer} onPress={() => console.log('Shared video')}> 111 - <Ionicons style={styles.icon} color="white" name="share-social-outline" size={30} /> 109 + <Ionicons style={styles.icon} color="white" name="share-social-outline" size={25} /> 112 110 <ThemedText 113 111 type="defaultBold" 114 112 style={styles.text}
+121 -6
components/Video/VideoTop.tsx
··· 1 - import React from 'react'; 2 - import { View, StyleSheet, useColorScheme, TouchableOpacity } from 'react-native'; 1 + import React, { useCallback, useRef } from 'react'; 2 + import { View, StyleSheet, useColorScheme, TouchableOpacity, Button, Dimensions } from 'react-native'; 3 3 import { ThemedText } from '../ThemedText'; 4 4 import { Colors } from '@/constants/Colors'; 5 + import { Ionicons } from '@expo/vector-icons'; 6 + 7 + import { 8 + BottomSheetModal, 9 + BottomSheetView, 10 + BottomSheetModalProvider, 11 + } from '@gorhom/bottom-sheet'; 12 + import { GestureHandlerRootView, Switch } from 'react-native-gesture-handler'; 5 13 6 14 const VideoTop: React.FC = () => { 7 15 const colorScheme = useColorScheme(); 8 16 17 + // ref 18 + const bottomSheetModalRef = useRef<BottomSheetModal>(null); 19 + 20 + const height = Dimensions.get('window').height; 21 + // callbacks 22 + const handlePresentModalPress = useCallback(() => { 23 + bottomSheetModalRef.current?.present(); 24 + }, []); 25 + const handleSheetChanges = useCallback((index: number) => { 26 + console.log('handleSheetChanges', index); 27 + }, []); 28 + 9 29 const styles = StyleSheet.create({ 10 30 container: { 11 31 alignItems: 'center', ··· 14 34 width: '100%', 15 35 top: '9%', 16 36 zIndex: 1, 37 + flexDirection: 'row', 17 38 }, 18 39 text: { 19 40 fontWeight: 'bold', ··· 23 44 shadowOffset: { width: 0, height: 2 }, 24 45 shadowOpacity: 0.25, 25 46 }, 47 + filtersButton: { 48 + position: 'absolute', 49 + right: 30, 50 + alignItems: 'center', 51 + justifyContent: 'center', 52 + display: 'flex', 53 + flexDirection: 'row', 54 + borderWidth: 0, 55 + }, 56 + contentContainer: { 57 + flex: 1, 58 + alignItems: 'center', 59 + }, 60 + containerb: { 61 + backgroundColor: 'transparent', 62 + width: '100%', 63 + height: height-150, 64 + position: 'absolute', 65 + top: 0, 66 + left: 0, 67 + right: 0, 68 + bottom: 0, 69 + justifyContent: 'center', 70 + alignItems: 'center', 71 + }, 72 + feedOptions: { 73 + flexDirection: 'column', 74 + alignItems: 'flex-start', 75 + justifyContent: 'center', 76 + marginTop: 10, 77 + gap: 10, 78 + width: '100%', 79 + paddingHorizontal: 10, 80 + 81 + }, 82 + feedOption: { 83 + flexDirection: 'row', 84 + alignItems: 'center', 85 + justifyContent: 'center', 86 + marginHorizontal: 10, 87 + gap: 10, 88 + }, 89 + addFeedOption: { 90 + flexDirection: 'row', 91 + alignItems: 'center', 92 + justifyContent: 'center', 93 + marginHorizontal: 10, 94 + gap: 10, 95 + }, 26 96 }); 27 97 28 - 98 + 29 99 return ( 30 100 <View style={styles.container}> 31 - <TouchableOpacity> 32 - <ThemedText type='defaultBold' darkColor={Colors.dark.text} lightColor={Colors.dark.text} style={styles.text}>For You</ThemedText> 33 - </TouchableOpacity> 101 + <View style={styles.containerb}> 102 + <BottomSheetModalProvider> 103 + <BottomSheetModal 104 + ref={bottomSheetModalRef} 105 + onChange={handleSheetChanges} 106 + > 107 + <BottomSheetView style={styles.contentContainer}> 108 + <View style={styles.feedOptions}> 109 + <ThemedText>Feed options</ThemedText> 110 + 111 + <View style={styles.feedOption}> 112 + <Switch /> 113 + <ThemedText>Images</ThemedText> 114 + </View> 115 + <View style={styles.feedOption}> 116 + <Switch /> 117 + <ThemedText>Videos</ThemedText> 118 + </View> 119 + <View style={styles.feedOption}> 120 + <Switch /> 121 + <ThemedText>Suggestive Content</ThemedText> 122 + </View> 123 + </View> 124 + <View style={styles.feedOptions}> 125 + <ThemedText>Custom Feeds</ThemedText> 126 + 127 + <View style={styles.feedOption}> 128 + <Switch value={true}/> 129 + <ThemedText>For You</ThemedText> 130 + </View> 131 + <View style={styles.addFeedOption}> 132 + <Ionicons name="add" size={24} color={Colors.dark.text} lightColor={Colors.dark.text} /> 133 + <ThemedText>Add Custom Feed</ThemedText> 134 + </View> 135 + </View> 136 + 137 + <Button title="Close" onPress={() => bottomSheetModalRef.current?.close()} /> 138 + 139 + </BottomSheetView> 140 + </BottomSheetModal> 141 + </BottomSheetModalProvider> 142 + </View> 143 + <TouchableOpacity> 144 + <ThemedText type='defaultBold' darkColor={Colors.dark.text} lightColor={Colors.dark.text} style={styles.text}>For You</ThemedText> 145 + </TouchableOpacity> 146 + <TouchableOpacity style={styles.filtersButton} onPress={handlePresentModalPress}> 147 + <Ionicons name="filter" size={24} color={Colors.dark.text} lightColor={Colors.dark.text} /> 148 + </TouchableOpacity> 34 149 </View> 35 150 ); 36 151 };
+9 -4
components/global/ActionButton.tsx
··· 1 1 import { Colors } from '@/constants/Colors'; 2 + import { Ionicons } from '@expo/vector-icons'; 2 3 import React from 'react'; 3 4 import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, useColorScheme } from 'react-native'; 4 5 5 6 interface ActionButtonProps { 6 7 title: string; 7 8 onPress: () => void; 8 - type?: 'primary' | 'secondary' | 'outline' | 'disabled'; // Different button styles 9 - isLoading?: boolean; // Optional loading state 10 - width?: number; // Optional full-width button 9 + type?: 'primary' | 'secondary' | 'outline' | 'disabled'; 10 + icon?: string; 11 + isLoading?: boolean; 12 + width?: number; 11 13 } 12 14 13 15 const ActionButton: React.FC<ActionButtonProps> = ({ ··· 16 18 type = 'primary', 17 19 isLoading = false, 18 20 width, 21 + icon, 19 22 }) => { 20 23 21 24 const colorScheme = useColorScheme(); ··· 27 30 borderRadius: 8, 28 31 alignItems: 'center', 29 32 justifyContent: 'center', 33 + flexDirection: 'row', 30 34 }, 31 35 primary: { 32 36 backgroundColor: Colors[colorScheme ?? 'light'].tint, ··· 45 49 buttonText: { 46 50 fontSize: 16, 47 51 fontWeight: 'bold', 48 - color: '#ffffff', 52 + color: Colors.dark.text, 49 53 }, 50 54 outlineText: { 51 55 color: Colors[colorScheme ?? 'light'].tint, ··· 62 66 onPress={onPress} 63 67 disabled={type === 'disabled' || isLoading} 64 68 > 69 + {icon && <Ionicons name={icon as any} size={25} color={Colors.dark.text} />} 65 70 {isLoading ? ( 66 71 <ActivityIndicator color={type === 'outline' ? '#000' : '#fff'} /> 67 72 ) : (
+77
package-lock.json
··· 9 9 "version": "1.0.0", 10 10 "dependencies": { 11 11 "@expo/vector-icons": "^14.0.2", 12 + "@gorhom/bottom-sheet": "^5.1.1", 12 13 "@react-navigation/bottom-tabs": "^7.2.0", 14 + "@react-navigation/drawer": "^7.1.1", 13 15 "@react-navigation/native": "^7.0.14", 14 16 "date-fns": "^4.1.0", 15 17 "expo": "~52.0.25", ··· 2707 2709 "js-yaml": "bin/js-yaml.js" 2708 2710 } 2709 2711 }, 2712 + "node_modules/@gorhom/bottom-sheet": { 2713 + "version": "5.1.1", 2714 + "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.1.1.tgz", 2715 + "integrity": "sha512-Y8FiuRmeZYaP+ZGQ0axDwWrrKqVp4ByYRs1D2fTJTxHMt081MHHRQsqmZ3SK7AFp3cSID+vTqnD8w/KAASpy+w==", 2716 + "license": "MIT", 2717 + "dependencies": { 2718 + "@gorhom/portal": "1.0.14", 2719 + "invariant": "^2.2.4" 2720 + }, 2721 + "peerDependencies": { 2722 + "@types/react": "*", 2723 + "@types/react-native": "*", 2724 + "react": "*", 2725 + "react-native": "*", 2726 + "react-native-gesture-handler": ">=2.16.1", 2727 + "react-native-reanimated": ">=3.16.0" 2728 + }, 2729 + "peerDependenciesMeta": { 2730 + "@types/react": { 2731 + "optional": true 2732 + }, 2733 + "@types/react-native": { 2734 + "optional": true 2735 + } 2736 + } 2737 + }, 2738 + "node_modules/@gorhom/portal": { 2739 + "version": "1.0.14", 2740 + "resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz", 2741 + "integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==", 2742 + "license": "MIT", 2743 + "dependencies": { 2744 + "nanoid": "^3.3.1" 2745 + }, 2746 + "peerDependencies": { 2747 + "react": "*", 2748 + "react-native": "*" 2749 + } 2750 + }, 2710 2751 "node_modules/@isaacs/cliui": { 2711 2752 "version": "8.0.2", 2712 2753 "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", ··· 3694 3735 }, 3695 3736 "peerDependencies": { 3696 3737 "react": ">= 18.2.0" 3738 + } 3739 + }, 3740 + "node_modules/@react-navigation/drawer": { 3741 + "version": "7.1.1", 3742 + "resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.1.1.tgz", 3743 + "integrity": "sha512-34UqRS5OLFaNXPs5ocz3Du9c7em0P7fFMPYCZn/MxadDzQ4Mn/74pmJczmiyvyvz8vcWsNRbZ3Qswm0Dv6z60w==", 3744 + "license": "MIT", 3745 + "dependencies": { 3746 + "@react-navigation/elements": "^2.2.5", 3747 + "color": "^4.2.3", 3748 + "react-native-drawer-layout": "^4.1.1", 3749 + "use-latest-callback": "^0.2.1" 3750 + }, 3751 + "peerDependencies": { 3752 + "@react-navigation/native": "^7.0.14", 3753 + "react": ">= 18.2.0", 3754 + "react-native": "*", 3755 + "react-native-gesture-handler": ">= 2.0.0", 3756 + "react-native-reanimated": ">= 2.0.0", 3757 + "react-native-safe-area-context": ">= 4.0.0", 3758 + "react-native-screens": ">= 4.0.0" 3697 3759 } 3698 3760 }, 3699 3761 "node_modules/@react-navigation/elements": { ··· 11260 11322 "@types/react": { 11261 11323 "optional": true 11262 11324 } 11325 + } 11326 + }, 11327 + "node_modules/react-native-drawer-layout": { 11328 + "version": "4.1.1", 11329 + "resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.1.1.tgz", 11330 + "integrity": "sha512-ob6O3ph7PZ3A2FpdlsSxHuMpHDXREZPR8A6S3q0dSxV7i6d+8Z6CPCTbegfN2QZyizSow9NLrKyXP93tlqZ3dA==", 11331 + "license": "MIT", 11332 + "dependencies": { 11333 + "use-latest-callback": "^0.2.1" 11334 + }, 11335 + "peerDependencies": { 11336 + "react": ">= 18.2.0", 11337 + "react-native": "*", 11338 + "react-native-gesture-handler": ">= 2.0.0", 11339 + "react-native-reanimated": ">= 2.0.0" 11263 11340 } 11264 11341 }, 11265 11342 "node_modules/react-native-gesture-handler": {
+5 -3
package.json
··· 16 16 }, 17 17 "dependencies": { 18 18 "@expo/vector-icons": "^14.0.2", 19 + "@gorhom/bottom-sheet": "^5.1.1", 19 20 "@react-navigation/bottom-tabs": "^7.2.0", 21 + "@react-navigation/drawer": "^7.1.1", 20 22 "@react-navigation/native": "^7.0.14", 21 23 "date-fns": "^4.1.0", 22 24 "expo": "~52.0.25", 25 + "expo-av": "~15.0.2", 23 26 "expo-blur": "~14.0.3", 24 27 "expo-camera": "~16.0.16", 25 28 "expo-constants": "~17.0.4", 26 29 "expo-font": "~13.0.3", 27 30 "expo-haptics": "~14.0.1", 31 + "expo-linear-gradient": "~14.0.2", 28 32 "expo-linking": "~7.0.4", 29 33 "expo-media-library": "~17.0.5", 30 34 "expo-router": "~4.0.16", ··· 44 48 "react-native-video": "^6.10.0", 45 49 "react-native-videoeditorsdk": "^3.3.0", 46 50 "react-native-web": "~0.19.13", 47 - "react-native-webview": "13.12.5", 48 - "expo-av": "~15.0.2", 49 - "expo-linear-gradient": "~14.0.2" 51 + "react-native-webview": "13.12.5" 50 52 }, 51 53 "devDependencies": { 52 54 "@babel/core": "^7.25.2",