diff --git a/babel.config.js b/babel.config.js index e11e3a9d..c21f570c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -14,5 +14,6 @@ module.exports = { verbose: false, }, ], + 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last ], } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9d373af9..46f25c97 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -291,8 +291,39 @@ PODS: - React-perflogger (= 0.68.2) - RNCAsyncStorage (1.17.6): - React-Core + - RNCClipboard (1.10.0): + - React-Core + - RNGestureHandler (2.5.0): + - React-Core - RNInAppBrowser (3.6.3): - React-Core + - RNReanimated (2.9.1): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText + - ReactCommon/turbomodule/core + - Yoga - RNScreens (3.13.1): - React-Core - React-RCTImage @@ -335,7 +366,10 @@ DEPENDENCIES: - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`) + - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -409,8 +443,14 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-clipboard/clipboard" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" RNInAppBrowser: :path: "../node_modules/react-native-inappbrowser-reborn" + RNReanimated: + :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" RNSVG: @@ -452,7 +492,10 @@ SPEC CHECKSUMS: React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23 ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2 RNCAsyncStorage: 466b9df1a14bccda91da86e0b7d9a345d78e1673 + RNCClipboard: f1736c75ab85b627a4d57587edb4b60999c4dd80 + RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 RNInAppBrowser: 3ff3a3b8f458aaf25aaee879d057352862edf357 + RNReanimated: 5c8c17e26787fd8984cd5accdc70fef2ca70aafd RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19 RNSVG: 3dd44d99d1c18e1342aee4bfa53ab3f6a8c4865f Yoga: 99652481fcd320aefa4a7ef90095b95acd181952 diff --git a/package.json b/package.json index 703548e5..93363c35 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.0", + "@gorhom/bottom-sheet": "^4", "@react-native-async-storage/async-storage": "^1.17.6", + "@react-native-clipboard/clipboard": "^1.10.0", "@react-navigation/bottom-tabs": "^6.3.1", "@react-navigation/native": "^6.0.10", "@react-navigation/native-stack": "^6.6.2", @@ -34,8 +36,12 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-native": "0.68.2", + "react-native-gesture-handler": "^2.5.0", "react-native-inappbrowser-reborn": "^3.6.3", "react-native-progress": "^5.0.0", + "react-native-reanimated": "^2.9.1", + "react-native-root-siblings": "^4.1.1", + "react-native-root-toast": "^3.4.0", "react-native-safe-area-context": "^4.3.1", "react-native-screens": "^3.13.1", "react-native-svg": "^12.4.0", diff --git a/src/App.native.tsx b/src/App.native.tsx index ffeb7d5f..4309fa3c 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -1,5 +1,7 @@ import 'react-native-url-polyfill/auto' import React, {useState, useEffect} from 'react' +import {RootSiblingParent} from 'react-native-root-siblings' +import {GestureHandlerRootView} from 'react-native-gesture-handler' import {whenWebCrypto} from './platform/polyfills.native' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' @@ -26,9 +28,13 @@ function App() { } return ( - - - + + + + + + + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index 018ac400..838b81ee 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,4 +1,6 @@ import React, {useState, useEffect} from 'react' +import {RootSiblingParent} from 'react-native-root-siblings' +import {GestureHandlerRootView} from 'react-native-gesture-handler' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import * as Routes from './view/routes' @@ -20,9 +22,13 @@ function App() { } return ( - - - + + + + + + + ) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 5a6ad521..c7ce3f4c 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1,6 +1,7 @@ import React, {useState, forwardRef, useImperativeHandle} from 'react' import {observer} from 'mobx-react-lite' import {KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native' +import Toast from 'react-native-root-toast' // @ts-ignore no type definition -prf import ProgressCircle from 'react-native-progress/Circle' import {useStores} from '../../../state' @@ -37,6 +38,13 @@ export const Composer = observer( return false } await apilib.post(store.api, 'alice.com', text, replyTo) + Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been created`, { + duration: Toast.durations.LONG, + position: Toast.positions.TOP, + shadow: true, + animation: true, + hideOnPress: true, + }) return true }, })) diff --git a/src/view/com/feed/Feed.tsx b/src/view/com/feed/Feed.tsx index c666fc05..8283e275 100644 --- a/src/view/com/feed/Feed.tsx +++ b/src/view/com/feed/Feed.tsx @@ -1,9 +1,10 @@ -import React from 'react' +import React, {useRef} from 'react' import {observer} from 'mobx-react-lite' import {Text, View, FlatList} from 'react-native' import {OnNavigateContent} from '../../routes/types' import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view' import {FeedItem} from './FeedItem' +import {ShareBottomSheet} from '../sheets/SharePost' export const Feed = observer(function Feed({ feed, @@ -12,12 +13,21 @@ export const Feed = observer(function Feed({ feed: FeedViewModel onNavigateContent: OnNavigateContent }) { + const shareSheetRef = useRef<{open: (uri: string) => void}>() + + const onPressShare = (uri: string) => { + shareSheetRef.current?.open(uri) + } // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // VirtualizedList: You have a large list that is slow to update - make sure your // renderItem function renders components that follow React performance best practices // like PureComponent, shouldComponentUpdate, etc const renderItem = ({item}: {item: FeedViewItemModel}) => ( - + ) const onRefresh = () => { feed.refresh().catch(err => console.error('Failed to refresh', err)) @@ -42,6 +52,7 @@ export const Feed = observer(function Feed({ /> )} {feed.isEmpty && This feed is empty!} + ) }) diff --git a/src/view/com/feed/FeedItem.tsx b/src/view/com/feed/FeedItem.tsx index 9f3ec7c5..018b5817 100644 --- a/src/view/com/feed/FeedItem.tsx +++ b/src/view/com/feed/FeedItem.tsx @@ -12,9 +12,11 @@ import {AVIS} from '../../lib/assets' export const FeedItem = observer(function FeedItem({ item, onNavigateContent, + onPressShare, }: { item: FeedViewItemModel onNavigateContent: OnNavigateContent + onPressShare: (uri: string) => void }) { const record = item.record as unknown as bsky.Post.Record @@ -118,12 +120,14 @@ export const FeedItem = observer(function FeedItem({ {item.likeCount} - + onPressShare(item.uri)}> - + diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index bc9562ea..784cc39d 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useState, useEffect, useRef} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, Text, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' @@ -9,6 +9,8 @@ import { } from '../../../state/models/post-thread-view' import {useStores} from '../../../state' import {PostThreadItem} from './PostThreadItem' +import {ShareBottomSheet} from '../sheets/SharePost' +import {s} from '../../lib/styles' const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates @@ -22,6 +24,7 @@ export const PostThread = observer(function PostThread({ const store = useStores() const [view, setView] = useState() const [lastUpdate, setLastUpdate] = useState(Date.now()) + const shareSheetRef = useRef<{open: (uri: string) => void}>() useEffect(() => { if (view?.params.uri === uri) { @@ -41,6 +44,13 @@ export const PostThread = observer(function PostThread({ } }) + const onPressShare = (uri: string) => { + shareSheetRef.current?.open(uri) + } + const onRefresh = () => { + view?.refresh().catch(err => console.error('Failed to refresh', err)) + } + // loading // = if ( @@ -69,22 +79,22 @@ export const PostThread = observer(function PostThread({ // = const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( - + ) - const onRefresh = () => { - view.refresh().catch(err => console.error('Failed to refresh', err)) - } return ( - - {view.hasContent && ( - item._reactKey} - renderItem={renderItem} - refreshing={view.isRefreshing} - onRefresh={onRefresh} - /> - )} + + item._reactKey} + renderItem={renderItem} + refreshing={view.isRefreshing} + onRefresh={onRefresh} + /> + ) }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index bd22ecf9..30a64bc0 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -21,9 +21,11 @@ function iter(n: number, fn: (_i: number) => T): Array { export const PostThreadItem = observer(function PostThreadItem({ item, onNavigateContent, + onPressShare, }: { item: PostThreadViewPostModel onNavigateContent: OnNavigateContent + onPressShare: (uri: string) => void }) { const record = item.record as unknown as bsky.Post.Record const hasEngagement = item.likeCount || item.repostCount @@ -169,12 +171,14 @@ export const PostThreadItem = observer(function PostThreadItem({ {item.likeCount} - + onPressShare(item.uri)}> - + diff --git a/src/view/com/sheets/SharePost.tsx b/src/view/com/sheets/SharePost.tsx new file mode 100644 index 00000000..b0f22c54 --- /dev/null +++ b/src/view/com/sheets/SharePost.tsx @@ -0,0 +1,114 @@ +import React, { + forwardRef, + useState, + useMemo, + useImperativeHandle, + useRef, +} from 'react' +import { + Button, + StyleSheet, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native' +import BottomSheet, {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' +import Animated, { + Extrapolate, + interpolate, + useAnimatedStyle, +} from 'react-native-reanimated' +import Toast from 'react-native-root-toast' +import Clipboard from '@react-native-clipboard/clipboard' +import {s} from '../../lib/styles' + +export const ShareBottomSheet = forwardRef(function ShareBottomSheet( + {}: {}, + ref, +) { + const [isOpen, setIsOpen] = useState(false) + const [uri, setUri] = useState('') + const bottomSheetRef = useRef(null) + + useImperativeHandle(ref, () => ({ + open(uri: string) { + console.log('sharing', uri) + setUri(uri) + setIsOpen(true) + }, + })) + + const onPressCopy = () => { + Clipboard.setString(uri) + Toast.show('Link copied', { + position: Toast.positions.TOP, + }) + } + const onShareBottomSheetChange = (snapPoint: number) => { + if (snapPoint === -1) { + console.log('unsharing') + setIsOpen(false) + } + } + const onClose = () => { + bottomSheetRef.current?.close() + } + + const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { + console.log('hit!', animatedIndex.value) + // animated variables + const opacity = useAnimatedStyle(() => ({ + opacity: interpolate( + animatedIndex.value, // current snap index + [-1, 0], // input range + [0, 0.5], // output range + Extrapolate.CLAMP, + ), + })) + + const containerStyle = useMemo( + () => [style, {backgroundColor: '#000'}, opacity], + [style, opacity], + ) + + return ( + + + + ) + } + return ( + <> + {isOpen && ( + + + Share this post + {uri} +