diff options
Diffstat (limited to 'src/view/screens/ProfileFeed.tsx')
-rw-r--r-- | src/view/screens/ProfileFeed.tsx | 780 |
1 files changed, 461 insertions, 319 deletions
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index a4d146d66..3a0bdcc0f 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -1,25 +1,21 @@ import React, {useMemo, useCallback} from 'react' import { - FlatList, - NativeScrollEvent, + Dimensions, StyleSheet, View, ActivityIndicator, + FlatList, } from 'react-native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useNavigation} from '@react-navigation/native' -import {useAnimatedScrollHandler} from 'react-native-reanimated' +import {useQueryClient} from '@tanstack/react-query' import {usePalette} from 'lib/hooks/usePalette' import {HeartIcon, HeartIconSolid} from 'lib/icons' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 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 {useStores} from 'state/index' -import {FeedSourceModel} from 'state/models/content/feed-source' -import {PostsFeedModel} from 'state/models/feeds/posts' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {FeedDescriptor} from '#/state/queries/post-feed' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {Feed} from 'view/com/posts/Feed' @@ -32,13 +28,13 @@ import {FAB} from 'view/com/util/fab/FAB' import {EmptyState} from 'view/com/util/EmptyState' import * as Toast from 'view/com/util/Toast' import {useSetTitle} from 'lib/hooks/useSetTitle' -import {useCustomFeed} from 'lib/hooks/useCustomFeed' +import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' +import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {Haptics} from 'lib/haptics' import {useAnalytics} from 'lib/analytics/analytics' import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' -import {resolveName} from 'lib/api' import {makeCustomFeedLink} from 'lib/routes/links' import {pluralize} from 'lib/strings/helpers' import {CenteredView, ScrollView} from 'view/com/util/Views' @@ -47,6 +43,28 @@ import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' import {ComposeIcon2} from 'lib/icons' import {logger} from '#/logger' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useModalControls} from '#/state/modals' +import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' +import { + useFeedSourceInfoQuery, + FeedSourceFeedInfo, + useIsFeedPublicQuery, +} from '#/state/queries/feed' +import {useResolveUriQuery} from '#/state/queries/resolve-uri' +import { + UsePreferencesQueryResponse, + usePreferencesQuery, + useSaveFeedMutation, + useRemoveFeedMutation, + usePinFeedMutation, + useUnpinFeedMutation, +} from '#/state/queries/preferences' +import {useSession} from '#/state/session' +import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' +import {useComposerControls} from '#/state/shell/composer' +import {truncateAndInvalidate} from '#/state/queries/util' const SECTION_TITLES = ['Posts', 'About'] @@ -55,315 +73,372 @@ interface SectionRef { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> -export const ProfileFeedScreen = withAuthRequired( - observer(function ProfileFeedScreenImpl(props: Props) { - const pal = usePalette('default') - const store = useStores() - const navigation = useNavigation<NavigationProp>() +export function ProfileFeedScreen(props: Props) { + const {rkey, name: handleOrDid} = props.route.params - const {name: handleOrDid} = props.route.params + const pal = usePalette('default') + const {_} = useLingui() + const navigation = useNavigation<NavigationProp>() - const [feedOwnerDid, setFeedOwnerDid] = React.useState<string | undefined>() - const [error, setError] = React.useState<string | undefined>() + const uri = useMemo( + () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), + [rkey, handleOrDid], + ) + const {error, data: resolvedUri} = useResolveUriQuery(uri) - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - React.useEffect(() => { - /* - * We must resolve the DID of the feed owner before we can fetch the feed. - */ - async function fetchDid() { - try { - const did = await resolveName(store, handleOrDid) - setFeedOwnerDid(did) - } catch (e) { - setError( - `We're sorry, but we were unable to resolve this feed. If this persists, please contact the feed creator, @${handleOrDid}.`, - ) - } - } + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + if (error) { + return ( + <CenteredView> + <View style={[pal.view, pal.border, styles.notFoundContainer]}> + <Text type="title-lg" style={[pal.text, s.mb10]}> + <Trans>Could not load feed</Trans> + </Text> + <Text type="md" style={[pal.text, s.mb20]}> + {error.toString()} + </Text> - fetchDid() - }, [store, handleOrDid, setFeedOwnerDid]) - - if (error) { - return ( - <CenteredView> - <View style={[pal.view, pal.border, styles.notFoundContainer]}> - <Text type="title-lg" style={[pal.text, s.mb10]}> - Could not load feed - </Text> - <Text type="md" style={[pal.text, s.mb20]}> - {error} - </Text> - - <View style={{flexDirection: 'row'}}> - <Button - type="default" - accessibilityLabel="Go Back" - accessibilityHint="Return to previous page" - onPress={onPressBack} - style={{flexShrink: 1}}> - <Text type="button" style={pal.text}> - Go Back - </Text> - </Button> - </View> + <View style={{flexDirection: 'row'}}> + <Button + type="default" + accessibilityLabel={_(msg`Go Back`)} + accessibilityHint="Return to previous page" + onPress={onPressBack} + style={{flexShrink: 1}}> + <Text type="button" style={pal.text}> + <Trans>Go Back</Trans> + </Text> + </Button> </View> - </CenteredView> - ) - } + </View> + </CenteredView> + ) + } - return feedOwnerDid ? ( - <ProfileFeedScreenInner {...props} feedOwnerDid={feedOwnerDid} /> - ) : ( + return resolvedUri ? ( + <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} /> + ) : ( + <CenteredView> + <View style={s.p20}> + <ActivityIndicator size="large" /> + </View> + </CenteredView> + ) +} + +function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { + const {data: preferences} = usePreferencesQuery() + const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) + const {isLoading: isPublicStatusLoading, data: isPublic} = + useIsFeedPublicQuery({uri: feedUri}) + + if (!preferences || !info || isPublicStatusLoading) { + return ( <CenteredView> <View style={s.p20}> <ActivityIndicator size="large" /> </View> </CenteredView> ) - }), -) + } -export const ProfileFeedScreenInner = observer( - function ProfileFeedScreenInnerImpl({ - route, - feedOwnerDid, - }: Props & {feedOwnerDid: string}) { - const pal = usePalette('default') - const store = useStores() - const {track} = useAnalytics() - const feedSectionRef = React.useRef<SectionRef>(null) - const {rkey, name: handleOrDid} = route.params - const uri = useMemo( - () => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey), - [rkey, feedOwnerDid], - ) - const feedInfo = useCustomFeed(uri) - const feed: PostsFeedModel = useMemo(() => { - const model = new PostsFeedModel(store, 'custom', { - feed: uri, - }) - model.setup() - return model - }, [store, uri]) - const isPinned = store.preferences.isPinnedFeed(uri) - useSetTitle(feedInfo?.displayName) - - // events - // = - - const onToggleSaved = React.useCallback(async () => { - try { - Haptics.default() - if (feedInfo?.isSaved) { - await feedInfo?.unsave() - } else { - await feedInfo?.save() - } - } catch (err) { - Toast.show( - 'There was an an issue updating your feeds, please check your internet connection and try again.', - ) - logger.error('Failed up update feeds', {error: err}) - } - }, [feedInfo]) + return ( + <ProfileFeedScreenInner + preferences={preferences} + feedInfo={info as FeedSourceFeedInfo} + isPublic={Boolean(isPublic)} + /> + ) +} - const onToggleLiked = React.useCallback(async () => { +export function ProfileFeedScreenInner({ + preferences, + feedInfo, + isPublic, +}: { + preferences: UsePreferencesQueryResponse + feedInfo: FeedSourceFeedInfo + isPublic: boolean +}) { + const {_} = useLingui() + const pal = usePalette('default') + const {hasSession, currentAccount} = useSession() + const {openModal} = useModalControls() + const {openComposer} = useComposerControls() + const {track} = useAnalytics() + const feedSectionRef = React.useRef<SectionRef>(null) + + const { + mutateAsync: saveFeed, + variables: savedFeed, + reset: resetSaveFeed, + isPending: isSavePending, + } = useSaveFeedMutation() + const { + mutateAsync: removeFeed, + variables: removedFeed, + reset: resetRemoveFeed, + isPending: isRemovePending, + } = useRemoveFeedMutation() + const { + mutateAsync: pinFeed, + variables: pinnedFeed, + reset: resetPinFeed, + isPending: isPinPending, + } = usePinFeedMutation() + const { + mutateAsync: unpinFeed, + variables: unpinnedFeed, + reset: resetUnpinFeed, + isPending: isUnpinPending, + } = useUnpinFeedMutation() + + const isSaved = + !removedFeed && + (!!savedFeed || preferences.feeds.saved.includes(feedInfo.uri)) + const isPinned = + !unpinnedFeed && + (!!pinnedFeed || preferences.feeds.pinned.includes(feedInfo.uri)) + + useSetTitle(feedInfo?.displayName) + + const onToggleSaved = React.useCallback(async () => { + try { Haptics.default() - try { - if (feedInfo?.isLiked) { - await feedInfo?.unlike() - } else { - await feedInfo?.like() - } - } catch (err) { - Toast.show( - 'There was an an issue contacting the server, please check your internet connection and try again.', - ) - logger.error('Failed up toggle like', {error: err}) + + if (isSaved) { + await removeFeed({uri: feedInfo.uri}) + resetRemoveFeed() + } else { + await saveFeed({uri: feedInfo.uri}) + resetSaveFeed() } - }, [feedInfo]) + } catch (err) { + Toast.show( + 'There was an an issue updating your feeds, please check your internet connection and try again.', + ) + logger.error('Failed up update feeds', {error: err}) + } + }, [feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed]) - const onTogglePinned = React.useCallback(async () => { + const onTogglePinned = React.useCallback(async () => { + try { Haptics.default() - if (feedInfo) { - feedInfo.togglePin().catch(e => { - Toast.show('There was an issue contacting the server') - logger.error('Failed to toggle pinned feed', {error: e}) - }) - } - }, [feedInfo]) - - const onPressShare = React.useCallback(() => { - const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`) - shareUrl(url) - track('CustomFeed:Share') - }, [handleOrDid, rkey, track]) - - const onPressReport = React.useCallback(() => { - if (!feedInfo) return - store.shell.openModal({ - name: 'report', - uri: feedInfo.uri, - cid: feedInfo.cid, - }) - }, [store, feedInfo]) - - const onCurrentPageSelected = React.useCallback( - (index: number) => { - if (index === 0) { - feedSectionRef.current?.scrollToTop() - } - }, - [feedSectionRef], - ) - // render - // = + if (isPinned) { + await unpinFeed({uri: feedInfo.uri}) + resetUnpinFeed() + } else { + await pinFeed({uri: feedInfo.uri}) + resetPinFeed() + } + } catch (e) { + Toast.show('There was an issue contacting the server') + logger.error('Failed to toggle pinned feed', {error: e}) + } + }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed]) + + const onPressShare = React.useCallback(() => { + const url = toShareUrl(feedInfo.route.href) + shareUrl(url) + track('CustomFeed:Share') + }, [feedInfo, track]) + + const onPressReport = React.useCallback(() => { + if (!feedInfo) return + openModal({ + name: 'report', + uri: feedInfo.uri, + cid: feedInfo.cid, + }) + }, [openModal, feedInfo]) + + const onCurrentPageSelected = React.useCallback( + (index: number) => { + if (index === 0) { + feedSectionRef.current?.scrollToTop() + } + }, + [feedSectionRef], + ) - const dropdownItems: DropdownItem[] = React.useMemo(() => { - return [ - { - testID: 'feedHeaderDropdownToggleSavedBtn', - label: feedInfo?.isSaved ? 'Remove from my feeds' : 'Add to my feeds', - onPress: onToggleSaved, - icon: feedInfo?.isSaved - ? { - ios: { - name: 'trash', - }, - android: 'ic_delete', - web: ['far', 'trash-can'], - } - : { - ios: { - name: 'plus', - }, - android: '', - web: 'plus', + // render + // = + + const dropdownItems: DropdownItem[] = React.useMemo(() => { + return [ + hasSession && { + testID: 'feedHeaderDropdownToggleSavedBtn', + label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`), + onPress: isSavePending || isRemovePending ? undefined : onToggleSaved, + icon: isSaved + ? { + ios: { + name: 'trash', }, - }, - { - testID: 'feedHeaderDropdownReportBtn', - label: 'Report feed', - onPress: onPressReport, - icon: { - ios: { - name: 'exclamationmark.triangle', + android: 'ic_delete', + web: ['far', 'trash-can'], + } + : { + ios: { + name: 'plus', + }, + android: '', + web: 'plus', }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', + }, + hasSession && { + testID: 'feedHeaderDropdownReportBtn', + label: _(msg`Report feed`), + onPress: onPressReport, + icon: { + ios: { + name: 'exclamationmark.triangle', }, + android: 'ic_menu_report_image', + web: 'circle-exclamation', }, - { - testID: 'feedHeaderDropdownShareBtn', - label: 'Share link', - onPress: onPressShare, - icon: { - ios: { - name: 'square.and.arrow.up', - }, - android: 'ic_menu_share', - web: 'share', + }, + { + testID: 'feedHeaderDropdownShareBtn', + label: _(msg`Share feed`), + onPress: onPressShare, + icon: { + ios: { + name: 'square.and.arrow.up', }, + android: 'ic_menu_share', + web: 'share', }, - ] as DropdownItem[] - }, [feedInfo, onToggleSaved, onPressReport, onPressShare]) - - const renderHeader = useCallback(() => { - return ( - <ProfileSubpageHeader - isLoading={!feedInfo?.hasLoaded} - href={makeCustomFeedLink(feedOwnerDid, rkey)} - title={feedInfo?.displayName} - avatar={feedInfo?.avatar} - isOwner={feedInfo?.isOwner} - creator={ - feedInfo - ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle} - : undefined - } - avatarType="algo"> - {feedInfo && ( - <> - <Button - type="default" - label={feedInfo?.isSaved ? 'Unsave' : 'Save'} - onPress={onToggleSaved} - style={styles.btn} - /> - <Button - type={isPinned ? 'default' : 'inverted'} - label={isPinned ? 'Unpin' : 'Pin to home'} - onPress={onTogglePinned} - style={styles.btn} - /> - </> - )} - <NativeDropdown - testID="headerDropdownBtn" - items={dropdownItems} - accessibilityLabel="More options" - accessibilityHint=""> - <View style={[pal.viewLight, styles.btn]}> - <FontAwesomeIcon - icon="ellipsis" - size={20} - color={pal.colors.text} - /> - </View> - </NativeDropdown> - </ProfileSubpageHeader> - ) - }, [ - pal, - feedOwnerDid, - rkey, - feedInfo, - isPinned, - onTogglePinned, - onToggleSaved, - dropdownItems, - ]) - + }, + ].filter(Boolean) as DropdownItem[] + }, [ + hasSession, + onToggleSaved, + onPressReport, + onPressShare, + isSaved, + isSavePending, + isRemovePending, + _, + ]) + + const renderHeader = useCallback(() => { return ( - <View style={s.hContentRegion}> - <PagerWithHeader - items={SECTION_TITLES} - isHeaderReady={feedInfo?.hasLoaded ?? false} - renderHeader={renderHeader} - onCurrentPageSelected={onCurrentPageSelected}> - {({onScroll, headerHeight, isScrolledDown}) => ( + <ProfileSubpageHeader + isLoading={false} + href={feedInfo.route.href} + title={feedInfo?.displayName} + avatar={feedInfo?.avatar} + isOwner={feedInfo.creatorDid === currentAccount?.did} + creator={ + feedInfo + ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle} + : undefined + } + avatarType="algo"> + {feedInfo && hasSession && ( + <> + <Button + disabled={isSavePending || isRemovePending} + type="default" + label={isSaved ? 'Unsave' : 'Save'} + onPress={onToggleSaved} + style={styles.btn} + /> + <Button + testID={isPinned ? 'unpinBtn' : 'pinBtn'} + disabled={isPinPending || isUnpinPending} + type={isPinned ? 'default' : 'inverted'} + label={isPinned ? 'Unpin' : 'Pin to home'} + onPress={onTogglePinned} + style={styles.btn} + /> + </> + )} + <NativeDropdown + testID="headerDropdownBtn" + items={dropdownItems} + accessibilityLabel={_(msg`More options`)} + accessibilityHint=""> + <View style={[pal.viewLight, styles.btn]}> + <FontAwesomeIcon + icon="ellipsis" + size={20} + color={pal.colors.text} + /> + </View> + </NativeDropdown> + </ProfileSubpageHeader> + ) + }, [ + _, + hasSession, + pal, + feedInfo, + isPinned, + onTogglePinned, + onToggleSaved, + dropdownItems, + currentAccount?.did, + isPinPending, + isRemovePending, + isSavePending, + isSaved, + isUnpinPending, + ]) + + return ( + <View style={s.hContentRegion}> + <PagerWithHeader + items={SECTION_TITLES} + isHeaderReady={true} + renderHeader={renderHeader} + onCurrentPageSelected={onCurrentPageSelected}> + {({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) => + isPublic ? ( <FeedSection ref={feedSectionRef} - feed={feed} + feed={`feedgen|${feedInfo.uri}`} onScroll={onScroll} headerHeight={headerHeight} isScrolledDown={isScrolledDown} + scrollElRef={ + scrollElRef as React.MutableRefObject<FlatList<any> | null> + } + isFocused={isFocused} /> - )} - {({onScroll, headerHeight}) => ( - <AboutSection - feedOwnerDid={feedOwnerDid} - feedRkey={rkey} - feedInfo={feedInfo} - headerHeight={headerHeight} - onToggleLiked={onToggleLiked} - onScroll={onScroll} - /> - )} - </PagerWithHeader> + ) : ( + <CenteredView sideBorders style={[{paddingTop: headerHeight}]}> + <NonPublicFeedMessage /> + </CenteredView> + ) + } + {({onScroll, headerHeight, scrollElRef}) => ( + <AboutSection + feedOwnerDid={feedInfo.creatorDid} + feedRkey={feedInfo.route.params.rkey} + feedInfo={feedInfo} + headerHeight={headerHeight} + onScroll={onScroll} + scrollElRef={ + scrollElRef as React.MutableRefObject<ScrollView | null> + } + isOwner={feedInfo.creatorDid === currentAccount?.did} + /> + )} + </PagerWithHeader> + {hasSession && ( <FAB testID="composeFAB" - onPress={() => store.shell.openComposer({})} + onPress={() => openComposer({})} icon={ <ComposeIcon2 strokeWidth={1.5} @@ -372,32 +447,67 @@ export const ProfileFeedScreenInner = observer( /> } accessibilityRole="button" - accessibilityLabel="New post" + accessibilityLabel={_(msg`New post`)} accessibilityHint="" /> + )} + </View> + ) +} + +function NonPublicFeedMessage() { + const pal = usePalette('default') + + return ( + <View + style={[ + pal.border, + { + padding: 18, + borderTopWidth: 1, + minHeight: Dimensions.get('window').height * 1.5, + }, + ]}> + <View + style={[ + pal.viewLight, + { + padding: 12, + borderRadius: 8, + }, + ]}> + <Text style={[pal.text]}> + <Trans> + Looks like this feed is only available to users with a Bluesky + account. Please sign up or sign in to view this feed! + </Trans> + </Text> </View> - ) - }, -) + </View> + ) +} interface FeedSectionProps { - feed: PostsFeedModel - onScroll: (e: NativeScrollEvent) => void + feed: FeedDescriptor + onScroll: OnScrollHandler headerHeight: number isScrolledDown: boolean + scrollElRef: React.MutableRefObject<FlatList<any> | null> + isFocused: boolean } const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( function FeedSectionImpl( - {feed, onScroll, headerHeight, isScrolledDown}, + {feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}, ref, ) { - const hasNew = feed.hasNewLatest && !feed.isRefreshing - const scrollElRef = React.useRef<FlatList>(null) + const [hasNew, setHasNew] = React.useState(false) + const queryClient = useQueryClient() const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({offset: -headerHeight}) - feed.refresh() - }, [feed, scrollElRef, headerHeight]) + truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) + setHasNew(false) + }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) React.useImperativeHandle(ref, () => ({ scrollToTop: onScrollToTop, @@ -407,13 +517,15 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( return <EmptyState icon="feed" message="This feed is empty!" /> }, []) - const scrollHandler = useAnimatedScrollHandler({onScroll}) return ( <View> <Feed + enabled={isFocused} feed={feed} + pollInterval={30e3} scrollElRef={scrollElRef} - onScroll={scrollHandler} + onHasNew={setHasNew} + onScroll={onScroll} scrollEventThrottle={5} renderEmptyState={renderPostsEmpty} headerOffset={headerHeight} @@ -430,32 +542,64 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( }, ) -const AboutSection = observer(function AboutPageImpl({ +function AboutSection({ feedOwnerDid, feedRkey, feedInfo, headerHeight, - onToggleLiked, onScroll, + scrollElRef, + isOwner, }: { feedOwnerDid: string feedRkey: string - feedInfo: FeedSourceModel | undefined + feedInfo: FeedSourceFeedInfo headerHeight: number - onToggleLiked: () => void - onScroll: (e: NativeScrollEvent) => void + onScroll: OnScrollHandler + scrollElRef: React.MutableRefObject<ScrollView | null> + isOwner: boolean }) { const pal = usePalette('default') - const scrollHandler = useAnimatedScrollHandler({onScroll}) + const {_} = useLingui() + const scrollHandler = useAnimatedScrollHandler(onScroll) + const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) + const {hasSession} = useSession() - if (!feedInfo) { - return <View /> - } + const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() + const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = + useUnlikeMutation() + + const isLiked = !!likeUri + const likeCount = + isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount + + const onToggleLiked = React.useCallback(async () => { + try { + Haptics.default() + + if (isLiked && likeUri) { + await unlikeFeed({uri: likeUri}) + setLikeUri('') + } else { + const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid}) + setLikeUri(res.uri) + } + } catch (err) { + Toast.show( + 'There was an an issue contacting the server, please check your internet connection and try again.', + ) + logger.error('Failed up toggle like', {error: err}) + } + }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed]) return ( <ScrollView + ref={scrollElRef} scrollEventThrottle={1} - contentContainerStyle={{paddingTop: headerHeight}} + contentContainerStyle={{ + paddingTop: headerHeight, + minHeight: Dimensions.get('window').height * 1.5, + }} onScroll={scrollHandler}> <View style={[ @@ -467,46 +611,44 @@ const AboutSection = observer(function AboutPageImpl({ }, pal.border, ]}> - {feedInfo.descriptionRT ? ( + {feedInfo.description ? ( <RichText testID="listDescription" type="lg" style={pal.text} - richText={feedInfo.descriptionRT} + richText={feedInfo.description} /> ) : ( <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}> - No description + <Trans>No description</Trans> </Text> )} <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}> <Button type="default" testID="toggleLikeBtn" - accessibilityLabel="Like this feed" + accessibilityLabel={_(msg`Like this feed`)} accessibilityHint="" + disabled={!hasSession || isLikePending || isUnlikePending} onPress={onToggleLiked} style={{paddingHorizontal: 10}}> - {feedInfo?.isLiked ? ( + {isLiked ? ( <HeartIconSolid size={19} style={styles.liked} /> ) : ( <HeartIcon strokeWidth={3} size={19} style={pal.textLight} /> )} </Button> - {typeof feedInfo.likeCount === 'number' && ( + {typeof likeCount === 'number' && ( <TextLink href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} - text={`Liked by ${feedInfo.likeCount} ${pluralize( - feedInfo.likeCount, - 'user', - )}`} + text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`} style={[pal.textLight, s.semiBold]} /> )} </View> <Text type="md" style={[pal.textLight]} numberOfLines={1}> Created by{' '} - {feedInfo.isOwner ? ( + {isOwner ? ( 'you' ) : ( <TextLink @@ -522,7 +664,7 @@ const AboutSection = observer(function AboutPageImpl({ </View> </ScrollView> ) -}) +} const styles = StyleSheet.create({ btn: { |