Add share bottom-sheet to feed and thread

This commit is contained in:
Paul Frazee 2022-07-25 18:31:42 -05:00
parent 3794eca88e
commit af55a89758
14 changed files with 574 additions and 34 deletions

View file

@ -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 (
<RootStoreProvider value={rootStore}>
<Routes.Root />
</RootStoreProvider>
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}>
<Routes.Root />
</RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>
)
}

View file

@ -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 (
<RootStoreProvider value={rootStore}>
<Routes.Root />
</RootStoreProvider>
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}>
<Routes.Root />
</RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>
)
}

View file

@ -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
},
}))

View file

@ -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}) => (
<FeedItem item={item} onNavigateContent={onNavigateContent} />
<FeedItem
item={item}
onNavigateContent={onNavigateContent}
onPressShare={onPressShare}
/>
)
const onRefresh = () => {
feed.refresh().catch(err => console.error('Failed to refresh', err))
@ -42,6 +52,7 @@ export const Feed = observer(function Feed({
/>
)}
{feed.isEmpty && <Text>This feed is empty!</Text>}
<ShareBottomSheet ref={shareSheetRef} />
</View>
)
})

View file

@ -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}
</Text>
</TouchableOpacity>
<View style={styles.ctrl}>
<TouchableOpacity
style={styles.ctrl}
onPress={() => onPressShare(item.uri)}>
<FontAwesomeIcon
style={styles.ctrlIcon}
icon="share-from-square"
/>
</View>
</TouchableOpacity>
</View>
</View>
</View>

View file

@ -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<PostThreadViewModel | undefined>()
const [lastUpdate, setLastUpdate] = useState<number>(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}) => (
<PostThreadItem item={item} onNavigateContent={onNavigateContent} />
<PostThreadItem
item={item}
onNavigateContent={onNavigateContent}
onPressShare={onPressShare}
/>
)
const onRefresh = () => {
view.refresh().catch(err => console.error('Failed to refresh', err))
}
return (
<View>
{view.hasContent && (
<FlatList
data={posts}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshing={view.isRefreshing}
onRefresh={onRefresh}
/>
)}
<View style={s.h100pct}>
<FlatList
data={posts}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshing={view.isRefreshing}
onRefresh={onRefresh}
/>
<ShareBottomSheet ref={shareSheetRef} />
</View>
)
})

View file

@ -21,9 +21,11 @@ function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
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}
</Text>
</TouchableOpacity>
<View style={styles.ctrl}>
<TouchableOpacity
style={styles.ctrl}
onPress={() => onPressShare(item.uri)}>
<FontAwesomeIcon
style={styles.ctrlIcon}
icon="share-from-square"
/>
</View>
</TouchableOpacity>
</View>
</View>
</View>

View file

@ -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<boolean>(false)
const [uri, setUri] = useState<string>('')
const bottomSheetRef = useRef<BottomSheet>(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 (
<TouchableWithoutFeedback onPress={onClose}>
<Animated.View style={containerStyle} />
</TouchableWithoutFeedback>
)
}
return (
<>
{isOpen && (
<BottomSheet
ref={bottomSheetRef}
snapPoints={['50%']}
enablePanDownToClose
backdropComponent={CustomBackdrop}
onChange={onShareBottomSheetChange}>
<View>
<Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
<Text style={[s.textCenter, s.mb10]}>{uri}</Text>
<Button title="Copy to clipboard" onPress={onPressCopy} />
<View style={s.p10}>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<Text style={s.textCenter}>Close</Text>
</TouchableOpacity>
</View>
</View>
</BottomSheet>
)}
</>
)
})
const styles = StyleSheet.create({
closeBtn: {
width: '100%',
borderColor: '#000',
borderWidth: 1,
borderRadius: 4,
padding: 10,
},
})

View file

@ -73,4 +73,13 @@ export const s = StyleSheet.create({
flexRow: {flexDirection: 'row'},
flexCol: {flexDirection: 'column'},
flex1: {flex: 1},
// dimensions
w100pct: {width: '100%'},
h100pct: {height: '100%'},
// text align
textLeft: {textAlign: 'left'},
textCenter: {textAlign: 'center'},
textRight: {textAlign: 'right'},
})