about summary refs log tree commit diff
path: root/src/view/com/profile/ProfileHeader.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/profile/ProfileHeader.tsx')
-rw-r--r--src/view/com/profile/ProfileHeader.tsx625
1 files changed, 367 insertions, 258 deletions
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index d1104d184..719b84e20 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -96,281 +96,377 @@ export const ProfileHeader = observer(
   },
 )
 
-const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
-  view,
-  onRefreshAll,
-  hideBackButton = false,
-}: Props) {
-  const pal = usePalette('default')
-  const store = useStores()
-  const navigation = useNavigation<NavigationProp>()
-  const {track} = useAnalytics()
-
-  const onPressBack = React.useCallback(() => {
-    navigation.goBack()
-  }, [navigation])
-
-  const onPressAvi = React.useCallback(() => {
-    if (view.avatar) {
-      store.shell.openLightbox(new ProfileImageLightbox(view))
-    }
-  }, [store, view])
-
-  const onPressToggleFollow = React.useCallback(() => {
-    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),
-    )
-  }, [view, store])
-
-  const onPressEditProfile = React.useCallback(() => {
-    track('ProfileHeader:EditProfileButtonClicked')
-    store.shell.openModal({
-      name: 'edit-profile',
-      profileView: view,
-      onUpdate: onRefreshAll,
-    })
-  }, [track, store, view, onRefreshAll])
-
-  const onPressFollowers = React.useCallback(() => {
-    track('ProfileHeader:FollowersButtonClicked')
-    navigation.push('ProfileFollowers', {name: view.handle})
-  }, [track, navigation, view])
-
-  const onPressFollows = React.useCallback(() => {
-    track('ProfileHeader:FollowsButtonClicked')
-    navigation.push('ProfileFollows', {name: view.handle})
-  }, [track, navigation, view])
-
-  const onPressShare = React.useCallback(async () => {
-    track('ProfileHeader:ShareButtonClicked')
-    const url = toShareUrl(`/profile/${view.handle}`)
-    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 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 onPressReportAccount = React.useCallback(() => {
-    track('ProfileHeader:ReportAccountButtonClicked')
-    store.shell.openModal({
-      name: 'report-account',
-      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: 'profileHeaderDropdownSahreBtn',
-        label: 'Share',
-        onPress: onPressShare,
-      },
-    ]
-    if (!isMe) {
-      items.push({
-        testID: 'profileHeaderDropdownMuteBtn',
-        label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
-        onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
+const ProfileHeaderLoaded = observer(
+  ({view, onRefreshAll, hideBackButton = false}: Props) => {
+    const pal = usePalette('default')
+    const store = useStores()
+    const navigation = useNavigation<NavigationProp>()
+    const {track} = useAnalytics()
+
+    const onPressBack = React.useCallback(() => {
+      navigation.goBack()
+    }, [navigation])
+
+    const onPressAvi = React.useCallback(() => {
+      if (view.avatar) {
+        store.shell.openLightbox(new ProfileImageLightbox(view))
+      }
+    }, [store, view])
+
+    const onPressToggleFollow = React.useCallback(() => {
+      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),
+      )
+    }, [view, store])
+
+    const onPressEditProfile = React.useCallback(() => {
+      track('ProfileHeader:EditProfileButtonClicked')
+      store.shell.openModal({
+        name: 'edit-profile',
+        profileView: view,
+        onUpdate: onRefreshAll,
+      })
+    }, [track, store, view, onRefreshAll])
+
+    const onPressFollowers = React.useCallback(() => {
+      track('ProfileHeader:FollowersButtonClicked')
+      navigation.push('ProfileFollowers', {name: view.handle})
+    }, [track, navigation, view])
+
+    const onPressFollows = React.useCallback(() => {
+      track('ProfileHeader:FollowsButtonClicked')
+      navigation.push('ProfileFollows', {name: view.handle})
+    }, [track, navigation, view])
+
+    const onPressShare = React.useCallback(async () => {
+      track('ProfileHeader:ShareButtonClicked')
+      const url = toShareUrl(`/profile/${view.handle}`)
+      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 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 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. You will not see their content and they will be prevented from seeing yours.',
+        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()}`)
+          }
+        },
       })
-      items.push({
-        testID: 'profileHeaderDropdownReportBtn',
-        label: 'Report Account',
-        onPress: onPressReportAccount,
+    }, [track, view, store, onRefreshAll])
+
+    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. (You can always block again in the future.)',
+        onPressConfirm: async () => {
+          try {
+            await view.unblockAccount()
+            onRefreshAll()
+            Toast.show('Account unblocked')
+          } catch (e: any) {
+            store.log.error('Failed to block unaccount', e)
+            Toast.show(`There was an issue! ${e.toString()}`)
+          }
+        },
       })
-    }
-    return items
-  }, [
-    isMe,
-    view.viewer.muted,
-    onPressShare,
-    onPressUnmuteAccount,
-    onPressMuteAccount,
-    onPressReportAccount,
-  ])
-  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]}>
-              <Text type="button" style={pal.text}>
-                Edit Profile
-              </Text>
-            </TouchableOpacity>
-          ) : (
+    }, [track, view, store, onRefreshAll])
+
+    const onPressReportAccount = React.useCallback(() => {
+      track('ProfileHeader:ReportAccountButtonClicked')
+      store.shell.openModal({
+        name: 'report-account',
+        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,
+        },
+      ]
+      if (!isMe) {
+        items.push({sep: true})
+        if (!view.viewer.blocking) {
+          items.push({
+            testID: 'profileHeaderDropdownMuteBtn',
+            label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
+            onPress: view.viewer.muted
+              ? onPressUnmuteAccount
+              : onPressMuteAccount,
+          })
+        }
+        items.push({
+          testID: 'profileHeaderDropdownBlockBtn',
+          label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
+          onPress: view.viewer.blocking
+            ? onPressUnblockAccount
+            : onPressBlockAccount,
+        })
+        items.push({
+          testID: 'profileHeaderDropdownReportBtn',
+          label: 'Report Account',
+          onPress: onPressReportAccount,
+        })
+      }
+      return items
+    }, [
+      isMe,
+      view.viewer.muted,
+      view.viewer.blocking,
+      onPressShare,
+      onPressUnmuteAccount,
+      onPressMuteAccount,
+      onPressUnblockAccount,
+      onPressBlockAccount,
+      onPressReportAccount,
+    ])
+
+    const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
+
+    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]}>
+                <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]}>
+                <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]}>
+                    <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.primaryBtn]}>
+                    <FontAwesomeIcon
+                      icon="plus"
+                      style={[s.white as FontAwesomeIconStyle, s.mr5]}
+                    />
+                    <Text type="button" style={[s.white, s.bold]}>
+                      Follow
+                    </Text>
+                  </TouchableOpacity>
+                )}
+              </>
+            ) : null}
+            {dropdownItems?.length ? (
+              <DropdownButton
+                testID="profileHeaderDropdownBtn"
+                type="bare"
+                items={dropdownItems}
+                style={[styles.btn, styles.secondaryBtn, pal.btn]}>
+                <FontAwesomeIcon icon="ellipsis" style={[pal.text]} />
+              </DropdownButton>
+            ) : undefined}
+          </View>
+          <View>
+            <Text
+              testID="profileHeaderDisplayName"
+              type="title-2xl"
+              style={[pal.text, styles.title]}>
+              {sanitizeDisplayName(view.displayName || view.handle)}
+            </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}
+            <Text style={pal.textLight}>@{view.handle}</Text>
+          </View>
+          {!blockHide && (
             <>
-              {store.me.follows.getFollowState(view.did) ===
-              FollowState.Following ? (
+              <View style={styles.metricsLine}>
                 <TouchableOpacity
-                  testID="unfollowBtn"
-                  onPress={onPressToggleFollow}
-                  style={[styles.btn, styles.mainBtn, pal.btn]}>
-                  <FontAwesomeIcon
-                    icon="check"
-                    style={[pal.text, s.mr5]}
-                    size={14}
-                  />
-                  <Text type="button" style={pal.text}>
-                    Following
+                  testID="profileHeaderFollowersButton"
+                  style={[s.flexRow, s.mr10]}
+                  onPress={onPressFollowers}>
+                  <Text type="md" style={[s.bold, s.mr2, pal.text]}>
+                    {view.followersCount}
+                  </Text>
+                  <Text type="md" style={[pal.textLight]}>
+                    {pluralize(view.followersCount, 'follower')}
                   </Text>
                 </TouchableOpacity>
-              ) : (
                 <TouchableOpacity
-                  testID="followBtn"
-                  onPress={onPressToggleFollow}
-                  style={[styles.btn, styles.primaryBtn]}>
-                  <FontAwesomeIcon
-                    icon="plus"
-                    style={[s.white as FontAwesomeIconStyle, s.mr5]}
-                  />
-                  <Text type="button" style={[s.white, s.bold]}>
-                    Follow
+                  testID="profileHeaderFollowsButton"
+                  style={[s.flexRow, s.mr10]}
+                  onPress={onPressFollows}>
+                  <Text type="md" style={[s.bold, s.mr2, pal.text]}>
+                    {view.followsCount}
+                  </Text>
+                  <Text type="md" style={[pal.textLight]}>
+                    following
                   </Text>
                 </TouchableOpacity>
-              )}
+                <View style={[s.flexRow, s.mr10]}>
+                  <Text type="md" style={[s.bold, s.mr2, pal.text]}>
+                    {view.postsCount}
+                  </Text>
+                  <Text type="md" style={[pal.textLight]}>
+                    {pluralize(view.postsCount, 'post')}
+                  </Text>
+                </View>
+              </View>
+              {view.descriptionRichText ? (
+                <RichText
+                  testID="profileHeaderDescription"
+                  style={[styles.description, pal.text]}
+                  numberOfLines={15}
+                  richText={view.descriptionRichText}
+                />
+              ) : undefined}
             </>
           )}
-          {dropdownItems?.length ? (
-            <DropdownButton
-              testID="profileHeaderDropdownBtn"
-              type="bare"
-              items={dropdownItems}
-              style={[styles.btn, styles.secondaryBtn, pal.btn]}>
-              <FontAwesomeIcon icon="ellipsis" style={[pal.text]} />
-            </DropdownButton>
-          ) : undefined}
-        </View>
-        <View>
-          <Text
-            testID="profileHeaderDisplayName"
-            type="title-2xl"
-            style={[pal.text, styles.title]}>
-            {sanitizeDisplayName(view.displayName || view.handle)}
-          </Text>
-        </View>
-        <View style={styles.handleLine}>
-          {view.viewer.followedBy ? (
-            <View style={[styles.pill, pal.btn, s.mr5]}>
-              <Text type="xs" style={[pal.text]}>
-                Follows you
-              </Text>
-            </View>
-          ) : undefined}
-          <Text style={pal.textLight}>@{view.handle}</Text>
-        </View>
-        <View style={styles.metricsLine}>
-          <TouchableOpacity
-            testID="profileHeaderFollowersButton"
-            style={[s.flexRow, s.mr10]}
-            onPress={onPressFollowers}>
-            <Text type="md" style={[s.bold, s.mr2, pal.text]}>
-              {view.followersCount}
-            </Text>
-            <Text type="md" style={[pal.textLight]}>
-              {pluralize(view.followersCount, 'follower')}
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="profileHeaderFollowsButton"
-            style={[s.flexRow, s.mr10]}
-            onPress={onPressFollows}>
-            <Text type="md" style={[s.bold, s.mr2, pal.text]}>
-              {view.followsCount}
-            </Text>
-            <Text type="md" style={[pal.textLight]}>
-              following
-            </Text>
-          </TouchableOpacity>
-          <View style={[s.flexRow, s.mr10]}>
-            <Text type="md" style={[s.bold, s.mr2, pal.text]}>
-              {view.postsCount}
-            </Text>
-            <Text type="md" style={[pal.textLight]}>
-              {pluralize(view.postsCount, 'post')}
-            </Text>
+          <ProfileHeaderWarnings moderation={view.moderation.view} />
+          <View style={styles.moderationLines}>
+            {view.viewer.blocking ? (
+              <View
+                testID="profileHeaderBlockedNotice"
+                style={[styles.moderationNotice, pal.view, pal.border]}>
+                <FontAwesomeIcon icon="ban" style={[pal.text, s.mr5]} />
+                <Text type="md" style={[s.mr2, pal.text]}>
+                  Account blocked
+                </Text>
+              </View>
+            ) : view.viewer.muted ? (
+              <View
+                testID="profileHeaderMutedNotice"
+                style={[styles.moderationNotice, pal.view, pal.border]}>
+                <FontAwesomeIcon
+                  icon={['far', 'eye-slash']}
+                  style={[pal.text, s.mr5]}
+                />
+                <Text type="md" style={[s.mr2, pal.text]}>
+                  Account muted
+                </Text>
+              </View>
+            ) : undefined}
+            {view.viewer.blockedBy && (
+              <View
+                testID="profileHeaderBlockedNotice"
+                style={[styles.moderationNotice, pal.view, pal.border]}>
+                <FontAwesomeIcon icon="ban" style={[pal.text, s.mr5]} />
+                <Text type="md" style={[s.mr2, pal.text]}>
+                  This account has blocked you
+                </Text>
+              </View>
+            )}
           </View>
         </View>
-        {view.descriptionRichText ? (
-          <RichText
-            testID="profileHeaderDescription"
-            style={[styles.description, pal.text]}
-            numberOfLines={15}
-            richText={view.descriptionRichText}
-          />
-        ) : undefined}
-        <ProfileHeaderWarnings moderation={view.moderation.view} />
-        {view.viewer.muted ? (
+        {!isDesktopWeb && !hideBackButton && (
+          <TouchableWithoutFeedback
+            onPress={onPressBack}
+            hitSlop={BACK_HITSLOP}>
+            <View style={styles.backBtnWrapper}>
+              <BlurView style={styles.backBtn} blurType="dark">
+                <FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
+              </BlurView>
+            </View>
+          </TouchableWithoutFeedback>
+        )}
+        <TouchableWithoutFeedback
+          testID="profileHeaderAviButton"
+          onPress={onPressAvi}>
           <View
-            testID="profileHeaderMutedNotice"
-            style={[styles.detailLine, pal.btn, s.p5]}>
-            <FontAwesomeIcon
-              icon={['far', 'eye-slash']}
-              style={[pal.text, s.mr5]}
+            style={[
+              pal.view,
+              {borderColor: pal.colors.background},
+              styles.avi,
+            ]}>
+            <UserAvatar
+              size={80}
+              avatar={view.avatar}
+              moderation={view.moderation.avatar}
             />
-            <Text type="md" style={[s.mr2, pal.text]}>
-              Account muted
-            </Text>
-          </View>
-        ) : undefined}
-      </View>
-      {!isDesktopWeb && !hideBackButton && (
-        <TouchableWithoutFeedback onPress={onPressBack} hitSlop={BACK_HITSLOP}>
-          <View style={styles.backBtnWrapper}>
-            <BlurView style={styles.backBtn} blurType="dark">
-              <FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
-            </BlurView>
           </View>
         </TouchableWithoutFeedback>
-      )}
-      <TouchableWithoutFeedback
-        testID="profileHeaderAviButton"
-        onPress={onPressAvi}>
-        <View
-          style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
-          <UserAvatar
-            size={80}
-            avatar={view.avatar}
-            moderation={view.moderation.avatar}
-          />
-        </View>
-      </TouchableWithoutFeedback>
-    </View>
-  )
-})
+      </View>
+    )
+  },
+)
 
 const styles = StyleSheet.create({
   banner: {
@@ -460,6 +556,19 @@ const styles = StyleSheet.create({
     paddingVertical: 2,
   },
 
+  moderationLines: {
+    gap: 6,
+  },
+
+  moderationNotice: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderWidth: 1,
+    borderRadius: 8,
+    paddingHorizontal: 12,
+    paddingVertical: 10,
+  },
+
   br40: {borderRadius: 40},
   br50: {borderRadius: 50},
 })