this repo has no description
0
fork

Configure Feed

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

rewrite image viewer

+203 -65
+3 -1
apps/expo/package.json
··· 37 37 "react": "18.2.0", 38 38 "react-dom": "18.2.0", 39 39 "react-native": "0.71.7", 40 - "react-native-reanimated": "^3.1.0", 40 + "react-native-gesture-handler": "~2.9.0", 41 + "react-native-reanimated": "^2.14.4", 42 + "react-native-redash": "^18.1.0", 41 43 "react-native-safe-area-context": "4.5.1", 42 44 "react-native-screens": "~3.20.0", 43 45 "react-native-svg": "^13.9.0",
+2
apps/expo/src/app/_layout.tsx
··· 109 109 <QueryClientProvider client={queryClient}> 110 110 <SafeAreaProvider> 111 111 <AgentProvider value={agent}> 112 + <StatusBar style="dark" /> 112 113 {loading && <SplashScreen />} 113 114 <Stack 114 115 screenOptions={{ 115 116 headerShown: true, 116 117 headerBackTitle: "", 118 + fullScreenGestureEnabled: true, 117 119 headerStyle: { 118 120 backgroundColor: "#fff", 119 121 },
+24 -18
apps/expo/src/app/images/[post].tsx
··· 1 1 import { Text, TextInput, TouchableOpacity, View } from "react-native"; 2 - import { SafeAreaView } from "react-native-safe-area-context"; 2 + import { 3 + SafeAreaView, 4 + useSafeAreaInsets, 5 + } from "react-native-safe-area-context"; 3 6 import { Stack, useLocalSearchParams, useRouter } from "expo-router"; 7 + import { StatusBar } from "expo-status-bar"; 4 8 import { AppBskyEmbedImages, AppBskyFeedDefs } from "@atproto/api"; 5 9 import { useQuery } from "@tanstack/react-query"; 6 10 import { X } from "lucide-react-native"; ··· 49 53 router.back(); 50 54 } 51 55 56 + const { top } = useSafeAreaInsets(); 57 + 52 58 return ( 53 - <SafeAreaView className="relative flex-1 bg-black"> 59 + <View className="relative flex-1 bg-black"> 60 + <StatusBar style="light" /> 54 61 <Stack.Screen 55 62 options={{ 56 - animation: "fade_from_bottom", 57 - customAnimationOnGesture: true, 63 + animation: "fade", 64 + fullScreenGestureEnabled: false, 58 65 headerShown: false, 59 66 }} 60 67 /> 61 - <View className="flex-1"> 62 - <TouchableOpacity 63 - onPress={() => router.back()} 64 - className="absolute right-5 top-5 z-10 h-10 w-10 items-center justify-center rounded-full bg-black/40" 65 - > 66 - <X color="#ffffff" /> 67 - </TouchableOpacity> 68 - <ImageViewer 69 - images={images.data ?? []} 70 - onClose={() => router.back()} 71 - initialIndex={Number(initial) || 0} 72 - /> 73 - </View> 74 - </SafeAreaView> 68 + <TouchableOpacity 69 + onPress={() => router.back()} 70 + className="absolute right-5 z-10 h-10 w-10 items-center justify-center rounded-full bg-black/40" 71 + style={{ top: top + 10 }} 72 + > 73 + <X color="#ffffff" /> 74 + </TouchableOpacity> 75 + <ImageViewer 76 + images={images.data ?? []} 77 + onClose={() => router.back()} 78 + initialIndex={Number(initial) || 0} 79 + /> 80 + </View> 75 81 ); 76 82 }
+1 -1
apps/expo/src/app/index.tsx
··· 17 17 return ( 18 18 <View className="flex-1"> 19 19 <Stack.Screen options={{ headerShown: false }} /> 20 - <StatusBar style="light" /> 20 + <StatusBar style="dark" /> 21 21 <ImageBackground className="flex-1" source={background}> 22 22 <SafeAreaView className="flex-1 items-stretch justify-between p-4"> 23 23 <Text className="mx-auto mt-16 text-6xl font-bold text-white">
+6 -2
apps/expo/src/app/profile/[handle]/post/[id].tsx
··· 109 109 110 110 // hacky but needed until https://github.com/Shopify/flash-list/issues/671 is fixed 111 111 useEffect(() => { 112 - if (thread.data) { 112 + if (thread.data && !hasScrolled.current) { 113 113 const index = thread.data.index; 114 114 hasScrolled.current = true; 115 115 setTimeout(() => { ··· 146 146 ref={ref} 147 147 data={thread.data.posts} 148 148 estimatedItemSize={91} 149 + refreshing={thread.isRefetching} 150 + onRefresh={() => { 151 + if (!thread.isRefetching) thread.refetch(); 152 + }} 153 + ListFooterComponent={<View className="h-20" />} 149 154 getItemType={(item) => (item.primary ? "big" : "small")} 150 155 renderItem={({ item, index }) => 151 156 item.primary ? ( ··· 158 163 /> 159 164 ) 160 165 } 161 - ListFooterComponent={<View className="h-screen" />} 162 166 /> 163 167 </> 164 168 );
+1 -1
apps/expo/src/components/feed-post.tsx
··· 114 114 )} 115 115 {/* text content */} 116 116 <Link href={postHref} asChild> 117 - <Pressable> 117 + <Pressable className="my-0.5"> 118 118 <RichText value={item.post.record.text} /> 119 119 </Pressable> 120 120 </Link>
+108 -19
apps/expo/src/components/image-viewer.tsx
··· 1 - import { useEffect, useRef } from "react"; 2 - import { Dimensions, Image, ScrollView, View } from "react-native"; 1 + import { Dimensions, Image, StyleSheet } from "react-native"; 2 + import { 3 + PanGestureHandler, 4 + PanGestureHandlerGestureEvent, 5 + } from "react-native-gesture-handler"; 6 + import Animated, { 7 + Extrapolate, 8 + interpolate, 9 + runOnJS, 10 + useAnimatedGestureHandler, 11 + useAnimatedStyle, 12 + useSharedValue, 13 + withDecay, 14 + withSpring, 15 + } from "react-native-reanimated"; 16 + import { snapPoint } from "react-native-redash"; 3 17 import { AppBskyEmbedImages } from "@atproto/api"; 4 18 5 19 const { width, height } = Dimensions.get("screen"); ··· 11 25 } 12 26 13 27 export const ImageViewer = ({ images, initialIndex = 0, onClose }: Props) => { 14 - return ( 15 - <ScrollView 16 - horizontal 17 - snapToInterval={width} 18 - decelerationRate="fast" 19 - contentOffset={{ x: initialIndex * width, y: 0 }} 20 - > 21 - {images.map((image, i) => { 28 + const snapPointsX = images.map((_, i) => i * -width); 29 + const snapPointsY = [height * 1.5, 0, -height * 1.5]; 30 + 31 + const x = useSharedValue(initialIndex * -width); 32 + const y = useSharedValue(0); 33 + 34 + const delayedClose = () => setTimeout(() => onClose(), 200); 35 + 36 + const gestureHandler = useAnimatedGestureHandler< 37 + PanGestureHandlerGestureEvent, 38 + { 39 + startX: number; 40 + } 41 + >({ 42 + onStart: (_, ctx) => { 43 + ctx.startX = x.value; 44 + }, 45 + onActive: (event, ctx) => { 46 + x.value = ctx.startX + event.translationX; 47 + y.value = event.translationY; 48 + }, 49 + onEnd: (event) => { 50 + const closestInitialIndex = Math.round(x.value / -width); 51 + const snapPointsEitherSide = snapPointsX.filter((_, i) => { 22 52 return ( 23 - <View key={i} className="w-screen flex-1"> 24 - <Image 25 - source={{ uri: image.fullsize }} 26 - className="flex-1" 27 - resizeMode="contain" 28 - /> 29 - </View> 53 + i === closestInitialIndex || 54 + i === closestInitialIndex + 1 || 55 + i === closestInitialIndex - 1 30 56 ); 31 - })} 32 - </ScrollView> 57 + }); 58 + x.value = withSpring( 59 + snapPoint(x.value, event.velocityX, snapPointsEitherSide), 60 + { 61 + damping: 20, 62 + mass: 0.5, 63 + }, 64 + ); 65 + const yDest = snapPoint(y.value, event.velocityY, snapPointsY); 66 + y.value = withSpring(yDest, { 67 + damping: 20, 68 + mass: 2, 69 + }); 70 + if (yDest !== 0) { 71 + runOnJS(delayedClose)(); 72 + } 73 + }, 74 + }); 75 + 76 + const animatedContainerStyle = useAnimatedStyle(() => { 77 + return { 78 + transform: [{ translateX: x.value }], 79 + }; 80 + }); 81 + 82 + const animatedImageStyle = useAnimatedStyle(() => { 83 + return { 84 + transform: [ 85 + { translateY: y.value }, 86 + { scale: 1 - Math.abs(y.value) / height }, 87 + ], 88 + opacity: 1 - Math.abs(y.value) / height, 89 + }; 90 + }); 91 + 92 + return ( 93 + <PanGestureHandler onGestureEvent={gestureHandler}> 94 + <Animated.View style={[styles.container, animatedContainerStyle]}> 95 + {images.map((image, i) => { 96 + return ( 97 + <Animated.View 98 + key={`${image.fullsize}-${i}`} 99 + style={[styles.image, animatedImageStyle]} 100 + > 101 + <Image 102 + source={{ uri: image.fullsize }} 103 + className="flex-1" 104 + resizeMode="contain" 105 + /> 106 + </Animated.View> 107 + ); 108 + })} 109 + </Animated.View> 110 + </PanGestureHandler> 33 111 ); 34 112 }; 113 + 114 + const styles = StyleSheet.create({ 115 + container: { 116 + flex: 1, 117 + flexDirection: "row", 118 + }, 119 + image: { 120 + width, 121 + height, 122 + }, 123 + });
+2 -1
apps/expo/src/components/rich-text.tsx
··· 75 75 76 76 return ( 77 77 <Text 78 - className={cx("text-base", { 78 + className={cx({ 79 79 "text-sm": size === "sm", 80 + "text-base leading-[22px]": size === "base", 80 81 "text-lg leading-6": size === "lg", 81 82 })} 82 83 >
+56 -22
pnpm-lock.yaml
··· 75 75 version: 4.0.1(expo@48.0.11) 76 76 expo-router: 77 77 specifier: ^1.5.3 78 - version: 1.5.3(expo-constants@14.2.1)(expo-linking@4.0.1)(expo-modules-autolinking@1.2.0)(expo-status-bar@1.4.4)(expo@48.0.11)(metro@0.76.2)(react-dom@18.2.0)(react-native-gesture-handler@2.9.0)(react-native-reanimated@3.1.0)(react-native-safe-area-context@4.5.1)(react-native-screens@3.20.0)(react-native@0.71.7)(react@18.2.0) 78 + version: 1.5.3(expo-constants@14.2.1)(expo-linking@4.0.1)(expo-modules-autolinking@1.2.0)(expo-status-bar@1.4.4)(expo@48.0.11)(metro@0.76.2)(react-dom@18.2.0)(react-native-gesture-handler@2.9.0)(react-native-reanimated@2.14.4)(react-native-safe-area-context@4.5.1)(react-native-screens@3.20.0)(react-native@0.71.7)(react@18.2.0) 79 79 expo-splash-screen: 80 80 specifier: ~0.18.1 81 81 version: 0.18.1(expo-modules-autolinking@1.2.0)(expo@48.0.11) ··· 106 106 react-native: 107 107 specifier: 0.71.7 108 108 version: 0.71.7(@babel/core@7.21.4)(@babel/preset-env@7.21.4)(react@18.2.0) 109 + react-native-gesture-handler: 110 + specifier: ~2.9.0 111 + version: 2.9.0(react-native@0.71.7)(react@18.2.0) 109 112 react-native-reanimated: 110 - specifier: ^3.1.0 111 - version: 3.1.0(@babel/core@7.21.4)(@babel/plugin-proposal-nullish-coalescing-operator@7.18.6)(@babel/plugin-proposal-optional-chaining@7.21.0)(@babel/plugin-transform-arrow-functions@7.20.7)(@babel/plugin-transform-shorthand-properties@7.18.6)(@babel/plugin-transform-template-literals@7.18.9)(react-native@0.71.7)(react@18.2.0) 113 + specifier: ^2.14.4 114 + version: 2.14.4(@babel/core@7.21.4)(react-native@0.71.7)(react@18.2.0) 115 + react-native-redash: 116 + specifier: ^18.1.0 117 + version: 18.1.0(react-native-gesture-handler@2.9.0)(react-native-reanimated@2.14.4)(react-native@0.71.7)(react@18.2.0) 112 118 react-native-safe-area-context: 113 119 specifier: 4.5.1 114 120 version: 4.5.1(react-native@0.71.7)(react@18.2.0) ··· 3179 3185 event-target-shim: 5.0.1 3180 3186 dev: false 3181 3187 3188 + /abs-svg-path@0.1.1: 3189 + resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} 3190 + dev: false 3191 + 3182 3192 /absolute-path@0.0.0: 3183 3193 resolution: {integrity: sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA==} 3184 3194 dev: false ··· 4167 4177 4168 4178 /convert-source-map@1.9.0: 4169 4179 resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} 4170 - 4171 - /convert-source-map@2.0.0: 4172 - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 4173 - dev: false 4174 4180 4175 4181 /copy-anything@3.0.3: 4176 4182 resolution: {integrity: sha512-fpW2W/BqEzqPp29QS+MwwfisHCQZtiduTe/m8idFo0xbti9fIZ2WVhAsCv4ggFVH3AgCkVdpoOCtQC6gBrdhjw==} ··· 5256 5262 invariant: 2.2.4 5257 5263 dev: false 5258 5264 5259 - /expo-router@1.5.3(expo-constants@14.2.1)(expo-linking@4.0.1)(expo-modules-autolinking@1.2.0)(expo-status-bar@1.4.4)(expo@48.0.11)(metro@0.76.2)(react-dom@18.2.0)(react-native-gesture-handler@2.9.0)(react-native-reanimated@3.1.0)(react-native-safe-area-context@4.5.1)(react-native-screens@3.20.0)(react-native@0.71.7)(react@18.2.0): 5265 + /expo-router@1.5.3(expo-constants@14.2.1)(expo-linking@4.0.1)(expo-modules-autolinking@1.2.0)(expo-status-bar@1.4.4)(expo@48.0.11)(metro@0.76.2)(react-dom@18.2.0)(react-native-gesture-handler@2.9.0)(react-native-reanimated@2.14.4)(react-native-safe-area-context@4.5.1)(react-native-screens@3.20.0)(react-native@0.71.7)(react@18.2.0): 5260 5266 resolution: {integrity: sha512-rZEoRpXjXpfcx549/MI7YRitaBGFOHpIGLO+cb18ecsShl3PzGPIDaBGMnTo0m1h7ip0sAIQg1EFrSAtM4LXLA==} 5261 5267 peerDependencies: 5262 5268 '@react-navigation/drawer': ^6.5.8 ··· 5290 5296 query-string: 7.1.3 5291 5297 react-helmet-async: 1.3.0(react-dom@18.2.0)(react@18.2.0) 5292 5298 react-native-gesture-handler: 2.9.0(react-native@0.71.7)(react@18.2.0) 5293 - react-native-reanimated: 3.1.0(@babel/core@7.21.4)(@babel/plugin-proposal-nullish-coalescing-operator@7.18.6)(@babel/plugin-proposal-optional-chaining@7.21.0)(@babel/plugin-transform-arrow-functions@7.20.7)(@babel/plugin-transform-shorthand-properties@7.18.6)(@babel/plugin-transform-template-literals@7.18.9)(react-native@0.71.7)(react@18.2.0) 5299 + react-native-reanimated: 2.14.4(@babel/core@7.21.4)(react-native@0.71.7)(react@18.2.0) 5294 5300 react-native-safe-area-context: 4.5.1(react-native@0.71.7)(react@18.2.0) 5295 5301 react-native-screens: 3.20.0(react-native@0.71.7)(react@18.2.0) 5296 5302 url: 0.11.0 ··· 7992 7998 engines: {node: '>=0.10.0'} 7993 7999 dev: true 7994 8000 8001 + /normalize-svg-path@1.1.0: 8002 + resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} 8003 + dependencies: 8004 + svg-arc-to-cubic-bezier: 3.2.0 8005 + dev: false 8006 + 7995 8007 /normalize-url@4.5.1: 7996 8008 resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} 7997 8009 engines: {node: '>=8'} ··· 8332 8344 engines: {node: '>=10'} 8333 8345 dependencies: 8334 8346 pngjs: 3.4.0 8347 + dev: false 8348 + 8349 + /parse-svg-path@0.1.2: 8350 + resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} 8335 8351 dev: false 8336 8352 8337 8353 /parseurl@1.3.3: ··· 8934 8950 resolution: {integrity: sha512-OXXYgpISEqERwjSlaCiaQY6cTY5CH6j73gdkWpK0hedxtiWMWgH+i5TOi4hIGYitm9kQBeyDu+wim9fA8ROFJA==} 8935 8951 dev: false 8936 8952 8937 - /react-native-reanimated@3.1.0(@babel/core@7.21.4)(@babel/plugin-proposal-nullish-coalescing-operator@7.18.6)(@babel/plugin-proposal-optional-chaining@7.21.0)(@babel/plugin-transform-arrow-functions@7.20.7)(@babel/plugin-transform-shorthand-properties@7.18.6)(@babel/plugin-transform-template-literals@7.18.9)(react-native@0.71.7)(react@18.2.0): 8938 - resolution: {integrity: sha512-8YJR7yHnrqK6yKWzkGLVEawi1WZqJ9bGIehKEnE8zG58yLrSwUZe1T220XTbftpkA3r37Sy0kJJ/HOOiaIU+HQ==} 8953 + /react-native-reanimated@2.14.4(@babel/core@7.21.4)(react-native@0.71.7)(react@18.2.0): 8954 + resolution: {integrity: sha512-DquSbl7P8j4SAmc+kRdd75Ianm8G+IYQ9T4AQ6lrpLVeDkhZmjWI0wkutKWnp6L7c5XNVUrFDUf69dwETLCItQ==} 8939 8955 peerDependencies: 8940 8956 '@babel/core': ^7.0.0-0 8941 - '@babel/plugin-proposal-nullish-coalescing-operator': ^7.0.0-0 8942 - '@babel/plugin-proposal-optional-chaining': ^7.0.0-0 8943 - '@babel/plugin-transform-arrow-functions': ^7.0.0-0 8944 - '@babel/plugin-transform-shorthand-properties': ^7.0.0-0 8945 - '@babel/plugin-transform-template-literals': ^7.0.0-0 8946 8957 react: '*' 8947 8958 react-native: '*' 8948 8959 dependencies: 8949 8960 '@babel/core': 7.21.4 8950 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.4) 8951 - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.4) 8952 - '@babel/plugin-transform-arrow-functions': 7.20.7(@babel/core@7.21.4) 8953 8961 '@babel/plugin-transform-object-assign': 7.18.6(@babel/core@7.21.4) 8954 - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.21.4) 8955 - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.21.4) 8956 8962 '@babel/preset-typescript': 7.21.4(@babel/core@7.21.4) 8957 - convert-source-map: 2.0.0 8963 + convert-source-map: 1.9.0 8958 8964 invariant: 2.2.4 8965 + lodash.isequal: 4.5.0 8959 8966 react: 18.2.0 8960 8967 react-native: 0.71.7(@babel/core@7.21.4)(@babel/preset-env@7.21.4)(react@18.2.0) 8968 + setimmediate: 1.0.5 8969 + string-hash-64: 1.0.3 8961 8970 transitivePeerDependencies: 8962 8971 - supports-color 8972 + dev: false 8973 + 8974 + /react-native-redash@18.1.0(react-native-gesture-handler@2.9.0)(react-native-reanimated@2.14.4)(react-native@0.71.7)(react@18.2.0): 8975 + resolution: {integrity: sha512-bdCFl/ZB7Rf2raIlU6SLV+Dc/rL6UXsQNjEVwTGBukHMeSKp1zs4zVtWaGimbN8P22N4qYvb9Jmw/K94ZWYG0Q==} 8976 + peerDependencies: 8977 + react: '*' 8978 + react-native: '*' 8979 + react-native-gesture-handler: '*' 8980 + react-native-reanimated: '>=2.0.0' 8981 + dependencies: 8982 + abs-svg-path: 0.1.1 8983 + normalize-svg-path: 1.1.0 8984 + parse-svg-path: 0.1.2 8985 + react: 18.2.0 8986 + react-native: 0.71.7(@babel/core@7.21.4)(@babel/preset-env@7.21.4)(react@18.2.0) 8987 + react-native-gesture-handler: 2.9.0(react-native@0.71.7)(react@18.2.0) 8988 + react-native-reanimated: 2.14.4(@babel/core@7.21.4)(react-native@0.71.7)(react@18.2.0) 8963 8989 dev: false 8964 8990 8965 8991 /react-native-safe-area-context@4.5.1(react-native@0.71.7)(react@18.2.0): ··· 9791 9817 engines: {node: '>=4'} 9792 9818 dev: false 9793 9819 9820 + /string-hash-64@1.0.3: 9821 + resolution: {integrity: sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==} 9822 + dev: false 9823 + 9794 9824 /string-width@4.2.3: 9795 9825 resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 9796 9826 engines: {node: '>=8'} ··· 9968 9998 /supports-preserve-symlinks-flag@1.0.0: 9969 9999 resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 9970 10000 engines: {node: '>= 0.4'} 10001 + 10002 + /svg-arc-to-cubic-bezier@3.2.0: 10003 + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} 10004 + dev: false 9971 10005 9972 10006 /synckit@0.8.5: 9973 10007 resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==}