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}))