diff options
Diffstat (limited to 'src/view/com/profile')
-rw-r--r-- | src/view/com/profile/FollowButton.tsx | 94 | ||||
-rw-r--r-- | src/view/com/profile/ProfileCard.tsx | 223 | ||||
-rw-r--r-- | src/view/com/profile/ProfileFollowers.tsx | 2 | ||||
-rw-r--r-- | src/view/com/profile/ProfileFollows.tsx | 2 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 898 |
5 files changed, 604 insertions, 615 deletions
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx index fcb2225da..6f6286e69 100644 --- a/src/view/com/profile/FollowButton.tsx +++ b/src/view/com/profile/FollowButton.tsx @@ -6,56 +6,54 @@ import {useStores} from 'state/index' import * as Toast from '../util/Toast' import {FollowState} from 'state/models/cache/my-follows' -export const FollowButton = observer( - ({ - unfollowedType = 'inverted', - followedType = 'default', - did, - onToggleFollow, - }: { - unfollowedType?: ButtonType - followedType?: ButtonType - did: string - onToggleFollow?: (v: boolean) => void - }) => { - const store = useStores() - const followState = store.me.follows.getFollowState(did) +export const FollowButton = observer(function FollowButtonImpl({ + unfollowedType = 'inverted', + followedType = 'default', + did, + onToggleFollow, +}: { + unfollowedType?: ButtonType + followedType?: ButtonType + did: string + onToggleFollow?: (v: boolean) => void +}) { + const store = useStores() + const followState = store.me.follows.getFollowState(did) - if (followState === FollowState.Unknown) { - return <View /> - } + if (followState === FollowState.Unknown) { + return <View /> + } - const onToggleFollowInner = async () => { - const updatedFollowState = await store.me.follows.fetchFollowState(did) - if (updatedFollowState === FollowState.Following) { - try { - await store.agent.deleteFollow(store.me.follows.getFollowUri(did)) - store.me.follows.removeFollow(did) - onToggleFollow?.(false) - } catch (e: any) { - store.log.error('Failed to delete follow', e) - Toast.show('An issue occurred, please try again.') - } - } else if (updatedFollowState === FollowState.NotFollowing) { - try { - const res = await store.agent.follow(did) - store.me.follows.addFollow(did, res.uri) - onToggleFollow?.(true) - } catch (e: any) { - store.log.error('Failed to create follow', e) - Toast.show('An issue occurred, please try again.') - } + const onToggleFollowInner = async () => { + const updatedFollowState = await store.me.follows.fetchFollowState(did) + if (updatedFollowState === FollowState.Following) { + try { + await store.agent.deleteFollow(store.me.follows.getFollowUri(did)) + store.me.follows.removeFollow(did) + onToggleFollow?.(false) + } catch (e: any) { + store.log.error('Failed to delete follow', e) + Toast.show('An issue occurred, please try again.') + } + } else if (updatedFollowState === FollowState.NotFollowing) { + try { + const res = await store.agent.follow(did) + store.me.follows.addFollow(did, res.uri) + onToggleFollow?.(true) + } catch (e: any) { + store.log.error('Failed to create follow', e) + Toast.show('An issue occurred, please try again.') } } + } - return ( - <Button - type={ - followState === FollowState.Following ? followedType : unfollowedType - } - onPress={onToggleFollowInner} - label={followState === FollowState.Following ? 'Unfollow' : 'Follow'} - /> - ) - }, -) + return ( + <Button + type={ + followState === FollowState.Following ? followedType : unfollowedType + } + onPress={onToggleFollowInner} + label={followState === FollowState.Following ? 'Unfollow' : 'Follow'} + /> + ) +}) diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 771785ee9..e0c8ad21a 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -22,89 +22,82 @@ import { getModerationCauseKey, } from 'lib/moderation' -export const ProfileCard = observer( - ({ - testID, - profile, - noBg, - noBorder, - followers, - renderButton, - }: { - testID?: string - profile: AppBskyActorDefs.ProfileViewBasic - noBg?: boolean - noBorder?: boolean - followers?: AppBskyActorDefs.ProfileView[] | undefined - renderButton?: ( - profile: AppBskyActorDefs.ProfileViewBasic, - ) => React.ReactNode - }) => { - const store = useStores() - const pal = usePalette('default') +export const ProfileCard = observer(function ProfileCardImpl({ + testID, + profile, + noBg, + noBorder, + followers, + renderButton, +}: { + testID?: string + profile: AppBskyActorDefs.ProfileViewBasic + noBg?: boolean + noBorder?: boolean + followers?: AppBskyActorDefs.ProfileView[] | undefined + renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode +}) { + const store = useStores() + const pal = usePalette('default') - const moderation = moderateProfile( - profile, - store.preferences.moderationOpts, - ) + const moderation = moderateProfile(profile, store.preferences.moderationOpts) - return ( - <Link - testID={testID} - style={[ - styles.outer, - pal.border, - noBorder && styles.outerNoBorder, - !noBg && pal.view, - ]} - href={makeProfileLink(profile)} - title={profile.handle} - asAnchor - anchorNoUnderline> - <View style={styles.layout}> - <View style={styles.layoutAvi}> - <UserAvatar - size={40} - avatar={profile.avatar} - moderation={moderation.avatar} - /> - </View> - <View style={styles.layoutContent}> - <Text - type="lg" - style={[s.bold, pal.text]} - numberOfLines={1} - lineHeight={1.2}> - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, - )} - </Text> - <Text type="md" style={[pal.textLight]} numberOfLines={1}> - {sanitizeHandle(profile.handle, '@')} - </Text> - <ProfileCardPills - followedBy={!!profile.viewer?.followedBy} - moderation={moderation} - /> - {!!profile.viewer?.followedBy && <View style={s.flexRow} />} - </View> - {renderButton ? ( - <View style={styles.layoutButton}>{renderButton(profile)}</View> - ) : undefined} + return ( + <Link + testID={testID} + style={[ + styles.outer, + pal.border, + noBorder && styles.outerNoBorder, + !noBg && pal.view, + ]} + href={makeProfileLink(profile)} + title={profile.handle} + asAnchor + anchorNoUnderline> + <View style={styles.layout}> + <View style={styles.layoutAvi}> + <UserAvatar + size={40} + avatar={profile.avatar} + moderation={moderation.avatar} + /> </View> - {profile.description ? ( - <View style={styles.details}> - <Text style={pal.text} numberOfLines={4}> - {profile.description as string} - </Text> - </View> + <View style={styles.layoutContent}> + <Text + type="lg" + style={[s.bold, pal.text]} + numberOfLines={1} + lineHeight={1.2}> + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.profile, + )} + </Text> + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + {sanitizeHandle(profile.handle, '@')} + </Text> + <ProfileCardPills + followedBy={!!profile.viewer?.followedBy} + moderation={moderation} + /> + {!!profile.viewer?.followedBy && <View style={s.flexRow} />} + </View> + {renderButton ? ( + <View style={styles.layoutButton}>{renderButton(profile)}</View> ) : undefined} - <FollowersList followers={followers} /> - </Link> - ) - }, -) + </View> + {profile.description ? ( + <View style={styles.details}> + <Text style={pal.text} numberOfLines={4}> + {profile.description as string} + </Text> + </View> + ) : undefined} + <FollowersList followers={followers} /> + </Link> + ) +}) function ProfileCardPills({ followedBy, @@ -146,45 +139,47 @@ function ProfileCardPills({ ) } -const FollowersList = observer( - ({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => { - const store = useStores() - const pal = usePalette('default') - if (!followers?.length) { - return null - } +const FollowersList = observer(function FollowersListImpl({ + followers, +}: { + followers?: AppBskyActorDefs.ProfileView[] | undefined +}) { + const store = useStores() + const pal = usePalette('default') + if (!followers?.length) { + return null + } - const followersWithMods = followers - .map(f => ({ - f, - mod: moderateProfile(f, store.preferences.moderationOpts), - })) - .filter(({mod}) => !mod.account.filter) + const followersWithMods = followers + .map(f => ({ + f, + mod: moderateProfile(f, store.preferences.moderationOpts), + })) + .filter(({mod}) => !mod.account.filter) - return ( - <View style={styles.followedBy}> - <Text - type="sm" - style={[styles.followsByDesc, pal.textLight]} - numberOfLines={2} - lineHeight={1.2}> - Followed by{' '} - {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')} - </Text> - {followersWithMods.slice(0, 3).map(({f, mod}) => ( - <View key={f.did} style={styles.followedByAviContainer}> - <View style={[styles.followedByAvi, pal.view]}> - <UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} /> - </View> + return ( + <View style={styles.followedBy}> + <Text + type="sm" + style={[styles.followsByDesc, pal.textLight]} + numberOfLines={2} + lineHeight={1.2}> + Followed by{' '} + {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')} + </Text> + {followersWithMods.slice(0, 3).map(({f, mod}) => ( + <View key={f.did} style={styles.followedByAviContainer}> + <View style={[styles.followedByAvi, pal.view]}> + <UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} /> </View> - ))} - </View> - ) - }, -) + </View> + ))} + </View> + ) +}) export const ProfileCardWithFollowBtn = observer( - ({ + function ProfileCardWithFollowBtnImpl({ profile, noBg, noBorder, @@ -194,7 +189,7 @@ export const ProfileCardWithFollowBtn = observer( noBg?: boolean noBorder?: boolean followers?: AppBskyActorDefs.ProfileView[] | undefined - }) => { + }) { const store = useStores() const isMe = store.me.did === profile.did diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index aeb2fcba9..beb9609b6 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -78,6 +78,8 @@ export const ProfileFollowers = observer(function ProfileFollowers({ onEndReached={onEndReached} renderItem={renderItem} initialNumToRender={15} + // FIXME(dan) + // eslint-disable-next-line react/no-unstable-nested-components ListFooterComponent={() => ( <View style={styles.footer}> {view.isLoading && <ActivityIndicator />} diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index 0632fac02..22722ee63 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -75,6 +75,8 @@ export const ProfileFollows = observer(function ProfileFollows({ onEndReached={onEndReached} renderItem={renderItem} initialNumToRender={15} + // FIXME(dan) + // eslint-disable-next-line react/no-unstable-nested-components ListFooterComponent={() => ( <View style={styles.footer}> {view.isLoading && <ActivityIndicator />} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 1c683ab9a..b52d338aa 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -45,510 +45,502 @@ interface Props { hideBackButton?: boolean } -export const ProfileHeader = observer( - ({view, onRefreshAll, hideBackButton = false}: Props) => { - const pal = usePalette('default') +export const ProfileHeader = observer(function ProfileHeaderImpl({ + view, + onRefreshAll, + hideBackButton = false, +}: Props) { + const pal = usePalette('default') - // loading - // = - if (!view || !view.hasLoaded) { - return ( - <View style={pal.view}> - <LoadingPlaceholder width="100%" height={120} /> - <View - style={[ - pal.view, - {borderColor: pal.colors.background}, - styles.avi, - ]}> - <LoadingPlaceholder width={80} height={80} style={styles.br40} /> + // loading + // = + if (!view || !view.hasLoaded) { + return ( + <View style={pal.view}> + <LoadingPlaceholder width="100%" height={120} /> + <View + style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> + <LoadingPlaceholder width={80} height={80} style={styles.br40} /> + </View> + <View style={styles.content}> + <View style={[styles.buttonsLine]}> + <LoadingPlaceholder width={100} height={31} style={styles.br50} /> </View> - <View style={styles.content}> - <View style={[styles.buttonsLine]}> - <LoadingPlaceholder width={100} 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> + <Text type="title-2xl" style={[pal.text, styles.title]}> + {sanitizeDisplayName( + view.displayName || sanitizeHandle(view.handle), + )} + </Text> </View> </View> - ) - } - - // error - // = - if (view.hasError) { - return ( - <View testID="profileHeaderHasError"> - <Text>{view.error}</Text> - </View> - ) - } + </View> + ) + } - // loaded - // = + // error + // = + if (view.hasError) { return ( - <ProfileHeaderLoaded - view={view} - onRefreshAll={onRefreshAll} - hideBackButton={hideBackButton} - /> + <View testID="profileHeaderHasError"> + <Text>{view.error}</Text> + </View> ) - }, -) + } -const ProfileHeaderLoaded = observer( - ({view, onRefreshAll, hideBackButton = false}: Props) => { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const store = useStores() - const navigation = useNavigation<NavigationProp>() - const {track} = useAnalytics() - const invalidHandle = isInvalidHandle(view.handle) - const {isDesktop} = useWebMediaQueries() + // loaded + // = + return ( + <ProfileHeaderLoaded + view={view} + onRefreshAll={onRefreshAll} + hideBackButton={hideBackButton} + /> + ) +}) - const onPressBack = React.useCallback(() => { - navigation.goBack() - }, [navigation]) +const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ + view, + onRefreshAll, + hideBackButton = false, +}: Props) { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const store = useStores() + const navigation = useNavigation<NavigationProp>() + const {track} = useAnalytics() + const invalidHandle = isInvalidHandle(view.handle) + const {isDesktop} = useWebMediaQueries() - const onPressAvi = React.useCallback(() => { - if ( - view.avatar && - !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) - ) { - store.shell.openLightbox(new ProfileImageLightbox(view)) - } - }, [store, view]) + const onPressBack = React.useCallback(() => { + navigation.goBack() + }, [navigation]) - const onPressToggleFollow = React.useCallback(() => { - track( - view.viewer.following - ? 'ProfileHeader:FollowButtonClicked' - : 'ProfileHeader:UnfollowButtonClicked', - ) - view?.toggleFollowing().then( - () => { - Toast.show( - `${ - view.viewer.following ? 'Following' : 'No longer following' - } ${sanitizeDisplayName(view.displayName || view.handle)}`, - ) - }, - err => store.log.error('Failed to toggle follow', err), - ) - }, [track, view, store.log]) + const onPressAvi = React.useCallback(() => { + if ( + view.avatar && + !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) + ) { + store.shell.openLightbox(new ProfileImageLightbox(view)) + } + }, [store, view]) - const onPressEditProfile = React.useCallback(() => { - track('ProfileHeader:EditProfileButtonClicked') - store.shell.openModal({ - name: 'edit-profile', - profileView: view, - onUpdate: onRefreshAll, - }) - }, [track, store, view, onRefreshAll]) + const onPressToggleFollow = React.useCallback(() => { + track( + view.viewer.following + ? 'ProfileHeader:FollowButtonClicked' + : 'ProfileHeader:UnfollowButtonClicked', + ) + view?.toggleFollowing().then( + () => { + Toast.show( + `${ + view.viewer.following ? 'Following' : 'No longer following' + } ${sanitizeDisplayName(view.displayName || view.handle)}`, + ) + }, + err => store.log.error('Failed to toggle follow', err), + ) + }, [track, view, store.log]) - const onPressFollowers = React.useCallback(() => { - track('ProfileHeader:FollowersButtonClicked') - navigate('ProfileFollowers', { - name: isInvalidHandle(view.handle) ? view.did : view.handle, - }) - store.shell.closeAllActiveElements() // for when used in the profile preview modal - }, [track, view, store.shell]) + const onPressEditProfile = React.useCallback(() => { + track('ProfileHeader:EditProfileButtonClicked') + store.shell.openModal({ + name: 'edit-profile', + profileView: view, + onUpdate: onRefreshAll, + }) + }, [track, store, view, onRefreshAll]) - const onPressFollows = React.useCallback(() => { - track('ProfileHeader:FollowsButtonClicked') - navigate('ProfileFollows', { - name: isInvalidHandle(view.handle) ? view.did : view.handle, - }) - store.shell.closeAllActiveElements() // for when used in the profile preview modal - }, [track, view, store.shell]) + const onPressFollowers = React.useCallback(() => { + track('ProfileHeader:FollowersButtonClicked') + navigate('ProfileFollowers', { + name: isInvalidHandle(view.handle) ? view.did : view.handle, + }) + store.shell.closeAllActiveElements() // for when used in the profile preview modal + }, [track, view, store.shell]) - const onPressShare = React.useCallback(() => { - track('ProfileHeader:ShareButtonClicked') - const url = toShareUrl(makeProfileLink(view)) - shareUrl(url) - }, [track, view]) + const onPressFollows = React.useCallback(() => { + track('ProfileHeader:FollowsButtonClicked') + navigate('ProfileFollows', { + name: isInvalidHandle(view.handle) ? view.did : view.handle, + }) + store.shell.closeAllActiveElements() // for when used in the profile preview modal + }, [track, view, store.shell]) - const onPressAddRemoveLists = React.useCallback(() => { - track('ProfileHeader:AddToListsButtonClicked') - store.shell.openModal({ - name: 'list-add-remove-user', - subject: view.did, - displayName: view.displayName || view.handle, - }) - }, [track, view, store]) + const onPressShare = React.useCallback(() => { + track('ProfileHeader:ShareButtonClicked') + const url = toShareUrl(makeProfileLink(view)) + shareUrl(url) + }, [track, view]) - const onPressMuteAccount = React.useCallback(async () => { - track('ProfileHeader:MuteAccountButtonClicked') - try { - await view.muteAccount() - Toast.show('Account muted') - } catch (e: any) { - store.log.error('Failed to mute account', e) - Toast.show(`There was an issue! ${e.toString()}`) - } - }, [track, view, store]) + const onPressAddRemoveLists = React.useCallback(() => { + track('ProfileHeader:AddToListsButtonClicked') + store.shell.openModal({ + name: 'list-add-remove-user', + subject: view.did, + displayName: view.displayName || view.handle, + }) + }, [track, view, store]) - const onPressUnmuteAccount = React.useCallback(async () => { - track('ProfileHeader:UnmuteAccountButtonClicked') - try { - await view.unmuteAccount() - Toast.show('Account unmuted') - } catch (e: any) { - store.log.error('Failed to unmute account', e) - Toast.show(`There was an issue! ${e.toString()}`) - } - }, [track, view, store]) + const onPressMuteAccount = React.useCallback(async () => { + track('ProfileHeader:MuteAccountButtonClicked') + try { + await view.muteAccount() + Toast.show('Account muted') + } catch (e: any) { + store.log.error('Failed to mute account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, [track, view, store]) - const onPressBlockAccount = React.useCallback(async () => { - track('ProfileHeader:BlockAccountButtonClicked') - store.shell.openModal({ - name: 'confirm', - title: 'Block Account', - message: - 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.', - onPressConfirm: async () => { - try { - await view.blockAccount() - onRefreshAll() - Toast.show('Account blocked') - } catch (e: any) { - store.log.error('Failed to block account', e) - Toast.show(`There was an issue! ${e.toString()}`) - } - }, - }) - }, [track, view, store, onRefreshAll]) + const onPressUnmuteAccount = React.useCallback(async () => { + track('ProfileHeader:UnmuteAccountButtonClicked') + try { + await view.unmuteAccount() + Toast.show('Account unmuted') + } catch (e: any) { + store.log.error('Failed to unmute account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, [track, view, store]) - const onPressUnblockAccount = React.useCallback(async () => { - track('ProfileHeader:UnblockAccountButtonClicked') - store.shell.openModal({ - name: 'confirm', - title: 'Unblock Account', - message: - 'The account will be able to interact with you after unblocking.', - onPressConfirm: async () => { - try { - await view.unblockAccount() - onRefreshAll() - Toast.show('Account unblocked') - } catch (e: any) { - store.log.error('Failed to unblock account', e) - Toast.show(`There was an issue! ${e.toString()}`) - } - }, - }) - }, [track, view, store, onRefreshAll]) + const onPressBlockAccount = React.useCallback(async () => { + track('ProfileHeader:BlockAccountButtonClicked') + store.shell.openModal({ + name: 'confirm', + title: 'Block Account', + message: + 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.', + onPressConfirm: async () => { + try { + await view.blockAccount() + onRefreshAll() + Toast.show('Account blocked') + } catch (e: any) { + store.log.error('Failed to block account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, + }) + }, [track, view, store, onRefreshAll]) - const onPressReportAccount = React.useCallback(() => { - track('ProfileHeader:ReportAccountButtonClicked') - store.shell.openModal({ - name: 'report', - did: view.did, - }) - }, [track, store, view]) + const onPressUnblockAccount = React.useCallback(async () => { + track('ProfileHeader:UnblockAccountButtonClicked') + store.shell.openModal({ + name: 'confirm', + title: 'Unblock Account', + message: + 'The account will be able to interact with you after unblocking.', + onPressConfirm: async () => { + try { + await view.unblockAccount() + onRefreshAll() + Toast.show('Account unblocked') + } catch (e: any) { + store.log.error('Failed to unblock account', e) + Toast.show(`There was an issue! ${e.toString()}`) + } + }, + }) + }, [track, view, store, onRefreshAll]) - const isMe = React.useMemo( - () => store.me.did === view.did, - [store.me.did, view.did], - ) - const dropdownItems: DropdownItem[] = React.useMemo(() => { - let items: DropdownItem[] = [ - { - testID: 'profileHeaderDropdownShareBtn', - label: 'Share', - onPress: onPressShare, - icon: { - ios: { - name: 'square.and.arrow.up', - }, - android: 'ic_menu_share', - web: 'share', + const onPressReportAccount = React.useCallback(() => { + track('ProfileHeader:ReportAccountButtonClicked') + store.shell.openModal({ + name: 'report', + did: view.did, + }) + }, [track, store, view]) + + const isMe = React.useMemo( + () => store.me.did === view.did, + [store.me.did, view.did], + ) + const dropdownItems: DropdownItem[] = React.useMemo(() => { + let items: DropdownItem[] = [ + { + testID: 'profileHeaderDropdownShareBtn', + label: 'Share', + onPress: onPressShare, + icon: { + ios: { + name: 'square.and.arrow.up', }, + android: 'ic_menu_share', + web: 'share', }, - ] - if (!isMe) { - items.push({label: 'separator'}) - // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self! - items.push({ - testID: 'profileHeaderDropdownListAddRemoveBtn', - label: 'Add to Lists', - onPress: onPressAddRemoveLists, - icon: { - ios: { - name: 'list.bullet', - }, - android: 'ic_menu_add', - web: 'list', - }, - }) - 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', - }, - android: 'ic_lock_silent_mode', - web: 'comment-slash', - }, - }) - } - items.push({ - testID: 'profileHeaderDropdownBlockBtn', - label: view.viewer.blocking ? 'Unblock Account' : 'Block Account', - onPress: view.viewer.blocking - ? onPressUnblockAccount - : onPressBlockAccount, - icon: { - ios: { - name: 'person.fill.xmark', - }, - android: 'ic_menu_close_clear_cancel', - web: 'user-slash', + }, + ] + if (!isMe) { + items.push({label: 'separator'}) + // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self! + items.push({ + testID: 'profileHeaderDropdownListAddRemoveBtn', + label: 'Add to Lists', + onPress: onPressAddRemoveLists, + icon: { + ios: { + name: 'list.bullet', }, - }) + android: 'ic_menu_add', + web: 'list', + }, + }) + if (!view.viewer.blocking) { items.push({ - testID: 'profileHeaderDropdownReportBtn', - label: 'Report Account', - onPress: onPressReportAccount, + testID: 'profileHeaderDropdownMuteBtn', + label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', + onPress: view.viewer.muted + ? onPressUnmuteAccount + : onPressMuteAccount, icon: { ios: { - name: 'exclamationmark.triangle', + name: 'speaker.slash', }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', + android: 'ic_lock_silent_mode', + web: 'comment-slash', }, }) } - return items - }, [ - isMe, - view.viewer.muted, - view.viewer.blocking, - onPressShare, - onPressUnmuteAccount, - onPressMuteAccount, - onPressUnblockAccount, - onPressBlockAccount, - onPressReportAccount, - onPressAddRemoveLists, - ]) + items.push({ + testID: 'profileHeaderDropdownBlockBtn', + label: view.viewer.blocking ? 'Unblock Account' : 'Block Account', + onPress: view.viewer.blocking + ? onPressUnblockAccount + : onPressBlockAccount, + icon: { + ios: { + name: 'person.fill.xmark', + }, + android: 'ic_menu_close_clear_cancel', + web: 'user-slash', + }, + }) + 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, + onPressShare, + onPressUnmuteAccount, + onPressMuteAccount, + onPressUnblockAccount, + 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 && (view.viewer.blocking || view.viewer.blockedBy) + const following = formatCount(view.followsCount) + const followers = formatCount(view.followersCount) + const pluralizedFollowers = pluralize(view.followersCount, 'follower') - return ( - <View style={pal.view}> - <UserBanner banner={view.banner} moderation={view.moderation.avatar} /> - <View style={styles.content}> - <View style={[styles.buttonsLine]}> - {isMe ? ( - <TouchableOpacity - testID="profileHeaderEditProfileButton" - onPress={onPressEditProfile} - style={[styles.btn, styles.mainBtn, pal.btn]} - accessibilityRole="button" - accessibilityLabel="Edit profile" - accessibilityHint="Opens editor for profile display name, avatar, background image, and description"> - <Text type="button" style={pal.text}> - Edit Profile - </Text> - </TouchableOpacity> - ) : view.viewer.blocking ? ( - <TouchableOpacity - testID="unblockBtn" - onPress={onPressUnblockAccount} - style={[styles.btn, styles.mainBtn, pal.btn]} - accessibilityRole="button" - accessibilityLabel="Unblock" - accessibilityHint=""> - <Text type="button" style={[pal.text, s.bold]}> - Unblock - </Text> - </TouchableOpacity> - ) : !view.viewer.blockedBy ? ( - <> - {store.me.follows.getFollowState(view.did) === - FollowState.Following ? ( - <TouchableOpacity - testID="unfollowBtn" - onPress={onPressToggleFollow} - style={[styles.btn, styles.mainBtn, pal.btn]} - accessibilityRole="button" - accessibilityLabel={`Unfollow ${view.handle}`} - accessibilityHint={`Hides posts from ${view.handle} in your feed`}> - <FontAwesomeIcon - icon="check" - style={[pal.text, s.mr5]} - size={14} - /> - <Text type="button" style={pal.text}> - Following - </Text> - </TouchableOpacity> - ) : ( - <TouchableOpacity - testID="followBtn" - onPress={onPressToggleFollow} - style={[styles.btn, styles.mainBtn, palInverted.view]} - accessibilityRole="button" - accessibilityLabel={`Follow ${view.handle}`} - accessibilityHint={`Shows posts from ${view.handle} in your feed`}> - <FontAwesomeIcon - icon="plus" - style={[palInverted.text, s.mr5]} - /> - <Text type="button" style={[palInverted.text, s.bold]}> - Follow - </Text> - </TouchableOpacity> - )} - </> - ) : null} - {dropdownItems?.length ? ( - <NativeDropdown - testID="profileHeaderDropdownBtn" - items={dropdownItems}> - <View style={[styles.btn, styles.secondaryBtn, pal.btn]}> - <FontAwesomeIcon - icon="ellipsis" - size={20} - style={[pal.text]} - /> - </View> - </NativeDropdown> - ) : undefined} - </View> - <View> - <Text - testID="profileHeaderDisplayName" - type="title-2xl" - style={[pal.text, styles.title]}> - {sanitizeDisplayName( - view.displayName || sanitizeHandle(view.handle), - view.moderation.profile, - )} - </Text> - </View> - <View style={styles.handleLine}> - {view.viewer.followedBy && !blockHide ? ( - <View style={[styles.pill, pal.btn, s.mr5]}> - <Text type="xs" style={[pal.text]}> - Follows you - </Text> - </View> - ) : undefined} - <ThemedText - type={invalidHandle ? 'xs' : 'md'} - fg={invalidHandle ? 'error' : 'light'} - border={invalidHandle ? 'error' : undefined} - style={[ - invalidHandle ? styles.invalidHandle : undefined, - styles.handle, - ]}> - {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`} - </ThemedText> - </View> - {!blockHide && ( + return ( + <View style={pal.view}> + <UserBanner banner={view.banner} moderation={view.moderation.avatar} /> + <View style={styles.content}> + <View style={[styles.buttonsLine]}> + {isMe ? ( + <TouchableOpacity + testID="profileHeaderEditProfileButton" + onPress={onPressEditProfile} + style={[styles.btn, styles.mainBtn, pal.btn]} + accessibilityRole="button" + accessibilityLabel="Edit profile" + accessibilityHint="Opens editor for profile display name, avatar, background image, and description"> + <Text type="button" style={pal.text}> + Edit Profile + </Text> + </TouchableOpacity> + ) : view.viewer.blocking ? ( + <TouchableOpacity + testID="unblockBtn" + onPress={onPressUnblockAccount} + style={[styles.btn, styles.mainBtn, pal.btn]} + accessibilityRole="button" + accessibilityLabel="Unblock" + accessibilityHint=""> + <Text type="button" style={[pal.text, s.bold]}> + Unblock + </Text> + </TouchableOpacity> + ) : !view.viewer.blockedBy ? ( <> - <View style={styles.metricsLine}> + {store.me.follows.getFollowState(view.did) === + FollowState.Following ? ( <TouchableOpacity - testID="profileHeaderFollowersButton" - style={[s.flexRow, s.mr10]} - onPress={onPressFollowers} + testID="unfollowBtn" + onPress={onPressToggleFollow} + style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" - accessibilityLabel={`${followers} ${pluralizedFollowers}`} - accessibilityHint={'Opens followers list'}> - <Text type="md" style={[s.bold, pal.text]}> - {followers}{' '} - </Text> - <Text type="md" style={[pal.textLight]}> - {pluralizedFollowers} + accessibilityLabel={`Unfollow ${view.handle}`} + accessibilityHint={`Hides posts from ${view.handle} in your feed`}> + <FontAwesomeIcon + icon="check" + style={[pal.text, s.mr5]} + size={14} + /> + <Text type="button" style={pal.text}> + Following </Text> </TouchableOpacity> + ) : ( <TouchableOpacity - testID="profileHeaderFollowsButton" - style={[s.flexRow, s.mr10]} - onPress={onPressFollows} + testID="followBtn" + onPress={onPressToggleFollow} + style={[styles.btn, styles.mainBtn, palInverted.view]} accessibilityRole="button" - accessibilityLabel={`${following} following`} - accessibilityHint={'Opens following list'}> - <Text type="md" style={[s.bold, pal.text]}> - {following}{' '} - </Text> - <Text type="md" style={[pal.textLight]}> - following + accessibilityLabel={`Follow ${view.handle}`} + accessibilityHint={`Shows posts from ${view.handle} in your feed`}> + <FontAwesomeIcon + icon="plus" + style={[palInverted.text, s.mr5]} + /> + <Text type="button" style={[palInverted.text, s.bold]}> + Follow </Text> </TouchableOpacity> - <Text type="md" style={[s.bold, pal.text]}> - {formatCount(view.postsCount)}{' '} - <Text type="md" style={[pal.textLight]}> - {pluralize(view.postsCount, 'post')} - </Text> - </Text> - </View> - {view.description && - view.descriptionRichText && - !view.moderation.profile.blur ? ( - <RichText - testID="profileHeaderDescription" - style={[styles.description, pal.text]} - numberOfLines={15} - richText={view.descriptionRichText} - /> - ) : undefined} + )} </> - )} - <ProfileHeaderAlerts moderation={view.moderation} /> + ) : null} + {dropdownItems?.length ? ( + <NativeDropdown + testID="profileHeaderDropdownBtn" + items={dropdownItems}> + <View style={[styles.btn, styles.secondaryBtn, pal.btn]}> + <FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} /> + </View> + </NativeDropdown> + ) : undefined} + </View> + <View> + <Text + testID="profileHeaderDisplayName" + type="title-2xl" + style={[pal.text, styles.title]}> + {sanitizeDisplayName( + view.displayName || sanitizeHandle(view.handle), + view.moderation.profile, + )} + </Text> + </View> + <View style={styles.handleLine}> + {view.viewer.followedBy && !blockHide ? ( + <View style={[styles.pill, pal.btn, s.mr5]}> + <Text type="xs" style={[pal.text]}> + Follows you + </Text> + </View> + ) : undefined} + <ThemedText + type={invalidHandle ? 'xs' : 'md'} + fg={invalidHandle ? 'error' : 'light'} + border={invalidHandle ? 'error' : undefined} + style={[ + invalidHandle ? styles.invalidHandle : undefined, + styles.handle, + ]}> + {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`} + </ThemedText> </View> - {!isDesktop && !hideBackButton && ( - <TouchableWithoutFeedback - onPress={onPressBack} - hitSlop={BACK_HITSLOP} - accessibilityRole="button" - accessibilityLabel="Back" - accessibilityHint=""> - <View style={styles.backBtnWrapper}> - <BlurView style={styles.backBtn} blurType="dark"> - <FontAwesomeIcon size={18} icon="angle-left" style={s.white} /> - </BlurView> + {!blockHide && ( + <> + <View style={styles.metricsLine}> + <TouchableOpacity + testID="profileHeaderFollowersButton" + style={[s.flexRow, s.mr10]} + onPress={onPressFollowers} + accessibilityRole="button" + accessibilityLabel={`${followers} ${pluralizedFollowers}`} + accessibilityHint={'Opens followers list'}> + <Text type="md" style={[s.bold, pal.text]}> + {followers}{' '} + </Text> + <Text type="md" style={[pal.textLight]}> + {pluralizedFollowers} + </Text> + </TouchableOpacity> + <TouchableOpacity + testID="profileHeaderFollowsButton" + style={[s.flexRow, s.mr10]} + onPress={onPressFollows} + accessibilityRole="button" + accessibilityLabel={`${following} following`} + accessibilityHint={'Opens following list'}> + <Text type="md" style={[s.bold, pal.text]}> + {following}{' '} + </Text> + <Text type="md" style={[pal.textLight]}> + following + </Text> + </TouchableOpacity> + <Text type="md" style={[s.bold, pal.text]}> + {formatCount(view.postsCount)}{' '} + <Text type="md" style={[pal.textLight]}> + {pluralize(view.postsCount, 'post')} + </Text> + </Text> </View> - </TouchableWithoutFeedback> + {view.description && + view.descriptionRichText && + !view.moderation.profile.blur ? ( + <RichText + testID="profileHeaderDescription" + style={[styles.description, pal.text]} + numberOfLines={15} + richText={view.descriptionRichText} + /> + ) : undefined} + </> )} + <ProfileHeaderAlerts moderation={view.moderation} /> + </View> + {!isDesktop && !hideBackButton && ( <TouchableWithoutFeedback - testID="profileHeaderAviButton" - onPress={onPressAvi} - accessibilityRole="image" - accessibilityLabel={`View ${view.handle}'s avatar`} + onPress={onPressBack} + hitSlop={BACK_HITSLOP} + accessibilityRole="button" + accessibilityLabel="Back" accessibilityHint=""> - <View - style={[ - pal.view, - {borderColor: pal.colors.background}, - styles.avi, - ]}> - <UserAvatar - size={80} - avatar={view.avatar} - moderation={view.moderation.avatar} - /> + <View style={styles.backBtnWrapper}> + <BlurView style={styles.backBtn} blurType="dark"> + <FontAwesomeIcon size={18} icon="angle-left" style={s.white} /> + </BlurView> </View> </TouchableWithoutFeedback> - </View> - ) - }, -) + )} + <TouchableWithoutFeedback + testID="profileHeaderAviButton" + onPress={onPressAvi} + accessibilityRole="image" + accessibilityLabel={`View ${view.handle}'s avatar`} + accessibilityHint=""> + <View + style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> + <UserAvatar + size={80} + avatar={view.avatar} + moderation={view.moderation.avatar} + /> + </View> + </TouchableWithoutFeedback> + </View> + ) +}) const styles = StyleSheet.create({ banner: { |