about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/Home.tsx30
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx6
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx6
-rw-r--r--src/view/screens/PreferencesFollowingFeed.tsx16
-rw-r--r--src/view/screens/ProfileFeed.tsx9
-rw-r--r--src/view/screens/ProfileList.tsx24
-rw-r--r--src/view/screens/Settings/index.tsx177
-rw-r--r--src/view/screens/Storybook/ListContained.tsx104
-rw-r--r--src/view/screens/Storybook/index.tsx164
9 files changed, 325 insertions, 211 deletions
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 3eaa1b875..665400f14 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -119,22 +119,24 @@ function HomeScreenReady({
   const gate = useGate()
   const mode = useMinimalShellMode()
   const {isMobile} = useWebMediaQueries()
-  React.useEffect(() => {
-    const listener = AppState.addEventListener('change', nextAppState => {
-      if (nextAppState === 'active') {
-        if (
-          isMobile &&
-          mode.value === 1 &&
-          gate('disable_min_shell_on_foregrounding_v2')
-        ) {
-          setMinimalShellMode(false)
+  useFocusEffect(
+    React.useCallback(() => {
+      const listener = AppState.addEventListener('change', nextAppState => {
+        if (nextAppState === 'active') {
+          if (
+            isMobile &&
+            mode.value === 1 &&
+            gate('disable_min_shell_on_foregrounding_v3')
+          ) {
+            setMinimalShellMode(false)
+          }
         }
+      })
+      return () => {
+        listener.remove()
       }
-    })
-    return () => {
-      listener.remove()
-    }
-  }, [setMinimalShellMode, mode, isMobile, gate])
+    }, [setMinimalShellMode, mode, isMobile, gate]),
+  )
 
   const onPageSelected = React.useCallback(
     (index: number) => {
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index b7ce8cdd0..ebd9bb23e 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -20,8 +20,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {useGate} from 'lib/statsig/statsig'
-import {isWeb} from 'platform/detection'
 import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {CenteredView} from 'view/com/util/Views'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
@@ -38,7 +36,6 @@ export function ModerationBlockedAccounts({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {isTabletOrDesktop} = useWebMediaQueries()
   const {screen} = useAnalytics()
-  const gate = useGate()
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
@@ -168,9 +165,6 @@ export function ModerationBlockedAccounts({}: Props) {
           )}
           // @ts-ignore our .web version only -prf
           desktopFixedHeight
-          showsVerticalScrollIndicator={
-            isWeb || !gate('hide_vertical_scroll_indicators')
-          }
         />
       )}
     </CenteredView>
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 4d7ca6294..e395a3a5b 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -20,8 +20,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {useGate} from 'lib/statsig/statsig'
-import {isWeb} from 'platform/detection'
 import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {CenteredView} from 'view/com/util/Views'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
@@ -38,7 +36,6 @@ export function ModerationMutedAccounts({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {isTabletOrDesktop} = useWebMediaQueries()
   const {screen} = useAnalytics()
-  const gate = useGate()
 
   const [isPTRing, setIsPTRing] = React.useState(false)
   const {
@@ -167,9 +164,6 @@ export function ModerationMutedAccounts({}: Props) {
           )}
           // @ts-ignore our .web version only -prf
           desktopFixedHeight
-          showsVerticalScrollIndicator={
-            isWeb || !gate('hide_vertical_scroll_indicators')
-          }
         />
       )}
     </CenteredView>
diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx
index b4acbcd44..724c3f265 100644
--- a/src/view/screens/PreferencesFollowingFeed.tsx
+++ b/src/view/screens/PreferencesFollowingFeed.tsx
@@ -12,7 +12,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import debounce from 'lodash.debounce'
-import {Trans, msg} from '@lingui/macro'
+import {Trans, msg, Plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
   usePreferencesQuery,
@@ -27,7 +27,6 @@ function RepliesThresholdInput({
   initialValue: number
 }) {
   const pal = usePalette('default')
-  const {_} = useLingui()
   const [value, setValue] = useState(initialValue)
   const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const preValue = React.useRef(initialValue)
@@ -64,13 +63,12 @@ function RepliesThresholdInput({
         thumbTintColor={colors.blue3}
       />
       <Text type="xs" style={pal.text}>
-        {value === 0
-          ? _(msg`Show all replies`)
-          : _(
-              msg`Show replies with at least ${value} ${
-                value > 1 ? `likes` : `like`
-              }`,
-            )}
+        <Plural
+          value={value}
+          _0="Show all replies"
+          one="Show replies with at least # like"
+          other="Show replies with at least # likes"
+        />
       </Text>
     </View>
   )
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 814c1e855..c6fac743a 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react'
 import {Pressable, StyleSheet, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useIsFocused, useNavigation} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -35,7 +35,6 @@ import {makeCustomFeedLink} from 'lib/routes/links'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {NavigationProp} from 'lib/routes/types'
 import {shareUrl} from 'lib/sharing'
-import {pluralize} from 'lib/strings/helpers'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {s} from 'lib/styles'
@@ -597,7 +596,11 @@ function AboutSection({
             label={_(msg`View users who like this feed`)}
             to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
             style={[t.atoms.text_contrast_medium, a.font_bold]}>
-            {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
+            <Plural
+              value={likeCount}
+              one="Liked by # user"
+              other="Liked by # users"
+            />
           </InlineLinkText>
         )}
       </View>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 1d93a9fd7..2902ccf5e 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -454,33 +454,29 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         },
       })
     }
-    if (isCurateList) {
+    if (isCurateList && (isBlocking || isMuting)) {
       items.push({label: 'separator'})
 
-      if (!isBlocking) {
+      if (isMuting) {
         items.push({
           testID: 'listHeaderDropdownMuteBtn',
-          label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`),
-          onPress: isMuting
-            ? onUnsubscribeMute
-            : subscribeMutePromptControl.open,
+          label: _(msg`Un-mute list`),
+          onPress: onUnsubscribeMute,
           icon: {
             ios: {
-              name: isMuting ? 'eye' : 'eye.slash',
+              name: 'eye',
             },
             android: '',
-            web: isMuting ? 'eye' : ['far', 'eye-slash'],
+            web: 'eye',
           },
         })
       }
 
-      if (!isMuting) {
+      if (isBlocking) {
         items.push({
           testID: 'listHeaderDropdownBlockBtn',
-          label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`),
-          onPress: isBlocking
-            ? onUnsubscribeBlock
-            : subscribeBlockPromptControl.open,
+          label: _(msg`Un-block list`),
+          onPress: onUnsubscribeBlock,
           icon: {
             ios: {
               name: 'person.fill.xmark',
@@ -508,9 +504,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     isBlocking,
     isMuting,
     onUnsubscribeMute,
-    subscribeMutePromptControl.open,
     onUnsubscribeBlock,
-    subscribeBlockPromptControl.open,
   ])
 
   const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index a0e4ff60f..c3864e5a9 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -1,7 +1,5 @@
 import React from 'react'
 import {
-  ActivityIndicator,
-  Linking,
   Platform,
   Pressable,
   StyleSheet,
@@ -41,7 +39,7 @@ import {
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {useCloseAllActiveElements} from '#/state/util'
 import {useAnalytics} from 'lib/analytics/analytics'
-import * as AppInfo from 'lib/app-info'
+import {appVersion, BUNDLE_DATE, bundleInfo} from 'lib/app-info'
 import {STATUS_PAGE_URL} from 'lib/constants'
 import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
 import {useCustomPalette} from 'lib/hooks/useCustomPalette'
@@ -51,7 +49,6 @@ import {HandIcon, HashtagIcon} from 'lib/icons'
 import {makeProfileLink} from 'lib/routes/links'
 import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {NavigationProp} from 'lib/routes/types'
-import {useGate} from 'lib/statsig/statsig'
 import {colors, s} from 'lib/styles'
 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
 import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
@@ -62,25 +59,40 @@ import {Text} from 'view/com/util/text/Text'
 import * as Toast from 'view/com/util/Toast'
 import {UserAvatar} from 'view/com/util/UserAvatar'
 import {ScrollView} from 'view/com/util/Views'
-import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
+import {useTheme} from '#/alf'
 import {useDialogControl} from '#/components/Dialog'
 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
-import * as TextField from '#/components/forms/TextField'
 import {navigate, resetToTab} from '#/Navigation'
 import {Email2FAToggle} from './Email2FAToggle'
 import {ExportCarDialog} from './ExportCarDialog'
 
-function SettingsAccountCard({account}: {account: SessionAccount}) {
+function SettingsAccountCard({
+  account,
+  pendingDid,
+  onPressSwitchAccount,
+}: {
+  account: SessionAccount
+  pendingDid: string | null
+  onPressSwitchAccount: (
+    account: SessionAccount,
+    logContext: 'Settings',
+  ) => void
+}) {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {isSwitchingAccounts, currentAccount} = useSession()
+  const t = useTheme()
+  const {currentAccount} = useSession()
   const {logout} = useSessionApi()
   const {data: profile} = useProfileQuery({did: account.did})
   const isCurrentAccount = account.did === currentAccount?.did
-  const {onPressSwitchAccount} = useAccountSwitcher()
 
   const contents = (
-    <View style={[pal.view, styles.linkCard]}>
+    <View
+      style={[
+        pal.view,
+        styles.linkCard,
+        account.did === pendingDid && t.atoms.bg_contrast_25,
+      ]}>
       <View style={styles.avi}>
         <UserAvatar
           size={40}
@@ -112,7 +124,8 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
           }}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Sign out`)}
-          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
+          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}
+          activeOpacity={0.8}>
           <Text type="lg" style={pal.link}>
             <Trans>Sign out</Trans>
           </Text>
@@ -138,13 +151,12 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
       testID={`switchToAccountBtn-${account.handle}`}
       key={account.did}
       onPress={
-        isSwitchingAccounts
-          ? undefined
-          : () => onPressSwitchAccount(account, 'Settings')
+        pendingDid ? undefined : () => onPressSwitchAccount(account, 'Settings')
       }
       accessibilityRole="button"
       accessibilityLabel={_(msg`Switch to ${account.handle}`)}
-      accessibilityHint={_(msg`Switches the account you are logged in to`)}>
+      accessibilityHint={_(msg`Switches the account you are logged in to`)}
+      activeOpacity={0.8}>
       {contents}
     </TouchableOpacity>
   )
@@ -165,17 +177,14 @@ export function SettingsScreen({}: Props) {
   const {isMobile} = useWebMediaQueries()
   const {screen, track} = useAnalytics()
   const {openModal} = useModalControls()
-  const {isSwitchingAccounts, accounts, currentAccount} = useSession()
+  const {accounts, currentAccount} = useSession()
   const {mutate: clearPreferences} = useClearPreferencesMutation()
   const {setShowLoggedOut} = useLoggedOutViewControls()
   const closeAllActiveElements = useCloseAllActiveElements()
   const exportCarControl = useDialogControl()
   const birthdayControl = useDialogControl()
-
-  // TODO: TEMP REMOVE WHEN CLOPS ARE RELEASED
-  const gate = useGate()
-  const {serviceUrl: dmServiceUrl, setServiceUrl: setDmServiceUrl} =
-    useDmServiceUrlStorage()
+  const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
+  const isSwitchingAccounts = !!pendingDid
 
   // const primaryBg = useCustomPalette<ViewStyle>({
   //   light: {backgroundColor: colors.blue0},
@@ -246,7 +255,7 @@ export function SettingsScreen({}: Props) {
 
   const onPressBuildInfo = React.useCallback(() => {
     setStringAsync(
-      `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
+      `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}`,
     )
     Toast.show(_(msg`Copied build version to clipboard`))
   }, [_])
@@ -283,10 +292,6 @@ export function SettingsScreen({}: Props) {
     navigation.navigate('AccessibilitySettings')
   }, [navigation])
 
-  const onPressStatusPage = React.useCallback(() => {
-    Linking.openURL(STATUS_PAGE_URL)
-  }, [])
-
   const onPressBirthday = React.useCallback(() => {
     birthdayControl.open()
   }, [birthdayControl])
@@ -371,50 +376,53 @@ export function SettingsScreen({}: Props) {
             <View style={styles.spacer20} />
 
             {!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
+
+            <View style={[s.flexRow, styles.heading]}>
+              <Text type="xl-bold" style={pal.text}>
+                <Trans>Signed in as</Trans>
+              </Text>
+              <View style={s.flex1} />
+            </View>
+            <View pointerEvents={pendingDid ? 'none' : 'auto'}>
+              <SettingsAccountCard
+                account={currentAccount}
+                onPressSwitchAccount={onPressSwitchAccount}
+                pendingDid={pendingDid}
+              />
+            </View>
           </>
         ) : null}
-        <View style={[s.flexRow, styles.heading]}>
-          <Text type="xl-bold" style={pal.text}>
-            <Trans>Signed in as</Trans>
-          </Text>
-          <View style={s.flex1} />
-        </View>
-
-        {isSwitchingAccounts ? (
-          <View style={[pal.view, styles.linkCard]}>
-            <ActivityIndicator />
-          </View>
-        ) : (
-          <SettingsAccountCard account={currentAccount!} />
-        )}
 
-        {accounts
-          .filter(a => a.did !== currentAccount?.did)
-          .map(account => (
-            <SettingsAccountCard key={account.did} account={account} />
-          ))}
+        <View pointerEvents={pendingDid ? 'none' : 'auto'}>
+          {accounts
+            .filter(a => a.did !== currentAccount?.did)
+            .map(account => (
+              <SettingsAccountCard
+                key={account.did}
+                account={account}
+                onPressSwitchAccount={onPressSwitchAccount}
+                pendingDid={pendingDid}
+              />
+            ))}
 
-        <TouchableOpacity
-          testID="switchToNewAccountBtn"
-          style={[
-            styles.linkCard,
-            pal.view,
-            isSwitchingAccounts && styles.dimmed,
-          ]}
-          onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Add account`)}
-          accessibilityHint={_(msg`Create a new Bluesky account`)}>
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="plus"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            <Trans>Add account</Trans>
-          </Text>
-        </TouchableOpacity>
+          <TouchableOpacity
+            testID="switchToNewAccountBtn"
+            style={[styles.linkCard, pal.view]}
+            onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`Add account`)}
+            accessibilityHint={_(msg`Create a new Bluesky account`)}>
+            <View style={[styles.iconContainer, pal.btn]}>
+              <FontAwesomeIcon
+                icon="plus"
+                style={pal.text as FontAwesomeIconStyle}
+              />
+            </View>
+            <Text type="lg" style={pal.text}>
+              <Trans>Add account</Trans>
+            </Text>
+          </TouchableOpacity>
+        </View>
 
         <View style={styles.spacer20} />
 
@@ -786,22 +794,6 @@ export function SettingsScreen({}: Props) {
             <Trans>System log</Trans>
           </Text>
         </TouchableOpacity>
-        {gate('dms') && (
-          <TextField.Root>
-            <TextField.Input
-              value={dmServiceUrl}
-              onChangeText={(text: string) => {
-                if (text.length > 9 && text.endsWith('/')) {
-                  text = text.slice(0, -1)
-                }
-                setDmServiceUrl(text)
-              }}
-              autoCapitalize="none"
-              keyboardType="url"
-              label="🐴"
-            />
-          </TextField.Root>
-        )}
         {__DEV__ ? (
           <>
             <TouchableOpacity
@@ -873,17 +865,9 @@ export function SettingsScreen({}: Props) {
             accessibilityRole="button"
             onPress={onPressBuildInfo}>
             <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-              <Trans>Version {AppInfo.appVersion}</Trans>
-            </Text>
-          </TouchableOpacity>
-          <Text type="sm" style={[pal.textLight]}>
-            &nbsp; &middot; &nbsp;
-          </Text>
-          <TouchableOpacity
-            accessibilityRole="button"
-            onPress={onPressStatusPage}>
-            <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-              <Trans>Status page</Trans>
+              <Trans>
+                Version {appVersion} {bundleInfo}
+              </Trans>
             </Text>
           </TouchableOpacity>
         </View>
@@ -905,6 +889,12 @@ export function SettingsScreen({}: Props) {
             href="https://bsky.social/about/support/privacy-policy"
             text={_(msg`Privacy Policy`)}
           />
+          <TextLink
+            type="md"
+            style={pal.link}
+            href={STATUS_PAGE_URL}
+            text={_(msg`Status Page`)}
+          />
         </View>
         <View style={s.footerSpacer} />
       </ScrollView>
@@ -1050,7 +1040,6 @@ const styles = StyleSheet.create({
   footer: {
     flex: 1,
     flexDirection: 'row',
-    alignItems: 'center',
     paddingLeft: 18,
   },
 })
diff --git a/src/view/screens/Storybook/ListContained.tsx b/src/view/screens/Storybook/ListContained.tsx
new file mode 100644
index 000000000..b3ea091f4
--- /dev/null
+++ b/src/view/screens/Storybook/ListContained.tsx
@@ -0,0 +1,104 @@
+import React from 'react'
+import {FlatList, View} from 'react-native'
+
+import {ScrollProvider} from 'lib/ScrollContext'
+import {List} from 'view/com/util/List'
+import {Button, ButtonText} from '#/components/Button'
+import * as Toggle from '#/components/forms/Toggle'
+import {Text} from '#/components/Typography'
+
+export function ListContained() {
+  const [animated, setAnimated] = React.useState(false)
+  const ref = React.useRef<FlatList>(null)
+
+  const data = React.useMemo(() => {
+    return Array.from({length: 100}, (_, i) => ({
+      id: i,
+      text: `Message ${i}`,
+    }))
+  }, [])
+
+  return (
+    <>
+      <View style={{width: '100%', height: 300}}>
+        <ScrollProvider
+          onScroll={e => {
+            'worklet'
+            console.log(
+              JSON.stringify({
+                contentOffset: e.contentOffset,
+                layoutMeasurement: e.layoutMeasurement,
+                contentSize: e.contentSize,
+              }),
+            )
+          }}>
+          <List
+            data={data}
+            renderItem={item => {
+              return (
+                <View
+                  style={{
+                    padding: 10,
+                    borderBottomWidth: 1,
+                    borderBottomColor: 'rgba(0,0,0,0.1)',
+                  }}>
+                  <Text>{item.item.text}</Text>
+                </View>
+              )
+            }}
+            keyExtractor={item => item.id.toString()}
+            containWeb={true}
+            style={{flex: 1}}
+            onStartReached={() => {
+              console.log('Start Reached')
+            }}
+            onEndReached={() => {
+              console.log('End Reached (threshold of 2)')
+            }}
+            onEndReachedThreshold={2}
+            ref={ref}
+            disableVirtualization={true}
+          />
+        </ScrollProvider>
+      </View>
+
+      <View style={{flexDirection: 'row', gap: 10, alignItems: 'center'}}>
+        <Toggle.Item
+          name="a"
+          label="Click me"
+          value={animated}
+          onChange={() => setAnimated(prev => !prev)}>
+          <Toggle.Checkbox />
+          <Toggle.LabelText>Animated Scrolling</Toggle.LabelText>
+        </Toggle.Item>
+      </View>
+
+      <Button
+        variant="solid"
+        color="primary"
+        size="large"
+        label="Scroll to End"
+        onPress={() => ref.current?.scrollToOffset({animated, offset: 0})}>
+        <ButtonText>Scroll to Top</ButtonText>
+      </Button>
+
+      <Button
+        variant="solid"
+        color="primary"
+        size="large"
+        label="Scroll to End"
+        onPress={() => ref.current?.scrollToEnd({animated})}>
+        <ButtonText>Scroll to End</ButtonText>
+      </Button>
+
+      <Button
+        variant="solid"
+        color="primary"
+        size="large"
+        label="Scroll to Offset 100"
+        onPress={() => ref.current?.scrollToOffset({animated, offset: 500})}>
+        <ButtonText>Scroll to Offset 500</ButtonText>
+      </Button>
+    </>
+  )
+}
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
index 35a666601..282b3ff5c 100644
--- a/src/view/screens/Storybook/index.tsx
+++ b/src/view/screens/Storybook/index.tsx
@@ -1,8 +1,10 @@
 import React from 'react'
-import {View} from 'react-native'
+import {ScrollView, View} from 'react-native'
 
 import {useSetThemePrefs} from '#/state/shell'
-import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {isWeb} from 'platform/detection'
+import {CenteredView} from '#/view/com/util/Views'
+import {ListContained} from 'view/screens/Storybook/ListContained'
 import {atoms as a, ThemeProvider, useTheme} from '#/alf'
 import {Button, ButtonText} from '#/components/Button'
 import {Breakpoints} from './Breakpoints'
@@ -18,77 +20,111 @@ import {Theming} from './Theming'
 import {Typography} from './Typography'
 
 export function Storybook() {
+  if (isWeb) return <StorybookInner />
+
+  return (
+    <ScrollView>
+      <StorybookInner />
+    </ScrollView>
+  )
+}
+
+function StorybookInner() {
   const t = useTheme()
   const {setColorMode, setDarkTheme} = useSetThemePrefs()
+  const [showContainedList, setShowContainedList] = React.useState(false)
 
   return (
-    <ScrollView>
-      <CenteredView style={[t.atoms.bg]}>
-        <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}>
-          <View style={[a.flex_row, a.align_start, a.gap_md]}>
-            <Button
-              variant="outline"
-              color="primary"
-              size="small"
-              label='Set theme to "system"'
-              onPress={() => setColorMode('system')}>
-              <ButtonText>System</ButtonText>
-            </Button>
-            <Button
-              variant="solid"
-              color="secondary"
-              size="small"
-              label='Set theme to "light"'
-              onPress={() => setColorMode('light')}>
-              <ButtonText>Light</ButtonText>
-            </Button>
+    <CenteredView style={[t.atoms.bg]}>
+      <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}>
+        {!showContainedList ? (
+          <>
+            <View style={[a.flex_row, a.align_start, a.gap_md]}>
+              <Button
+                variant="outline"
+                color="primary"
+                size="small"
+                label='Set theme to "system"'
+                onPress={() => setColorMode('system')}>
+                <ButtonText>System</ButtonText>
+              </Button>
+              <Button
+                variant="solid"
+                color="secondary"
+                size="small"
+                label='Set theme to "light"'
+                onPress={() => setColorMode('light')}>
+                <ButtonText>Light</ButtonText>
+              </Button>
+              <Button
+                variant="solid"
+                color="secondary"
+                size="small"
+                label='Set theme to "dim"'
+                onPress={() => {
+                  setColorMode('dark')
+                  setDarkTheme('dim')
+                }}>
+                <ButtonText>Dim</ButtonText>
+              </Button>
+              <Button
+                variant="solid"
+                color="secondary"
+                size="small"
+                label='Set theme to "dark"'
+                onPress={() => {
+                  setColorMode('dark')
+                  setDarkTheme('dark')
+                }}>
+                <ButtonText>Dark</ButtonText>
+              </Button>
+            </View>
+
+            <Dialogs />
+            <ThemeProvider theme="light">
+              <Theming />
+            </ThemeProvider>
+            <ThemeProvider theme="dim">
+              <Theming />
+            </ThemeProvider>
+            <ThemeProvider theme="dark">
+              <Theming />
+            </ThemeProvider>
+
+            <Typography />
+            <Spacing />
+            <Shadows />
+            <Buttons />
+            <Icons />
+            <Links />
+            <Forms />
+            <Dialogs />
+            <Menus />
+            <Breakpoints />
+
             <Button
               variant="solid"
-              color="secondary"
-              size="small"
-              label='Set theme to "dim"'
-              onPress={() => {
-                setColorMode('dark')
-                setDarkTheme('dim')
-              }}>
-              <ButtonText>Dim</ButtonText>
+              color="primary"
+              size="large"
+              label="Switch to Contained List"
+              onPress={() => setShowContainedList(true)}>
+              <ButtonText>Switch to Contained List</ButtonText>
             </Button>
+          </>
+        ) : (
+          <>
             <Button
               variant="solid"
-              color="secondary"
-              size="small"
-              label='Set theme to "dark"'
-              onPress={() => {
-                setColorMode('dark')
-                setDarkTheme('dark')
-              }}>
-              <ButtonText>Dark</ButtonText>
+              color="primary"
+              size="large"
+              label="Switch to Storybook"
+              onPress={() => setShowContainedList(false)}>
+              <ButtonText>Switch to Storybook</ButtonText>
             </Button>
-          </View>
-
-          <Dialogs />
-          <ThemeProvider theme="light">
-            <Theming />
-          </ThemeProvider>
-          <ThemeProvider theme="dim">
-            <Theming />
-          </ThemeProvider>
-          <ThemeProvider theme="dark">
-            <Theming />
-          </ThemeProvider>
-
-          <Typography />
-          <Spacing />
-          <Shadows />
-          <Buttons />
-          <Icons />
-          <Links />
-          <Forms />
-          <Dialogs />
-          <Menus />
-          <Breakpoints />
-        </View>
-      </CenteredView>
-    </ScrollView>
+            <ListContained />
+          </>
+        )}
+      </View>
+    </CenteredView>
   )
 }