about summary refs log tree commit diff
path: root/src/screens/Profile/Header/ProfileHeaderStandard.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/Profile/Header/ProfileHeaderStandard.tsx')
-rw-r--r--src/screens/Profile/Header/ProfileHeaderStandard.tsx286
1 files changed, 286 insertions, 0 deletions
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
new file mode 100644
index 000000000..8b9038244
--- /dev/null
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -0,0 +1,286 @@
+import React, {memo, useMemo} from 'react'
+import {View} from 'react-native'
+import {
+  AppBskyActorDefs,
+  ModerationOpts,
+  moderateProfile,
+  RichText as RichTextAPI,
+} from '@atproto/api'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+
+import {useModalControls} from '#/state/modals'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useSession, useRequireAuth} from '#/state/session'
+import {Shadow} from '#/state/cache/types'
+import {useProfileShadow} from 'state/cache/profile-shadow'
+import {
+  useProfileFollowMutationQueue,
+  useProfileBlockMutationQueue,
+} from '#/state/queries/profile'
+import {logger} from '#/logger'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText, ButtonIcon} from '#/components/Button'
+import * as Toast from '#/view/com/util/Toast'
+import {ProfileHeaderShell} from './Shell'
+import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
+import {ProfileHeaderDisplayName} from './DisplayName'
+import {ProfileHeaderHandle} from './Handle'
+import {ProfileHeaderMetrics} from './Metrics'
+import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
+import {RichText} from '#/components/RichText'
+import * as Prompt from '#/components/Prompt'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+
+interface Props {
+  profile: AppBskyActorDefs.ProfileViewDetailed
+  descriptionRT: RichTextAPI | null
+  moderationOpts: ModerationOpts
+  hideBackButton?: boolean
+  isPlaceholderProfile?: boolean
+}
+
+let ProfileHeaderStandard = ({
+  profile: profileUnshadowed,
+  descriptionRT,
+  moderationOpts,
+  hideBackButton = false,
+  isPlaceholderProfile,
+}: Props): React.ReactNode => {
+  const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
+    useProfileShadow(profileUnshadowed)
+  const t = useTheme()
+  const {currentAccount, hasSession} = useSession()
+  const {_} = useLingui()
+  const {openModal} = useModalControls()
+  const {track} = useAnalytics()
+  const moderation = useMemo(
+    () => moderateProfile(profile, moderationOpts),
+    [profile, moderationOpts],
+  )
+  const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'ProfileHeader',
+  )
+  const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
+  const unblockPromptControl = Prompt.usePromptControl()
+  const requireAuth = useRequireAuth()
+
+  const onPressEditProfile = React.useCallback(() => {
+    track('ProfileHeader:EditProfileButtonClicked')
+    openModal({
+      name: 'edit-profile',
+      profile,
+    })
+  }, [track, openModal, profile])
+
+  const onPressFollow = () => {
+    requireAuth(async () => {
+      try {
+        track('ProfileHeader:FollowButtonClicked')
+        await queueFollow()
+        Toast.show(
+          _(
+            msg`Following ${sanitizeDisplayName(
+              profile.displayName || profile.handle,
+              moderation.ui('displayName'),
+            )}`,
+          ),
+        )
+      } 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,
+              moderation.ui('displayName'),
+            )}`,
+          ),
+        )
+      } 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 unblockAccount = React.useCallback(async () => {
+    track('ProfileHeader:UnblockAccountButtonClicked')
+    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()}`))
+      }
+    }
+  }, [_, queueUnblock, track])
+
+  const isMe = React.useMemo(
+    () => currentAccount?.did === profile.did,
+    [currentAccount, profile],
+  )
+
+  return (
+    <ProfileHeaderShell
+      profile={profile}
+      moderation={moderation}
+      hideBackButton={hideBackButton}
+      isPlaceholderProfile={isPlaceholderProfile}>
+      <View style={[a.px_lg, a.pt_md, a.pb_sm]} pointerEvents="box-none">
+        <View
+          style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_sm]}
+          pointerEvents="box-none">
+          {isMe ? (
+            <Button
+              testID="profileHeaderEditProfileButton"
+              size="small"
+              color="secondary"
+              variant="solid"
+              onPress={onPressEditProfile}
+              label={_(msg`Edit profile`)}
+              style={a.rounded_full}>
+              <ButtonText>
+                <Trans>Edit Profile</Trans>
+              </ButtonText>
+            </Button>
+          ) : profile.viewer?.blocking ? (
+            profile.viewer?.blockingByList ? null : (
+              <Button
+                testID="unblockBtn"
+                size="small"
+                color="secondary"
+                variant="solid"
+                label={_(msg`Unblock`)}
+                disabled={!hasSession}
+                onPress={() => unblockPromptControl.open()}
+                style={a.rounded_full}>
+                <ButtonText>
+                  <Trans context="action">Unblock</Trans>
+                </ButtonText>
+              </Button>
+            )
+          ) : !profile.viewer?.blockedBy ? (
+            <>
+              {hasSession && (
+                <Button
+                  testID="suggestedFollowsBtn"
+                  size="small"
+                  color={showSuggestedFollows ? 'primary' : 'secondary'}
+                  variant="solid"
+                  shape="round"
+                  onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
+                  label={_(msg`Show follows similar to ${profile.handle}`)}>
+                  <FontAwesomeIcon
+                    icon="user-plus"
+                    style={
+                      showSuggestedFollows
+                        ? {color: t.palette.white}
+                        : t.atoms.text
+                    }
+                    size={14}
+                  />
+                </Button>
+              )}
+
+              <Button
+                testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'}
+                size="small"
+                color={profile.viewer?.following ? 'secondary' : 'primary'}
+                variant="solid"
+                label={
+                  profile.viewer?.following
+                    ? _(msg`Unfollow ${profile.handle}`)
+                    : _(msg`Follow ${profile.handle}`)
+                }
+                disabled={!hasSession}
+                onPress={
+                  profile.viewer?.following ? onPressUnfollow : onPressFollow
+                }
+                style={[a.rounded_full, a.gap_xs]}>
+                <ButtonIcon
+                  position="left"
+                  icon={profile.viewer?.following ? Check : Plus}
+                />
+                <ButtonText>
+                  {profile.viewer?.following ? (
+                    <Trans>Following</Trans>
+                  ) : (
+                    <Trans>Follow</Trans>
+                  )}
+                </ButtonText>
+              </Button>
+            </>
+          ) : null}
+          <ProfileMenu profile={profile} />
+        </View>
+        <View style={[a.flex_col, a.gap_xs, a.pb_sm]}>
+          <ProfileHeaderDisplayName profile={profile} moderation={moderation} />
+          <ProfileHeaderHandle profile={profile} />
+        </View>
+        {!isPlaceholderProfile && (
+          <>
+            <ProfileHeaderMetrics profile={profile} />
+            {descriptionRT && !moderation.ui('profileView').blur ? (
+              <View pointerEvents="auto">
+                <RichText
+                  testID="profileHeaderDescription"
+                  style={[a.text_md]}
+                  numberOfLines={15}
+                  value={descriptionRT}
+                />
+              </View>
+            ) : undefined}
+          </>
+        )}
+      </View>
+      {showSuggestedFollows && (
+        <ProfileHeaderSuggestedFollows
+          actorDid={profile.did}
+          requestDismiss={() => {
+            if (showSuggestedFollows) {
+              setShowSuggestedFollows(false)
+            } else {
+              track('ProfileHeader:SuggestedFollowsOpened')
+              setShowSuggestedFollows(true)
+            }
+          }}
+        />
+      )}
+      <Prompt.Basic
+        control={unblockPromptControl}
+        title={_(msg`Unblock Account?`)}
+        description={_(
+          msg`The account will be able to interact with you after unblocking.`,
+        )}
+        onConfirm={unblockAccount}
+        confirmButtonCta={
+          profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
+        }
+        confirmButtonColor="negative"
+      />
+    </ProfileHeaderShell>
+  )
+}
+ProfileHeaderStandard = memo(ProfileHeaderStandard)
+export {ProfileHeaderStandard}