diff options
Diffstat (limited to 'src/view/com/profile/ProfileHeader.tsx')
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 505 |
1 files changed, 283 insertions, 222 deletions
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 1a1d38e4b..8058551c2 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -1,5 +1,4 @@ -import React from 'react' -import {observer} from 'mobx-react-lite' +import React, {memo} from 'react' import { StyleSheet, TouchableOpacity, @@ -8,15 +7,17 @@ import { } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' +import {useQueryClient} from '@tanstack/react-query' +import { + AppBskyActorDefs, + ProfileModeration, + 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 {ProfileModel} from 'state/models/content/profile' -import {useStores} from 'state/index' -import {ProfileImageLightbox} from 'state/models/ui/shell' -import {pluralize} from 'lib/strings/helpers' -import {toShareUrl} from 'lib/strings/url-helpers' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {s, colors} from 'lib/styles' import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {Text} from '../util/text/Text' @@ -25,32 +26,45 @@ import {RichText} from '../util/text/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 {NavigationProp} from 'lib/routes/types' -import {isNative} from 'platform/detection' -import {FollowState} from 'state/models/cache/my-follows' -import {shareUrl} from 'lib/sharing' -import {formatCount} from '../util/numeric/format' -import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown' import {BACK_HITSLOP} from 'lib/constants' import {isInvalidHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' -import {Link} from '../util/Link' -import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows' +import {pluralize} from 'lib/strings/helpers' +import {toShareUrl} from 'lib/strings/url-helpers' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +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' interface Props { - view: ProfileModel - onRefreshAll: () => void + profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null + moderation: ProfileModeration | null hideBackButton?: boolean isProfilePreview?: boolean } -export const ProfileHeader = observer(function ProfileHeaderImpl({ - view, - onRefreshAll, +export function ProfileHeader({ + profile, + moderation, hideBackButton = false, isProfilePreview, }: Props) { @@ -58,7 +72,7 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({ // loading // = - if (!view || !view.hasLoaded) { + if (!profile || !moderation) { return ( <View style={pal.view}> <LoadingPlaceholder width="100%" height={153} /> @@ -70,54 +84,65 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({ <View style={[styles.buttonsLine]}> <LoadingPlaceholder width={167} height={31} style={styles.br50} /> </View> - <View> - <Text type="title-2xl" style={[pal.text, styles.title]}> - {sanitizeDisplayName( - view.displayName || sanitizeHandle(view.handle), - )} - </Text> - </View> </View> </View> ) } - // error - // = - if (view.hasError) { - return ( - <View testID="profileHeaderHasError"> - <Text>{view.error}</Text> - </View> - ) - } - // loaded // = return ( <ProfileHeaderLoaded - view={view} - onRefreshAll={onRefreshAll} + profile={profile} + moderation={moderation} hideBackButton={hideBackButton} isProfilePreview={isProfilePreview} /> ) -}) +} + +interface LoadedProps { + profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> + moderation: ProfileModeration + hideBackButton?: boolean + isProfilePreview?: boolean +} -const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ - view, - onRefreshAll, +let ProfileHeaderLoaded = ({ + profile, + moderation, hideBackButton = false, isProfilePreview, -}: Props) { +}: LoadedProps): React.ReactNode => { const pal = usePalette('default') const palInverted = usePalette('inverted') - const store = useStores() + const {currentAccount, hasSession} = useSession() + const requireAuth = useRequireAuth() + const {_} = useLingui() + const {openModal} = useModalControls() + const {openLightbox} = useLightboxControls() const navigation = useNavigation<NavigationProp>() const {track} = useAnalytics() - const invalidHandle = isInvalidHandle(view.handle) + const invalidHandle = isInvalidHandle(profile.handle) const {isDesktop} = useWebMediaQueries() const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) + const descriptionRT = React.useMemo( + () => + profile.description + ? new RichTextAPI({text: profile.description}) + : undefined, + [profile], + ) + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) + const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) + const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) + const queryClient = useQueryClient() + + const invalidateProfileQuery = React.useCallback(() => { + queryClient.invalidateQueries({ + queryKey: profileQueryKey(profile.did), + }) + }, [queryClient, profile.did]) const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { @@ -129,144 +154,162 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ const onPressAvi = React.useCallback(() => { if ( - view.avatar && - !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) + profile.avatar && + !(moderation.avatar.blur && moderation.avatar.noOverride) ) { - store.shell.openLightbox(new ProfileImageLightbox(view)) + openLightbox(new ProfileImageLightbox(profile)) } - }, [store, view]) + }, [openLightbox, profile, moderation]) - const onPressToggleFollow = React.useCallback(() => { - view?.toggleFollowing().then( - () => { - setShowSuggestedFollows(Boolean(view.viewer.following)) + const onPressFollow = () => { + requireAuth(async () => { + try { + track('ProfileHeader:FollowButtonClicked') + await queueFollow() Toast.show( - `${ - view.viewer.following ? 'Following' : 'No longer following' - } ${sanitizeDisplayName(view.displayName || view.handle)}`, + `Following ${sanitizeDisplayName( + profile.displayName || profile.handle, + )}`, ) - track( - view.viewer.following - ? 'ProfileHeader:FollowButtonClicked' - : 'ProfileHeader:UnfollowButtonClicked', + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to follow', {error: String(e)}) + Toast.show(`There was an issue! ${e.toString()}`) + } + } + }) + } + + const onPressUnfollow = () => { + requireAuth(async () => { + try { + track('ProfileHeader:UnfollowButtonClicked') + await queueUnfollow() + Toast.show( + `No longer following ${sanitizeDisplayName( + profile.displayName || profile.handle, + )}`, ) - }, - err => logger.error('Failed to toggle follow', {error: err}), - ) - }, [track, view, setShowSuggestedFollows]) + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to unfollow', {error: String(e)}) + Toast.show(`There was an issue! ${e.toString()}`) + } + } + }) + } const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') - store.shell.openModal({ + openModal({ name: 'edit-profile', - profileView: view, - onUpdate: onRefreshAll, + profile, }) - }, [track, store, view, onRefreshAll]) - - const trackPress = React.useCallback( - (f: 'Followers' | 'Follows') => { - track(`ProfileHeader:${f}ButtonClicked`, { - handle: view.handle, - }) - }, - [track, view], - ) + }, [track, openModal, profile]) const onPressShare = React.useCallback(() => { track('ProfileHeader:ShareButtonClicked') - const url = toShareUrl(makeProfileLink(view)) - shareUrl(url) - }, [track, view]) + shareUrl(toShareUrl(makeProfileLink(profile))) + }, [track, profile]) const onPressAddRemoveLists = React.useCallback(() => { track('ProfileHeader:AddToListsButtonClicked') - store.shell.openModal({ + openModal({ name: 'user-add-remove-lists', - subject: view.did, - displayName: view.displayName || view.handle, + subject: profile.did, + displayName: profile.displayName || profile.handle, + onAdd: invalidateProfileQuery, + onRemove: invalidateProfileQuery, }) - }, [track, view, store]) + }, [track, profile, openModal, invalidateProfileQuery]) const onPressMuteAccount = React.useCallback(async () => { track('ProfileHeader:MuteAccountButtonClicked') try { - await view.muteAccount() + await queueMute() Toast.show('Account muted') } catch (e: any) { - logger.error('Failed to mute account', {error: e}) - Toast.show(`There was an issue! ${e.toString()}`) + if (e?.name !== 'AbortError') { + logger.error('Failed to mute account', {error: e}) + Toast.show(`There was an issue! ${e.toString()}`) + } } - }, [track, view]) + }, [track, queueMute]) const onPressUnmuteAccount = React.useCallback(async () => { track('ProfileHeader:UnmuteAccountButtonClicked') try { - await view.unmuteAccount() + await queueUnmute() Toast.show('Account unmuted') } catch (e: any) { - logger.error('Failed to unmute account', {error: e}) - Toast.show(`There was an issue! ${e.toString()}`) + if (e?.name !== 'AbortError') { + logger.error('Failed to unmute account', {error: e}) + Toast.show(`There was an issue! ${e.toString()}`) + } } - }, [track, view]) + }, [track, queueUnmute]) const onPressBlockAccount = React.useCallback(async () => { track('ProfileHeader:BlockAccountButtonClicked') - store.shell.openModal({ + openModal({ name: 'confirm', - title: 'Block Account', - message: - 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.', + title: _(msg`Block Account`), + message: _( + msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, + ), onPressConfirm: async () => { try { - await view.blockAccount() - onRefreshAll() + await queueBlock() Toast.show('Account blocked') } catch (e: any) { - logger.error('Failed to block account', {error: e}) - Toast.show(`There was an issue! ${e.toString()}`) + if (e?.name !== 'AbortError') { + logger.error('Failed to block account', {error: e}) + Toast.show(`There was an issue! ${e.toString()}`) + } } }, }) - }, [track, view, store, onRefreshAll]) + }, [track, queueBlock, openModal, _]) const onPressUnblockAccount = React.useCallback(async () => { track('ProfileHeader:UnblockAccountButtonClicked') - store.shell.openModal({ + openModal({ name: 'confirm', - title: 'Unblock Account', - message: - 'The account will be able to interact with you after unblocking.', + title: _(msg`Unblock Account`), + message: _( + msg`The account will be able to interact with you after unblocking.`, + ), onPressConfirm: async () => { try { - await view.unblockAccount() - onRefreshAll() + await queueUnblock() Toast.show('Account unblocked') } catch (e: any) { - logger.error('Failed to unblock account', {error: e}) - Toast.show(`There was an issue! ${e.toString()}`) + if (e?.name !== 'AbortError') { + logger.error('Failed to unblock account', {error: e}) + Toast.show(`There was an issue! ${e.toString()}`) + } } }, }) - }, [track, view, store, onRefreshAll]) + }, [track, queueUnblock, openModal, _]) const onPressReportAccount = React.useCallback(() => { track('ProfileHeader:ReportAccountButtonClicked') - store.shell.openModal({ + openModal({ name: 'report', - did: view.did, + did: profile.did, }) - }, [track, store, view]) + }, [track, openModal, profile]) const isMe = React.useMemo( - () => store.me.did === view.did, - [store.me.did, view.did], + () => currentAccount?.did === profile.did, + [currentAccount, profile], ) const dropdownItems: DropdownItem[] = React.useMemo(() => { let items: DropdownItem[] = [ { testID: 'profileHeaderDropdownShareBtn', - label: 'Share', + label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`), onPress: onPressShare, icon: { ios: { @@ -277,71 +320,81 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ }, }, ] - items.push({label: 'separator'}) - items.push({ - testID: 'profileHeaderDropdownListAddRemoveBtn', - label: 'Add to Lists', - onPress: onPressAddRemoveLists, - icon: { - ios: { - name: 'list.bullet', + 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', }, - android: 'ic_menu_add', - web: 'list', - }, - }) - if (!isMe) { - if (!view.viewer.blocking) { - items.push({ - testID: 'profileHeaderDropdownMuteBtn', - label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', - onPress: view.viewer.muted - ? onPressUnmuteAccount - : onPressMuteAccount, - icon: { - ios: { - name: 'speaker.slash', + }) + 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', }, - android: 'ic_lock_silent_mode', - web: 'comment-slash', - }, - }) - } - if (!view.viewer.blockingByList) { + }) + } items.push({ - testID: 'profileHeaderDropdownBlockBtn', - label: view.viewer.blocking ? 'Unblock Account' : 'Block Account', - onPress: view.viewer.blocking - ? onPressUnblockAccount - : onPressBlockAccount, + testID: 'profileHeaderDropdownReportBtn', + label: _(msg`Report Account`), + onPress: onPressReportAccount, icon: { ios: { - name: 'person.fill.xmark', + name: 'exclamationmark.triangle', }, - android: 'ic_menu_close_clear_cancel', - web: 'user-slash', + android: 'ic_menu_report_image', + web: 'circle-exclamation', }, }) } - items.push({ - testID: 'profileHeaderDropdownReportBtn', - label: 'Report Account', - onPress: onPressReportAccount, - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }) } return items }, [ isMe, - view.viewer.muted, - view.viewer.blocking, - view.viewer.blockingByList, + hasSession, + profile.viewer?.muted, + profile.viewer?.mutedByList, + profile.viewer?.blocking, + profile.viewer?.blockingByList, onPressShare, onPressUnmuteAccount, onPressMuteAccount, @@ -349,16 +402,18 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ onPressBlockAccount, onPressReportAccount, onPressAddRemoveLists, + _, ]) - const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy) - const following = formatCount(view.followsCount) - const followers = formatCount(view.followersCount) - const pluralizedFollowers = pluralize(view.followersCount, 'follower') + 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 ( <View style={pal.view}> - <UserBanner banner={view.banner} moderation={view.moderation.avatar} /> + <UserBanner banner={profile.banner} moderation={moderation.avatar} /> <View style={styles.content}> <View style={[styles.buttonsLine]}> {isMe ? ( @@ -367,29 +422,29 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ onPress={onPressEditProfile} style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" - accessibilityLabel="Edit profile" + accessibilityLabel={_(msg`Edit profile`)} accessibilityHint="Opens editor for profile display name, avatar, background image, and description"> <Text type="button" style={pal.text}> - Edit Profile + <Trans>Edit Profile</Trans> </Text> </TouchableOpacity> - ) : view.viewer.blocking ? ( - view.viewer.blockingByList ? null : ( + ) : profile.viewer?.blocking ? ( + profile.viewer?.blockingByList ? null : ( <TouchableOpacity testID="unblockBtn" onPress={onPressUnblockAccount} style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" - accessibilityLabel="Unblock" + accessibilityLabel={_(msg`Unblock`)} accessibilityHint=""> <Text type="button" style={[pal.text, s.bold]}> - Unblock + <Trans>Unblock</Trans> </Text> </TouchableOpacity> ) - ) : !view.viewer.blockedBy ? ( + ) : !profile.viewer?.blockedBy ? ( <> - {!isProfilePreview && ( + {!isProfilePreview && hasSession && ( <TouchableOpacity testID="suggestedFollowsBtn" onPress={() => setShowSuggestedFollows(!showSuggestedFollows)} @@ -405,7 +460,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ }, ]} accessibilityRole="button" - accessibilityLabel={`Show follows similar to ${view.handle}`} + accessibilityLabel={`Show follows similar to ${profile.handle}`} accessibilityHint={`Shows a list of users similar to this user.`}> <FontAwesomeIcon icon="user-plus" @@ -413,7 +468,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ pal.text, { color: showSuggestedFollows - ? colors.white + ? pal.textInverted.color : pal.text.color, }, ]} @@ -422,38 +477,37 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ </TouchableOpacity> )} - {store.me.follows.getFollowState(view.did) === - FollowState.Following ? ( + {profile.viewer?.following ? ( <TouchableOpacity testID="unfollowBtn" - onPress={onPressToggleFollow} + onPress={onPressUnfollow} style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" - accessibilityLabel={`Unfollow ${view.handle}`} - accessibilityHint={`Hides posts from ${view.handle} in your feed`}> + accessibilityLabel={`Unfollow ${profile.handle}`} + accessibilityHint={`Hides posts from ${profile.handle} in your feed`}> <FontAwesomeIcon icon="check" style={[pal.text, s.mr5]} size={14} /> <Text type="button" style={pal.text}> - Following + <Trans>Following</Trans> </Text> </TouchableOpacity> ) : ( <TouchableOpacity testID="followBtn" - onPress={onPressToggleFollow} + onPress={onPressFollow} style={[styles.btn, styles.mainBtn, palInverted.view]} accessibilityRole="button" - accessibilityLabel={`Follow ${view.handle}`} - accessibilityHint={`Shows posts from ${view.handle} in your feed`}> + accessibilityLabel={`Follow ${profile.handle}`} + accessibilityHint={`Shows posts from ${profile.handle} in your feed`}> <FontAwesomeIcon icon="plus" style={[palInverted.text, s.mr5]} /> <Text type="button" style={[palInverted.text, s.bold]}> - Follow + <Trans>Follow</Trans> </Text> </TouchableOpacity> )} @@ -463,7 +517,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ <NativeDropdown testID="profileHeaderDropdownBtn" items={dropdownItems} - accessibilityLabel="More options" + accessibilityLabel={_(msg`More options`)} accessibilityHint=""> <View style={[styles.btn, styles.secondaryBtn, pal.btn]}> <FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} /> @@ -477,16 +531,16 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ type="title-2xl" style={[pal.text, styles.title]}> {sanitizeDisplayName( - view.displayName || sanitizeHandle(view.handle), - view.moderation.profile, + profile.displayName || sanitizeHandle(profile.handle), + moderation.profile, )} </Text> </View> <View style={styles.handleLine}> - {view.viewer.followedBy && !blockHide ? ( + {profile.viewer?.followedBy && !blockHide ? ( <View style={[styles.pill, pal.btn, s.mr5]}> <Text type="xs" style={[pal.text]}> - Follows you + <Trans>Follows you</Trans> </Text> </View> ) : undefined} @@ -498,7 +552,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ invalidHandle ? styles.invalidHandle : undefined, styles.handle, ]}> - {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`} + {invalidHandle ? '⚠Invalid Handle' : `@${profile.handle}`} </ThemedText> </View> {!blockHide && ( @@ -507,8 +561,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ <Link testID="profileHeaderFollowersButton" style={[s.flexRow, s.mr10]} - href={makeProfileLink(view, 'followers')} - onPressOut={() => trackPress('Followers')} + href={makeProfileLink(profile, 'followers')} + onPressOut={() => + track(`ProfileHeader:FollowersButtonClicked`, { + handle: profile.handle, + }) + } asAnchor accessibilityLabel={`${followers} ${pluralizedFollowers}`} accessibilityHint={'Opens followers list'}> @@ -522,8 +580,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ <Link testID="profileHeaderFollowsButton" style={[s.flexRow, s.mr10]} - href={makeProfileLink(view, 'follows')} - onPressOut={() => trackPress('Follows')} + href={makeProfileLink(profile, 'follows')} + onPressOut={() => + track(`ProfileHeader:FollowsButtonClicked`, { + handle: profile.handle, + }) + } asAnchor accessibilityLabel={`${following} following`} accessibilityHint={'Opens following list'}> @@ -531,34 +593,32 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ {following}{' '} </Text> <Text type="md" style={[pal.textLight]}> - following + <Trans>following</Trans> </Text> </Link> <Text type="md" style={[s.bold, pal.text]}> - {formatCount(view.postsCount)}{' '} + {formatCount(profile.postsCount || 0)}{' '} <Text type="md" style={[pal.textLight]}> - {pluralize(view.postsCount, 'post')} + {pluralize(profile.postsCount || 0, 'post')} </Text> </Text> </View> - {view.description && - view.descriptionRichText && - !view.moderation.profile.blur ? ( + {descriptionRT && !moderation.profile.blur ? ( <RichText testID="profileHeaderDescription" style={[styles.description, pal.text]} numberOfLines={15} - richText={view.descriptionRichText} + richText={descriptionRT} /> ) : undefined} </> )} - <ProfileHeaderAlerts moderation={view.moderation} /> + <ProfileHeaderAlerts moderation={moderation} /> </View> {!isProfilePreview && ( <ProfileHeaderSuggestedFollows - actorDid={view.did} + actorDid={profile.did} active={showSuggestedFollows} requestDismiss={() => setShowSuggestedFollows(!showSuggestedFollows)} /> @@ -570,7 +630,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ onPress={onPressBack} hitSlop={BACK_HITSLOP} accessibilityRole="button" - accessibilityLabel="Back" + accessibilityLabel={_(msg`Back`)} accessibilityHint=""> <View style={styles.backBtnWrapper}> <BlurView style={styles.backBtn} blurType="dark"> @@ -583,20 +643,21 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ testID="profileHeaderAviButton" onPress={onPressAvi} accessibilityRole="image" - accessibilityLabel={`View ${view.handle}'s avatar`} + accessibilityLabel={`View ${profile.handle}'s avatar`} accessibilityHint=""> <View style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> <UserAvatar size={80} - avatar={view.avatar} - moderation={view.moderation.avatar} + avatar={profile.avatar} + moderation={moderation.avatar} /> </View> </TouchableWithoutFeedback> </View> ) -}) +} +ProfileHeaderLoaded = memo(ProfileHeaderLoaded) const styles = StyleSheet.create({ banner: { |