about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-12-13 21:38:43 +0000
committerGitHub <noreply@github.com>2024-12-13 21:38:43 +0000
commitda6bcc54cfff198e629a34987746028414867ec9 (patch)
treecedbce914a1118db78f305189112ab0f0c76244c /src
parentadbc27059a6e7f59ad86f3df52038a163ee21981 (diff)
downloadvoidsky-da6bcc54cfff198e629a34987746028414867ec9.tar.zst
Tweak ProfileList design (#7100)
* Remove "No description"

* Move Lists about into header

* Remove pager with one tab

* Layout tweaks
Diffstat (limited to 'src')
-rw-r--r--src/screens/StarterPack/StarterPackScreen.tsx1
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx4
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx65
-rw-r--r--src/view/screens/ProfileList.tsx317
4 files changed, 179 insertions, 208 deletions
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 1b2f61bd5..3a3e4234f 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -407,6 +407,7 @@ function Header({
         isOwner={isOwn}
         avatar={undefined}
         creator={creator}
+        purpose="app.bsky.graph.defs#referencelist"
         avatarType="starter-pack">
         {hasSession ? (
           <View style={[a.flex_row, a.gap_sm, a.align_center]}>
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index dcf141f84..1746d2ca1 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -21,6 +21,7 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {ScrollProvider} from '#/lib/ScrollContext'
 import {isIOS} from '#/platform/detection'
 import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
+import {useTheme} from '#/alf'
 import {ListMethods} from '../util/List'
 import {PagerHeaderProvider} from './PagerHeaderContext'
 import {TabBar} from './TabBar'
@@ -256,6 +257,7 @@ let PagerTabBar = ({
   dragProgress: SharedValue<number>
   dragState: SharedValue<'idle' | 'dragging' | 'settling'>
 }): React.ReactNode => {
+  const t = useTheme()
   const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
   const headerTransform = useAnimatedStyle(() => {
     const translateY =
@@ -277,7 +279,7 @@ let PagerTabBar = ({
   return (
     <Animated.View
       pointerEvents={isIOS ? 'auto' : 'box-none'}
-      style={[styles.tabBarMobile, headerTransform]}>
+      style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}>
       <View
         ref={headerRef}
         pointerEvents={isIOS ? 'auto' : 'box-none'}
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index cd11611a8..b0cf4d10e 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import {Pressable, View} from 'react-native'
 import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
+import {AppBskyGraphDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
@@ -26,6 +27,7 @@ export function ProfileSubpageHeader({
   title,
   avatar,
   isOwner,
+  purpose,
   creator,
   avatarType,
   children,
@@ -35,6 +37,7 @@ export function ProfileSubpageHeader({
   title: string | undefined
   avatar: string | undefined
   isOwner: boolean | undefined
+  purpose: AppBskyGraphDefs.ListPurpose | undefined
   creator:
     | {
         did: string
@@ -105,7 +108,7 @@ export function ProfileSubpageHeader({
           alignItems: 'flex-start',
           gap: 10,
           paddingTop: 14,
-          paddingBottom: 6,
+          paddingBottom: 14,
           paddingHorizontal: isMobile ? 12 : 14,
         }}>
         <View ref={aviRef} collapsable={false}>
@@ -123,7 +126,7 @@ export function ProfileSubpageHeader({
             )}
           </Pressable>
         </View>
-        <View style={{flex: 1}}>
+        <View style={{flex: 1, gap: 4}}>
           {isLoading ? (
             <LoadingPlaceholder
               width={200}
@@ -142,24 +145,50 @@ export function ProfileSubpageHeader({
             />
           )}
 
-          {isLoading ? (
+          {isLoading || !creator ? (
             <LoadingPlaceholder width={50} height={8} />
           ) : (
-            <Text type="xl" style={[pal.textLight]} numberOfLines={1}>
-              {!creator ? (
-                <Trans>by —</Trans>
-              ) : isOwner ? (
-                <Trans>by you</Trans>
-              ) : (
-                <Trans>
-                  by{' '}
-                  <TextLink
-                    text={sanitizeHandle(creator.handle, '@')}
-                    href={makeProfileLink(creator)}
-                    style={pal.textLight}
-                  />
-                </Trans>
-              )}
+            <Text type="lg" style={[pal.textLight]} numberOfLines={1}>
+              {purpose === 'app.bsky.graph.defs#curatelist' ? (
+                isOwner ? (
+                  <Trans>List by you</Trans>
+                ) : (
+                  <Trans>
+                    List by{' '}
+                    <TextLink
+                      text={sanitizeHandle(creator.handle || '', '@')}
+                      href={makeProfileLink(creator)}
+                      style={pal.textLight}
+                    />
+                  </Trans>
+                )
+              ) : purpose === 'app.bsky.graph.defs#modlist' ? (
+                isOwner ? (
+                  <Trans>Moderation list by you</Trans>
+                ) : (
+                  <Trans>
+                    Moderation list by{' '}
+                    <TextLink
+                      text={sanitizeHandle(creator.handle || '', '@')}
+                      href={makeProfileLink(creator)}
+                      style={pal.textLight}
+                    />
+                  </Trans>
+                )
+              ) : purpose === 'app.bsky.graph.defs#referencelist' ? (
+                isOwner ? (
+                  <Trans>Starter pack by you</Trans>
+                ) : (
+                  <Trans>
+                    Starter pack by{' '}
+                    <TextLink
+                      text={sanitizeHandle(creator.handle || '', '@')}
+                      href={makeProfileLink(creator)}
+                      style={pal.textLight}
+                    />
+                  </Trans>
+                )
+              ) : null}
             </Text>
           )}
         </View>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 27ede80a3..2e661ff46 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -1,5 +1,6 @@
 import React, {useCallback, useMemo} from 'react'
 import {Pressable, StyleSheet, View} from 'react-native'
+import {useAnimatedRef} from 'react-native-reanimated'
 import {
   AppBskyGraphDefs,
   AtUri,
@@ -19,12 +20,11 @@ import {usePalette} from '#/lib/hooks/usePalette'
 import {useSetTitle} from '#/lib/hooks/useSetTitle'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {ComposeIcon2} from '#/lib/icons'
-import {makeListLink, makeProfileLink} from '#/lib/routes/links'
+import {makeListLink} from '#/lib/routes/links'
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {NavigationProp} from '#/lib/routes/types'
 import {shareUrl} from '#/lib/sharing'
 import {cleanError} from '#/lib/strings/errors'
-import {sanitizeHandle} from '#/lib/strings/handles'
 import {toShareUrl} from '#/lib/strings/url-helpers'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
@@ -63,14 +63,13 @@ import {
   DropdownItem,
   NativeDropdown,
 } from '#/view/com/util/forms/NativeDropdown'
-import {TextLink} from '#/view/com/util/Link'
 import {ListRef} from '#/view/com/util/List'
 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
 import {LoadingScreen} from '#/view/com/util/LoadingScreen'
 import {Text} from '#/view/com/util/text/Text'
 import * as Toast from '#/view/com/util/Toast'
 import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a} from '#/alf'
 import {useDialogControl} from '#/components/Dialog'
 import * as Layout from '#/components/Layout'
 import * as Hider from '#/components/moderation/Hider'
@@ -78,8 +77,7 @@ import * as Prompt from '#/components/Prompt'
 import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
 import {RichText} from '#/components/RichText'
 
-const SECTION_TITLES_CURATE = ['Posts', 'About']
-const SECTION_TITLES_MOD = ['About']
+const SECTION_TITLES_CURATE = ['Posts', 'People']
 
 interface SectionRef {
   scrollToTop: () => void
@@ -161,6 +159,7 @@ function ProfileListScreenLoaded({
   const isScreenFocused = useIsFocused()
   const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
   const isOwner = currentAccount?.did === list.creator.did
+  const scrollElRef = useAnimatedRef()
 
   const moderation = React.useMemo(() => {
     return moderateUserList(list, moderationOpts)
@@ -259,19 +258,13 @@ function ProfileListScreenLoaded({
       </Hider.Mask>
       <Hider.Content>
         <View style={s.hContentRegion}>
-          <PagerWithHeader
-            items={SECTION_TITLES_MOD}
-            isHeaderReady={true}
-            renderHeader={renderHeader}>
-            {({headerHeight, scrollElRef}) => (
-              <AboutSection
-                list={list}
-                scrollElRef={scrollElRef as ListRef}
-                onPressAddUser={onPressAddUser}
-                headerHeight={headerHeight}
-              />
-            )}
-          </PagerWithHeader>
+          <Layout.Center>{renderHeader()}</Layout.Center>
+          <AboutSection
+            list={list}
+            scrollElRef={scrollElRef as ListRef}
+            onPressAddUser={onPressAddUser}
+            headerHeight={0}
+          />
           <FAB
             testID="composeFAB"
             onPress={() => openComposer({})}
@@ -652,101 +645,124 @@ function Header({
     ]
   }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open])
 
+  const descriptionRT = useMemo(
+    () =>
+      list.description
+        ? new RichTextAPI({
+            text: list.description,
+            facets: list.descriptionFacets,
+          })
+        : undefined,
+    [list],
+  )
+
   return (
-    <ProfileSubpageHeader
-      href={makeListLink(list.creator.handle || list.creator.did || '', rkey)}
-      title={list.name}
-      avatar={list.avatar}
-      isOwner={list.creator.did === currentAccount?.did}
-      creator={list.creator}
-      avatarType="list">
-      <ReportDialog
-        control={reportDialogControl}
-        params={{
-          type: 'list',
-          uri: list.uri,
-          cid: list.cid,
-        }}
-      />
-      {isCurateList ? (
-        <Button
-          testID={isPinned ? 'unpinBtn' : 'pinBtn'}
-          type={isPinned ? 'default' : 'inverted'}
-          label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
-          onPress={onTogglePinned}
-          disabled={isPending}
+    <>
+      <ProfileSubpageHeader
+        href={makeListLink(list.creator.handle || list.creator.did || '', rkey)}
+        title={list.name}
+        avatar={list.avatar}
+        isOwner={list.creator.did === currentAccount?.did}
+        creator={list.creator}
+        purpose={list.purpose}
+        avatarType="list">
+        <ReportDialog
+          control={reportDialogControl}
+          params={{
+            type: 'list',
+            uri: list.uri,
+            cid: list.cid,
+          }}
         />
-      ) : isModList ? (
-        isBlocking ? (
+        {isCurateList ? (
           <Button
-            testID="unblockBtn"
-            type="default"
-            label={_(msg`Unblock`)}
-            onPress={onUnsubscribeBlock}
+            testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+            type={isPinned ? 'default' : 'inverted'}
+            label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
+            onPress={onTogglePinned}
+            disabled={isPending}
           />
-        ) : isMuting ? (
-          <Button
-            testID="unmuteBtn"
-            type="default"
-            label={_(msg`Unmute`)}
-            onPress={onUnsubscribeMute}
-          />
-        ) : (
-          <NativeDropdown
-            testID="subscribeBtn"
-            items={subscribeDropdownItems}
-            accessibilityLabel={_(msg`Subscribe to this list`)}
-            accessibilityHint="">
-            <View style={[palInverted.view, styles.btn]}>
-              <Text style={palInverted.text}>
-                <Trans>Subscribe</Trans>
-              </Text>
-            </View>
-          </NativeDropdown>
-        )
-      ) : null}
-      <NativeDropdown
-        testID="headerDropdownBtn"
-        items={dropdownItems}
-        accessibilityLabel={_(msg`More options`)}
-        accessibilityHint="">
-        <View style={[pal.viewLight, styles.btn]}>
-          <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
-        </View>
-      </NativeDropdown>
+        ) : isModList ? (
+          isBlocking ? (
+            <Button
+              testID="unblockBtn"
+              type="default"
+              label={_(msg`Unblock`)}
+              onPress={onUnsubscribeBlock}
+            />
+          ) : isMuting ? (
+            <Button
+              testID="unmuteBtn"
+              type="default"
+              label={_(msg`Unmute`)}
+              onPress={onUnsubscribeMute}
+            />
+          ) : (
+            <NativeDropdown
+              testID="subscribeBtn"
+              items={subscribeDropdownItems}
+              accessibilityLabel={_(msg`Subscribe to this list`)}
+              accessibilityHint="">
+              <View style={[palInverted.view, styles.btn]}>
+                <Text style={palInverted.text}>
+                  <Trans>Subscribe</Trans>
+                </Text>
+              </View>
+            </NativeDropdown>
+          )
+        ) : null}
+        <NativeDropdown
+          testID="headerDropdownBtn"
+          items={dropdownItems}
+          accessibilityLabel={_(msg`More options`)}
+          accessibilityHint="">
+          <View style={[pal.viewLight, styles.btn]}>
+            <FontAwesomeIcon
+              icon="ellipsis"
+              size={20}
+              color={pal.colors.text}
+            />
+          </View>
+        </NativeDropdown>
 
-      <Prompt.Basic
-        control={deleteListPromptControl}
-        title={_(msg`Delete this list?`)}
-        description={_(
-          msg`If you delete this list, you won't be able to recover it.`,
-        )}
-        onConfirm={onPressDelete}
-        confirmButtonCta={_(msg`Delete`)}
-        confirmButtonColor="negative"
-      />
+        <Prompt.Basic
+          control={deleteListPromptControl}
+          title={_(msg`Delete this list?`)}
+          description={_(
+            msg`If you delete this list, you won't be able to recover it.`,
+          )}
+          onConfirm={onPressDelete}
+          confirmButtonCta={_(msg`Delete`)}
+          confirmButtonColor="negative"
+        />
 
-      <Prompt.Basic
-        control={subscribeMutePromptControl}
-        title={_(msg`Mute these accounts?`)}
-        description={_(
-          msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
-        )}
-        onConfirm={onSubscribeMute}
-        confirmButtonCta={_(msg`Mute list`)}
-      />
+        <Prompt.Basic
+          control={subscribeMutePromptControl}
+          title={_(msg`Mute these accounts?`)}
+          description={_(
+            msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
+          )}
+          onConfirm={onSubscribeMute}
+          confirmButtonCta={_(msg`Mute list`)}
+        />
 
-      <Prompt.Basic
-        control={subscribeBlockPromptControl}
-        title={_(msg`Block these accounts?`)}
-        description={_(
-          msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
-        )}
-        onConfirm={onSubscribeBlock}
-        confirmButtonCta={_(msg`Block list`)}
-        confirmButtonColor="negative"
-      />
-    </ProfileSubpageHeader>
+        <Prompt.Basic
+          control={subscribeBlockPromptControl}
+          title={_(msg`Block these accounts?`)}
+          description={_(
+            msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
+          )}
+          onConfirm={onSubscribeBlock}
+          confirmButtonCta={_(msg`Block list`)}
+          confirmButtonColor="negative"
+        />
+      </ProfileSubpageHeader>
+      {descriptionRT ? (
+        <View style={[a.px_lg, a.pt_sm, a.pb_sm, a.gap_md]}>
+          <RichText value={descriptionRT} style={[a.text_md, a.leading_snug]} />
+        </View>
+      ) : null}
+    </>
   )
 }
 
@@ -825,25 +841,12 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
     ref,
   ) {
     const pal = usePalette('default')
-    const t = useTheme()
     const {_} = useLingui()
     const {isMobile} = useWebMediaQueries()
     const {currentAccount} = useSession()
     const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-    const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
     const isOwner = list.creator.did === currentAccount?.did
 
-    const descriptionRT = useMemo(
-      () =>
-        list.description
-          ? new RichTextAPI({
-              text: list.description,
-              facets: list.descriptionFacets,
-            })
-          : undefined,
-      [list],
-    )
-
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({
         animated: isNative,
@@ -856,59 +859,11 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
     }))
 
     const renderHeader = React.useCallback(() => {
+      if (!isOwner) {
+        return <View />
+      }
       return (
-        <View>
-          <View
-            style={[
-              {
-                borderTopWidth: StyleSheet.hairlineWidth,
-                padding: isMobile ? 14 : 20,
-                gap: 12,
-              },
-              pal.border,
-            ]}>
-            {descriptionRT ? (
-              <RichText
-                testID="listDescription"
-                style={[a.text_md]}
-                value={descriptionRT}
-              />
-            ) : (
-              <Text
-                testID="listDescriptionEmpty"
-                type="lg"
-                style={[{fontStyle: 'italic'}, pal.textLight]}>
-                <Trans>No description</Trans>
-              </Text>
-            )}
-            <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-              {isCurateList ? (
-                isOwner ? (
-                  <Trans>User list by you</Trans>
-                ) : (
-                  <Trans>
-                    User list by{' '}
-                    <TextLink
-                      text={sanitizeHandle(list.creator.handle || '', '@')}
-                      href={makeProfileLink(list.creator)}
-                      style={pal.textLight}
-                    />
-                  </Trans>
-                )
-              ) : isOwner ? (
-                <Trans>Moderation list by you</Trans>
-              ) : (
-                <Trans>
-                  Moderation list by{' '}
-                  <TextLink
-                    text={sanitizeHandle(list.creator.handle || '', '@')}
-                    href={makeProfileLink(list.creator)}
-                    style={pal.textLight}
-                  />
-                </Trans>
-              )}
-            </Text>
-          </View>
+        <View style={a.pt_lg}>
           <View
             style={[
               {
@@ -919,9 +874,6 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                 paddingBottom: isMobile ? 14 : 18,
               },
             ]}>
-            <Text type="lg-bold" style={t.atoms.text}>
-              <Trans>Users</Trans>
-            </Text>
             {isOwner && (
               <Pressable
                 testID="addUserBtn"
@@ -943,20 +895,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
           </View>
         </View>
       )
-    }, [
-      isMobile,
-      pal.border,
-      pal.textLight,
-      pal.colors.link,
-      pal.link,
-      descriptionRT,
-      isCurateList,
-      isOwner,
-      list.creator,
-      t.atoms.text,
-      _,
-      onPressAddUser,
-    ])
+    }, [isMobile, pal.colors.link, pal.link, isOwner, _, onPressAddUser])
 
     const renderEmptyState = useCallback(() => {
       return (