about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx6
-rw-r--r--src/view/com/composer/text-input/web/EmojiPicker.tsx37
-rw-r--r--src/view/com/composer/text-input/web/EmojiPicker.web.tsx11
-rw-r--r--src/view/com/modals/InAppBrowserConsent.tsx99
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/util/PostMeta.tsx4
-rw-r--r--src/view/com/util/forms/NativeDropdown.web.tsx14
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx186
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx182
-rw-r--r--src/view/shell/Composer.web.tsx22
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx25
-rw-r--r--src/view/shell/desktop/LeftNav.tsx2
-rw-r--r--src/view/shell/index.tsx2
13 files changed, 253 insertions, 341 deletions
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 8ec4fefa8..06ff9836c 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -12,14 +12,14 @@ import {Placeholder} from '@tiptap/extension-placeholder'
 import {Text as TiptapText} from '@tiptap/extension-text'
 import {generateJSON} from '@tiptap/html'
 import {Fragment, Node, Slice} from '@tiptap/pm/model'
-import {EditorContent, JSONContent, useEditor} from '@tiptap/react'
+import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
 
 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {blobToDataUri, isUriImage} from '#/lib/media/util'
 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
 import {
-  LinkFacetMatch,
+  type LinkFacetMatch,
   suggestLinkCardUri,
 } from '#/view/com/composer/text-input/text-input-util'
 import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
@@ -28,7 +28,7 @@ import {normalizeTextStyles} from '#/alf/typography'
 import {Portal} from '#/components/Portal'
 import {Text} from '../../util/text/Text'
 import {createSuggestion} from './web/Autocomplete'
-import {Emoji} from './web/EmojiPicker.web'
+import {type Emoji} from './web/EmojiPicker'
 import {LinkDecorator} from './web/LinkDecorator'
 import {TagDecorator} from './web/TagDecorator'
 
diff --git a/src/view/com/composer/text-input/web/EmojiPicker.tsx b/src/view/com/composer/text-input/web/EmojiPicker.tsx
new file mode 100644
index 000000000..5001753a5
--- /dev/null
+++ b/src/view/com/composer/text-input/web/EmojiPicker.tsx
@@ -0,0 +1,37 @@
+export type Emoji = {
+  aliases?: string[]
+  emoticons: string[]
+  id: string
+  keywords: string[]
+  name: string
+  native: string
+  shortcodes?: string
+  unified: string
+}
+
+export interface EmojiPickerPosition {
+  top: number
+  left: number
+  right: number
+  bottom: number
+  nextFocusRef: React.MutableRefObject<HTMLElement> | null
+}
+
+export interface EmojiPickerState {
+  isOpen: boolean
+  pos: EmojiPickerPosition
+}
+
+interface IProps {
+  state: EmojiPickerState
+  close: () => void
+  /**
+   * If `true`, overrides position and ensures picker is pinned to the top of
+   * the target element.
+   */
+  pinToTop?: boolean
+}
+
+export function EmojiPicker(_opts: IProps) {
+  return null
+}
diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
index b3659f22d..c0cae620f 100644
--- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
+++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx
@@ -3,8 +3,7 @@ import {Pressable, useWindowDimensions, View} from 'react-native'
 import Picker from '@emoji-mart/react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
-import {FocusScope} from '@radix-ui/react-focus-scope'
+import {DismissableLayer, FocusScope} from 'radix-ui/internal'
 
 import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
 import {atoms as a, flatten} from '#/alf'
@@ -121,7 +120,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
 
   return (
     <Portal>
-      <FocusScope
+      <FocusScope.FocusScope
         loop
         trapped
         onUnmountAutoFocus={e => {
@@ -154,7 +153,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
             },
           ])}>
           <View style={[{position: 'absolute'}, position]}>
-            <DismissableLayer
+            <DismissableLayer.DismissableLayer
               onFocusOutside={evt => evt.preventDefault()}
               onDismiss={close}>
               <Picker
@@ -164,7 +163,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
                 onEmojiSelect={onInsert}
                 autoFocus={true}
               />
-            </DismissableLayer>
+            </DismissableLayer.DismissableLayer>
           </View>
         </View>
 
@@ -175,7 +174,7 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
           onPress={close}
           style={[a.fixed, a.inset_0]}
         />
-      </FocusScope>
+      </FocusScope.FocusScope>
     </Portal>
   )
 }
diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx
deleted file mode 100644
index 105edfbc6..000000000
--- a/src/view/com/modals/InAppBrowserConsent.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useOpenLink} from '#/lib/hooks/useOpenLink'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {s} from '#/lib/styles'
-import {useModalControls} from '#/state/modals'
-import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
-import {ScrollView} from '#/view/com/modals/util'
-import {Button} from '#/view/com/util/forms/Button'
-import {Text} from '#/view/com/util/text/Text'
-
-export const snapPoints = [350]
-
-export function Component({href}: {href: string}) {
-  const pal = usePalette('default')
-  const {closeModal} = useModalControls()
-  const {_} = useLingui()
-  const setInAppBrowser = useSetInAppBrowser()
-  const openLink = useOpenLink()
-
-  const onUseIAB = React.useCallback(() => {
-    setInAppBrowser(true)
-    closeModal()
-    openLink(href, true)
-  }, [closeModal, setInAppBrowser, href, openLink])
-
-  const onUseLinking = React.useCallback(() => {
-    setInAppBrowser(false)
-    closeModal()
-    openLink(href, false)
-  }, [closeModal, setInAppBrowser, href, openLink])
-
-  return (
-    <ScrollView
-      testID="inAppBrowserConsentModal"
-      style={[s.flex1, pal.view, {paddingHorizontal: 20, paddingTop: 10}]}>
-      <Text style={[pal.text, styles.title]}>
-        <Trans>How should we open this link?</Trans>
-      </Text>
-      <Text style={pal.text}>
-        <Trans>
-          Your choice will be saved, but can be changed later in settings.
-        </Trans>
-      </Text>
-      <View style={[styles.btnContainer]}>
-        <Button
-          testID="confirmBtn"
-          type="inverted"
-          onPress={onUseIAB}
-          accessibilityLabel={_(msg`Use in-app browser`)}
-          accessibilityHint=""
-          label={_(msg`Use in-app browser`)}
-          labelContainerStyle={{justifyContent: 'center', padding: 8}}
-          labelStyle={[s.f18]}
-        />
-        <Button
-          testID="confirmBtn"
-          type="inverted"
-          onPress={onUseLinking}
-          accessibilityLabel={_(msg`Use my default browser`)}
-          accessibilityHint=""
-          label={_(msg`Use my default browser`)}
-          labelContainerStyle={{justifyContent: 'center', padding: 8}}
-          labelStyle={[s.f18]}
-        />
-        <Button
-          testID="cancelBtn"
-          type="default"
-          onPress={() => {
-            closeModal()
-          }}
-          accessibilityLabel={_(msg`Cancel`)}
-          accessibilityHint=""
-          label={_(msg`Cancel`)}
-          labelContainerStyle={{justifyContent: 'center', padding: 8}}
-          labelStyle={[s.f18]}
-        />
-      </View>
-    </ScrollView>
-  )
-}
-
-const styles = StyleSheet.create({
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-    fontSize: 24,
-    marginBottom: 12,
-  },
-  btnContainer: {
-    marginTop: 20,
-    flexDirection: 'column',
-    justifyContent: 'center',
-    rowGap: 10,
-  },
-})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index d0b50c857..8fd927f16 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -11,7 +11,6 @@ import * as ChangePasswordModal from './ChangePassword'
 import * as CreateOrEditListModal from './CreateOrEditList'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as EditProfileModal from './EditProfile'
-import * as InAppBrowserConsentModal from './InAppBrowserConsent'
 import * as InviteCodesModal from './InviteCodes'
 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
@@ -76,9 +75,6 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'link-warning') {
     snapPoints = LinkWarningModal.snapPoints
     element = <LinkWarningModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'in-app-browser-consent') {
-    snapPoints = InAppBrowserConsentModal.snapPoints
-    element = <InAppBrowserConsentModal.Component {...activeModal} />
   } else {
     return null
   }
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index d5af32236..fd8e3a38b 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -107,11 +107,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
                   a.pl_2xs,
                   a.self_center,
                   {
-                    marginTop: platform({web: -1, ios: -1, android: -2}),
+                    marginTop: platform({web: 0, ios: 0, android: -1}),
                   },
                 ]}>
                 <VerificationCheck
-                  width={14}
+                  width={platform({android: 13, default: 12})}
                   verifier={verification.role === 'verifier'}
                 />
               </View>
diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx
index b3ec319e3..9b4a84e05 100644
--- a/src/view/com/util/forms/NativeDropdown.web.tsx
+++ b/src/view/com/util/forms/NativeDropdown.web.tsx
@@ -1,9 +1,15 @@
 import React from 'react'
-import {Pressable, StyleSheet, Text, View, ViewStyle} from 'react-native'
-import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {
+  Pressable,
+  StyleSheet,
+  Text,
+  type View,
+  type ViewStyle,
+} from 'react-native'
+import {type IconProp} from '@fortawesome/fontawesome-svg-core'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
-import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
+import {DropdownMenu} from 'radix-ui'
+import {type MenuItemCommonProps} from 'zeego/lib/typescript/menu'
 
 import {HITSLOP_10} from '#/lib/constants'
 import {usePalette} from '#/lib/hooks/usePalette'
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index cefa29f6c..bb94f8083 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -1,19 +1,10 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  FlatList,
-  RefreshControl,
-  StyleSheet,
-  View,
-} from 'react-native'
+import {useCallback, useMemo, useState} from 'react'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
 import {useFocusEffect} from '@react-navigation/native'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
@@ -21,11 +12,12 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {Text} from '#/view/com/util/text/Text'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {List} from '#/view/com/util/List'
 import {atoms as a, useTheme} from '#/alf'
 import * as Layout from '#/components/Layout'
+import {ListFooter} from '#/components/Lists'
 import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -33,13 +25,10 @@ type Props = NativeStackScreenProps<
 >
 export function ModerationBlockedAccounts({}: Props) {
   const t = useTheme()
-  const pal = usePalette('default')
-  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isTabletOrDesktop} = useWebMediaQueries()
   const moderationOpts = useModerationOpts()
 
-  const [isPTRing, setIsPTRing] = React.useState(false)
+  const [isPTRing, setIsPTRing] = useState(false)
   const {
     data,
     isFetching,
@@ -51,7 +40,7 @@ export function ModerationBlockedAccounts({}: Props) {
     isFetchingNextPage,
   } = useMyBlockedAccountsQuery()
   const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
-  const profiles = React.useMemo(() => {
+  const profiles = useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.blocks)
     }
@@ -59,12 +48,12 @@ export function ModerationBlockedAccounts({}: Props) {
   }, [data])
 
   useFocusEffect(
-    React.useCallback(() => {
+    useCallback(() => {
       setMinimalShellMode(false)
     }, [setMinimalShellMode]),
   )
 
-  const onRefresh = React.useCallback(async () => {
+  const onRefresh = useCallback(async () => {
     setIsPTRing(true)
     try {
       await refetch()
@@ -74,7 +63,7 @@ export function ModerationBlockedAccounts({}: Props) {
     setIsPTRing(false)
   }, [refetch, setIsPTRing])
 
-  const onEndReached = React.useCallback(async () => {
+  const onEndReached = useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
 
     try {
@@ -104,28 +93,22 @@ export function ModerationBlockedAccounts({}: Props) {
       </View>
     )
   }
+
   return (
     <Layout.Screen testID="blockedAccountsScreen">
-      <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
-        <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
-        <Text
-          type="sm"
-          style={[
-            styles.description,
-            pal.text,
-            isTabletOrDesktop && styles.descriptionDesktop,
-            {
-              marginTop: 20,
-            },
-          ]}>
-          <Trans>
-            Blocked accounts cannot reply in your threads, mention you, or
-            otherwise interact with you. You will not see their content and they
-            will be prevented from seeing yours.
-          </Trans>
-        </Text>
+      <Layout.Center>
+        <Layout.Header.Outer>
+          <Layout.Header.BackButton />
+          <Layout.Header.Content>
+            <Layout.Header.TitleText>
+              <Trans>Blocked Accounts</Trans>
+            </Layout.Header.TitleText>
+          </Layout.Header.Content>
+          <Layout.Header.Slot />
+        </Layout.Header.Outer>
         {isEmpty ? (
-          <View style={[pal.border]}>
+          <View>
+            <Info style={[a.border_b]} />
             {isError ? (
               <ErrorScreen
                 title="Oops!"
@@ -133,42 +116,29 @@ export function ModerationBlockedAccounts({}: Props) {
                 onPressTryAgain={refetch}
               />
             ) : (
-              <View style={[styles.empty, pal.viewLight]}>
-                <Text type="lg" style={[pal.text, styles.emptyText]}>
-                  <Trans>
-                    You have not blocked any accounts yet. To block an account,
-                    go to their profile and select "Block account" from the menu
-                    on their account.
-                  </Trans>
-                </Text>
-              </View>
+              <Empty />
             )}
           </View>
         ) : (
-          <FlatList
-            style={[!isTabletOrDesktop && styles.flex1]}
+          <List
             data={profiles}
             keyExtractor={(item: ActorDefs.ProfileView) => item.did}
-            refreshControl={
-              <RefreshControl
-                refreshing={isPTRing}
-                onRefresh={onRefresh}
-                tintColor={pal.colors.text}
-                titleColor={pal.colors.text}
-              />
-            }
+            refreshing={isPTRing}
+            onRefresh={onRefresh}
             onEndReached={onEndReached}
             renderItem={renderItem}
             initialNumToRender={15}
             // FIXME(dan)
 
-            ListFooterComponent={() => (
-              <View style={styles.footer}>
-                {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
-              </View>
-            )}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
+            ListHeaderComponent={Info}
+            ListFooterComponent={
+              <ListFooter
+                isFetchingNextPage={isFetchingNextPage}
+                hasNextPage={hasNextPage}
+                error={cleanError(error)}
+                onRetry={fetchNextPage}
+              />
+            }
           />
         )}
       </Layout.Center>
@@ -176,37 +146,53 @@ export function ModerationBlockedAccounts({}: Props) {
   )
 }
 
-const styles = StyleSheet.create({
-  title: {
-    textAlign: 'center',
-    marginTop: 12,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 30,
-    marginBottom: 14,
-  },
-  descriptionDesktop: {
-    marginTop: 14,
-  },
-
-  flex1: {
-    flex: 1,
-  },
-  empty: {
-    paddingHorizontal: 20,
-    paddingVertical: 20,
-    borderRadius: 16,
-    marginHorizontal: 24,
-    marginTop: 10,
-  },
-  emptyText: {
-    textAlign: 'center',
-  },
+function Empty() {
+  const t = useTheme()
+  return (
+    <View style={[a.pt_2xl, a.px_xl, a.align_center]}>
+      <View
+        style={[
+          a.py_md,
+          a.px_lg,
+          a.rounded_sm,
+          t.atoms.bg_contrast_25,
+          a.border,
+          t.atoms.border_contrast_low,
+          {maxWidth: 400},
+        ]}>
+        <Text style={[a.text_sm, a.text_center, t.atoms.text_contrast_high]}>
+          <Trans>
+            You have not blocked any accounts yet. To block an account, go to
+            their profile and select "Block account" from the menu on their
+            account.
+          </Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
 
-  footer: {
-    height: 200,
-    paddingTop: 20,
-  },
-})
+function Info({style}: {style?: StyleProp<ViewStyle>}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.w_full,
+        t.atoms.bg_contrast_25,
+        a.py_md,
+        a.px_xl,
+        a.border_t,
+        {marginTop: a.border.borderWidth * -1},
+        t.atoms.border_contrast_low,
+        style,
+      ]}>
+      <Text style={[a.text_center, a.text_sm, t.atoms.text_contrast_high]}>
+        <Trans>
+          Blocked accounts cannot reply in your threads, mention you, or
+          otherwise interact with you. You will not see their content and they
+          will be prevented from seeing yours.
+        </Trans>
+      </Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index f49337b7c..11d787ca1 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -1,19 +1,11 @@
-import React from 'react'
-import {
-  ActivityIndicator,
-  FlatList,
-  RefreshControl,
-  StyleSheet,
-  View,
-} from 'react-native'
+import {useCallback, useMemo, useState} from 'react'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
@@ -21,11 +13,12 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {Text} from '#/view/com/util/text/Text'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {List} from '#/view/com/util/List'
 import {atoms as a, useTheme} from '#/alf'
 import * as Layout from '#/components/Layout'
+import {ListFooter} from '#/components/Lists'
 import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -33,13 +26,11 @@ type Props = NativeStackScreenProps<
 >
 export function ModerationMutedAccounts({}: Props) {
   const t = useTheme()
-  const pal = usePalette('default')
+  const moderationOpts = useModerationOpts()
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isTabletOrDesktop} = useWebMediaQueries()
-  const moderationOpts = useModerationOpts()
 
-  const [isPTRing, setIsPTRing] = React.useState(false)
+  const [isPTRing, setIsPTRing] = useState(false)
   const {
     data,
     isFetching,
@@ -51,7 +42,7 @@ export function ModerationMutedAccounts({}: Props) {
     isFetchingNextPage,
   } = useMyMutedAccountsQuery()
   const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
-  const profiles = React.useMemo(() => {
+  const profiles = useMemo(() => {
     if (data?.pages) {
       return data.pages.flatMap(page => page.mutes)
     }
@@ -59,12 +50,12 @@ export function ModerationMutedAccounts({}: Props) {
   }, [data])
 
   useFocusEffect(
-    React.useCallback(() => {
+    useCallback(() => {
       setMinimalShellMode(false)
     }, [setMinimalShellMode]),
   )
 
-  const onRefresh = React.useCallback(async () => {
+  const onRefresh = useCallback(async () => {
     setIsPTRing(true)
     try {
       await refetch()
@@ -74,7 +65,7 @@ export function ModerationMutedAccounts({}: Props) {
     setIsPTRing(false)
   }, [refetch, setIsPTRing])
 
-  const onEndReached = React.useCallback(async () => {
+  const onEndReached = useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
 
     try {
@@ -120,25 +111,19 @@ export function ModerationMutedAccounts({}: Props) {
   }
   return (
     <Layout.Screen testID="mutedAccountsScreen">
-      <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
-      <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
-        <Text
-          type="sm"
-          style={[
-            styles.description,
-            pal.text,
-            isTabletOrDesktop && styles.descriptionDesktop,
-            {
-              marginTop: 20,
-            },
-          ]}>
-          <Trans>
-            Muted accounts have their posts removed from your feed and from your
-            notifications. Mutes are completely private.
-          </Trans>
-        </Text>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Muted Accounts</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Center>
         {isEmpty ? (
-          <View style={[pal.border]}>
+          <View>
+            <Info style={[a.border_b]} />
             {isError ? (
               <ErrorScreen
                 title="Oops!"
@@ -146,42 +131,29 @@ export function ModerationMutedAccounts({}: Props) {
                 onPressTryAgain={refetch}
               />
             ) : (
-              <View style={[styles.empty, pal.viewLight]}>
-                <Text type="lg" style={[pal.text, styles.emptyText]}>
-                  <Trans>
-                    You have not muted any accounts yet. To mute an account, go
-                    to their profile and select "Mute account" from the menu on
-                    their account.
-                  </Trans>
-                </Text>
-              </View>
+              <Empty />
             )}
           </View>
         ) : (
-          <FlatList
-            style={[!isTabletOrDesktop && styles.flex1]}
+          <List
             data={profiles}
             keyExtractor={item => item.did}
-            refreshControl={
-              <RefreshControl
-                refreshing={isPTRing}
-                onRefresh={onRefresh}
-                tintColor={pal.colors.text}
-                titleColor={pal.colors.text}
-              />
-            }
+            refreshing={isPTRing}
+            onRefresh={onRefresh}
             onEndReached={onEndReached}
             renderItem={renderItem}
             initialNumToRender={15}
             // FIXME(dan)
 
-            ListFooterComponent={() => (
-              <View style={styles.footer}>
-                {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
-              </View>
-            )}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
+            ListHeaderComponent={Info}
+            ListFooterComponent={
+              <ListFooter
+                isFetchingNextPage={isFetchingNextPage}
+                hasNextPage={hasNextPage}
+                error={cleanError(error)}
+                onRetry={fetchNextPage}
+              />
+            }
           />
         )}
       </Layout.Center>
@@ -189,37 +161,51 @@ export function ModerationMutedAccounts({}: Props) {
   )
 }
 
-const styles = StyleSheet.create({
-  title: {
-    textAlign: 'center',
-    marginTop: 12,
-    marginBottom: 12,
-  },
-  description: {
-    textAlign: 'center',
-    paddingHorizontal: 30,
-    marginBottom: 14,
-  },
-  descriptionDesktop: {
-    marginTop: 14,
-  },
-
-  flex1: {
-    flex: 1,
-  },
-  empty: {
-    paddingHorizontal: 20,
-    paddingVertical: 20,
-    borderRadius: 16,
-    marginHorizontal: 24,
-    marginTop: 10,
-  },
-  emptyText: {
-    textAlign: 'center',
-  },
+function Empty() {
+  const t = useTheme()
+  return (
+    <View style={[a.pt_2xl, a.px_xl, a.align_center]}>
+      <View
+        style={[
+          a.py_md,
+          a.px_lg,
+          a.rounded_sm,
+          t.atoms.bg_contrast_25,
+          a.border,
+          t.atoms.border_contrast_low,
+          {maxWidth: 400},
+        ]}>
+        <Text style={[a.text_sm, a.text_center, t.atoms.text_contrast_high]}>
+          <Trans>
+            You have not muted any accounts yet. To mute an account, go to their
+            profile and select "Mute account" from the menu on their account.
+          </Trans>
+        </Text>
+      </View>
+    </View>
+  )
+}
 
-  footer: {
-    height: 200,
-    paddingTop: 20,
-  },
-})
+function Info({style}: {style?: StyleProp<ViewStyle>}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.w_full,
+        t.atoms.bg_contrast_25,
+        a.py_md,
+        a.px_xl,
+        a.border_t,
+        {marginTop: a.border.borderWidth * -1},
+        t.atoms.border_contrast_low,
+        style,
+      ]}>
+      <Text style={[a.text_center, a.text_sm, t.atoms.text_contrast_high]}>
+        <Trans>
+          Muted accounts have their posts removed from your feed and from your
+          notifications. Mutes are completely private.
+        </Trans>
+      </Text>
+    </View>
+  )
+}
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index b76e88372..ce3695212 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -1,18 +1,16 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
-import {useFocusGuards} from '@radix-ui/react-focus-guards'
-import {FocusScope} from '@radix-ui/react-focus-scope'
+import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal'
 import {RemoveScrollBar} from 'react-remove-scroll-bar'
 
 import {useA11y} from '#/state/a11y'
 import {useModals} from '#/state/modals'
-import {ComposerOpts, useComposerState} from '#/state/shell/composer'
+import {type ComposerOpts, useComposerState} from '#/state/shell/composer'
 import {
   EmojiPicker,
-  EmojiPickerPosition,
-  EmojiPickerState,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
+  type EmojiPickerPosition,
+  type EmojiPickerState,
+} from '#/view/com/composer/text-input/web/EmojiPicker'
 import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
 import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
 
@@ -66,11 +64,11 @@ function Inner({state}: {state: ComposerOpts}) {
     }))
   }, [])
 
-  useFocusGuards()
+  FocusGuards.useFocusGuards()
 
   return (
-    <FocusScope loop trapped asChild>
-      <DismissableLayer
+    <FocusScope.FocusScope loop trapped asChild>
+      <DismissableLayer.DismissableLayer
         role="dialog"
         aria-modal
         style={flatten([
@@ -114,8 +112,8 @@ function Inner({state}: {state: ComposerOpts}) {
           />
         </View>
         <EmojiPicker state={pickerState} close={onClosePicker} />
-      </DismissableLayer>
-    </FocusScope>
+      </DismissableLayer.DismissableLayer>
+    </FocusScope.FocusScope>
   )
 }
 
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
index 11beaa2e9..868bba5b0 100644
--- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -5,21 +5,21 @@ import {View} from 'react-native'
 // Copyright (c) 2017 React Navigation Contributors
 import {
   createNavigatorFactory,
-  EventArg,
-  ParamListBase,
-  StackActionHelpers,
+  type EventArg,
+  type ParamListBase,
+  type StackActionHelpers,
   StackActions,
-  StackNavigationState,
+  type StackNavigationState,
   StackRouter,
-  StackRouterOptions,
+  type StackRouterOptions,
   useNavigationBuilder,
 } from '@react-navigation/native'
-import type {
-  NativeStackNavigationEventMap,
-  NativeStackNavigationOptions,
+import {
+  type NativeStackNavigationEventMap,
+  type NativeStackNavigationOptions,
 } from '@react-navigation/native-stack'
 import {NativeStackView} from '@react-navigation/native-stack'
-import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
+import {type NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
 
 import {PWI_ENABLED} from '#/lib/build-flags'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -35,7 +35,7 @@ import {Deactivated} from '#/screens/Deactivated'
 import {Onboarding} from '#/screens/Onboarding'
 import {SignupQueued} from '#/screens/SignupQueued'
 import {Takendown} from '#/screens/Takendown'
-import {atoms as a} from '#/alf'
+import {atoms as a, useLayoutBreakpoints} from '#/alf'
 import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
 import {DesktopLeftNav} from './desktop/LeftNav'
 import {DesktopRightNav} from './desktop/RightNav'
@@ -101,7 +101,8 @@ function NativeStackNavigator({
   const onboardingState = useOnboardingState()
   const {showLoggedOut} = useLoggedOutView()
   const {setShowLoggedOut} = useLoggedOutViewControls()
-  const {isMobile, isTabletOrMobile} = useWebMediaQueries()
+  const {isMobile} = useWebMediaQueries()
+  const {leftNavMinimal} = useLayoutBreakpoints()
   if (!hasSession && (!PWI_ENABLED || activeRouteRequiresAuth || isNative)) {
     return <LoggedOut />
   }
@@ -138,7 +139,7 @@ function NativeStackNavigator({
 
   // Show the bottom bar if we have a session only on mobile web. If we don't have a session, we want to show it
   // on both tablet and mobile web so that we see the create account CTA.
-  const showBottomBar = hasSession ? isMobile : isTabletOrMobile
+  const showBottomBar = hasSession ? isMobile : leftNavMinimal
 
   return (
     <NavigationContent>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 5cef18ebf..7d7c0ac8d 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -571,7 +571,7 @@ export function DesktopLeftNav() {
       ]}>
       {hasSession ? (
         <ProfileCard />
-      ) : isDesktop ? (
+      ) : !leftNavMinimal ? (
         <View style={[a.pt_xl]}>
           <NavSignupCard />
         </View>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 3d3a5520c..1e34f6da5 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -25,6 +25,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
 import {atoms as a, select, useTheme} from '#/alf'
 import {setSystemUITheme} from '#/alf/util/systemUI'
+import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 import {SigninDialog} from '#/components/dialogs/Signin'
 import {Outlet as PortalOutlet} from '#/components/Portal'
@@ -151,6 +152,7 @@ function ShellInner() {
       <ModalsContainer />
       <MutedWordsDialog />
       <SigninDialog />
+      <InAppBrowserConsentDialog />
       <Lightbox />
       <PortalOutlet />
       <BottomSheetOutlet />