diff options
Diffstat (limited to 'src/view/screens/ProfileFeed.tsx')
-rw-r--r-- | src/view/screens/ProfileFeed.tsx | 621 |
1 files changed, 0 insertions, 621 deletions
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx deleted file mode 100644 index c3f98c067..000000000 --- a/src/view/screens/ProfileFeed.tsx +++ /dev/null @@ -1,621 +0,0 @@ -import React, {useCallback, useMemo} from 'react' -import {Pressable, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Plural, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useIsFocused, useNavigation} from '@react-navigation/native' -import {NativeStackScreenProps} from '@react-navigation/native-stack' -import {useQueryClient} from '@tanstack/react-query' - -import {HITSLOP_20} from '#/lib/constants' -import {useHaptics} from '#/lib/haptics' -import {usePalette} from '#/lib/hooks/usePalette' -import {useSetTitle} from '#/lib/hooks/useSetTitle' -import {ComposeIcon2} from '#/lib/icons' -import {makeCustomFeedLink} from '#/lib/routes/links' -import {CommonNavigatorParams} from '#/lib/routes/types' -import {NavigationProp} from '#/lib/routes/types' -import {shareUrl} from '#/lib/sharing' -import {makeRecordUri} from '#/lib/strings/url-helpers' -import {toShareUrl} from '#/lib/strings/url-helpers' -import {s} from '#/lib/styles' -import {logger} from '#/logger' -import {isNative} from '#/platform/detection' -import {listenSoftReset} from '#/state/events' -import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' -import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' -import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' -import {FeedDescriptor} from '#/state/queries/post-feed' -import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' -import { - useAddSavedFeedsMutation, - usePreferencesQuery, - UsePreferencesQueryResponse, - useRemoveFeedMutation, - useUpdateSavedFeedsMutation, -} from '#/state/queries/preferences' -import {useResolveUriQuery} from '#/state/queries/resolve-uri' -import {truncateAndInvalidate} from '#/state/queries/util' -import {useSession} from '#/state/session' -import {useComposerControls} from '#/state/shell/composer' -import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' -import {PostFeed} from '#/view/com/posts/PostFeed' -import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader' -import {EmptyState} from '#/view/com/util/EmptyState' -import {FAB} from '#/view/com/util/fab/FAB' -import {Button} from '#/view/com/util/forms/Button' -import {ListRef} from '#/view/com/util/List' -import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' -import {LoadingScreen} from '#/view/com/util/LoadingScreen' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useTheme} from '#/alf' -import {Button as NewButton, ButtonText} from '#/components/Button' -import {useRichText} from '#/components/hooks/useRichText' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import { - Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, - Heart2_Stroke2_Corner0_Rounded as HeartOutline, -} from '#/components/icons/Heart2' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' -import * as Layout from '#/components/Layout' -import {InlineLinkText} from '#/components/Link' -import * as Menu from '#/components/Menu' -import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' -import {RichText} from '#/components/RichText' - -const SECTION_TITLES = ['Posts'] - -interface SectionRef { - scrollToTop: () => void -} - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> -export function ProfileFeedScreen(props: Props) { - const {rkey, name: handleOrDid} = props.route.params - - const pal = usePalette('default') - const {_} = useLingui() - const navigation = useNavigation<NavigationProp>() - - 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]) - - if (error) { - return ( - <Layout.Screen testID="profileFeedScreenError"> - <Layout.Content> - <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> - - <View style={{flexDirection: 'row'}}> - <Button - type="default" - accessibilityLabel={_(msg`Go back`)} - accessibilityHint={_(msg`Returns to previous page`)} - onPress={onPressBack} - style={{flexShrink: 1}}> - <Text type="button" style={pal.text}> - <Trans>Go Back</Trans> - </Text> - </Button> - </View> - </View> - </Layout.Content> - </Layout.Screen> - ) - } - - return resolvedUri ? ( - <Layout.Screen> - <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} /> - </Layout.Screen> - ) : ( - <Layout.Screen> - <LoadingScreen /> - </Layout.Screen> - ) -} - -function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { - const {data: preferences} = usePreferencesQuery() - const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) - - if (!preferences || !info) { - return <LoadingScreen /> - } - - return ( - <ProfileFeedScreenInner - preferences={preferences} - feedInfo={info as FeedSourceFeedInfo} - /> - ) -} - -export function ProfileFeedScreenInner({ - preferences, - feedInfo, -}: { - preferences: UsePreferencesQueryResponse - feedInfo: FeedSourceFeedInfo -}) { - const {_} = useLingui() - const t = useTheme() - const {hasSession, currentAccount} = useSession() - const reportDialogControl = useReportDialogControl() - const {openComposer} = useComposerControls() - const playHaptic = useHaptics() - const feedSectionRef = React.useRef<SectionRef>(null) - const isScreenFocused = useIsFocused() - - const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} = - useAddSavedFeedsMutation() - const {mutateAsync: removeFeed, isPending: isRemovePending} = - useRemoveFeedMutation() - const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} = - useUpdateSavedFeedsMutation() - - const isPending = - isAddSavedFeedPending || isRemovePending || isUpdateFeedPending - const savedFeedConfig = preferences.savedFeeds.find( - f => f.value === feedInfo.uri, - ) - const isSaved = Boolean(savedFeedConfig) - const isPinned = Boolean(savedFeedConfig?.pinned) - - useSetTitle(feedInfo?.displayName) - - // event handlers - // - - const onToggleSaved = React.useCallback(async () => { - try { - playHaptic() - - if (savedFeedConfig) { - await removeFeed(savedFeedConfig) - Toast.show(_(msg`Removed from your feeds`)) - } else { - await addSavedFeeds([ - { - type: 'feed', - value: feedInfo.uri, - pinned: false, - }, - ]) - Toast.show(_(msg`Saved to your feeds`)) - } - } catch (err) { - Toast.show( - _( - msg`There was an issue updating your feeds, please check your internet connection and try again.`, - ), - 'xmark', - ) - logger.error('Failed to update feeds', {message: err}) - } - }, [_, playHaptic, feedInfo, removeFeed, addSavedFeeds, savedFeedConfig]) - - const onTogglePinned = React.useCallback(async () => { - try { - playHaptic() - - if (savedFeedConfig) { - await updateSavedFeeds([ - { - ...savedFeedConfig, - pinned: !savedFeedConfig.pinned, - }, - ]) - } else { - await addSavedFeeds([ - { - type: 'feed', - value: feedInfo.uri, - pinned: true, - }, - ]) - } - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to toggle pinned feed', {message: e}) - } - }, [ - playHaptic, - feedInfo, - _, - savedFeedConfig, - updateSavedFeeds, - addSavedFeeds, - ]) - - const onPressShare = React.useCallback(() => { - const url = toShareUrl(feedInfo.route.href) - shareUrl(url) - }, [feedInfo]) - - const onPressReport = React.useCallback(() => { - reportDialogControl.open() - }, [reportDialogControl]) - - const onCurrentPageSelected = React.useCallback( - (index: number) => { - if (index === 0) { - feedSectionRef.current?.scrollToTop() - } - }, - [feedSectionRef], - ) - - const renderHeader = useCallback(() => { - return ( - <> - <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"> - <View style={[a.flex_row, a.align_center, a.gap_sm]}> - {feedInfo && hasSession && ( - <NewButton - testID={isPinned ? 'unpinBtn' : 'pinBtn'} - disabled={isPending} - size="small" - variant="solid" - color={isPinned ? 'secondary' : 'primary'} - label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)} - onPress={onTogglePinned}> - <ButtonText> - {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)} - </ButtonText> - </NewButton> - )} - <Menu.Root> - <Menu.Trigger label={_(msg`Open feed options menu`)}> - {({props, state}) => { - return ( - <Pressable - {...props} - hitSlop={HITSLOP_20} - style={[ - a.justify_center, - a.align_center, - a.rounded_full, - {height: 36, width: 36}, - t.atoms.bg_contrast_25, - (state.hovered || state.pressed) && [ - t.atoms.bg_contrast_50, - ], - ]} - testID="headerDropdownBtn"> - <FontAwesomeIcon - icon="ellipsis" - size={20} - style={t.atoms.text} - /> - </Pressable> - ) - }} - </Menu.Trigger> - - <Menu.Outer> - <Menu.Group> - {hasSession && ( - <> - <Menu.Item - disabled={isPending} - testID="feedHeaderDropdownToggleSavedBtn" - label={ - isSaved - ? _(msg`Remove from my feeds`) - : _(msg`Save to my feeds`) - } - onPress={onToggleSaved}> - <Menu.ItemText> - {isSaved - ? _(msg`Remove from my feeds`) - : _(msg`Save to my feeds`)} - </Menu.ItemText> - <Menu.ItemIcon - icon={isSaved ? Trash : Plus} - position="right" - /> - </Menu.Item> - - <Menu.Item - testID="feedHeaderDropdownReportBtn" - label={_(msg`Report feed`)} - onPress={onPressReport}> - <Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText> - <Menu.ItemIcon icon={CircleInfo} position="right" /> - </Menu.Item> - </> - )} - - <Menu.Item - testID="feedHeaderDropdownShareBtn" - label={_(msg`Share feed`)} - onPress={onPressShare}> - <Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText> - <Menu.ItemIcon icon={Share} position="right" /> - </Menu.Item> - </Menu.Group> - </Menu.Outer> - </Menu.Root> - </View> - </ProfileSubpageHeader> - <AboutSection - feedOwnerDid={feedInfo.creatorDid} - feedRkey={feedInfo.route.params.rkey} - feedInfo={feedInfo} - /> - </> - ) - }, [ - _, - hasSession, - feedInfo, - isPinned, - onTogglePinned, - onToggleSaved, - currentAccount?.did, - isSaved, - onPressReport, - onPressShare, - t, - isPending, - ]) - - return ( - <> - <ReportDialog - control={reportDialogControl} - params={{ - type: 'feedgen', - uri: feedInfo.uri, - cid: feedInfo.cid, - }} - /> - <PagerWithHeader - items={SECTION_TITLES} - isHeaderReady={true} - renderHeader={renderHeader} - onCurrentPageSelected={onCurrentPageSelected}> - {({headerHeight, scrollElRef, isFocused}) => ( - <FeedSection - ref={feedSectionRef} - feed={`feedgen|${feedInfo.uri}`} - headerHeight={headerHeight} - scrollElRef={scrollElRef as ListRef} - isFocused={isScreenFocused && isFocused} - /> - )} - </PagerWithHeader> - {hasSession && ( - <FAB - testID="composeFAB" - onPress={() => openComposer({})} - icon={ - <ComposeIcon2 - strokeWidth={1.5} - size={29} - style={{color: 'white'}} - /> - } - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - )} - </> - ) -} - -interface FeedSectionProps { - feed: FeedDescriptor - headerHeight: number - scrollElRef: ListRef - isFocused: boolean -} -const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( - function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) { - const {_} = useLingui() - const [hasNew, setHasNew] = React.useState(false) - const [isScrolledDown, setIsScrolledDown] = React.useState(false) - const queryClient = useQueryClient() - const isScreenFocused = useIsFocused() - const {hasSession} = useSession() - const feedFeedback = useFeedFeedback(feed, hasSession) - - const onScrollToTop = useCallback(() => { - scrollElRef.current?.scrollToOffset({ - animated: isNative, - offset: -headerHeight, - }) - truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) - setHasNew(false) - }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) - - React.useImperativeHandle(ref, () => ({ - scrollToTop: onScrollToTop, - })) - - React.useEffect(() => { - if (!isScreenFocused) { - return - } - return listenSoftReset(onScrollToTop) - }, [onScrollToTop, isScreenFocused]) - - const renderPostsEmpty = useCallback(() => { - return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} /> - }, [_]) - - return ( - <View> - <FeedFeedbackProvider value={feedFeedback}> - <PostFeed - enabled={isFocused} - feed={feed} - pollInterval={60e3} - disablePoll={hasNew} - scrollElRef={scrollElRef} - onHasNew={setHasNew} - onScrolledDownChange={setIsScrolledDown} - renderEmptyState={renderPostsEmpty} - headerOffset={headerHeight} - /> - </FeedFeedbackProvider> - {(isScrolledDown || hasNew) && ( - <LoadLatestBtn - onPress={onScrollToTop} - label={_(msg`Load new posts`)} - showIndicator={hasNew} - /> - )} - </View> - ) - }, -) - -function AboutSection({ - feedOwnerDid, - feedRkey, - feedInfo, -}: { - feedOwnerDid: string - feedRkey: string - feedInfo: FeedSourceFeedInfo -}) { - const t = useTheme() - const pal = usePalette('default') - const {_} = useLingui() - const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) - const {hasSession} = useSession() - const playHaptic = useHaptics() - const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() - const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = - useUnlikeMutation() - const [resolvedRT] = useRichText(feedInfo.description.text || '') - - const isLiked = !!likeUri - const likeCount = - isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount - - const onToggleLiked = React.useCallback(async () => { - try { - playHaptic() - - 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( - _( - msg`There was an issue contacting the server, please check your internet connection and try again.`, - ), - 'xmark', - ) - logger.error('Failed to toggle like', {message: err}) - } - }, [playHaptic, isLiked, likeUri, unlikeFeed, likeFeed, feedInfo, _]) - - return ( - <View style={[styles.aboutSectionContainer]}> - <View style={[a.pt_sm]}> - {feedInfo.description ? ( - <RichText - testID="listDescription" - style={[a.text_md]} - value={resolvedRT ?? feedInfo.description} - /> - ) : ( - <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}> - <Trans>No description</Trans> - </Text> - )} - </View> - - <View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}> - <NewButton - size="small" - variant="solid" - color="secondary" - shape="round" - label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)} - testID="toggleLikeBtn" - disabled={!hasSession || isLikePending || isUnlikePending} - onPress={onToggleLiked}> - {isLiked ? ( - <HeartFilled size="md" fill={s.likeColor.color} /> - ) : ( - <HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} /> - )} - </NewButton> - {typeof likeCount === 'number' && ( - <InlineLinkText - label={_(msg`View users who like this feed`)} - to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} - style={[t.atoms.text_contrast_medium, a.font_bold]}> - <Trans> - Liked by <Plural value={likeCount} one="# user" other="# users" /> - </Trans> - </InlineLinkText> - )} - </View> - </View> - ) -} - -const styles = StyleSheet.create({ - btn: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingVertical: 7, - paddingHorizontal: 14, - borderRadius: 50, - marginLeft: 6, - }, - notFoundContainer: { - margin: 10, - paddingHorizontal: 18, - paddingVertical: 14, - borderRadius: 6, - }, - aboutSectionContainer: { - paddingVertical: 4, - paddingHorizontal: 16, - gap: 12, - }, -}) |