import React, {useMemo, useCallback} from 'react' import {Dimensions, StyleSheet, View} from 'react-native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useIsFocused, useNavigation} from '@react-navigation/native' 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 {s} from 'lib/styles' 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' import {TextLink} from 'view/com/util/Link' import {ListRef} from 'view/com/util/List' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' import {RichText} from 'view/com/util/text/RichText' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {FAB} from 'view/com/util/fab/FAB' import {EmptyState} from 'view/com/util/EmptyState' import {LoadingScreen} from 'view/com/util/LoadingScreen' import * as Toast from 'view/com/util/Toast' import {useSetTitle} from 'lib/hooks/useSetTitle' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 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 {useScrollHandlers} from '#/lib/ScrollContext' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {makeCustomFeedLink} from 'lib/routes/links' import {pluralize} from 'lib/strings/helpers' import {CenteredView, ScrollView} from 'view/com/util/Views' import {NavigationProp} from 'lib/routes/types' 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 {useFeedSourceInfoQuery, FeedSourceFeedInfo} 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' import {isNative} from '#/platform/detection' import {listenSoftReset} from '#/state/events' const SECTION_TITLES = ['Posts', 'About'] 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 pal = usePalette('default') const {hasSession, currentAccount} = useSession() const {openModal} = useModalControls() const {openComposer} = useComposerControls() const {track} = useAnalytics() const feedSectionRef = React.useRef(null) const isScreenFocused = useIsFocused() 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) // event handlers // const onToggleSaved = React.useCallback(async () => { try { Haptics.default() if (isSaved) { await removeFeed({uri: feedInfo.uri}) resetRemoveFeed() } else { await saveFeed({uri: feedInfo.uri}) resetSaveFeed() } } 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}) } }, [ feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed, _, ]) const onTogglePinned = React.useCallback(async () => { try { Haptics.default() if (isPinned) { await unpinFeed({uri: feedInfo.uri}) resetUnpinFeed() } else { await pinFeed({uri: feedInfo.uri}) resetPinFeed() } } catch (e) { Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {message: 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], ) // 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', }, android: 'ic_delete', web: ['far', 'trash-can'], } : { ios: { name: 'plus', }, android: '', web: 'plus', }, }, 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: _(msg`Share feed`), onPress: onPressShare, icon: { ios: { name: 'square.and.arrow.up', }, android: 'ic_menu_share', web: 'share', }, }, ].filter(Boolean) as DropdownItem[] }, [ hasSession, onToggleSaved, onPressReport, onPressShare, isSaved, isSavePending, isRemovePending, _, ]) const renderHeader = useCallback(() => { return ( {feedInfo && hasSession && ( <> {typeof likeCount === 'number' && ( )} {isOwner ? ( Created by you ) : ( Created by{' '} )} ) } 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, }, })