import React, {useCallback, useMemo} from 'react' import {Pressable, StyleSheet, View} from 'react-native' 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 {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 {useAnalytics} from 'lib/analytics/analytics' 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 {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {Feed} from 'view/com/posts/Feed' 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 {CenteredView} from 'view/com/util/Views' import {atoms as a, useTheme} from '#/alf' import {Button as NewButton, ButtonText} from '#/components/Button' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' 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 {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 export function ProfileFeedScreen(props: Props) { const {rkey, name: handleOrDid} = props.route.params const pal = usePalette('default') const {_} = useLingui() const navigation = useNavigation() 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 ( Could not load feed {error.toString()} ) } return resolvedUri ? ( ) : ( ) } function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { const {data: preferences} = usePreferencesQuery() const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) if (!preferences || !info) { return } return ( ) } 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 {track} = useAnalytics() const playHaptic = useHaptics() const feedSectionRef = React.useRef(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 an issue updating your feeds, please check your internet connection and try again.`, ), ) logger.error('Failed up 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`)) 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) track('CustomFeed:Share') }, [feedInfo, track]) const onPressReport = React.useCallback(() => { reportDialogControl.open() }, [reportDialogControl]) const onCurrentPageSelected = React.useCallback( (index: number) => { if (index === 0) { feedSectionRef.current?.scrollToTop() } }, [feedSectionRef], ) const renderHeader = useCallback(() => { return ( <> {feedInfo && hasSession && ( {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)} )} {({props, state}) => { return ( ) }} {hasSession && ( <> {isSaved ? _(msg`Remove from my feeds`) : _(msg`Save to my feeds`)} {_(msg`Report feed`)} )} {_(msg`Share feed`)} ) }, [ _, hasSession, feedInfo, isPinned, onTogglePinned, onToggleSaved, currentAccount?.did, isSaved, onPressReport, onPressShare, t, isPending, ]) return ( {({headerHeight, scrollElRef, isFocused}) => ( )} {hasSession && ( openComposer({})} icon={ } accessibilityRole="button" accessibilityLabel={_(msg`New post`)} accessibilityHint="" /> )} ) } interface FeedSectionProps { feed: FeedDescriptor headerHeight: number scrollElRef: ListRef isFocused: boolean } const FeedSection = React.forwardRef( 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 }, [_]) return ( {(isScrolledDown || hasNew) && ( )} ) }, ) 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 {track} = useAnalytics() const playHaptic = useHaptics() 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 { playHaptic() if (isLiked && likeUri) { await unlikeFeed({uri: likeUri}) track('CustomFeed:Unlike') setLikeUri('') } else { const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid}) track('CustomFeed:Like') setLikeUri(res.uri) } } catch (err) { Toast.show( _( msg`There was an an issue contacting the server, please check your internet connection and try again.`, ), ) logger.error('Failed up toggle like', {message: err}) } }, [playHaptic, isLiked, likeUri, unlikeFeed, track, likeFeed, feedInfo, _]) return ( {feedInfo.description ? ( ) : ( No description )} {isLiked ? ( ) : ( )} {typeof likeCount === 'number' && ( )} ) } 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, }, })