diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx new file mode 100644 index 0000000000..0a5e2208d6 --- /dev/null +++ b/src/screens/Profile/Sections/Feed.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {ListRef} from 'view/com/util/List' +import {Feed} from 'view/com/posts/Feed' +import {EmptyState} from 'view/com/util/EmptyState' +import {FeedDescriptor} from '#/state/queries/post-feed' +import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' +import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {useQueryClient} from '@tanstack/react-query' +import {truncateAndInvalidate} from '#/state/queries/util' +import {Text} from '#/view/com/util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {isNative} from '#/platform/detection' +import {SectionRef} from './types' + +interface FeedSectionProps { + feed: FeedDescriptor + headerHeight: number + isFocused: boolean + scrollElRef: ListRef + ignoreFilterFor?: string +} +export const ProfileFeedSection = React.forwardRef< + SectionRef, + FeedSectionProps +>(function FeedSectionImpl( + {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor}, + ref, +) { + const {_} = useLingui() + const queryClient = useQueryClient() + const [hasNew, setHasNew] = React.useState(false) + const [isScrolledDown, setIsScrolledDown] = React.useState(false) + + const onScrollToTop = React.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, + })) + + const renderPostsEmpty = React.useCallback(() => { + return + }, [_]) + + return ( + + + {(isScrolledDown || hasNew) && ( + + )} + + ) +}) + +function ProfileEndOfFeed() { + const pal = usePalette('default') + + return ( + + + End of feed + + + ) +} diff --git a/src/screens/Profile/Sections/types.ts b/src/screens/Profile/Sections/types.ts new file mode 100644 index 0000000000..a7f77d648c --- /dev/null +++ b/src/screens/Profile/Sections/types.ts @@ -0,0 +1,3 @@ +export interface SectionRef { + scrollToTop: () => void +} diff --git a/src/state/queries/profile-extra-info.ts b/src/state/queries/profile-extra-info.ts deleted file mode 100644 index 8fc32c33eb..0000000000 --- a/src/state/queries/profile-extra-info.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {useQuery} from '@tanstack/react-query' - -import {getAgent} from '#/state/session' -import {STALE} from '#/state/queries' - -// TODO refactor invalidate on mutate? -export const RQKEY = (did: string) => ['profile-extra-info', did] - -/** - * Fetches some additional information for the profile screen which - * is not available in the API's ProfileView - */ -export function useProfileExtraInfoQuery(did: string) { - return useQuery({ - staleTime: STALE.MINUTES.ONE, - queryKey: RQKEY(did), - async queryFn() { - const [listsRes, feedsRes] = await Promise.all([ - getAgent().app.bsky.graph.getLists({ - actor: did, - limit: 1, - }), - getAgent().app.bsky.feed.getActorFeeds({ - actor: did, - limit: 1, - }), - ]) - return { - hasLists: listsRes.data.lists.length > 0, - hasFeedgens: feedsRes.data.feeds.length > 0, - } - }, - }) -} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx deleted file mode 100644 index 22af040f9b..0000000000 --- a/src/view/com/profile/ProfileHeader.tsx +++ /dev/null @@ -1,798 +0,0 @@ -import React, {memo, useMemo} from 'react' -import { - StyleSheet, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useNavigation} from '@react-navigation/native' -import {useQueryClient} from '@tanstack/react-query' -import { - AppBskyActorDefs, - ModerationOpts, - moderateProfile, - RichText as RichTextAPI, -} from '@atproto/api' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {NavigationProp} from 'lib/routes/types' -import {isNative, isWeb} from 'platform/detection' -import {BlurView} from '../util/BlurView' -import * as Toast from '../util/Toast' -import {LoadingPlaceholder} from '../util/LoadingPlaceholder' -import {Text} from '../util/text/Text' -import {ThemedText} from '../util/text/ThemedText' -import {RichText} from '#/components/RichText' -import {UserAvatar} from '../util/UserAvatar' -import {UserBanner} from '../util/UserBanner' -import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' -import {formatCount} from '../util/numeric/format' -import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown' -import {Link} from '../util/Link' -import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows' -import {useModalControls} from '#/state/modals' -import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox' -import { - RQKEY as profileQueryKey, - useProfileMuteMutationQueue, - useProfileBlockMutationQueue, - useProfileFollowMutationQueue, -} from '#/state/queries/profile' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {BACK_HITSLOP} from 'lib/constants' -import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {pluralize} from 'lib/strings/helpers' -import {toShareUrl} from 'lib/strings/url-helpers' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {shareUrl} from 'lib/sharing' -import {s, colors} from 'lib/styles' -import {logger} from '#/logger' -import {useSession} from '#/state/session' -import {Shadow} from '#/state/cache/types' -import {useRequireAuth} from '#/state/session' -import {LabelsOnMe} from '../util/moderation/LabelsOnMe' -import {useProfileShadow} from 'state/cache/profile-shadow' -import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' -import {NEW_REPORT_DIALOG_ENABLED} from '#/lib/build-flags' -import {atoms as a} from '#/alf' - -let ProfileHeaderLoading = (_props: {}): React.ReactNode => { - const pal = usePalette('default') - return ( - - - - - - - - - - - - ) -} -ProfileHeaderLoading = memo(ProfileHeaderLoading) -export {ProfileHeaderLoading} - -interface Props { - profile: AppBskyActorDefs.ProfileViewDetailed - descriptionRT: RichTextAPI | null - moderationOpts: ModerationOpts - hideBackButton?: boolean - isPlaceholderProfile?: boolean -} - -let ProfileHeader = ({ - profile: profileUnshadowed, - descriptionRT, - moderationOpts, - hideBackButton = false, - isPlaceholderProfile, -}: Props): React.ReactNode => { - const profile: Shadow = - useProfileShadow(profileUnshadowed) - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const {currentAccount, hasSession} = useSession() - const requireAuth = useRequireAuth() - const {_} = useLingui() - const {openModal} = useModalControls() - const {openLightbox} = useLightboxControls() - const navigation = useNavigation() - const {track} = useAnalytics() - const invalidHandle = isInvalidHandle(profile.handle) - const {isDesktop} = useWebMediaQueries() - const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) - const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) - const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) - const queryClient = useQueryClient() - const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), - [profile, moderationOpts], - ) - const control = useReportDialogControl() - - const invalidateProfileQuery = React.useCallback(() => { - queryClient.invalidateQueries({ - queryKey: profileQueryKey(profile.did), - }) - }, [queryClient, profile.did]) - - const onPressBack = React.useCallback(() => { - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, [navigation]) - - const onPressAvi = React.useCallback(() => { - const modui = moderation.ui('avatar') - if (profile.avatar && !(modui.blur && modui.noOverride)) { - openLightbox(new ProfileImageLightbox(profile)) - } - }, [openLightbox, profile, moderation]) - - const onPressFollow = () => { - requireAuth(async () => { - try { - track('ProfileHeader:FollowButtonClicked') - await queueFollow() - Toast.show( - _( - msg`Following ${sanitizeDisplayName( - profile.displayName || profile.handle, - )}`, - ), - ) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to follow', {message: String(e)}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }) - } - - const onPressUnfollow = () => { - requireAuth(async () => { - try { - track('ProfileHeader:UnfollowButtonClicked') - await queueUnfollow() - Toast.show( - _( - msg`No longer following ${sanitizeDisplayName( - profile.displayName || profile.handle, - )}`, - ), - ) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to unfollow', {message: String(e)}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }) - } - - const onPressEditProfile = React.useCallback(() => { - track('ProfileHeader:EditProfileButtonClicked') - openModal({ - name: 'edit-profile', - profile, - }) - }, [track, openModal, profile]) - - const onPressShare = React.useCallback(() => { - track('ProfileHeader:ShareButtonClicked') - shareUrl(toShareUrl(makeProfileLink(profile))) - }, [track, profile]) - - const onPressAddRemoveLists = React.useCallback(() => { - track('ProfileHeader:AddToListsButtonClicked') - openModal({ - name: 'user-add-remove-lists', - subject: profile.did, - handle: profile.handle, - displayName: profile.displayName || profile.handle, - onAdd: invalidateProfileQuery, - onRemove: invalidateProfileQuery, - }) - }, [track, profile, openModal, invalidateProfileQuery]) - - const onPressMuteAccount = React.useCallback(async () => { - track('ProfileHeader:MuteAccountButtonClicked') - try { - await queueMute() - Toast.show(_(msg`Account muted`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to mute account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, [track, queueMute, _]) - - const onPressUnmuteAccount = React.useCallback(async () => { - track('ProfileHeader:UnmuteAccountButtonClicked') - try { - await queueUnmute() - Toast.show(_(msg`Account unmuted`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to unmute account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, [track, queueUnmute, _]) - - const onPressBlockAccount = React.useCallback(async () => { - track('ProfileHeader:BlockAccountButtonClicked') - openModal({ - name: 'confirm', - title: _(msg`Block Account`), - message: _( - msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, - ), - onPressConfirm: async () => { - try { - await queueBlock() - Toast.show(_(msg`Account blocked`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to block account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, - }) - }, [track, queueBlock, openModal, _]) - - const onPressUnblockAccount = React.useCallback(async () => { - track('ProfileHeader:UnblockAccountButtonClicked') - openModal({ - name: 'confirm', - title: _(msg`Unblock Account`), - message: _( - msg`The account will be able to interact with you after unblocking.`, - ), - onPressConfirm: async () => { - try { - await queueUnblock() - Toast.show(_(msg`Account unblocked`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to unblock account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, - }) - }, [track, queueUnblock, openModal, _]) - - const onPressReportAccount = React.useCallback(() => { - track('ProfileHeader:ReportAccountButtonClicked') - if (NEW_REPORT_DIALOG_ENABLED) { - control.open() - } else { - openModal({ - name: 'report', - did: profile.did, - }) - } - }, [track, openModal, profile, control]) - - const isMe = React.useMemo( - () => currentAccount?.did === profile.did, - [currentAccount, profile], - ) - const dropdownItems: DropdownItem[] = React.useMemo(() => { - let items: DropdownItem[] = [ - { - testID: 'profileHeaderDropdownShareBtn', - label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`), - onPress: onPressShare, - icon: { - ios: { - name: 'square.and.arrow.up', - }, - android: 'ic_menu_share', - web: 'share', - }, - }, - ] - if (hasSession) { - items.push({label: 'separator'}) - items.push({ - testID: 'profileHeaderDropdownListAddRemoveBtn', - label: _(msg`Add to Lists`), - onPress: onPressAddRemoveLists, - icon: { - ios: { - name: 'list.bullet', - }, - android: 'ic_menu_add', - web: 'list', - }, - }) - if (!isMe) { - if (!profile.viewer?.blocking) { - if (!profile.viewer?.mutedByList) { - items.push({ - testID: 'profileHeaderDropdownMuteBtn', - label: profile.viewer?.muted - ? _(msg`Unmute Account`) - : _(msg`Mute Account`), - onPress: profile.viewer?.muted - ? onPressUnmuteAccount - : onPressMuteAccount, - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_lock_silent_mode', - web: 'comment-slash', - }, - }) - } - } - if (!profile.viewer?.blockingByList) { - items.push({ - testID: 'profileHeaderDropdownBlockBtn', - label: profile.viewer?.blocking - ? _(msg`Unblock Account`) - : _(msg`Block Account`), - onPress: profile.viewer?.blocking - ? onPressUnblockAccount - : onPressBlockAccount, - icon: { - ios: { - name: 'person.fill.xmark', - }, - android: 'ic_menu_close_clear_cancel', - web: 'user-slash', - }, - }) - } - items.push({ - testID: 'profileHeaderDropdownReportBtn', - label: _(msg`Report Account`), - onPress: onPressReportAccount, - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }) - } - } - return items - }, [ - isMe, - hasSession, - profile.viewer?.muted, - profile.viewer?.mutedByList, - profile.viewer?.blocking, - profile.viewer?.blockingByList, - onPressShare, - onPressUnmuteAccount, - onPressMuteAccount, - onPressUnblockAccount, - onPressBlockAccount, - onPressReportAccount, - onPressAddRemoveLists, - _, - ]) - - const blockHide = - !isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy) - const following = formatCount(profile.followsCount || 0) - const followers = formatCount(profile.followersCount || 0) - const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') - - return ( - - - - - {isPlaceholderProfile ? ( - - ) : ( - - )} - - - - {isMe ? ( - - - Edit Profile - - - ) : profile.viewer?.blocking ? ( - profile.viewer?.blockingByList ? null : ( - - - Unblock - - - ) - ) : !profile.viewer?.blockedBy ? ( - <> - {hasSession && ( - setShowSuggestedFollows(!showSuggestedFollows)} - style={[ - styles.btn, - styles.mainBtn, - pal.btn, - { - paddingHorizontal: 10, - backgroundColor: showSuggestedFollows - ? pal.colors.text - : pal.colors.backgroundLight, - }, - ]} - accessibilityRole="button" - accessibilityLabel={_( - msg`Show follows similar to ${profile.handle}`, - )} - accessibilityHint={_( - msg`Shows a list of users similar to this user.`, - )}> - - - )} - - {profile.viewer?.following ? ( - - - - Following - - - ) : ( - - - - Follow - - - )} - - ) : null} - {dropdownItems?.length ? ( - - - - - - ) : undefined} - - - - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - - - - {profile.viewer?.followedBy && !blockHide ? ( - - - Follows you - - - ) : undefined} - - {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} - - - {!isPlaceholderProfile && !blockHide && ( - <> - - - track(`ProfileHeader:FollowersButtonClicked`, { - handle: profile.handle, - }) - } - asAnchor - accessibilityLabel={`${followers} ${pluralizedFollowers}`} - accessibilityHint={_(msg`Opens followers list`)}> - - {followers}{' '} - - - {pluralizedFollowers} - - - - track(`ProfileHeader:FollowsButtonClicked`, { - handle: profile.handle, - }) - } - asAnchor - accessibilityLabel={_(msg`${following} following`)} - accessibilityHint={_(msg`Opens following list`)}> - - - {following}{' '} - - - following - - - - - {formatCount(profile.postsCount || 0)}{' '} - - {pluralize(profile.postsCount || 0, 'post')} - - - - {descriptionRT && !moderation.ui('profileView').blur ? ( - - - - ) : undefined} - - )} - - {isMe && ( - - )} - - - {showSuggestedFollows && ( - { - if (showSuggestedFollows) { - setShowSuggestedFollows(false) - } else { - track('ProfileHeader:SuggestedFollowsOpened') - setShowSuggestedFollows(true) - } - }} - /> - )} - - {!isDesktop && !hideBackButton && ( - - - - - - - - )} - - - - - - - ) -} -ProfileHeader = memo(ProfileHeader) -export {ProfileHeader} - -const styles = StyleSheet.create({ - banner: { - width: '100%', - height: 120, - }, - backBtnWrapper: { - position: 'absolute', - top: 10, - left: 10, - width: 30, - height: 30, - overflow: 'hidden', - borderRadius: 15, - // @ts-ignore web only - cursor: 'pointer', - }, - backBtn: { - width: 30, - height: 30, - borderRadius: 15, - alignItems: 'center', - justifyContent: 'center', - }, - avi: { - position: 'absolute', - top: 110, - left: 10, - width: 84, - height: 84, - borderRadius: 42, - borderWidth: 2, - }, - content: { - paddingTop: 8, - paddingHorizontal: 14, - paddingBottom: 4, - }, - - buttonsLine: { - flexDirection: 'row', - marginLeft: 'auto', - marginBottom: 12, - }, - primaryBtn: { - backgroundColor: colors.blue3, - paddingHorizontal: 24, - paddingVertical: 6, - }, - mainBtn: { - paddingHorizontal: 24, - }, - secondaryBtn: { - paddingHorizontal: 14, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 7, - borderRadius: 50, - marginLeft: 6, - }, - title: {lineHeight: 38}, - - // Word wrapping appears fine on - // mobile but overflows on desktop - handle: isNative - ? {} - : { - // @ts-ignore web only -prf - wordBreak: 'break-all', - }, - invalidHandle: { - borderWidth: 1, - borderRadius: 4, - paddingHorizontal: 4, - }, - - handleLine: { - flexDirection: 'row', - marginBottom: 8, - }, - - metricsLine: { - flexDirection: 'row', - marginBottom: 8, - }, - - description: { - marginBottom: 8, - }, - - detailLine: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 5, - }, - - pill: { - borderRadius: 4, - paddingHorizontal: 6, - paddingVertical: 2, - }, - - br40: {borderRadius: 40}, - br50: {borderRadius: 50}, -}) diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx index f548b1fbe7..ca7763ab91 100644 --- a/src/view/screens/DebugMod.tsx +++ b/src/view/screens/DebugMod.tsx @@ -38,7 +38,7 @@ import { ChevronTop_Stroke2_Corner0_Rounded as ChevronTop, } from '#/components/icons/Chevron' import {ScreenHider} from '../com/util/moderation/ScreenHider' -import {ProfileHeader} from '../com/profile/ProfileHeader' +import {ProfileHeader} from '#/screens/Profile/Header' import {ProfileCard} from '../com/profile/ProfileCard' import {FeedItem} from '../com/posts/FeedItem' import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem' diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 16cac657f5..0d795cab0b 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react' -import {StyleSheet, View} from 'react-native' +import {StyleSheet} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import { AppBskyActorDefs, @@ -7,54 +7,38 @@ import { ModerationOpts, RichText as RichTextAPI, } from '@atproto/api' -import {msg, Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {CenteredView, ScrollView} from '../com/util/Views' +import {CenteredView} from '../com/util/Views' import {ListRef} from '../com/util/List' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' -import {Feed} from 'view/com/posts/Feed' import {ProfileLists} from '../com/lists/ProfileLists' import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens' -import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ErrorScreen} from '../com/util/error/ErrorScreen' -import {EmptyState} from '../com/util/EmptyState' import {FAB} from '../com/util/fab/FAB' import {s, colors} from 'lib/styles' import {useAnalytics} from 'lib/analytics/analytics' import {ComposeIcon2} from 'lib/icons' import {useSetTitle} from 'lib/hooks/useSetTitle' import {combinedDisplayName} from 'lib/strings/display-names' -import { - FeedDescriptor, - resetProfilePostsQueries, -} from '#/state/queries/post-feed' +import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useProfileQuery} from '#/state/queries/profile' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useSession, getAgent} from '#/state/session' import {useModerationOpts} from '#/state/queries/preferences' -import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info' import {useLabelerInfoQuery} from '#/state/queries/labeler' -import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {cleanError} from '#/lib/strings/errors' -import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' -import {useQueryClient} from '@tanstack/react-query' import {useComposerControls} from '#/state/shell/composer' import {listenSoftReset} from '#/state/events' -import {truncateAndInvalidate} from '#/state/queries/util' -import {Text} from '#/view/com/util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {isNative} from '#/platform/detection' import {isInvalidHandle} from '#/lib/strings/handles' -import {useTheme, atoms as a} from '#/alf' -import * as ModerationServiceCard from '#/components/ModerationServiceCard' -import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand' +import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileContentFiltersSection} from '#/screens/Profile/Sections/ContentFilters' -import {ProfileHeader as ProfileHeaderV2} from '#/screens/Profile/Header' +import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' interface SectionRef { scrollToTop: () => void @@ -119,7 +103,7 @@ export function ProfileScreen({route}: Props) { } if (profile && moderationOpts) { return ( - (null) - const repliesSectionRef = React.useRef(null) - const mediaSectionRef = React.useRef(null) - const likesSectionRef = React.useRef(null) - const feedsSectionRef = React.useRef(null) - const listsSectionRef = React.useRef(null) - const moderationSectionRef = React.useRef(null) - - useSetTitle(combinedDisplayName(profile)) - - const description = profile.description ?? '' - const hasDescription = description !== '' - const [descriptionRT, isResolvingDescriptionRT] = useRichText(description) - const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT - const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), - [profile, moderationOpts], - ) - - const isMe = profile.did === currentAccount?.did - const showRepliesTab = hasSession - const showLikesTab = isMe - const showFeedsTab = hasSession && (isMe || extraInfoQuery.data?.hasFeedgens) - const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists) - const showModerationTab = - hasSession && (isMe || true) /* TODO true should be associated modservice */ - - const sectionTitles = useMemo(() => { - return [ - _(msg`Posts`), - showRepliesTab ? _(msg`Replies`) : undefined, - _(msg`Media`), - showLikesTab ? _(msg`Likes`) : undefined, - showFeedsTab ? _(msg`Feeds`) : undefined, - showListsTab ? _(msg`Lists`) : undefined, - showModerationTab ? _(msg`Moderation`) : undefined, - ].filter(Boolean) as string[] - }, [ - showRepliesTab, - showLikesTab, - showFeedsTab, - showListsTab, - showModerationTab, - _, - ]) - - let nextIndex = 0 - const postsIndex = nextIndex++ - let repliesIndex: number | null = null - if (showRepliesTab) { - repliesIndex = nextIndex++ - } - const mediaIndex = nextIndex++ - let likesIndex: number | null = null - if (showLikesTab) { - likesIndex = nextIndex++ - } - let feedsIndex: number | null = null - if (showFeedsTab) { - feedsIndex = nextIndex++ - } - let listsIndex: number | null = null - if (showListsTab) { - listsIndex = nextIndex++ - } - let moderationIndex: number | null = null - if (showModerationTab) { - moderationIndex = nextIndex++ - } - - const scrollSectionToTop = React.useCallback( - (index: number) => { - if (index === postsIndex) { - postsSectionRef.current?.scrollToTop() - } else if (index === repliesIndex) { - repliesSectionRef.current?.scrollToTop() - } else if (index === mediaIndex) { - mediaSectionRef.current?.scrollToTop() - } else if (index === likesIndex) { - likesSectionRef.current?.scrollToTop() - } else if (index === feedsIndex) { - feedsSectionRef.current?.scrollToTop() - } else if (index === listsIndex) { - listsSectionRef.current?.scrollToTop() - } else if (index === moderationIndex) { - moderationSectionRef.current?.scrollToTop() - } - }, - [ - postsIndex, - repliesIndex, - mediaIndex, - likesIndex, - feedsIndex, - listsIndex, - moderationIndex, - ], - ) - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - screen('Profile') - return listenSoftReset(() => { - scrollSectionToTop(currentPage) - }) - }, [setMinimalShellMode, screen, currentPage, scrollSectionToTop]), - ) - - useFocusEffect( - React.useCallback(() => { - setDrawerSwipeDisabled(currentPage > 0) - return () => { - setDrawerSwipeDisabled(false) - } - }, [setDrawerSwipeDisabled, currentPage]), - ) - - // events - // = - - const onPressCompose = React.useCallback(() => { - track('ProfileScreen:PressCompose') - const mention = - profile.handle === currentAccount?.handle || - isInvalidHandle(profile.handle) - ? undefined - : profile.handle - openComposer({mention}) - }, [openComposer, currentAccount, track, profile]) - - const onPageSelected = React.useCallback( - (i: number) => { - setCurrentPage(i) - }, - [setCurrentPage], - ) - - const onCurrentPageSelected = React.useCallback( - (index: number) => { - scrollSectionToTop(index) - }, - [scrollSectionToTop], - ) - - // rendering - // = - - const renderHeader = React.useCallback(() => { - return ( - - ) - }, [ - profile, - descriptionRT, - hasDescription, - moderationOpts, - hideBackButton, - showPlaceholder, - ]) - - return ( - - - {({headerHeight, isFocused, scrollElRef}) => ( - - )} - {showRepliesTab - ? ({headerHeight, isFocused, scrollElRef}) => ( - - ) - : null} - {({headerHeight, isFocused, scrollElRef}) => ( - - )} - {showLikesTab - ? ({headerHeight, isFocused, scrollElRef}) => ( - - ) - : null} - {showFeedsTab - ? ({headerHeight, isFocused, scrollElRef}) => ( - - ) - : null} - {showListsTab - ? ({headerHeight, isFocused, scrollElRef}) => ( - - ) - : null} - {showModerationTab - ? ({headerHeight, isFocused, scrollElRef}) => ( - - ) - : null} - - {hasSession && ( - } - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - )} - - ) -} - -function ProfileScreenLoadedV2({ - profile: profileUnshadowed, - isPlaceholderProfile, - moderationOpts, - hideBackButton, -}: { - profile: AppBskyActorDefs.ProfileViewDetailed - moderationOpts: ModerationOpts - hideBackButton: boolean - isPlaceholderProfile: boolean }) { const profile = useProfileShadow(profileUnshadowed) const {hasSession, currentAccount} = useSession() @@ -618,7 +310,7 @@ function ProfileScreenLoadedV2({ const renderHeader = React.useCallback(() => { return ( - ( - ( - ( - ( - ( - function FeedSectionImpl( - {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor}, - ref, - ) { - const {_} = useLingui() - const queryClient = useQueryClient() - const [hasNew, setHasNew] = React.useState(false) - const [isScrolledDown, setIsScrolledDown] = React.useState(false) - - const onScrollToTop = React.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, - })) - - const renderPostsEmpty = React.useCallback(() => { - return - }, [_]) - - return ( - - - {(isScrolledDown || hasNew) && ( - - )} - - ) - }, -) - -function ProfileEndOfFeed() { - const pal = usePalette('default') - - return ( - - - End of feed - - - ) -} - -function ModerationSection({did}: {did: string}) { - const t = useTheme() - return ( - - ( - - {ctx => ( - - - - - - - )} - - )} - /> - - ) -} - function useRichText(text: string): [RichTextAPI, boolean] { const [prevText, setPrevText] = React.useState(text) const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))