diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 4 | ||||
-rw-r--r-- | src/lib/hooks/useCustomFeed.ts | 4 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 2 | ||||
-rw-r--r-- | src/view/com/feeds/CustomFeed.tsx | 3 | ||||
-rw-r--r-- | src/view/screens/CustomFeed.tsx | 164 | ||||
-rw-r--r-- | src/view/screens/ProfileCustomFeed.tsx | 291 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 5 |
8 files changed, 300 insertions, 177 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 4521068f2..025020afa 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -40,6 +40,7 @@ import {SettingsScreen} from './view/screens/Settings' import {ProfileScreen} from './view/screens/Profile' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowsScreen} from './view/screens/ProfileFollows' +import {ProfileCustomFeed} from './view/screens/ProfileCustomFeed' import {ProfileListScreen} from './view/screens/ProfileList' import {PostThreadScreen} from './view/screens/PostThread' import {PostLikedByScreen} from './view/screens/PostLikedBy' @@ -56,7 +57,6 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' import {SavedFeeds} from './view/screens/SavedFeeds' -import {CustomFeed} from './view/screens/CustomFeed' import {PinnedFeeds} from 'view/screens/PinnedFeeds' import {bskyTitle} from 'lib/strings/headings' @@ -127,6 +127,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { title: title(`People followed by @${route.params.name}`), })} /> + <Stack.Screen name="ProfileCustomFeed" component={ProfileCustomFeed} /> <Stack.Screen name="ProfileList" component={ProfileListScreen} @@ -189,7 +190,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> <Stack.Screen name="SavedFeeds" component={SavedFeeds} /> <Stack.Screen name="PinnedFeeds" component={PinnedFeeds} /> - <Stack.Screen name="CustomFeed" component={CustomFeed} /> </> ) } diff --git a/src/lib/hooks/useCustomFeed.ts b/src/lib/hooks/useCustomFeed.ts index ee40cf49e..d7a27050d 100644 --- a/src/lib/hooks/useCustomFeed.ts +++ b/src/lib/hooks/useCustomFeed.ts @@ -2,9 +2,9 @@ import {useEffect, useState} from 'react' import {useStores} from 'state/index' import {CustomFeedModel} from 'state/models/feeds/custom-feed' -export function useCustomFeed(uri: string) { +export function useCustomFeed(uri: string): CustomFeedModel | undefined { const store = useStores() - const [item, setItem] = useState<CustomFeedModel>() + const [item, setItem] = useState<CustomFeedModel | undefined>() useEffect(() => { async function fetchView() { const res = await store.agent.app.bsky.feed.getFeedGenerator({ diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 8b96aaad7..52d0e9af2 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -13,11 +13,11 @@ export type CommonNavigatorParams = { Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} ProfileFollows: {name: string} + ProfileCustomFeed: {name: string; rkey: string} ProfileList: {name: string; rkey: string} PostThread: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string} PostRepostedBy: {name: string; rkey: string} - CustomFeed: {name: string; rkey: string; displayName?: string} Debug: undefined Log: undefined Support: undefined diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index 8e1a78453..5a93020a0 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -40,10 +40,9 @@ export const CustomFeed = observer( accessibilityRole="button" style={[styles.container, pal.border, style]} onPress={() => { - navigation.navigate('CustomFeed', { + navigation.navigate('ProfileCustomFeed', { name: item.data.creator.did, rkey: new AtUri(item.data.uri).rkey, - displayName: item.displayName, }) }} key={item.data.uri}> diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx deleted file mode 100644 index 76125fa5c..000000000 --- a/src/view/screens/CustomFeed.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {usePalette} from 'lib/hooks/usePalette' -import {HeartIcon, HeartIconSolid} from 'lib/icons' -import {CommonNavigatorParams} from 'lib/routes/types' -import {makeRecordUri} from 'lib/strings/url-helpers' -import {colors, s} from 'lib/styles' -import {observer} from 'mobx-react-lite' -import React, {useMemo, useRef} from 'react' -import {FlatList, StyleSheet, TouchableOpacity, View} from 'react-native' -import {useStores} from 'state/index' -import {PostsFeedModel} from 'state/models/feeds/posts' -import {useCustomFeed} from 'lib/hooks/useCustomFeed' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {Feed} from 'view/com/posts/Feed' -import {Link} from 'view/com/util/Link' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {Button} from 'view/com/util/forms/Button' -import {Text} from 'view/com/util/text/Text' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'> -export const CustomFeed = withAuthRequired( - observer(({route}: Props) => { - const rootStore = useStores() - const {rkey, name, displayName} = route.params - const uri = useMemo( - () => makeRecordUri(name, 'app.bsky.feed.generator', rkey), - [rkey, name], - ) - const currentFeed = useCustomFeed(uri) - const scrollElRef = useRef<FlatList>(null) - const algoFeed: PostsFeedModel = useMemo(() => { - const feed = new PostsFeedModel(rootStore, 'custom', { - feed: uri, - }) - feed.setup() - return feed - }, [rootStore, uri]) - - return ( - <View style={[styles.container]}> - <ViewHeader - title={ - displayName ?? `${currentFeed?.data.creator.displayName}'s feed` - } - showOnDesktop - /> - <Feed - scrollElRef={scrollElRef} - testID={'test-feed'} - key="default" - feed={algoFeed} - headerOffset={12} - ListHeaderComponent={() => <ListHeaderComponent uri={uri} />} - extraData={uri} - /> - </View> - ) - }), -) - -const ListHeaderComponent = observer(({uri}: {uri: string}) => { - const currentFeed = useCustomFeed(uri) - const pal = usePalette('default') - const rootStore = useStores() - return ( - <View style={[styles.headerContainer]}> - <View style={[styles.header]}> - <View style={styles.avatarContainer}> - <UserAvatar - type="algo" - size={28} - avatar={currentFeed?.data.creator.avatar} - /> - <Link href={`/profile/${currentFeed?.data.creator.handle}`}> - <Text style={[pal.textLight]}> - @{currentFeed?.data.creator.handle} - </Text> - </Link> - </View> - <Text style={[pal.text]}>{currentFeed?.data.description}</Text> - </View> - - <View style={[styles.buttonsContainer]}> - <Button - type={currentFeed?.isSaved ? 'default' : 'inverted'} - style={[styles.saveButton]} - onPress={() => { - if (currentFeed?.data.viewer?.saved) { - rootStore.me.savedFeeds.unsave(currentFeed!) - } else { - rootStore.me.savedFeeds.save(currentFeed!) - } - }} - label={currentFeed?.data.viewer?.saved ? 'Unsave' : 'Save'} - /> - - <TouchableOpacity - accessibilityRole="button" - onPress={() => { - if (currentFeed?.isLiked) { - currentFeed?.unlike() - } else { - currentFeed?.like() - } - }} - style={[styles.likeButton, pal.viewLight]}> - <Text style={[pal.text, s.semiBold]}> - {currentFeed?.data.likeCount} - </Text> - {currentFeed?.isLiked ? ( - <HeartIconSolid size={18} style={styles.liked} /> - ) : ( - <HeartIcon strokeWidth={3} size={18} style={styles.liked} /> - )} - </TouchableOpacity> - </View> - </View> - ) -}) - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - headerContainer: { - alignItems: 'center', - justifyContent: 'center', - gap: 8, - marginBottom: 12, - }, - header: { - alignItems: 'center', - gap: 4, - }, - avatarContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - buttonsContainer: { - flexDirection: 'row', - gap: 8, - }, - saveButton: { - minWidth: 100, - alignItems: 'center', - }, - liked: { - color: colors.red3, - }, - notLiked: { - color: colors.gray3, - }, - likeButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 4, - paddingHorizontal: 8, - borderRadius: 24, - gap: 4, - }, -}) diff --git a/src/view/screens/ProfileCustomFeed.tsx b/src/view/screens/ProfileCustomFeed.tsx new file mode 100644 index 000000000..1113ebf01 --- /dev/null +++ b/src/view/screens/ProfileCustomFeed.tsx @@ -0,0 +1,291 @@ +import React, {useMemo, useRef} from 'react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {usePalette} from 'lib/hooks/usePalette' +import {HeartIcon, HeartIconSolid} from 'lib/icons' +import {CommonNavigatorParams} from 'lib/routes/types' +import {makeRecordUri} from 'lib/strings/url-helpers' +import {colors, s} from 'lib/styles' +import {observer} from 'mobx-react-lite' +import {FlatList, StyleSheet, View} from 'react-native' +import {useStores} from 'state/index' +import {PostsFeedModel} from 'state/models/feeds/posts' +import {useCustomFeed} from 'lib/hooks/useCustomFeed' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {Feed} from 'view/com/posts/Feed' +import {pluralize} from 'lib/strings/helpers' +import {TextLink} from 'view/com/util/Link' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {Button} from 'view/com/util/forms/Button' +import {Text} from 'view/com/util/text/Text' +import * as Toast from 'view/com/util/Toast' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileCustomFeed'> +export const ProfileCustomFeed = withAuthRequired( + observer(({route}: Props) => { + const store = useStores() + const pal = usePalette('default') + const {rkey, name} = route.params + const uri = useMemo( + () => makeRecordUri(name, 'app.bsky.feed.generator', rkey), + [rkey, name], + ) + const scrollElRef = useRef<FlatList>(null) + const currentFeed = useCustomFeed(uri) + const algoFeed: PostsFeedModel = useMemo(() => { + const feed = new PostsFeedModel(store, 'custom', { + feed: uri, + }) + feed.setup() + return feed + }, [store, uri]) + + const onToggleSaved = React.useCallback(async () => { + try { + if (currentFeed.isSaved) { + await currentFeed.unsave() + } else { + await currentFeed.save() + } + } catch (err) { + Toast.show( + 'There was an an issue updating your feeds, please check your internet connection and try again.', + ) + store.log.error('Failed up update feeds', {err}) + } + }, [store, currentFeed]) + + const onToggleLiked = React.useCallback(async () => { + try { + if (currentFeed.isLiked) { + await currentFeed.unlike() + } else { + await currentFeed.like() + } + } catch (err) { + Toast.show( + 'There was an an issue contacting the server, please check your internet connection and try again.', + ) + store.log.error('Failed up toggle like', {err}) + } + }, [store, currentFeed]) + + const renderHeaderBtns = React.useCallback(() => { + return ( + <View style={styles.headerBtns}> + <Button + type="default" + testID="toggleLikeBtn" + accessibilityLabel="Like this feed" + accessibilityHint="" + onPress={onToggleLiked}> + {currentFeed?.isLiked ? ( + <HeartIconSolid size={18} style={styles.liked} /> + ) : ( + <HeartIcon strokeWidth={3} size={18} style={pal.textLight} /> + )} + </Button> + <Button + type={currentFeed?.isSaved ? 'default' : 'inverted'} + onPress={onToggleSaved} + accessibilityLabel={ + currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed' + } + accessibilityHint="" + label={currentFeed?.isSaved ? 'Unsave' : 'Save'} + /> + </View> + ) + }, [ + pal, + currentFeed?.isSaved, + currentFeed?.isLiked, + onToggleSaved, + onToggleLiked, + ]) + + const renderListHeaderComponent = React.useCallback(() => { + return ( + <> + <View style={[styles.header, pal.border]}> + <View style={s.flex1}> + <Text + testID="feedName" + type="title-xl" + style={[pal.text, s.bold]}> + {currentFeed?.displayName} + </Text> + {currentFeed && ( + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + by{' '} + {currentFeed.data.creator.did === store.me.did ? ( + 'you' + ) : ( + <TextLink + text={`@${currentFeed.data.creator.handle}`} + href={`/profile/${currentFeed.data.creator.did}`} + /> + )} + </Text> + )} + </View> + <View> + <UserAvatar + type="algo" + avatar={currentFeed?.data.avatar} + size={64} + /> + </View> + </View> + <View style={styles.headerDetails}> + {currentFeed?.data.description ? ( + <Text style={[pal.text, s.mb10]} numberOfLines={6}> + {currentFeed.data.description} + </Text> + ) : null} + <Text type="md-medium" style={pal.textLight}> + Liked by {currentFeed?.data.likeCount}{' '} + {pluralize(currentFeed?.data.likeCount || 0, 'user')} + </Text> + {isDesktopWeb && ( + <View style={styles.headerBtns}> + <Button + type={currentFeed?.isSaved ? 'default' : 'inverted'} + onPress={onToggleSaved} + accessibilityLabel={ + currentFeed?.isSaved ? 'Unsave this feed' : 'Save this feed' + } + accessibilityHint="" + label={currentFeed?.isSaved ? 'Unsave' : 'Save'} + /> + + <Button type="default" onPress={onToggleLiked}> + {currentFeed?.isLiked ? ( + <HeartIconSolid size={18} style={styles.liked} /> + ) : ( + <HeartIcon strokeWidth={3} size={18} style={pal.icon} /> + )} + </Button> + </View> + )} + </View> + <View style={[styles.fakeSelector, pal.border]}> + <View + style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}> + <Text type="md-medium" style={[pal.text]}> + Feed + </Text> + </View> + </View> + </> + ) + }, [store.me.did, pal, currentFeed, onToggleLiked, onToggleSaved]) + + return ( + <View style={s.hContentRegion}> + <ViewHeader title="" renderButton={renderHeaderBtns} /> + <Feed + scrollElRef={scrollElRef} + feed={algoFeed} + ListHeaderComponent={renderListHeaderComponent} + extraData={uri} + /> + </View> + ) + }), +) + +/* + + <View style={[styles.headerContainer]}> + <View style={[styles.header]}> + <View style={styles.avatarContainer}> + <UserAvatar + type="algo" + size={28} + avatar={currentFeed?.data.avatar} + /> + <Link href={`/profile/${currentFeed?.data.creator.handle}`}> + <Text style={[pal.textLight]}> + @{currentFeed?.data.creator.handle} + </Text> + </Link> + </View> + <Text style={[pal.text]}>{currentFeed?.data.description}</Text> + </View> + + <View style={[styles.buttonsContainer]}> + </View> + </View> + */ + +const styles = StyleSheet.create({ + headerBtns: { + flexDirection: 'row', + gap: 8, + }, + header: { + flexDirection: 'row', + gap: 12, + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 16, + borderTopWidth: 1, + }, + headerDetails: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + fakeSelector: { + flexDirection: 'row', + paddingHorizontal: isDesktopWeb ? 16 : 6, + }, + fakeSelectorItem: { + paddingHorizontal: 12, + paddingBottom: 8, + borderBottomWidth: 3, + }, + liked: { + color: colors.red3, + }, + + /* headerContainer: { + alignItems: 'center', + justifyContent: 'center', + gap: 8, + marginBottom: 12, + }, + header: { + alignItems: 'center', + gap: 4, + }, + avatarContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + buttonsContainer: { + flexDirection: 'row', + gap: 8, + }, + saveButton: { + minWidth: 100, + alignItems: 'center', + }, + liked: { + color: colors.red3, + }, + notLiked: { + color: colors.gray3, + }, + likeButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 4, + paddingHorizontal: 8, + borderRadius: 24, + gap: 4, + },*/ +}) diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 01f27bae1..7c3ed831c 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -87,7 +87,7 @@ export const ProfileListScreen = withAuthRequired( return <EmptyState icon="users-slash" message="This list is empty!" /> }, []) - const renderHeaderBtn = React.useCallback(() => { + const renderHeaderBtns = React.useCallback(() => { return ( <View style={styles.headerBtns}> {list?.isOwner && ( @@ -148,7 +148,7 @@ export const ProfileListScreen = withAuthRequired( pal.border, ]} testID="moderationMutelistsScreen"> - <ViewHeader title="" renderButton={renderHeaderBtn} /> + <ViewHeader title="" renderButton={renderHeaderBtns} /> <ListItems list={list} renderEmptyState={renderEmptyState} diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 6d55649f7..c32639889 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -137,12 +137,9 @@ const ListHeaderComponent = observer( key={item.data.uri} accessibilityRole="button" onPress={() => { - navigation.navigate('CustomFeed', { + navigation.navigate('ProfileCustomFeed', { name: item.data.creator.did, rkey: new AtUri(item.data.uri).rkey, - displayName: - item.data.displayName ?? - `${item.data.creator.displayName}'s feed`, }) }} style={styles.pinnedItem}> |