about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/Navigation.tsx2
-rw-r--r--src/screens/ProfileList/AboutSection.tsx136
-rw-r--r--src/screens/ProfileList/FeedSection.tsx111
-rw-r--r--src/screens/ProfileList/components/ErrorScreen.tsx46
-rw-r--r--src/screens/ProfileList/components/Header.tsx208
-rw-r--r--src/screens/ProfileList/components/MoreOptionsMenu.tsx298
-rw-r--r--src/screens/ProfileList/components/SubscribeMenu.tsx130
-rw-r--r--src/screens/ProfileList/index.tsx296
-rw-r--r--src/view/com/util/LoadingScreen.tsx17
-rw-r--r--src/view/screens/ProfileList.tsx1061
10 files changed, 1226 insertions, 1079 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 003f9c2e8..45f078625 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -64,7 +64,6 @@ import {PostThreadScreen} from '#/view/screens/PostThread'
 import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
 import {ProfileScreen} from '#/view/screens/Profile'
 import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
-import {ProfileListScreen} from '#/view/screens/ProfileList'
 import {SavedFeeds} from '#/view/screens/SavedFeeds'
 import {Storybook} from '#/view/screens/Storybook'
 import {SupportScreen} from '#/view/screens/Support'
@@ -92,6 +91,7 @@ import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
 import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
 import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch'
+import {ProfileListScreen} from '#/screens/ProfileList'
 import {SearchScreen} from '#/screens/Search'
 import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
 import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
diff --git a/src/screens/ProfileList/AboutSection.tsx b/src/screens/ProfileList/AboutSection.tsx
new file mode 100644
index 000000000..47f29b838
--- /dev/null
+++ b/src/screens/ProfileList/AboutSection.tsx
@@ -0,0 +1,136 @@
+import {useCallback, useImperativeHandle, useState} from 'react'
+import {View} from 'react-native'
+import {type AppBskyGraphDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {isNative} from '#/platform/detection'
+import {useSession} from '#/state/session'
+import {ListMembers} from '#/view/com/lists/ListMembers'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {type ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
+import {atoms as a, useBreakpoints} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
+
+interface SectionRef {
+  scrollToTop: () => void
+}
+
+interface AboutSectionProps {
+  ref?: React.Ref<SectionRef>
+  list: AppBskyGraphDefs.ListView
+  onPressAddUser: () => void
+  headerHeight: number
+  scrollElRef: ListRef
+}
+
+export function AboutSection({
+  ref,
+  list,
+  onPressAddUser,
+  headerHeight,
+  scrollElRef,
+}: AboutSectionProps) {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const {gtMobile} = useBreakpoints()
+  const [isScrolledDown, setIsScrolledDown] = useState(false)
+  const isOwner = list.creator.did === currentAccount?.did
+
+  const onScrollToTop = useCallback(() => {
+    scrollElRef.current?.scrollToOffset({
+      animated: isNative,
+      offset: -headerHeight,
+    })
+  }, [scrollElRef, headerHeight])
+
+  useImperativeHandle(ref, () => ({
+    scrollToTop: onScrollToTop,
+  }))
+
+  const renderHeader = useCallback(() => {
+    if (!isOwner) {
+      return <View />
+    }
+    if (!gtMobile) {
+      return (
+        <View style={[a.px_sm, a.py_sm]}>
+          <Button
+            testID="addUserBtn"
+            label={_(msg`Add a user to this list`)}
+            onPress={onPressAddUser}
+            color="primary"
+            size="small"
+            variant="outline"
+            style={[a.py_md]}>
+            <ButtonIcon icon={PersonPlusIcon} />
+            <ButtonText>
+              <Trans>Add people</Trans>
+            </ButtonText>
+          </Button>
+        </View>
+      )
+    }
+    return (
+      <View style={[a.px_lg, a.py_md, a.flex_row_reverse]}>
+        <Button
+          testID="addUserBtn"
+          label={_(msg`Add a user to this list`)}
+          onPress={onPressAddUser}
+          color="primary"
+          size="small"
+          variant="ghost"
+          style={[a.py_sm]}>
+          <ButtonIcon icon={PersonPlusIcon} />
+          <ButtonText>
+            <Trans>Add people</Trans>
+          </ButtonText>
+        </Button>
+      </View>
+    )
+  }, [isOwner, _, onPressAddUser, gtMobile])
+
+  const renderEmptyState = useCallback(() => {
+    return (
+      <View style={[a.gap_xl, a.align_center]}>
+        <EmptyState icon="users-slash" message={_(msg`This list is empty.`)} />
+        {isOwner && (
+          <Button
+            testID="emptyStateAddUserBtn"
+            label={_(msg`Start adding people`)}
+            onPress={onPressAddUser}
+            color="primary"
+            size="small">
+            <ButtonIcon icon={PersonPlusIcon} />
+            <ButtonText>
+              <Trans>Start adding people!</Trans>
+            </ButtonText>
+          </Button>
+        )}
+      </View>
+    )
+  }, [_, onPressAddUser, isOwner])
+
+  return (
+    <View>
+      <ListMembers
+        testID="listItems"
+        list={list.uri}
+        scrollElRef={scrollElRef}
+        renderHeader={renderHeader}
+        renderEmptyState={renderEmptyState}
+        headerOffset={headerHeight}
+        onScrolledDownChange={setIsScrolledDown}
+      />
+      {isScrolledDown && (
+        <LoadLatestBtn
+          onPress={onScrollToTop}
+          label={_(msg`Scroll to top`)}
+          showIndicator={false}
+        />
+      )}
+    </View>
+  )
+}
diff --git a/src/screens/ProfileList/FeedSection.tsx b/src/screens/ProfileList/FeedSection.tsx
new file mode 100644
index 000000000..96b1452e2
--- /dev/null
+++ b/src/screens/ProfileList/FeedSection.tsx
@@ -0,0 +1,111 @@
+import {useCallback, useEffect, useImperativeHandle, useState} from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useIsFocused} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {isNative} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {type FeedDescriptor} from '#/state/queries/post-feed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {PostFeed} from '#/view/com/posts/PostFeed'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {type ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
+
+interface SectionRef {
+  scrollToTop: () => void
+}
+
+interface FeedSectionProps {
+  ref?: React.Ref<SectionRef>
+  feed: FeedDescriptor
+  headerHeight: number
+  scrollElRef: ListRef
+  isFocused: boolean
+  isOwner: boolean
+  onPressAddUser: () => void
+}
+
+export function FeedSection({
+  ref,
+  feed,
+  scrollElRef,
+  headerHeight,
+  isFocused,
+  isOwner,
+  onPressAddUser,
+}: FeedSectionProps) {
+  const queryClient = useQueryClient()
+  const [hasNew, setHasNew] = useState(false)
+  const [isScrolledDown, setIsScrolledDown] = useState(false)
+  const isScreenFocused = useIsFocused()
+  const {_} = useLingui()
+
+  const onScrollToTop = useCallback(() => {
+    scrollElRef.current?.scrollToOffset({
+      animated: isNative,
+      offset: -headerHeight,
+    })
+    queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
+    setHasNew(false)
+  }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
+  useImperativeHandle(ref, () => ({
+    scrollToTop: onScrollToTop,
+  }))
+
+  useEffect(() => {
+    if (!isScreenFocused) {
+      return
+    }
+    return listenSoftReset(onScrollToTop)
+  }, [onScrollToTop, isScreenFocused])
+
+  const renderPostsEmpty = useCallback(() => {
+    return (
+      <View style={[a.gap_xl, a.align_center]}>
+        <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
+        {isOwner && (
+          <Button
+            label={_(msg`Start adding people`)}
+            onPress={onPressAddUser}
+            color="primary"
+            size="small">
+            <ButtonIcon icon={PersonPlusIcon} />
+            <ButtonText>
+              <Trans>Start adding people!</Trans>
+            </ButtonText>
+          </Button>
+        )}
+      </View>
+    )
+  }, [_, onPressAddUser, isOwner])
+
+  return (
+    <View>
+      <PostFeed
+        testID="listFeed"
+        enabled={isFocused}
+        feed={feed}
+        pollInterval={60e3}
+        disablePoll={hasNew}
+        scrollElRef={scrollElRef}
+        onHasNew={setHasNew}
+        onScrolledDownChange={setIsScrolledDown}
+        renderEmptyState={renderPostsEmpty}
+        headerOffset={headerHeight}
+      />
+      {(isScrolledDown || hasNew) && (
+        <LoadLatestBtn
+          onPress={onScrollToTop}
+          label={_(msg`Load new posts`)}
+          showIndicator={hasNew}
+        />
+      )}
+    </View>
+  )
+}
diff --git a/src/screens/ProfileList/components/ErrorScreen.tsx b/src/screens/ProfileList/components/ErrorScreen.tsx
new file mode 100644
index 000000000..7ce343def
--- /dev/null
+++ b/src/screens/ProfileList/components/ErrorScreen.tsx
@@ -0,0 +1,46 @@
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {type NavigationProp} from '#/lib/routes/types'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+
+export function ErrorScreen({error}: {error: React.ReactNode}) {
+  const t = useTheme()
+  const navigation = useNavigation<NavigationProp>()
+  const {_} = useLingui()
+  const onPressBack = () => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }
+
+  return (
+    <View style={[a.px_xl, a.py_md, a.gap_md]}>
+      <Text style={[a.text_4xl, a.font_heavy]}>
+        <Trans>Could not load list</Trans>
+      </Text>
+      <Text style={[a.text_md, t.atoms.text_contrast_high, a.leading_snug]}>
+        {error}
+      </Text>
+
+      <View style={[a.flex_row, a.mt_lg]}>
+        <Button
+          label={_(msg`Go back`)}
+          accessibilityHint={_(msg`Returns to previous page`)}
+          onPress={onPressBack}
+          size="small"
+          color="secondary">
+          <ButtonText>
+            <Trans>Go back</Trans>
+          </ButtonText>
+        </Button>
+      </View>
+    </View>
+  )
+}
diff --git a/src/screens/ProfileList/components/Header.tsx b/src/screens/ProfileList/components/Header.tsx
new file mode 100644
index 000000000..fe4b33c75
--- /dev/null
+++ b/src/screens/ProfileList/components/Header.tsx
@@ -0,0 +1,208 @@
+import {useMemo} from 'react'
+import {View} from 'react-native'
+import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useHaptics} from '#/lib/haptics'
+import {makeListLink} from '#/lib/routes/links'
+import {logger} from '#/logger'
+import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
+import {
+  useAddSavedFeedsMutation,
+  type UsePreferencesQueryResponse,
+  useUpdateSavedFeedsMutation,
+} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
+import {Loader} from '#/components/Loader'
+import {RichText} from '#/components/RichText'
+import * as Toast from '#/components/Toast'
+import {MoreOptionsMenu} from './MoreOptionsMenu'
+import {SubscribeMenu} from './SubscribeMenu'
+
+export function Header({
+  rkey,
+  list,
+  preferences,
+}: {
+  rkey: string
+  list: AppBskyGraphDefs.ListView
+  preferences: UsePreferencesQueryResponse
+}) {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
+  const isModList = list.purpose === AppBskyGraphDefs.MODLIST
+  const isBlocking = !!list.viewer?.blocked
+  const isMuting = !!list.viewer?.muted
+  const playHaptic = useHaptics()
+
+  const {mutateAsync: muteList, isPending: isMutePending} =
+    useListMuteMutation()
+  const {mutateAsync: blockList, isPending: isBlockPending} =
+    useListBlockMutation()
+  const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
+    useAddSavedFeedsMutation()
+  const {mutateAsync: updateSavedFeeds, isPending: isUpdatingSavedFeeds} =
+    useUpdateSavedFeedsMutation()
+
+  const isPending = isAddSavedFeedPending || isUpdatingSavedFeeds
+
+  const savedFeedConfig = preferences?.savedFeeds?.find(
+    f => f.value === list.uri,
+  )
+  const isPinned = Boolean(savedFeedConfig?.pinned)
+
+  const onTogglePinned = async () => {
+    playHaptic()
+
+    try {
+      if (savedFeedConfig) {
+        const pinned = !savedFeedConfig.pinned
+        await updateSavedFeeds([
+          {
+            ...savedFeedConfig,
+            pinned,
+          },
+        ])
+        Toast.show(
+          pinned
+            ? _(msg`Pinned to your feeds`)
+            : _(msg`Unpinned from your feeds`),
+        )
+      } else {
+        await addSavedFeeds([
+          {
+            type: 'list',
+            value: list.uri,
+            pinned: true,
+          },
+        ])
+        Toast.show(_(msg`Saved to your feeds`))
+      }
+    } catch (e) {
+      Toast.show(_(msg`There was an issue contacting the server`), {
+        type: 'error',
+      })
+      logger.error('Failed to toggle pinned feed', {message: e})
+    }
+  }
+
+  const onUnsubscribeMute = async () => {
+    try {
+      await muteList({uri: list.uri, mute: false})
+      Toast.show(_(msg({message: 'List unmuted', context: 'toast'})))
+      logger.metric(
+        'moderation:unsubscribedFromList',
+        {listType: 'mute'},
+        {statsig: true},
+      )
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }
+
+  const onUnsubscribeBlock = async () => {
+    try {
+      await blockList({uri: list.uri, block: false})
+      Toast.show(_(msg({message: 'List unblocked', context: 'toast'})))
+      logger.metric(
+        'moderation:unsubscribedFromList',
+        {listType: 'block'},
+        {statsig: true},
+      )
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }
+
+  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}
+        purpose={list.purpose}
+        avatarType="list">
+        {isCurateList ? (
+          <Button
+            testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+            color={isPinned ? 'secondary' : 'primary_subtle'}
+            label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
+            onPress={onTogglePinned}
+            disabled={isPending}
+            size="small"
+            style={[a.rounded_full]}>
+            {!isPinned && <ButtonIcon icon={isPending ? Loader : PinIcon} />}
+            <ButtonText>
+              {isPinned ? <Trans>Unpin</Trans> : <Trans>Pin to home</Trans>}
+            </ButtonText>
+          </Button>
+        ) : isModList ? (
+          isBlocking ? (
+            <Button
+              testID="unblockBtn"
+              color="secondary"
+              label={_(msg`Unblock`)}
+              onPress={onUnsubscribeBlock}
+              size="small"
+              style={[a.rounded_full]}
+              disabled={isBlockPending}>
+              {isBlockPending && <ButtonIcon icon={Loader} />}
+              <ButtonText>
+                <Trans>Unblock</Trans>
+              </ButtonText>
+            </Button>
+          ) : isMuting ? (
+            <Button
+              testID="unmuteBtn"
+              color="secondary"
+              label={_(msg`Unmute`)}
+              onPress={onUnsubscribeMute}
+              size="small"
+              style={[a.rounded_full]}
+              disabled={isMutePending}>
+              {isMutePending && <ButtonIcon icon={Loader} />}
+              <ButtonText>
+                <Trans>Unmute</Trans>
+              </ButtonText>
+            </Button>
+          ) : (
+            <SubscribeMenu list={list} />
+          )
+        ) : null}
+        <MoreOptionsMenu list={list} />
+      </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}
+    </>
+  )
+}
diff --git a/src/screens/ProfileList/components/MoreOptionsMenu.tsx b/src/screens/ProfileList/components/MoreOptionsMenu.tsx
new file mode 100644
index 000000000..17ca43a82
--- /dev/null
+++ b/src/screens/ProfileList/components/MoreOptionsMenu.tsx
@@ -0,0 +1,298 @@
+import {type AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {type NavigationProp} from '#/lib/routes/types'
+import {shareUrl} from '#/lib/sharing'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {useModalControls} from '#/state/modals'
+import {
+  useListBlockMutation,
+  useListDeleteMutation,
+  useListMuteMutation,
+} from '#/state/queries/list'
+import {useRemoveFeedMutation} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {Button, ButtonIcon} from '#/components/Button'
+import {useDialogControl} from '#/components/Dialog'
+import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
+import {ChainLink_Stroke2_Corner0_Rounded as ChainLink} from '#/components/icons/ChainLink'
+import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
+import {PencilLine_Stroke2_Corner0_Rounded as PencilLineIcon} from '#/components/icons/Pencil'
+import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheckIcon} from '#/components/icons/Person'
+import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
+import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
+import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
+import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
+import * as Menu from '#/components/Menu'
+import {
+  ReportDialog,
+  useReportDialogControl,
+} from '#/components/moderation/ReportDialog'
+import * as Prompt from '#/components/Prompt'
+import * as Toast from '#/components/Toast'
+
+export function MoreOptionsMenu({
+  list,
+  savedFeedConfig,
+}: {
+  list: AppBskyGraphDefs.ListView
+  savedFeedConfig?: AppBskyActorDefs.SavedFeed
+}) {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const {openModal} = useModalControls()
+  const deleteListPromptControl = useDialogControl()
+  const reportDialogControl = useReportDialogControl()
+  const navigation = useNavigation<NavigationProp>()
+
+  const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation()
+  const {mutateAsync: deleteList} = useListDeleteMutation()
+  const {mutateAsync: muteList} = useListMuteMutation()
+  const {mutateAsync: blockList} = useListBlockMutation()
+
+  const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
+  const isModList = list.purpose === AppBskyGraphDefs.MODLIST
+  const isBlocking = !!list.viewer?.blocked
+  const isMuting = !!list.viewer?.muted
+  const isPinned = Boolean(savedFeedConfig?.pinned)
+  const isOwner = currentAccount?.did === list.creator.did
+
+  const onPressShare = () => {
+    const {rkey} = new AtUri(list.uri)
+    const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
+    shareUrl(url)
+  }
+
+  const onRemoveFromSavedFeeds = async () => {
+    if (!savedFeedConfig) return
+    try {
+      await removeSavedFeed(savedFeedConfig)
+      Toast.show(_(msg`Removed from your feeds`))
+    } catch (e) {
+      Toast.show(_(msg`There was an issue contacting the server`), {
+        type: 'error',
+      })
+      logger.error('Failed to remove pinned list', {message: e})
+    }
+  }
+
+  const onPressEdit = () => {
+    openModal({
+      name: 'create-or-edit-list',
+      list,
+    })
+  }
+
+  const onPressDelete = async () => {
+    await deleteList({uri: list.uri})
+
+    if (savedFeedConfig) {
+      await removeSavedFeed(savedFeedConfig)
+    }
+
+    Toast.show(_(msg({message: 'List deleted', context: 'toast'})))
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }
+
+  const onUnpinModList = async () => {
+    try {
+      if (!savedFeedConfig) return
+      await removeSavedFeed(savedFeedConfig)
+      Toast.show(_(msg`Unpinned list`))
+    } catch {
+      Toast.show(_(msg`Failed to unpin list`), {
+        type: 'error',
+      })
+    }
+  }
+
+  const onUnsubscribeMute = async () => {
+    try {
+      await muteList({uri: list.uri, mute: false})
+      Toast.show(_(msg({message: 'List unmuted', context: 'toast'})))
+      logger.metric(
+        'moderation:unsubscribedFromList',
+        {listType: 'mute'},
+        {statsig: true},
+      )
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }
+
+  const onUnsubscribeBlock = async () => {
+    try {
+      await blockList({uri: list.uri, block: false})
+      Toast.show(_(msg({message: 'List unblocked', context: 'toast'})))
+      logger.metric(
+        'moderation:unsubscribedFromList',
+        {listType: 'block'},
+        {statsig: true},
+      )
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }
+
+  return (
+    <>
+      <Menu.Root>
+        <Menu.Trigger label={_(msg`More options`)}>
+          {({props}) => (
+            <Button
+              label={props.accessibilityLabel}
+              testID="moreOptionsBtn"
+              size="small"
+              color="secondary"
+              shape="round"
+              {...props}>
+              <ButtonIcon icon={DotGridIcon} />
+            </Button>
+          )}
+        </Menu.Trigger>
+        <Menu.Outer>
+          <Menu.Group>
+            <Menu.Item
+              label={isWeb ? _(msg`Copy link to list`) : _(msg`Share via...`)}
+              onPress={onPressShare}>
+              <Menu.ItemText>
+                {isWeb ? (
+                  <Trans>Copy link to list</Trans>
+                ) : (
+                  <Trans>Share via...</Trans>
+                )}
+              </Menu.ItemText>
+              <Menu.ItemIcon
+                position="right"
+                icon={isWeb ? ChainLink : ShareIcon}
+              />
+            </Menu.Item>
+            {savedFeedConfig && (
+              <Menu.Item
+                label={_(msg`Remove from my feeds`)}
+                onPress={onRemoveFromSavedFeeds}>
+                <Menu.ItemText>
+                  <Trans>Remove from my feeds</Trans>
+                </Menu.ItemText>
+                <Menu.ItemIcon position="right" icon={TrashIcon} />
+              </Menu.Item>
+            )}
+          </Menu.Group>
+
+          <Menu.Divider />
+
+          {isOwner ? (
+            <Menu.Group>
+              <Menu.Item
+                label={_(msg`Edit list details`)}
+                onPress={onPressEdit}>
+                <Menu.ItemText>
+                  <Trans>Edit list details</Trans>
+                </Menu.ItemText>
+                <Menu.ItemIcon position="right" icon={PencilLineIcon} />
+              </Menu.Item>
+              <Menu.Item
+                label={_(msg`Delete list`)}
+                onPress={deleteListPromptControl.open}>
+                <Menu.ItemText>
+                  <Trans>Delete list</Trans>
+                </Menu.ItemText>
+                <Menu.ItemIcon position="right" icon={TrashIcon} />
+              </Menu.Item>
+            </Menu.Group>
+          ) : (
+            <Menu.Group>
+              <Menu.Item
+                label={_(msg`Report list`)}
+                onPress={reportDialogControl.open}>
+                <Menu.ItemText>
+                  <Trans>Report list</Trans>
+                </Menu.ItemText>
+                <Menu.ItemIcon position="right" icon={WarningIcon} />
+              </Menu.Item>
+            </Menu.Group>
+          )}
+
+          {isModList && isPinned && (
+            <>
+              <Menu.Divider />
+              <Menu.Group>
+                <Menu.Item
+                  label={_(msg`Unpin moderation list`)}
+                  onPress={onUnpinModList}>
+                  <Menu.ItemText>
+                    <Trans>Unpin moderation list</Trans>
+                  </Menu.ItemText>
+                  <Menu.ItemIcon icon={PinIcon} />
+                </Menu.Item>
+              </Menu.Group>
+            </>
+          )}
+
+          {isCurateList && (isBlocking || isMuting) && (
+            <>
+              <Menu.Divider />
+              <Menu.Group>
+                {isBlocking && (
+                  <Menu.Item
+                    label={_(msg`Unblock list`)}
+                    onPress={onUnsubscribeBlock}>
+                    <Menu.ItemText>
+                      <Trans>Unblock list</Trans>
+                    </Menu.ItemText>
+                    <Menu.ItemIcon icon={PersonCheckIcon} />
+                  </Menu.Item>
+                )}
+                {isMuting && (
+                  <Menu.Item
+                    label={_(msg`Unmute list`)}
+                    onPress={onUnsubscribeMute}>
+                    <Menu.ItemText>
+                      <Trans>Unmute list</Trans>
+                    </Menu.ItemText>
+                    <Menu.ItemIcon icon={UnmuteIcon} />
+                  </Menu.Item>
+                )}
+              </Menu.Group>
+            </>
+          )}
+        </Menu.Outer>
+      </Menu.Root>
+
+      <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"
+      />
+
+      <ReportDialog
+        control={reportDialogControl}
+        subject={{
+          ...list,
+          $type: 'app.bsky.graph.defs#listView',
+        }}
+      />
+    </>
+  )
+}
diff --git a/src/screens/ProfileList/components/SubscribeMenu.tsx b/src/screens/ProfileList/components/SubscribeMenu.tsx
new file mode 100644
index 000000000..5b6b9ba09
--- /dev/null
+++ b/src/screens/ProfileList/components/SubscribeMenu.tsx
@@ -0,0 +1,130 @@
+import {type AppBskyGraphDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
+import {PersonX_Stroke2_Corner0_Rounded as PersonXIcon} from '#/components/icons/Person'
+import {Loader} from '#/components/Loader'
+import * as Menu from '#/components/Menu'
+import * as Prompt from '#/components/Prompt'
+import * as Toast from '#/components/Toast'
+
+export function SubscribeMenu({list}: {list: AppBskyGraphDefs.ListView}) {
+  const {_} = useLingui()
+  const subscribeMutePromptControl = Prompt.usePromptControl()
+  const subscribeBlockPromptControl = Prompt.usePromptControl()
+
+  const {mutateAsync: muteList, isPending: isMutePending} =
+    useListMuteMutation()
+  const {mutateAsync: blockList, isPending: isBlockPending} =
+    useListBlockMutation()
+
+  const isPending = isMutePending || isBlockPending
+
+  const onSubscribeMute = async () => {
+    try {
+      await muteList({uri: list.uri, mute: true})
+      Toast.show(_(msg({message: 'List muted', context: 'toast'})))
+      logger.metric(
+        'moderation:subscribedToList',
+        {listType: 'mute'},
+        {statsig: true},
+      )
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+        {type: 'error'},
+      )
+    }
+  }
+
+  const onSubscribeBlock = async () => {
+    try {
+      await blockList({uri: list.uri, block: true})
+      Toast.show(_(msg({message: 'List blocked', context: 'toast'})))
+      logger.metric(
+        'moderation:subscribedToList',
+        {listType: 'block'},
+        {statsig: true},
+      )
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+        {type: 'error'},
+      )
+    }
+  }
+
+  return (
+    <>
+      <Menu.Root>
+        <Menu.Trigger label={_(msg`Subscribe to this list`)}>
+          {({props}) => (
+            <Button
+              label={props.accessibilityLabel}
+              testID="subscribeBtn"
+              size="small"
+              color="primary_subtle"
+              style={[a.rounded_full]}
+              disabled={isPending}
+              {...props}>
+              {isPending && <ButtonIcon icon={Loader} />}
+              <ButtonText>
+                <Trans>Subscribe</Trans>
+              </ButtonText>
+            </Button>
+          )}
+        </Menu.Trigger>
+        <Menu.Outer showCancel>
+          <Menu.Group>
+            <Menu.Item
+              label={_(msg`Mute accounts`)}
+              onPress={subscribeMutePromptControl.open}>
+              <Menu.ItemText>
+                <Trans>Mute accounts</Trans>
+              </Menu.ItemText>
+              <Menu.ItemIcon position="right" icon={MuteIcon} />
+            </Menu.Item>
+            <Menu.Item
+              label={_(msg`Block accounts`)}
+              onPress={subscribeBlockPromptControl.open}>
+              <Menu.ItemText>
+                <Trans>Block accounts</Trans>
+              </Menu.ItemText>
+              <Menu.ItemIcon position="right" icon={PersonXIcon} />
+            </Menu.Item>
+          </Menu.Group>
+        </Menu.Outer>
+      </Menu.Root>
+
+      <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"
+      />
+    </>
+  )
+}
diff --git a/src/screens/ProfileList/index.tsx b/src/screens/ProfileList/index.tsx
new file mode 100644
index 000000000..b3928c3d0
--- /dev/null
+++ b/src/screens/ProfileList/index.tsx
@@ -0,0 +1,296 @@
+import {useCallback, useMemo, useRef} from 'react'
+import {View} from 'react-native'
+import {useAnimatedRef} from 'react-native-reanimated'
+import {
+  AppBskyGraphDefs,
+  AtUri,
+  moderateUserList,
+  type ModerationOpts,
+} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useFocusEffect, useIsFocused} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {ComposeIcon2} from '#/lib/icons'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {cleanError} from '#/lib/strings/errors'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useListQuery} from '#/state/queries/list'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {
+  usePreferencesQuery,
+  type UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {truncateAndInvalidate} from '#/state/queries/util'
+import {useSession} from '#/state/session'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {type ListRef} from '#/view/com/util/List'
+import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
+import {atoms as a, platform} from '#/alf'
+import {useDialogControl} from '#/components/Dialog'
+import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog'
+import * as Layout from '#/components/Layout'
+import {Loader} from '#/components/Loader'
+import * as Hider from '#/components/moderation/Hider'
+import {AboutSection} from './AboutSection'
+import {ErrorScreen} from './components/ErrorScreen'
+import {Header} from './components/Header'
+import {FeedSection} from './FeedSection'
+
+interface SectionRef {
+  scrollToTop: () => void
+}
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
+export function ProfileListScreen(props: Props) {
+  return (
+    <Layout.Screen testID="profileListScreen">
+      <ProfileListScreenInner {...props} />
+    </Layout.Screen>
+  )
+}
+
+function ProfileListScreenInner(props: Props) {
+  const {_} = useLingui()
+  const {name: handleOrDid, rkey} = props.route.params
+  const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
+    AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
+  )
+  const {data: preferences} = usePreferencesQuery()
+  const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
+  const moderationOpts = useModerationOpts()
+
+  if (resolveError) {
+    return (
+      <>
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Could not load list</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot />
+        </Layout.Header.Outer>
+        <Layout.Content centerContent>
+          <ErrorScreen
+            error={_(
+              msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
+            )}
+          />
+        </Layout.Content>
+      </>
+    )
+  }
+  if (listError) {
+    return (
+      <>
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Could not load list</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot />
+        </Layout.Header.Outer>
+        <Layout.Content centerContent>
+          <ErrorScreen error={cleanError(listError)} />
+        </Layout.Content>
+      </>
+    )
+  }
+
+  return resolvedUri && list && moderationOpts && preferences ? (
+    <ProfileListScreenLoaded
+      {...props}
+      uri={resolvedUri.uri}
+      list={list}
+      moderationOpts={moderationOpts}
+      preferences={preferences}
+    />
+  ) : (
+    <>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content />
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Content
+        centerContent
+        contentContainerStyle={platform({
+          web: [a.mx_auto],
+          native: [a.align_center],
+        })}>
+        <Loader size="2xl" />
+      </Layout.Content>
+    </>
+  )
+}
+
+function ProfileListScreenLoaded({
+  route,
+  uri,
+  list,
+  moderationOpts,
+  preferences,
+}: Props & {
+  uri: string
+  list: AppBskyGraphDefs.ListView
+  moderationOpts: ModerationOpts
+  preferences: UsePreferencesQueryResponse
+}) {
+  const {_} = useLingui()
+  const queryClient = useQueryClient()
+  const {openComposer} = useOpenComposer()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {currentAccount} = useSession()
+  const {rkey} = route.params
+  const feedSectionRef = useRef<SectionRef>(null)
+  const aboutSectionRef = useRef<SectionRef>(null)
+  const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
+  const isScreenFocused = useIsFocused()
+  const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
+  const isOwner = currentAccount?.did === list.creator.did
+  const scrollElRef = useAnimatedRef()
+  const addUserDialogControl = useDialogControl()
+  const sectionTitlesCurate = [_(msg`Posts`), _(msg`People`)]
+
+  const moderation = useMemo(() => {
+    return moderateUserList(list, moderationOpts)
+  }, [list, moderationOpts])
+
+  useSetTitle(isHidden ? _(msg`List Hidden`) : list.name)
+
+  useFocusEffect(
+    useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
+
+  const onChangeMembers = () => {
+    if (isCurateList) {
+      truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`))
+    }
+  }
+
+  const onCurrentPageSelected = useCallback(
+    (index: number) => {
+      if (index === 0) {
+        feedSectionRef.current?.scrollToTop()
+      } else if (index === 1) {
+        aboutSectionRef.current?.scrollToTop()
+      }
+    },
+    [feedSectionRef],
+  )
+
+  const renderHeader = useCallback(() => {
+    return <Header rkey={rkey} list={list} preferences={preferences} />
+  }, [rkey, list, preferences])
+
+  if (isCurateList) {
+    return (
+      <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}>
+        <Hider.Mask>
+          <ListHiddenScreen list={list} preferences={preferences} />
+        </Hider.Mask>
+        <Hider.Content>
+          <View style={[a.util_screen_outer]}>
+            <PagerWithHeader
+              items={sectionTitlesCurate}
+              isHeaderReady={true}
+              renderHeader={renderHeader}
+              onCurrentPageSelected={onCurrentPageSelected}>
+              {({headerHeight, scrollElRef, isFocused}) => (
+                <FeedSection
+                  ref={feedSectionRef}
+                  feed={`list|${uri}`}
+                  scrollElRef={scrollElRef as ListRef}
+                  headerHeight={headerHeight}
+                  isFocused={isScreenFocused && isFocused}
+                  isOwner={isOwner}
+                  onPressAddUser={addUserDialogControl.open}
+                />
+              )}
+              {({headerHeight, scrollElRef}) => (
+                <AboutSection
+                  ref={aboutSectionRef}
+                  scrollElRef={scrollElRef as ListRef}
+                  list={list}
+                  onPressAddUser={addUserDialogControl.open}
+                  headerHeight={headerHeight}
+                />
+              )}
+            </PagerWithHeader>
+            <FAB
+              testID="composeFAB"
+              onPress={() => openComposer({})}
+              icon={
+                <ComposeIcon2
+                  strokeWidth={1.5}
+                  size={29}
+                  style={{color: 'white'}}
+                />
+              }
+              accessibilityRole="button"
+              accessibilityLabel={_(msg`New post`)}
+              accessibilityHint=""
+            />
+          </View>
+          <ListAddRemoveUsersDialog
+            control={addUserDialogControl}
+            list={list}
+            onChange={onChangeMembers}
+          />
+        </Hider.Content>
+      </Hider.Outer>
+    )
+  }
+  return (
+    <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}>
+      <Hider.Mask>
+        <ListHiddenScreen list={list} preferences={preferences} />
+      </Hider.Mask>
+      <Hider.Content>
+        <View style={[a.util_screen_outer]}>
+          <Layout.Center>{renderHeader()}</Layout.Center>
+          <AboutSection
+            list={list}
+            scrollElRef={scrollElRef as ListRef}
+            onPressAddUser={addUserDialogControl.open}
+            headerHeight={0}
+          />
+          <FAB
+            testID="composeFAB"
+            onPress={() => openComposer({})}
+            icon={
+              <ComposeIcon2
+                strokeWidth={1.5}
+                size={29}
+                style={{color: 'white'}}
+              />
+            }
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`New post`)}
+            accessibilityHint=""
+          />
+        </View>
+        <ListAddRemoveUsersDialog
+          control={addUserDialogControl}
+          list={list}
+          onChange={onChangeMembers}
+        />
+      </Hider.Content>
+    </Hider.Outer>
+  )
+}
diff --git a/src/view/com/util/LoadingScreen.tsx b/src/view/com/util/LoadingScreen.tsx
deleted file mode 100644
index 1086c9d17..000000000
--- a/src/view/com/util/LoadingScreen.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import {ActivityIndicator, View} from 'react-native'
-
-import {s} from '#/lib/styles'
-import * as Layout from '#/components/Layout'
-
-/**
- * @deprecated use Layout compoenents directly
- */
-export function LoadingScreen() {
-  return (
-    <Layout.Content>
-      <View style={s.p20}>
-        <ActivityIndicator size="large" />
-      </View>
-    </Layout.Content>
-  )
-}
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
deleted file mode 100644
index 78cf5d11e..000000000
--- a/src/view/screens/ProfileList.tsx
+++ /dev/null
@@ -1,1061 +0,0 @@
-import React, {useCallback, useMemo} from 'react'
-import {StyleSheet, View} from 'react-native'
-import {useAnimatedRef} from 'react-native-reanimated'
-import {
-  AppBskyGraphDefs,
-  AtUri,
-  moderateUserList,
-  type ModerationOpts,
-  RichText as RichTextAPI,
-} from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect, useIsFocused} from '@react-navigation/native'
-import {useNavigation} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
-
-import {useHaptics} from '#/lib/haptics'
-import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-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} from '#/lib/routes/links'
-import {
-  type CommonNavigatorParams,
-  type NativeStackScreenProps,
-} from '#/lib/routes/types'
-import {type NavigationProp} from '#/lib/routes/types'
-import {shareUrl} from '#/lib/sharing'
-import {cleanError} from '#/lib/strings/errors'
-import {toShareUrl} from '#/lib/strings/url-helpers'
-import {s} from '#/lib/styles'
-import {logger} from '#/logger'
-import {isNative, isWeb} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {useModalControls} from '#/state/modals'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {
-  useListBlockMutation,
-  useListDeleteMutation,
-  useListMuteMutation,
-  useListQuery,
-} from '#/state/queries/list'
-import {type FeedDescriptor} from '#/state/queries/post-feed'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
-import {
-  useAddSavedFeedsMutation,
-  usePreferencesQuery,
-  type UsePreferencesQueryResponse,
-  useRemoveFeedMutation,
-  useUpdateSavedFeedsMutation,
-} from '#/state/queries/preferences'
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {truncateAndInvalidate} from '#/state/queries/util'
-import {useSession} from '#/state/session'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {ListMembers} from '#/view/com/lists/ListMembers'
-import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
-import {PostFeed} from '#/view/com/posts/PostFeed'
-import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
-import {EmptyState} from '#/view/com/util/EmptyState'
-import {FAB} from '#/view/com/util/fab/FAB'
-import {Button} from '#/view/com/util/forms/Button'
-import {
-  type DropdownItem,
-  NativeDropdown,
-} from '#/view/com/util/forms/NativeDropdown'
-import {type 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} from '#/alf'
-import {Button as NewButton, ButtonIcon, ButtonText} from '#/components/Button'
-import {useDialogControl} from '#/components/Dialog'
-import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog'
-import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
-import * as Layout from '#/components/Layout'
-import * as Hider from '#/components/moderation/Hider'
-import {
-  ReportDialog,
-  useReportDialogControl,
-} from '#/components/moderation/ReportDialog'
-import * as Prompt from '#/components/Prompt'
-import {RichText} from '#/components/RichText'
-
-interface SectionRef {
-  scrollToTop: () => void
-}
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
-export function ProfileListScreen(props: Props) {
-  return (
-    <Layout.Screen testID="profileListScreen">
-      <ProfileListScreenInner {...props} />
-    </Layout.Screen>
-  )
-}
-
-function ProfileListScreenInner(props: Props) {
-  const {_} = useLingui()
-  const {name: handleOrDid, rkey} = props.route.params
-  const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
-    AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
-  )
-  const {data: preferences} = usePreferencesQuery()
-  const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
-  const moderationOpts = useModerationOpts()
-
-  if (resolveError) {
-    return (
-      <Layout.Content>
-        <ErrorScreen
-          error={_(
-            msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
-          )}
-        />
-      </Layout.Content>
-    )
-  }
-  if (listError) {
-    return (
-      <Layout.Content>
-        <ErrorScreen error={cleanError(listError)} />
-      </Layout.Content>
-    )
-  }
-
-  return resolvedUri && list && moderationOpts && preferences ? (
-    <ProfileListScreenLoaded
-      {...props}
-      uri={resolvedUri.uri}
-      list={list}
-      moderationOpts={moderationOpts}
-      preferences={preferences}
-    />
-  ) : (
-    <LoadingScreen />
-  )
-}
-
-function ProfileListScreenLoaded({
-  route,
-  uri,
-  list,
-  moderationOpts,
-  preferences,
-}: Props & {
-  uri: string
-  list: AppBskyGraphDefs.ListView
-  moderationOpts: ModerationOpts
-  preferences: UsePreferencesQueryResponse
-}) {
-  const {_} = useLingui()
-  const queryClient = useQueryClient()
-  const {openComposer} = useOpenComposer()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {currentAccount} = useSession()
-  const {rkey} = route.params
-  const feedSectionRef = React.useRef<SectionRef>(null)
-  const aboutSectionRef = React.useRef<SectionRef>(null)
-  const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
-  const isScreenFocused = useIsFocused()
-  const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
-  const isOwner = currentAccount?.did === list.creator.did
-  const scrollElRef = useAnimatedRef()
-  const addUserDialogControl = useDialogControl()
-  const sectionTitlesCurate = [_(msg`Posts`), _(msg`People`)]
-
-  const moderation = React.useMemo(() => {
-    return moderateUserList(list, moderationOpts)
-  }, [list, moderationOpts])
-
-  useSetTitle(isHidden ? _(msg`List Hidden`) : list.name)
-
-  useFocusEffect(
-    useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
-
-  const onChangeMembers = useCallback(() => {
-    if (isCurateList) {
-      truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`))
-    }
-  }, [list.uri, isCurateList, queryClient])
-
-  const onCurrentPageSelected = React.useCallback(
-    (index: number) => {
-      if (index === 0) {
-        feedSectionRef.current?.scrollToTop()
-      } else if (index === 1) {
-        aboutSectionRef.current?.scrollToTop()
-      }
-    },
-    [feedSectionRef],
-  )
-
-  const renderHeader = useCallback(() => {
-    return <Header rkey={rkey} list={list} preferences={preferences} />
-  }, [rkey, list, preferences])
-
-  if (isCurateList) {
-    return (
-      <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}>
-        <Hider.Mask>
-          <ListHiddenScreen list={list} preferences={preferences} />
-        </Hider.Mask>
-        <Hider.Content>
-          <View style={s.hContentRegion}>
-            <PagerWithHeader
-              items={sectionTitlesCurate}
-              isHeaderReady={true}
-              renderHeader={renderHeader}
-              onCurrentPageSelected={onCurrentPageSelected}>
-              {({headerHeight, scrollElRef, isFocused}) => (
-                <FeedSection
-                  ref={feedSectionRef}
-                  feed={`list|${uri}`}
-                  scrollElRef={scrollElRef as ListRef}
-                  headerHeight={headerHeight}
-                  isFocused={isScreenFocused && isFocused}
-                  isOwner={isOwner}
-                  onPressAddUser={addUserDialogControl.open}
-                />
-              )}
-              {({headerHeight, scrollElRef}) => (
-                <AboutSection
-                  ref={aboutSectionRef}
-                  scrollElRef={scrollElRef as ListRef}
-                  list={list}
-                  onPressAddUser={addUserDialogControl.open}
-                  headerHeight={headerHeight}
-                />
-              )}
-            </PagerWithHeader>
-            <FAB
-              testID="composeFAB"
-              onPress={() => openComposer({})}
-              icon={
-                <ComposeIcon2
-                  strokeWidth={1.5}
-                  size={29}
-                  style={{color: 'white'}}
-                />
-              }
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`New post`)}
-              accessibilityHint=""
-            />
-          </View>
-          <ListAddRemoveUsersDialog
-            control={addUserDialogControl}
-            list={list}
-            onChange={onChangeMembers}
-          />
-        </Hider.Content>
-      </Hider.Outer>
-    )
-  }
-  return (
-    <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}>
-      <Hider.Mask>
-        <ListHiddenScreen list={list} preferences={preferences} />
-      </Hider.Mask>
-      <Hider.Content>
-        <View style={s.hContentRegion}>
-          <Layout.Center>{renderHeader()}</Layout.Center>
-          <AboutSection
-            list={list}
-            scrollElRef={scrollElRef as ListRef}
-            onPressAddUser={addUserDialogControl.open}
-            headerHeight={0}
-          />
-          <FAB
-            testID="composeFAB"
-            onPress={() => openComposer({})}
-            icon={
-              <ComposeIcon2
-                strokeWidth={1.5}
-                size={29}
-                style={{color: 'white'}}
-              />
-            }
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`New post`)}
-            accessibilityHint=""
-          />
-        </View>
-        <ListAddRemoveUsersDialog
-          control={addUserDialogControl}
-          list={list}
-          onChange={onChangeMembers}
-        />
-      </Hider.Content>
-    </Hider.Outer>
-  )
-}
-
-function Header({
-  rkey,
-  list,
-  preferences,
-}: {
-  rkey: string
-  list: AppBskyGraphDefs.ListView
-  preferences: UsePreferencesQueryResponse
-}) {
-  const pal = usePalette('default')
-  const palInverted = usePalette('inverted')
-  const {_} = useLingui()
-  const navigation = useNavigation<NavigationProp>()
-  const {currentAccount} = useSession()
-  const reportDialogControl = useReportDialogControl()
-  const {openModal} = useModalControls()
-  const listMuteMutation = useListMuteMutation()
-  const listBlockMutation = useListBlockMutation()
-  const listDeleteMutation = useListDeleteMutation()
-  const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
-  const isModList = list.purpose === 'app.bsky.graph.defs#modlist'
-  const isBlocking = !!list.viewer?.blocked
-  const isMuting = !!list.viewer?.muted
-  const isOwner = list.creator.did === currentAccount?.did
-  const playHaptic = useHaptics()
-
-  const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
-    useAddSavedFeedsMutation()
-  const {mutateAsync: removeSavedFeed, isPending: isRemovePending} =
-    useRemoveFeedMutation()
-  const {mutateAsync: updateSavedFeeds, isPending: isUpdatingSavedFeeds} =
-    useUpdateSavedFeedsMutation()
-
-  const isPending =
-    isAddSavedFeedPending || isRemovePending || isUpdatingSavedFeeds
-
-  const deleteListPromptControl = useDialogControl()
-  const subscribeMutePromptControl = useDialogControl()
-  const subscribeBlockPromptControl = useDialogControl()
-
-  const savedFeedConfig = preferences?.savedFeeds?.find(
-    f => f.value === list.uri,
-  )
-  const isPinned = Boolean(savedFeedConfig?.pinned)
-
-  const onTogglePinned = React.useCallback(async () => {
-    playHaptic()
-
-    try {
-      if (savedFeedConfig) {
-        const pinned = !savedFeedConfig.pinned
-        await updateSavedFeeds([
-          {
-            ...savedFeedConfig,
-            pinned,
-          },
-        ])
-        Toast.show(
-          pinned
-            ? _(msg`Pinned to your feeds`)
-            : _(msg`Unpinned from your feeds`),
-        )
-      } else {
-        await addSavedFeeds([
-          {
-            type: 'list',
-            value: list.uri,
-            pinned: true,
-          },
-        ])
-        Toast.show(_(msg`Saved to your feeds`))
-      }
-    } catch (e) {
-      Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
-      logger.error('Failed to toggle pinned feed', {message: e})
-    }
-  }, [
-    playHaptic,
-    addSavedFeeds,
-    updateSavedFeeds,
-    list.uri,
-    _,
-    savedFeedConfig,
-  ])
-
-  const onRemoveFromSavedFeeds = React.useCallback(async () => {
-    playHaptic()
-    if (!savedFeedConfig) return
-    try {
-      await removeSavedFeed(savedFeedConfig)
-      Toast.show(_(msg`Removed from your feeds`))
-    } catch (e) {
-      Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
-      logger.error('Failed to remove pinned list', {message: e})
-    }
-  }, [playHaptic, removeSavedFeed, _, savedFeedConfig])
-
-  const onSubscribeMute = useCallback(async () => {
-    try {
-      await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
-      Toast.show(_(msg({message: 'List muted', context: 'toast'})))
-      logger.metric(
-        'moderation:subscribedToList',
-        {listType: 'mute'},
-        {statsig: true},
-      )
-    } catch {
-      Toast.show(
-        _(
-          msg`There was an issue. Please check your internet connection and try again.`,
-        ),
-      )
-    }
-  }, [list, listMuteMutation, _])
-
-  const onUnsubscribeMute = useCallback(async () => {
-    try {
-      await listMuteMutation.mutateAsync({uri: list.uri, mute: false})
-      Toast.show(_(msg({message: 'List unmuted', context: 'toast'})))
-      logger.metric(
-        'moderation:unsubscribedFromList',
-        {listType: 'mute'},
-        {statsig: true},
-      )
-    } catch {
-      Toast.show(
-        _(
-          msg`There was an issue. Please check your internet connection and try again.`,
-        ),
-      )
-    }
-  }, [list, listMuteMutation, _])
-
-  const onSubscribeBlock = useCallback(async () => {
-    try {
-      await listBlockMutation.mutateAsync({uri: list.uri, block: true})
-      Toast.show(_(msg({message: 'List blocked', context: 'toast'})))
-      logger.metric(
-        'moderation:subscribedToList',
-        {listType: 'block'},
-        {statsig: true},
-      )
-    } catch {
-      Toast.show(
-        _(
-          msg`There was an issue. Please check your internet connection and try again.`,
-        ),
-      )
-    }
-  }, [list, listBlockMutation, _])
-
-  const onUnsubscribeBlock = useCallback(async () => {
-    try {
-      await listBlockMutation.mutateAsync({uri: list.uri, block: false})
-      Toast.show(_(msg({message: 'List unblocked', context: 'toast'})))
-      logger.metric(
-        'moderation:unsubscribedFromList',
-        {listType: 'block'},
-        {statsig: true},
-      )
-    } catch {
-      Toast.show(
-        _(
-          msg`There was an issue. Please check your internet connection and try again.`,
-        ),
-      )
-    }
-  }, [list, listBlockMutation, _])
-
-  const onPressEdit = useCallback(() => {
-    openModal({
-      name: 'create-or-edit-list',
-      list,
-    })
-  }, [openModal, list])
-
-  const onPressDelete = useCallback(async () => {
-    await listDeleteMutation.mutateAsync({uri: list.uri})
-
-    if (savedFeedConfig) {
-      await removeSavedFeed(savedFeedConfig)
-    }
-
-    Toast.show(_(msg({message: 'List deleted', context: 'toast'})))
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [
-    list,
-    listDeleteMutation,
-    navigation,
-    _,
-    removeSavedFeed,
-    savedFeedConfig,
-  ])
-
-  const onPressReport = useCallback(() => {
-    reportDialogControl.open()
-  }, [reportDialogControl])
-
-  const onPressShare = useCallback(() => {
-    const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
-    shareUrl(url)
-  }, [list, rkey])
-
-  const dropdownItems: DropdownItem[] = useMemo(() => {
-    let items: DropdownItem[] = [
-      {
-        testID: 'listHeaderDropdownShareBtn',
-        label: isWeb ? _(msg`Copy link to list`) : _(msg`Share`),
-        onPress: onPressShare,
-        icon: {
-          ios: {
-            name: 'square.and.arrow.up',
-          },
-          android: '',
-          web: 'share',
-        },
-      },
-    ]
-
-    if (savedFeedConfig) {
-      items.push({
-        testID: 'listHeaderDropdownRemoveFromFeedsBtn',
-        label: _(msg`Remove from my feeds`),
-        onPress: onRemoveFromSavedFeeds,
-        icon: {
-          ios: {
-            name: 'trash',
-          },
-          android: '',
-          web: ['far', 'trash-can'],
-        },
-      })
-    }
-
-    if (isOwner) {
-      items.push({label: 'separator'})
-      items.push({
-        testID: 'listHeaderDropdownEditBtn',
-        label: _(msg`Edit list details`),
-        onPress: onPressEdit,
-        icon: {
-          ios: {
-            name: 'pencil',
-          },
-          android: '',
-          web: 'pen',
-        },
-      })
-      items.push({
-        testID: 'listHeaderDropdownDeleteBtn',
-        label: _(msg`Delete list`),
-        onPress: deleteListPromptControl.open,
-        icon: {
-          ios: {
-            name: 'trash',
-          },
-          android: '',
-          web: ['far', 'trash-can'],
-        },
-      })
-    } else {
-      items.push({label: 'separator'})
-      items.push({
-        testID: 'listHeaderDropdownReportBtn',
-        label: _(msg`Report list`),
-        onPress: onPressReport,
-        icon: {
-          ios: {
-            name: 'exclamationmark.triangle',
-          },
-          android: '',
-          web: 'circle-exclamation',
-        },
-      })
-    }
-    if (isModList && isPinned) {
-      items.push({label: 'separator'})
-      items.push({
-        testID: 'listHeaderDropdownUnpinBtn',
-        label: _(msg`Unpin moderation list`),
-        onPress:
-          isPending || !savedFeedConfig
-            ? undefined
-            : () => removeSavedFeed(savedFeedConfig),
-        icon: {
-          ios: {
-            name: 'pin',
-          },
-          android: '',
-          web: 'thumbtack',
-        },
-      })
-    }
-    if (isCurateList && (isBlocking || isMuting)) {
-      items.push({label: 'separator'})
-
-      if (isMuting) {
-        items.push({
-          testID: 'listHeaderDropdownMuteBtn',
-          label: _(msg`Unmute list`),
-          onPress: onUnsubscribeMute,
-          icon: {
-            ios: {
-              name: 'eye',
-            },
-            android: '',
-            web: 'eye',
-          },
-        })
-      }
-
-      if (isBlocking) {
-        items.push({
-          testID: 'listHeaderDropdownBlockBtn',
-          label: _(msg`Unblock list`),
-          onPress: onUnsubscribeBlock,
-          icon: {
-            ios: {
-              name: 'person.fill.xmark',
-            },
-            android: '',
-            web: 'user-slash',
-          },
-        })
-      }
-    }
-    return items
-  }, [
-    _,
-    onPressShare,
-    isOwner,
-    isModList,
-    isPinned,
-    isCurateList,
-    onPressEdit,
-    deleteListPromptControl.open,
-    onPressReport,
-    isPending,
-    isBlocking,
-    isMuting,
-    onUnsubscribeMute,
-    onUnsubscribeBlock,
-    removeSavedFeed,
-    savedFeedConfig,
-    onRemoveFromSavedFeeds,
-  ])
-
-  const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
-    return [
-      {
-        testID: 'subscribeDropdownMuteBtn',
-        label: _(msg`Mute accounts`),
-        onPress: subscribeMutePromptControl.open,
-        icon: {
-          ios: {
-            name: 'speaker.slash',
-          },
-          android: '',
-          web: 'user-slash',
-        },
-      },
-      {
-        testID: 'subscribeDropdownBlockBtn',
-        label: _(msg`Block accounts`),
-        onPress: subscribeBlockPromptControl.open,
-        icon: {
-          ios: {
-            name: 'person.fill.xmark',
-          },
-          android: '',
-          web: 'ban',
-        },
-      },
-    ]
-  }, [_, 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}
-        purpose={list.purpose}
-        avatarType="list">
-        <ReportDialog
-          control={reportDialogControl}
-          subject={{
-            ...list,
-            $type: 'app.bsky.graph.defs#listView',
-          }}
-        />
-        {isCurateList ? (
-          <Button
-            testID={isPinned ? 'unpinBtn' : 'pinBtn'}
-            type={isPinned ? 'default' : 'inverted'}
-            label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
-            onPress={onTogglePinned}
-            disabled={isPending}
-          />
-        ) : 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={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>
-      {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}
-    </>
-  )
-}
-
-interface FeedSectionProps {
-  feed: FeedDescriptor
-  headerHeight: number
-  scrollElRef: ListRef
-  isFocused: boolean
-  isOwner: boolean
-  onPressAddUser: () => void
-}
-const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
-  function FeedSectionImpl(
-    {feed, scrollElRef, headerHeight, isFocused, isOwner, onPressAddUser},
-    ref,
-  ) {
-    const queryClient = useQueryClient()
-    const [hasNew, setHasNew] = React.useState(false)
-    const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-    const isScreenFocused = useIsFocused()
-    const {_} = useLingui()
-
-    const onScrollToTop = useCallback(() => {
-      scrollElRef.current?.scrollToOffset({
-        animated: isNative,
-        offset: -headerHeight,
-      })
-      queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
-      setHasNew(false)
-    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
-    React.useImperativeHandle(ref, () => ({
-      scrollToTop: onScrollToTop,
-    }))
-
-    React.useEffect(() => {
-      if (!isScreenFocused) {
-        return
-      }
-      return listenSoftReset(onScrollToTop)
-    }, [onScrollToTop, isScreenFocused])
-
-    const renderPostsEmpty = useCallback(() => {
-      return (
-        <View style={[a.gap_xl, a.align_center]}>
-          <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
-          {isOwner && (
-            <NewButton
-              label={_(msg`Start adding people`)}
-              onPress={onPressAddUser}
-              color="primary"
-              size="small"
-              variant="solid">
-              <ButtonIcon icon={PersonPlusIcon} />
-              <ButtonText>
-                <Trans>Start adding people!</Trans>
-              </ButtonText>
-            </NewButton>
-          )}
-        </View>
-      )
-    }, [_, onPressAddUser, isOwner])
-
-    return (
-      <View>
-        <PostFeed
-          testID="listFeed"
-          enabled={isFocused}
-          feed={feed}
-          pollInterval={60e3}
-          disablePoll={hasNew}
-          scrollElRef={scrollElRef}
-          onHasNew={setHasNew}
-          onScrolledDownChange={setIsScrolledDown}
-          renderEmptyState={renderPostsEmpty}
-          headerOffset={headerHeight}
-        />
-        {(isScrolledDown || hasNew) && (
-          <LoadLatestBtn
-            onPress={onScrollToTop}
-            label={_(msg`Load new posts`)}
-            showIndicator={hasNew}
-          />
-        )}
-      </View>
-    )
-  },
-)
-
-interface AboutSectionProps {
-  list: AppBskyGraphDefs.ListView
-  onPressAddUser: () => void
-  headerHeight: number
-  scrollElRef: ListRef
-}
-const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
-  function AboutSectionImpl(
-    {list, onPressAddUser, headerHeight, scrollElRef},
-    ref,
-  ) {
-    const {_} = useLingui()
-    const {currentAccount} = useSession()
-    const {isMobile} = useWebMediaQueries()
-    const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-    const isOwner = list.creator.did === currentAccount?.did
-
-    const onScrollToTop = useCallback(() => {
-      scrollElRef.current?.scrollToOffset({
-        animated: isNative,
-        offset: -headerHeight,
-      })
-    }, [scrollElRef, headerHeight])
-
-    React.useImperativeHandle(ref, () => ({
-      scrollToTop: onScrollToTop,
-    }))
-
-    const renderHeader = React.useCallback(() => {
-      if (!isOwner) {
-        return <View />
-      }
-      if (isMobile) {
-        return (
-          <View style={[a.px_sm, a.py_sm]}>
-            <NewButton
-              testID="addUserBtn"
-              label={_(msg`Add a user to this list`)}
-              onPress={onPressAddUser}
-              color="primary"
-              size="small"
-              variant="outline"
-              style={[a.py_md]}>
-              <ButtonIcon icon={PersonPlusIcon} />
-              <ButtonText>
-                <Trans>Add people</Trans>
-              </ButtonText>
-            </NewButton>
-          </View>
-        )
-      }
-      return (
-        <View style={[a.px_lg, a.py_md, a.flex_row_reverse]}>
-          <NewButton
-            testID="addUserBtn"
-            label={_(msg`Add a user to this list`)}
-            onPress={onPressAddUser}
-            color="primary"
-            size="small"
-            variant="ghost"
-            style={[a.py_sm]}>
-            <ButtonIcon icon={PersonPlusIcon} />
-            <ButtonText>
-              <Trans>Add people</Trans>
-            </ButtonText>
-          </NewButton>
-        </View>
-      )
-    }, [isOwner, _, onPressAddUser, isMobile])
-
-    const renderEmptyState = useCallback(() => {
-      return (
-        <View style={[a.gap_xl, a.align_center]}>
-          <EmptyState
-            icon="users-slash"
-            message={_(msg`This list is empty.`)}
-          />
-          {isOwner && (
-            <NewButton
-              testID="emptyStateAddUserBtn"
-              label={_(msg`Start adding people`)}
-              onPress={onPressAddUser}
-              color="primary"
-              size="small"
-              variant="solid">
-              <ButtonIcon icon={PersonPlusIcon} />
-              <ButtonText>
-                <Trans>Start adding people!</Trans>
-              </ButtonText>
-            </NewButton>
-          )}
-        </View>
-      )
-    }, [_, onPressAddUser, isOwner])
-
-    return (
-      <View>
-        <ListMembers
-          testID="listItems"
-          list={list.uri}
-          scrollElRef={scrollElRef}
-          renderHeader={renderHeader}
-          renderEmptyState={renderEmptyState}
-          headerOffset={headerHeight}
-          onScrolledDownChange={setIsScrolledDown}
-        />
-        {isScrolledDown && (
-          <LoadLatestBtn
-            onPress={onScrollToTop}
-            label={_(msg`Scroll to top`)}
-            showIndicator={false}
-          />
-        )}
-      </View>
-    )
-  },
-)
-
-function ErrorScreen({error}: {error: string}) {
-  const pal = usePalette('default')
-  const navigation = useNavigation<NavigationProp>()
-  const {_} = useLingui()
-  const onPressBack = useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Home')
-    }
-  }, [navigation])
-
-  return (
-    <View
-      style={[
-        pal.view,
-        pal.border,
-        {
-          paddingHorizontal: 18,
-          paddingVertical: 14,
-          borderTopWidth: StyleSheet.hairlineWidth,
-        },
-      ]}>
-      <Text type="title-lg" style={[pal.text, s.mb10]}>
-        <Trans>Could not load list</Trans>
-      </Text>
-      <Text type="md" style={[pal.text, s.mb20]}>
-        {error}
-      </Text>
-
-      <View style={{flexDirection: 'row'}}>
-        <Button
-          type="default"
-          accessibilityLabel={_(msg`Go back`)}
-          accessibilityHint={_(msg`Returns to previous page`)}
-          onPress={onPressBack}
-          style={{flexShrink: 1}}>
-          <Text type="button" style={pal.text}>
-            <Trans>Go Back</Trans>
-          </Text>
-        </Button>
-      </View>
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    gap: 6,
-    paddingVertical: 7,
-    paddingHorizontal: 14,
-    borderRadius: 50,
-    marginLeft: 6,
-  },
-})