[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

Photos, Home Icon And some improvements

C3B bbf9ca21 b6ea5f97

+267 -43
+37 -16
app/(tabs)/ProfileScreen.tsx
··· 154 154 profileContent: { 155 155 marginTop: 20, 156 156 }, 157 + profileTabs: { 158 + flexDirection: 'row', 159 + justifyContent: 'space-around', 160 + marginBottom: 10, 161 + width: '100%', 162 + borderBottomWidth: 1, 163 + borderBottomColor: Colors[colorScheme ?? 'light'].underlineColor, 164 + }, 157 165 tabButton: { 158 166 flexDirection: 'row', 159 167 alignItems: 'center', 160 168 justifyContent: 'center', 161 169 padding: 10, 162 - borderBottomWidth: 1, 163 - borderBottomColor: Colors[colorScheme ?? 'light'].underlineColor, 164 - marginBottom: 5, 165 170 }, 166 171 videoGrid: { 167 172 flexDirection: 'row', ··· 201 206 <ActionButton title="Follow" onPress={() => { }} width={250} /> 202 207 </View> 203 208 <View style={styles.profileContent}> 204 - <View style={styles.tabButton}> 205 - <Ionicons 206 - name="albums" 207 - size={24} 208 - color={Colors[colorScheme ?? 'light'].selectedIcon} 209 - /> 210 - <ThemedText 211 - style={{ 212 - color: Colors[colorScheme ?? 'light'].selectedIcon, 213 - marginLeft: 5, 214 - }}> 215 - Videos 216 - </ThemedText> 209 + <View style={styles.profileTabs}> 210 + <View style={styles.tabButton}> 211 + <Ionicons 212 + name="film" 213 + size={24} 214 + color={Colors[colorScheme ?? 'light'].selectedIcon} 215 + /> 216 + <ThemedText 217 + style={{ 218 + color: Colors[colorScheme ?? 'light'].selectedIcon, 219 + marginLeft: 5, 220 + }}> 221 + Videos 222 + </ThemedText> 223 + </View> 224 + <View style={styles.tabButton}> 225 + <Ionicons 226 + name="image" 227 + size={24} 228 + color={Colors[colorScheme ?? 'light'].notSelectedIcon} 229 + /> 230 + <ThemedText 231 + style={{ 232 + color: Colors[colorScheme ?? 'light'].notSelectedIcon, 233 + marginLeft: 5, 234 + }}> 235 + Photos 236 + </ThemedText> 237 + </View> 217 238 </View> 218 239 <View style={styles.videoGrid}> 219 240 {paddedVideoData.map((item, index) => {
+1 -5
app/(tabs)/SearchScreen.tsx
··· 18 18 const remainder = posts.length % 3; 19 19 const placeholdersNeeded = remainder === 0 ? 0 : 3 - remainder; 20 20 21 - // Create placeholder objects with minimal valid PostProps values 22 21 const placeholders: (PostProps & { isPlaceholder?: boolean })[] = Array(placeholdersNeeded) 23 22 .fill(null) 24 23 .map((_, i) => ({ ··· 68 67 useEffect(() => { 69 68 const loadTrendingData = async () => { 70 69 try { 71 - // fetchTrendingVideos now returns posts (with embed type video) 72 70 const posts = await fetchTrendingPosts('video'); 73 71 setVideoData(posts); 74 72 if (posts.length > 0) { 75 - // Use author.did instead of creator.did 76 - const did = posts[0]?.author?.did; 73 + const did = posts[2]?.author?.did; 77 74 if (did) { 78 75 const profileData = await getProfile(did); 79 76 if (profileData) { ··· 89 86 followsCount: profileData.followsCount, 90 87 likes: 0, 91 88 views: 0, 92 - // Pass the posts as the user's videos 93 89 postsCount: profileData.postsCount, 94 90 associated: profileData.associated, 95 91 joinedViaStarterPack: profileData.joinedViaStarterPack,
+1 -1
app/(tabs)/_layout.tsx
··· 73 73 }, 74 74 default: { 75 75 backgroundColor: Colors[colorScheme ?? 'light'].background, 76 - height: 60, 76 + height: '10%', 77 77 alignItems: 'center', 78 78 justifyContent: 'space-between', 79 79 display: 'flex',
+31 -13
app/(tabs)/index.tsx
··· 1 - // HomeScreen.tsx 2 1 import React, { useEffect, useRef, useState } from 'react'; 3 2 import { 4 3 StyleSheet, ··· 9 8 import { ThemedView } from '@/components/ThemedView'; 10 9 import VideoScreen from '@/components/Video/VideoScreen'; 11 10 import VideoTop from '@/components/Video/VideoTop'; 11 + import ImageScreen from '@/components/Image/ImageScreen'; 12 12 import { PostProps } from '@/types/Interfaces'; 13 13 import { fetchTrendingPosts } from '@/api/videoServices'; 14 14 15 15 export default function HomeScreen() { 16 - const flatListRef = useRef<FlatList>(null); 16 + const flatListRef = useRef<FlatList<PostProps>>(null); 17 17 const [postData, setPostData] = useState<PostProps[]>([]); 18 18 const [currentVisibleIndex, setCurrentVisibleIndex] = useState(0); 19 19 ··· 31 31 }); 32 32 33 33 const { height: windowHeight } = Dimensions.get('window'); 34 - const TAB_BAR_HEIGHT = windowHeight * 0.1; 34 + const TAB_BAR_HEIGHT = windowHeight * 0.10017; 35 35 const availableHeight = windowHeight - TAB_BAR_HEIGHT; 36 36 37 37 const getItemLayout = (_: any, index: number) => ({ ··· 40 40 index, 41 41 }); 42 42 43 + const shuffleArray = (array: any[]) => { 44 + return array.sort(() => Math.random() - 0.5); 45 + }; 46 + 43 47 useEffect(() => { 44 - const loadVideos = async () => { 45 - const videos = await fetchTrendingPosts('video'); 46 - setPostData(videos); 48 + const loadContent = async () => { 49 + try { 50 + const videoPosts = await fetchTrendingPosts('video'); 51 + const imagePosts = await fetchTrendingPosts('image'); 52 + const mergedData = shuffleArray([...videoPosts, ...imagePosts]); 53 + setPostData(mergedData); 54 + } catch (error) { 55 + console.error('Error fetching posts:', error); 56 + } 47 57 }; 48 - loadVideos(); 58 + 59 + loadContent(); 49 60 }, []); 50 61 51 62 return ( ··· 54 65 <FlatList 55 66 ref={flatListRef} 56 67 data={postData} 57 - keyExtractor={(item, index) => `${item.id}-${index}`} 68 + keyExtractor={(item, index) => `${item.cid}-${index}`} 58 69 renderItem={({ item, index }) => { 59 70 const isActive = index === currentVisibleIndex; 71 + const embedType = item.embed?.$type || ''; 72 + 60 73 return ( 61 - <View style={[styles.videoContainer, { height: availableHeight }]}> 62 - <VideoScreen videoData={{ ...item, isActive }} /> 74 + <View style={[styles.container, { height: availableHeight }]}> 75 + {embedType === 'app.bsky.embed.video' || 76 + embedType === 'app.bsky.embed.video#view' ? ( 77 + <VideoScreen videoData={{ ...item, isActive }} /> 78 + ) : embedType === 'app.bsky.embed.images' || 79 + embedType === 'app.bsky.embed.images#view' ? ( 80 + <ImageScreen imageData={item} /> 81 + ) : null} 63 82 </View> 64 83 ); 65 84 }} ··· 73 92 windowSize={3} 74 93 removeClippedSubviews 75 94 scrollEventThrottle={16} 76 - style={styles.flatList} 95 + style={[styles.flatList, { height: availableHeight }]} 77 96 contentContainerStyle={styles.flatListContent} 78 97 getItemLayout={getItemLayout} 79 98 /> ··· 87 106 backgroundColor: '#000', 88 107 }, 89 108 flatList: { 90 - flex: 1, 91 109 }, 92 110 flatListContent: { 93 111 paddingBottom: 0, 94 112 }, 95 - videoContainer: { 113 + container: { 96 114 width: '100%', 97 115 backgroundColor: '#000', 98 116 justifyContent: 'center',
+29
components/Image/ImageIndex.tsx
··· 1 + import React from 'react'; 2 + import { View, StyleSheet } from 'react-native'; 3 + 4 + interface ImageIndexProps { 5 + index: number; 6 + currentIndex: number; 7 + } 8 + 9 + const ImageIndex: React.FC<ImageIndexProps> = ({ index, currentIndex }) => { 10 + return ( 11 + <View 12 + style={[ 13 + styles.dot, 14 + { backgroundColor: index === currentIndex ? 'white' : 'rgba(255, 255, 255, 0.5)' }, 15 + ]} 16 + /> 17 + ); 18 + }; 19 + 20 + const styles = StyleSheet.create({ 21 + dot: { 22 + width: 10, 23 + height: 10, 24 + borderRadius: 5, 25 + marginHorizontal: 5, 26 + }, 27 + }); 28 + 29 + export default ImageIndex;
+132
components/Image/ImageScreen.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { View, StyleSheet, Image, ScrollView, Dimensions, NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; 3 + import { BlurView } from 'expo-blur'; 4 + import { ThemedText } from '@/components/ThemedText'; 5 + import { ImageScreenProps } from '@/types/Interfaces'; 6 + import ImageIndex from './ImageIndex'; 7 + import VideoInfoOverlay from '../Video/VideoInfoOverlay'; 8 + import { LinearGradient } from 'expo-linear-gradient'; 9 + 10 + export default function ImageScreen({ imageData }: ImageScreenProps) { 11 + const images = imageData?.embed?.images || []; 12 + const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); 13 + const TAB_BAR_HEIGHT = screenHeight * 0.1; 14 + const availableHeight = screenHeight - TAB_BAR_HEIGHT; 15 + 16 + const [currentIndex, setCurrentIndex] = useState(0); 17 + 18 + if (!images || images.length === 0) { 19 + return ( 20 + <View style={styles.noImageContainer}> 21 + <ThemedText>No image available</ThemedText> 22 + </View> 23 + ); 24 + } 25 + 26 + const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => { 27 + const newIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth); 28 + setCurrentIndex(newIndex); 29 + }; 30 + 31 + return ( 32 + <View style={{ ...styles.root, height: screenHeight - TAB_BAR_HEIGHT }}> 33 + 34 + <VideoInfoOverlay videoData={imageData} /> 35 + {images.length > 1 && ( 36 + 37 + <View style={styles.pageIndexContainer}> 38 + {images.map((_, index) => ( 39 + <ImageIndex key={index} index={index} currentIndex={currentIndex} /> 40 + ))} 41 + 42 + </View> 43 + 44 + )} 45 + 46 + <ScrollView 47 + horizontal 48 + pagingEnabled 49 + decelerationRate="fast" 50 + snapToInterval={screenWidth} 51 + snapToAlignment="center" 52 + showsHorizontalScrollIndicator={false} 53 + onScroll={handleScroll} 54 + scrollEventThrottle={16} 55 + > 56 + {images.map((img, index) => ( 57 + <View key={index} style={[styles.pageContainer, { width: screenWidth }]}> 58 + <Image source={{ uri: img.fullsize || '' }} style={styles.imageBg} resizeMode="cover" /> 59 + <BlurView intensity={50} style={styles.blurOverlay} tint="dark" /> 60 + <Image source={{ uri: img.fullsize || '' }} style={styles.image} resizeMode="contain" /> 61 + </View> 62 + ))} 63 + 64 + <LinearGradient 65 + colors={['transparent', 'rgba(0,0,0,0.8)']} 66 + style={styles.background} 67 + /> 68 + </ScrollView> 69 + </View> 70 + ); 71 + } 72 + 73 + const styles = StyleSheet.create({ 74 + root: { 75 + backgroundColor: '#000', 76 + justifyContent: 'center', 77 + alignItems: 'center', 78 + width: '100%', 79 + flex: 1, 80 + }, 81 + noImageContainer: { 82 + flex: 1, 83 + justifyContent: 'center', 84 + alignItems: 'center', 85 + backgroundColor: '#000', 86 + }, 87 + pageContainer: { 88 + justifyContent: 'center', 89 + alignItems: 'center', 90 + flex: 1, 91 + width: '100%', 92 + height: '100%', 93 + }, 94 + image: { 95 + width: '100%', 96 + height: '100%', 97 + }, 98 + imageBg: { 99 + position: 'absolute', 100 + width: '100%', 101 + height: '100%', 102 + top: 0, 103 + left: 0, 104 + right: 0, 105 + bottom: 0, 106 + }, 107 + blurOverlay: { 108 + position: 'absolute', 109 + width: '100%', 110 + height: '100%', 111 + top: 0, 112 + left: 0, 113 + right: 0, 114 + bottom: 0, 115 + }, 116 + pageIndexContainer: { 117 + position: 'absolute', 118 + bottom: 10, 119 + flexDirection: 'row', 120 + justifyContent: 'center', 121 + alignItems: 'center', 122 + width: '100%', 123 + zIndex: 1, 124 + }, 125 + background: { 126 + position: 'absolute', 127 + left: 0, 128 + right: 0, 129 + bottom: 0, 130 + height: 230, 131 + }, 132 + });
components/Image/ImageView.tsx

This is a binary file and will not be displayed.

-1
components/Video/VideoBottom.tsx
··· 18 18 19 19 const styles = StyleSheet.create({ 20 20 container: { 21 - backgroundColor: '#00000020', 22 21 padding: 10, 23 22 borderRadius: 10, 24 23 display: 'flex',
+12 -1
components/Video/VideoScreen.tsx
··· 13 13 import VideoInfoOverlay from './VideoInfoOverlay'; 14 14 import { VideoScreenProps } from '@/types/Interfaces'; 15 15 import { ThemedText } from '../ThemedText'; 16 + import { LinearGradient } from 'expo-linear-gradient'; 16 17 17 18 export default function VideoScreen({ 18 19 videoData, ··· 119 120 nativeControls={false} 120 121 allowsVideoFrameAnalysis={false} 121 122 /> 123 + <LinearGradient 124 + colors={['transparent', 'rgba(0,0,0,0.8)']} 125 + style={styles.background} 126 + /> 122 127 <View style={styles.progressContainer}> 123 128 <View style={[styles.progressBar, { width: `${progressPercentage}%` }]} /> 124 129 <View style={[styles.progressIndicator, { left: `${progressPercentage}%` }]} /> 125 130 </View> 126 - 127 131 128 132 {showControls && ( 129 133 <> ··· 178 182 progressBar: { 179 183 height: '100%', 180 184 backgroundColor: 'rgba(255, 255, 255, 0.5)', 185 + }, 186 + background: { 187 + position: 'absolute', 188 + left: 0, 189 + right: 0, 190 + bottom: 0, 191 + height: 230, 181 192 }, 182 193 progressIndicator: { 183 194 position: 'absolute',
+17 -4
package-lock.json
··· 14 14 "date-fns": "^4.1.0", 15 15 "expo": "~52.0.25", 16 16 "expo-av": "~15.0.2", 17 - "expo-blur": "~14.0.2", 17 + "expo-blur": "~14.0.3", 18 18 "expo-camera": "~16.0.16", 19 19 "expo-constants": "~17.0.4", 20 20 "expo-font": "~13.0.3", 21 21 "expo-haptics": "~14.0.1", 22 + "expo-linear-gradient": "~14.0.2", 22 23 "expo-linking": "~7.0.4", 23 24 "expo-media-library": "~17.0.5", 24 25 "expo-router": "~4.0.16", ··· 6435 6436 } 6436 6437 }, 6437 6438 "node_modules/expo-blur": { 6438 - "version": "14.0.2", 6439 - "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.2.tgz", 6440 - "integrity": "sha512-6ZStKz/7F3nWfmfdeAzhJeNAtxPQAetU1FQ742XHX9uEfZjhq00CrAjyZNx2+nXpE3tGFQtXyhEE5hQJwug8yQ==", 6439 + "version": "14.0.3", 6440 + "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz", 6441 + "integrity": "sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==", 6442 + "license": "MIT", 6441 6443 "peerDependencies": { 6442 6444 "expo": "*", 6443 6445 "react": "*", ··· 6516 6518 "peerDependencies": { 6517 6519 "expo": "*", 6518 6520 "react": "*" 6521 + } 6522 + }, 6523 + "node_modules/expo-linear-gradient": { 6524 + "version": "14.0.2", 6525 + "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz", 6526 + "integrity": "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ==", 6527 + "license": "MIT", 6528 + "peerDependencies": { 6529 + "expo": "*", 6530 + "react": "*", 6531 + "react-native": "*" 6519 6532 } 6520 6533 }, 6521 6534 "node_modules/expo-linking": {
+3 -2
package.json
··· 20 20 "@react-navigation/native": "^7.0.14", 21 21 "date-fns": "^4.1.0", 22 22 "expo": "~52.0.25", 23 - "expo-blur": "~14.0.2", 23 + "expo-blur": "~14.0.3", 24 24 "expo-camera": "~16.0.16", 25 25 "expo-constants": "~17.0.4", 26 26 "expo-font": "~13.0.3", ··· 45 45 "react-native-videoeditorsdk": "^3.3.0", 46 46 "react-native-web": "~0.19.13", 47 47 "react-native-webview": "13.12.5", 48 - "expo-av": "~15.0.2" 48 + "expo-av": "~15.0.2", 49 + "expo-linear-gradient": "~14.0.2" 49 50 }, 50 51 "devDependencies": { 51 52 "@babel/core": "^7.25.2",
+4
types/Interfaces.ts
··· 162 162 videoData: PostProps; 163 163 } 164 164 165 + export interface ImageScreenProps { 166 + imageData: PostProps; 167 + } 168 + 165 169 export interface VideoSideProps { 166 170 videoData: PostProps; 167 171 onComments?: () => void;