about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/AvatarStack.tsx1
-rw-r--r--src/components/Button.tsx1
-rw-r--r--src/components/Dialog/index.tsx2
-rw-r--r--src/components/Dialog/index.web.tsx43
-rw-r--r--src/components/Dialog/shared.tsx8
-rw-r--r--src/components/Divider.tsx1
-rw-r--r--src/components/Error.tsx1
-rw-r--r--src/components/GradientFill.tsx1
-rw-r--r--src/components/IconCircle.tsx1
-rw-r--r--src/components/LikesDialog.tsx2
-rw-r--r--src/components/Link.tsx22
-rw-r--r--src/components/Loader.tsx7
-rw-r--r--src/components/Loader.web.tsx1
-rw-r--r--src/components/Menu/index.web.tsx2
-rw-r--r--src/components/ProfileCard.tsx4
-rw-r--r--src/components/ProfileHoverCard/index.web.tsx4
-rw-r--r--src/components/ProgressGuide/List.tsx1
-rw-r--r--src/components/ProgressGuide/Task.tsx1
-rw-r--r--src/components/ProgressGuide/Toast.tsx48
-rw-r--r--src/components/ReportDialog/SelectLabelerView.tsx1
-rw-r--r--src/components/RichText.tsx33
-rw-r--r--src/components/StarterPack/Main/ProfilesList.tsx2
-rw-r--r--src/components/StarterPack/ProfileStarterPacks.tsx27
-rw-r--r--src/components/StarterPack/ShareDialog.tsx1
-rw-r--r--src/components/StarterPack/Wizard/WizardEditListDialog.tsx2
-rw-r--r--src/components/StarterPack/Wizard/WizardListCard.tsx1
-rw-r--r--src/components/SubtleWebHover.tsx4
-rw-r--r--src/components/SubtleWebHover.web.tsx13
-rw-r--r--src/components/TagMenu/index.tsx314
-rw-r--r--src/components/Typography.tsx149
-rw-r--r--src/components/anim/AnimatedCheck.tsx20
-rw-r--r--src/components/dialogs/EmbedConsent.tsx2
-rw-r--r--src/components/dialogs/PostInteractionSettingsDialog.tsx7
-rw-r--r--src/components/dialogs/SwitchAccount.tsx2
-rw-r--r--src/components/dialogs/VerifyEmailDialog.tsx101
-rw-r--r--src/components/dialogs/nuxs/NeueTypography.tsx117
-rw-r--r--src/components/dialogs/nuxs/index.tsx26
-rw-r--r--src/components/dms/ActionsWrapper.tsx16
-rw-r--r--src/components/dms/ChatEmptyPill.tsx6
-rw-r--r--src/components/dms/ConvoMenu.tsx2
-rw-r--r--src/components/dms/LeaveConvoPrompt.tsx1
-rw-r--r--src/components/dms/MessageItem.tsx3
-rw-r--r--src/components/dms/MessageMenu.tsx2
-rw-r--r--src/components/dms/MessageProfileButton.tsx56
-rw-r--r--src/components/dms/NewMessagesPill.tsx6
-rw-r--r--src/components/dms/ReportConversationPrompt.tsx1
-rw-r--r--src/components/dms/dialogs/NewChatDialog.tsx22
-rw-r--r--src/components/dms/dialogs/SearchablePeopleList.tsx3
-rw-r--r--src/components/dms/dialogs/ShareViaChatDialog.tsx2
-rw-r--r--src/components/forms/DateField/index.shared.tsx1
-rw-r--r--src/components/forms/FormError.tsx1
-rw-r--r--src/components/hooks/dates.ts6
-rw-r--r--src/components/hooks/useFollowMethods.ts4
-rw-r--r--src/components/icons/CalendarClock.tsx5
-rw-r--r--src/components/icons/ChainLink.tsx5
-rw-r--r--src/components/icons/common.tsx1
-rw-r--r--src/components/moderation/ContentHider.tsx40
-rw-r--r--src/components/moderation/LabelPreference.tsx9
-rw-r--r--src/components/moderation/LabelsOnMe.tsx1
-rw-r--r--src/components/moderation/ModerationDetailsDialog.tsx1
-rw-r--r--src/components/moderation/PostAlerts.tsx1
-rw-r--r--src/components/moderation/ProfileHeaderAlerts.tsx1
-rw-r--r--src/components/video/PlayButtonIcon.tsx37
63 files changed, 542 insertions, 664 deletions
diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx
index 5f790fb67..aea472512 100644
--- a/src/components/AvatarStack.tsx
+++ b/src/components/AvatarStack.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 import {moderateProfile} from '@atproto/api'
 
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index aaac73bd3..3329dca05 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -458,7 +458,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
         // @ts-ignore - this will always be a pressable
         ref={ref}
         aria-label={label}
-        aria-pressed={state.pressed}
         accessibilityLabel={label}
         disabled={disabled || false}
         accessibilityState={{
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index 0e78fcf97..c9455c5cc 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -75,7 +75,7 @@ export function Outer({
       try {
         cb()
       } catch (e: any) {
-        logger.error('Error running close callback', e)
+        logger.error(e || 'Error running close callback')
       }
     }
 
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 41a39ffda..6b92eee3e 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -7,7 +7,6 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
@@ -42,7 +41,6 @@ export function Outer({
   onClose,
 }: React.PropsWithChildren<DialogOuterProps>) {
   const {_} = useLingui()
-  const t = useTheme()
   const {gtMobile} = useBreakpoints()
   const [isOpen, setIsOpen] = React.useState(false)
   const {setDialogIsOpen} = useDialogStateControlContext()
@@ -118,16 +116,7 @@ export function Outer({
                   gtMobile ? a.p_lg : a.p_md,
                   {overflowY: 'auto'},
                 ]}>
-                <Animated.View
-                  entering={FadeIn.duration(150)}
-                  // exiting={FadeOut.duration(150)}
-                  style={[
-                    web(a.fixed),
-                    a.inset_0,
-                    {opacity: 0.8, backgroundColor: t.palette.black},
-                  ]}
-                />
-
+                <Backdrop />
                 <View
                   style={[
                     a.w_full,
@@ -164,7 +153,7 @@ export function Inner({
   useFocusGuards()
   return (
     <FocusScope loop asChild trapped>
-      <Animated.View
+      <View
         role="dialog"
         aria-role="dialog"
         aria-label={label}
@@ -174,8 +163,6 @@ export function Inner({
         onClick={stopPropagation}
         onStartShouldSetResponder={_ => true}
         onTouchEnd={stopPropagation}
-        entering={FadeInDown.duration(100)}
-        // exiting={FadeOut.duration(100)}
         style={flatten([
           a.relative,
           a.rounded_md,
@@ -188,6 +175,8 @@ export function Inner({
             shadowColor: t.palette.black,
             shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
             shadowRadius: 30,
+            // @ts-ignore web only
+            animation: 'fadeIn ease-out 0.1s',
           },
           flatten(style),
         ])}>
@@ -201,7 +190,7 @@ export function Inner({
             {children}
           </View>
         </DismissableLayer>
-      </Animated.View>
+      </View>
     </FocusScope>
   )
 }
@@ -268,3 +257,25 @@ export function Close() {
 export function Handle() {
   return null
 }
+
+function Backdrop() {
+  const t = useTheme()
+  return (
+    <View
+      style={{
+        opacity: 0.8,
+      }}>
+      <View
+        style={[
+          a.fixed,
+          a.inset_0,
+          {
+            backgroundColor: t.palette.black,
+            // @ts-ignore web only
+            animation: 'fadeIn ease-out 0.15s',
+          },
+        ]}
+      />
+    </View>
+  )
+}
diff --git a/src/components/Dialog/shared.tsx b/src/components/Dialog/shared.tsx
index 6f9bc2678..44a4f6b0b 100644
--- a/src/components/Dialog/shared.tsx
+++ b/src/components/Dialog/shared.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'
 
-import {atoms as a, useTheme, web} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
 import {Text} from '#/components/Typography'
 
 export function Header({
@@ -29,10 +29,8 @@ export function Header({
         a.border_b,
         t.atoms.border_contrast_medium,
         t.atoms.bg,
-        web([
-          {borderRadiusTopLeft: a.rounded_md.borderRadius},
-          {borderRadiusTopRight: a.rounded_md.borderRadius},
-        ]),
+        {borderTopLeftRadius: a.rounded_md.borderRadius},
+        {borderTopRightRadius: a.rounded_md.borderRadius},
         style,
       ]}>
       {renderLeft && (
diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx
index ff0bbb045..e4891aacb 100644
--- a/src/components/Divider.tsx
+++ b/src/components/Divider.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 
 import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
diff --git a/src/components/Error.tsx b/src/components/Error.tsx
index 2819986b3..dc8e53b46 100644
--- a/src/components/Error.tsx
+++ b/src/components/Error.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/components/GradientFill.tsx b/src/components/GradientFill.tsx
index 3c64c8960..fa39577d4 100644
--- a/src/components/GradientFill.tsx
+++ b/src/components/GradientFill.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {LinearGradient} from 'expo-linear-gradient'
 
 import {atoms as a, tokens} from '#/alf'
diff --git a/src/components/IconCircle.tsx b/src/components/IconCircle.tsx
index 806d35c38..2119c9f8d 100644
--- a/src/components/IconCircle.tsx
+++ b/src/components/IconCircle.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 
 import {
diff --git a/src/components/LikesDialog.tsx b/src/components/LikesDialog.tsx
index 4c68596f7..cb000b433 100644
--- a/src/components/LikesDialog.tsx
+++ b/src/components/LikesDialog.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useMemo} from 'react'
+import {useCallback, useMemo} from 'react'
 import {ActivityIndicator, FlatList, View} from 'react-native'
 import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 054a543c1..ef31ea0c5 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -223,7 +223,7 @@ export function Link({
       {...web({
         hrefAttrs: {
           target: download ? undefined : isExternal ? 'blank' : undefined,
-          rel: isExternal ? 'noopener noreferrer' : undefined,
+          rel: isExternal ? 'noopener' : undefined,
           download,
         },
         dataSet: {
@@ -274,11 +274,6 @@ export function InlineLinkText({
     onOut: onHoverOut,
   } = useInteractionState()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-  const {
-    state: pressed,
-    onIn: onPressIn,
-    onOut: onPressOut,
-  } = useInteractionState()
   const flattenedStyle = flatten(style) || {}
 
   return (
@@ -289,19 +284,20 @@ export function InlineLinkText({
       {...rest}
       style={[
         {color: t.palette.primary_500},
-        (hovered || focused || pressed) &&
+        (hovered || focused) &&
           !disableUnderline && {
-            ...web({outline: 0}),
-            textDecorationLine: 'underline',
-            textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
+            ...web({
+              outline: 0,
+              textDecorationLine: 'underline',
+              textDecorationColor:
+                flattenedStyle.color ?? t.palette.primary_500,
+            }),
           },
         flattenedStyle,
       ]}
       role="link"
       onPress={download ? undefined : onPress}
       onLongPress={onLongPress}
-      onPressIn={onPressIn}
-      onPressOut={onPressOut}
       onFocus={onFocus}
       onBlur={onBlur}
       onMouseEnter={onHoverIn}
@@ -311,7 +307,7 @@ export function InlineLinkText({
       {...web({
         hrefAttrs: {
           target: download ? undefined : isExternal ? 'blank' : undefined,
-          rel: isExternal ? 'noopener noreferrer' : undefined,
+          rel: isExternal ? 'noopener' : undefined,
           download,
         },
         dataSet: {
diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx
index e0b3be637..149554912 100644
--- a/src/components/Loader.tsx
+++ b/src/components/Loader.tsx
@@ -17,13 +17,12 @@ export function Loader(props: Props) {
   const rotation = useSharedValue(0)
 
   const animatedStyles = useAnimatedStyle(() => ({
-    transform: [{rotate: rotation.value + 'deg'}],
+    transform: [{rotate: rotation.get() + 'deg'}],
   }))
 
   React.useEffect(() => {
-    rotation.value = withRepeat(
-      withTiming(360, {duration: 500, easing: Easing.linear}),
-      -1,
+    rotation.set(() =>
+      withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
     )
   }, [rotation])
 
diff --git a/src/components/Loader.web.tsx b/src/components/Loader.web.tsx
index d8182673f..acf0acfc4 100644
--- a/src/components/Loader.web.tsx
+++ b/src/components/Loader.web.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 
 import {atoms as a, flatten, useTheme} from '#/alf'
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index d68dcba51..37ad67e29 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable react/prop-types */
-
 import React from 'react'
 import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
 import {msg} from '@lingui/macro'
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index 50b34ba99..668bd0f3c 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -283,8 +283,8 @@ export function DescriptionPlaceholder({
 export type FollowButtonProps = {
   profile: AppBskyActorDefs.ProfileViewBasic
   moderationOpts: ModerationOpts
-  logContext: LogEvents['profile:follow:sampled']['logContext'] &
-    LogEvents['profile:unfollow:sampled']['logContext']
+  logContext: LogEvents['profile:follow']['logContext'] &
+    LogEvents['profile:unfollow']['logContext']
 } & Partial<ButtonProps>
 
 export function FollowButton(props: FollowButtonProps) {
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index 4cda42fdb..3e58ced90 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -302,8 +302,8 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
   const animationStyle = {
     animation:
       currentState.stage === 'hiding'
-        ? `avatarHoverFadeOut ${HIDE_DURATION}ms both`
-        : `avatarHoverFadeIn ${SHOW_DURATION}ms both`,
+        ? `fadeOut ${HIDE_DURATION}ms both`
+        : `fadeIn ${SHOW_DURATION}ms both`,
   }
 
   return (
diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx
index d0fd55d9c..299d1e69f 100644
--- a/src/components/ProgressGuide/List.tsx
+++ b/src/components/ProgressGuide/List.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {StyleProp, View, ViewStyle} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx
index f2ceba52a..973ee1ac7 100644
--- a/src/components/ProgressGuide/Task.tsx
+++ b/src/components/ProgressGuide/Task.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 import * as Progress from 'react-native-progress'
 
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx
index 69e008260..b26c718f8 100644
--- a/src/components/ProgressGuide/Toast.tsx
+++ b/src/components/ProgressGuide/Toast.tsx
@@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef<
 
     // animate the opacity then set isOpen to false when done
     const setIsntOpen = () => setIsOpen(false)
-    opacity.value = withTiming(
-      0,
-      {
-        duration: 400,
-        easing: Easing.out(Easing.cubic),
-      },
-      () => runOnJS(setIsntOpen)(),
+    opacity.set(() =>
+      withTiming(
+        0,
+        {
+          duration: 400,
+          easing: Easing.out(Easing.cubic),
+        },
+        () => runOnJS(setIsntOpen)(),
+      ),
     )
   }, [setIsOpen, opacity])
 
@@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef<
 
     // animate the vertical translation, the opacity, and the checkmark
     const playCheckmark = () => animatedCheckRef.current?.play()
-    opacity.value = 0
-    opacity.value = withTiming(
-      1,
-      {
-        duration: 100,
+    opacity.set(0)
+    opacity.set(() =>
+      withTiming(
+        1,
+        {
+          duration: 100,
+          easing: Easing.out(Easing.cubic),
+        },
+        () => runOnJS(playCheckmark)(),
+      ),
+    )
+    translateY.set(0)
+    translateY.set(() =>
+      withTiming(insets.top + 10, {
+        duration: 500,
         easing: Easing.out(Easing.cubic),
-      },
-      () => runOnJS(playCheckmark)(),
+      }),
     )
-    translateY.value = 0
-    translateY.value = withTiming(insets.top + 10, {
-      duration: 500,
-      easing: Easing.out(Easing.cubic),
-    })
 
     // start the countdown timer to autoclose
     timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
@@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef<
   }, [winDim.width])
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{translateY: translateY.value}],
-    opacity: opacity.value,
+    transform: [{translateY: translateY.get()}],
+    opacity: opacity.get(),
   }))
 
   return (
diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx
index 039bbf123..df472241e 100644
--- a/src/components/ReportDialog/SelectLabelerView.tsx
+++ b/src/components/ReportDialog/SelectLabelerView.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 import {AppBskyLabelerDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 8f6358dd5..6d7e50e48 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -9,6 +9,7 @@ import {NavigationProp} from '#/lib/routes/types'
 import {toShortUrl} from '#/lib/strings/url-helpers'
 import {isNative} from '#/platform/detection'
 import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
+import {isOnlyEmoji} from '#/alf/typography'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {InlineLinkText, LinkProps} from '#/components/Link'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -53,7 +54,6 @@ export function RichText({
   const plainStyles = [a.leading_snug, flattenedStyle]
   const interactiveStyles = [
     a.leading_snug,
-    a.pointer_events_auto,
     flatten(interactiveStyle),
     flattenedStyle,
   ]
@@ -151,17 +151,14 @@ export function RichText({
         />,
       )
     } else {
-      els.push(
-        <Text key={key} emoji style={plainStyles}>
-          {segment.text}
-        </Text>,
-      )
+      els.push(segment.text)
     }
     key++
   }
 
   return (
     <Text
+      emoji
       selectable={selectable}
       testID={testID}
       style={plainStyles}
@@ -194,11 +191,6 @@ function RichTextTag({
     onOut: onHoverOut,
   } = useInteractionState()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-  const {
-    state: pressed,
-    onIn: onPressIn,
-    onOut: onPressOut,
-  } = useInteractionState()
   const navigation = useNavigation<NavigationProp>()
 
   const navigateToPage = React.useCallback(() => {
@@ -228,8 +220,6 @@ function RichTextTag({
             accessibilityRole: isNative ? 'button' : undefined,
             onPress: navigateToPage,
             onLongPress: openDialog,
-            onPressIn: onPressIn,
-            onPressOut: onPressOut,
           })}
           {...web({
             onMouseEnter: onHoverIn,
@@ -243,10 +233,12 @@ function RichTextTag({
               cursor: 'pointer',
             }),
             {color: t.palette.primary_500},
-            (hovered || focused || pressed) && {
-              ...web({outline: 0}),
-              textDecorationLine: 'underline',
-              textDecorationColor: t.palette.primary_500,
+            (hovered || focused) && {
+              ...web({
+                outline: 0,
+                textDecorationLine: 'underline',
+                textDecorationColor: t.palette.primary_500,
+              }),
             },
             style,
           ]}>
@@ -256,10 +248,3 @@ function RichTextTag({
     </React.Fragment>
   )
 }
-
-export function isOnlyEmoji(text: string) {
-  return (
-    text.length <= 15 &&
-    /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
-  )
-}
diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx
index ecd6225bb..c1c10c76b 100644
--- a/src/components/StarterPack/Main/ProfilesList.tsx
+++ b/src/components/StarterPack/Main/ProfilesList.tsx
@@ -40,7 +40,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
     ref,
   ) {
     const t = useTheme()
-    const bottomBarOffset = useBottomBarOffset(300)
+    const bottomBarOffset = useBottomBarOffset(headerHeight)
     const initialNumToRender = useInitialNumToRender()
     const {currentAccount} = useSession()
     const {data, refetch, isError} = useAllListMembersQuery(listUri)
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx
index 00afbdcfe..5f58a19df 100644
--- a/src/components/StarterPack/ProfileStarterPacks.tsx
+++ b/src/components/StarterPack/ProfileStarterPacks.tsx
@@ -14,6 +14,7 @@ import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
 
 import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
+import {useEmail} from '#/lib/hooks/useEmail'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {NavigationProp} from '#/lib/routes/types'
 import {parseStarterPackUri} from '#/lib/strings/starter-pack'
@@ -27,6 +28,7 @@ import {LinearGradientBackground} from '#/components/LinearGradientBackground'
 import {Loader} from '#/components/Loader'
 import * as Prompt from '#/components/Prompt'
 import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
+import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
 
 interface SectionRef {
@@ -186,6 +188,9 @@ function Empty() {
   const followersDialogControl = useDialogControl()
   const errorDialogControl = useDialogControl()
 
+  const {needsEmailVerification} = useEmail()
+  const verifyEmailControl = useDialogControl()
+
   const [isGenerating, setIsGenerating] = React.useState(false)
 
   const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
@@ -249,7 +254,13 @@ function Empty() {
           color="primary"
           size="small"
           disabled={isGenerating}
-          onPress={confirmDialogControl.open}
+          onPress={() => {
+            if (needsEmailVerification) {
+              verifyEmailControl.open()
+            } else {
+              confirmDialogControl.open()
+            }
+          }}
           style={{backgroundColor: 'transparent'}}>
           <ButtonText style={{color: 'white'}}>
             <Trans>Make one for me</Trans>
@@ -262,7 +273,13 @@ function Empty() {
           color="primary"
           size="small"
           disabled={isGenerating}
-          onPress={() => navigation.navigate('StarterPackWizard')}
+          onPress={() => {
+            if (needsEmailVerification) {
+              verifyEmailControl.open()
+            } else {
+              navigation.navigate('StarterPackWizard')
+            }
+          }}
           style={{
             backgroundColor: 'white',
             borderColor: 'white',
@@ -318,6 +335,12 @@ function Empty() {
         onConfirm={generate}
         confirmButtonCta={_(msg`Retry`)}
       />
+      <VerifyEmailDialog
+        reasonText={_(
+          msg`Before creating a starter pack, you must first verify your email.`,
+        )}
+        control={verifyEmailControl}
+      />
     </LinearGradientBackground>
   )
 }
diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx
index 997c6479c..354d7bc4e 100644
--- a/src/components/StarterPack/ShareDialog.tsx
+++ b/src/components/StarterPack/ShareDialog.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 import {Image} from 'expo-image'
 import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index 1e9f1c52d..b67a8d302 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -1,4 +1,4 @@
-import React, {useRef} from 'react'
+import {useRef} from 'react'
 import type {ListRenderItemInfo} from 'react-native'
 import {View} from 'react-native'
 import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index 44f01a154..75d2bff60 100644
--- a/src/components/StarterPack/Wizard/WizardListCard.tsx
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {Keyboard, View} from 'react-native'
 import {
   AppBskyActorDefs,
diff --git a/src/components/SubtleWebHover.tsx b/src/components/SubtleWebHover.tsx
index e6f427237..5cbbfc898 100644
--- a/src/components/SubtleWebHover.tsx
+++ b/src/components/SubtleWebHover.tsx
@@ -1,3 +1,5 @@
-export function SubtleWebHover({}: {hover: boolean}) {
+import {ViewStyleProp} from '#/alf'
+
+export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
   return null
 }
diff --git a/src/components/SubtleWebHover.web.tsx b/src/components/SubtleWebHover.web.tsx
index e98251e0d..8943147e4 100644
--- a/src/components/SubtleWebHover.web.tsx
+++ b/src/components/SubtleWebHover.web.tsx
@@ -1,10 +1,12 @@
-import React from 'react'
 import {StyleSheet, View} from 'react-native'
 
 import {isTouchDevice} from '#/lib/browser'
-import {useTheme} from '#/alf'
+import {useTheme, ViewStyleProp} from '#/alf'
 
-export function SubtleWebHover({hover}: {hover: boolean}) {
+export function SubtleWebHover({
+  style,
+  hover,
+}: ViewStyleProp & {hover: boolean}) {
   const t = useTheme()
   if (isTouchDevice) {
     return null
@@ -26,9 +28,8 @@ export function SubtleWebHover({hover}: {hover: boolean}) {
       style={[
         t.atoms.bg_contrast_25,
         styles.container,
-        {
-          opacity: hover ? opacity : 0,
-        },
+        {opacity: hover ? opacity : 0},
+        style,
       ]}
     />
   )
diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx
index ae9fcdae2..310ecc4c2 100644
--- a/src/components/TagMenu/index.tsx
+++ b/src/components/TagMenu/index.tsx
@@ -40,9 +40,37 @@ export function TagMenu({
   tag: string
   authorHandle?: string
 }>) {
+  const navigation = useNavigation<NavigationProp>()
+  return (
+    <>
+      {children}
+      <Dialog.Outer control={control}>
+        <Dialog.Handle />
+        <TagMenuInner
+          control={control}
+          tag={tag}
+          authorHandle={authorHandle}
+          navigation={navigation}
+        />
+      </Dialog.Outer>
+    </>
+  )
+}
+
+function TagMenuInner({
+  control,
+  tag,
+  authorHandle,
+  navigation,
+}: {
+  control: Dialog.DialogOuterProps['control']
+  tag: string
+  authorHandle?: string
+  // Passed down because on native, we don't use real portals (and context would be wrong).
+  navigation: NavigationProp
+}) {
   const {_} = useLingui()
   const t = useTheme()
-  const navigation = useNavigation<NavigationProp>()
   const {isLoading: isPreferencesLoading, data: preferences} =
     usePreferencesQuery()
   const {
@@ -79,32 +107,75 @@ export function TagMenu({
   }, [tag, preferences?.moderationPrefs?.mutedWords])
 
   return (
-    <>
-      {children}
-
-      <Dialog.Outer control={control}>
-        <Dialog.Handle />
-        <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
-          {isPreferencesLoading ? (
-            <View style={[a.w_full, a.align_center]}>
-              <Loader size="lg" />
-            </View>
-          ) : (
-            <>
+    <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
+      {isPreferencesLoading ? (
+        <View style={[a.w_full, a.align_center]}>
+          <Loader size="lg" />
+        </View>
+      ) : (
+        <>
+          <View
+            style={[
+              a.rounded_md,
+              a.border,
+              a.mb_md,
+              t.atoms.border_contrast_low,
+              t.atoms.bg_contrast_25,
+            ]}>
+            <Link
+              label={_(msg`View all posts with tag ${displayTag}`)}
+              {...createStaticClick(() => {
+                control.close(() => {
+                  navigation.push('Hashtag', {
+                    tag: encodeURIComponent(tag),
+                  })
+                })
+              })}>
               <View
                 style={[
-                  a.rounded_md,
-                  a.border,
-                  a.mb_md,
-                  t.atoms.border_contrast_low,
-                  t.atoms.bg_contrast_25,
+                  a.w_full,
+                  a.flex_row,
+                  a.align_center,
+                  a.justify_start,
+                  a.gap_md,
+                  a.px_lg,
+                  a.py_md,
                 ]}>
+                <Search size="lg" style={[t.atoms.text_contrast_medium]} />
+                <Text
+                  numberOfLines={1}
+                  ellipsizeMode="middle"
+                  style={[
+                    a.flex_1,
+                    a.text_md,
+                    a.font_bold,
+                    native({top: 2}),
+                    t.atoms.text_contrast_medium,
+                  ]}>
+                  <Trans>
+                    See{' '}
+                    <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
+                      {displayTag}
+                    </Text>{' '}
+                    posts
+                  </Trans>
+                </Text>
+              </View>
+            </Link>
+
+            {authorHandle && !isInvalidHandle(authorHandle) && (
+              <>
+                <Divider />
+
                 <Link
-                  label={_(msg`View all posts with tag ${displayTag}`)}
+                  label={_(
+                    msg`View all posts by @${authorHandle} with tag ${displayTag}`,
+                  )}
                   {...createStaticClick(() => {
                     control.close(() => {
                       navigation.push('Hashtag', {
                         tag: encodeURIComponent(tag),
+                        author: authorHandle,
                       })
                     })
                   })}>
@@ -118,7 +189,7 @@ export function TagMenu({
                       a.px_lg,
                       a.py_md,
                     ]}>
-                    <Search size="lg" style={[t.atoms.text_contrast_medium]} />
+                    <Person size="lg" style={[t.atoms.text_contrast_medium]} />
                     <Text
                       numberOfLines={1}
                       ellipsizeMode="middle"
@@ -134,143 +205,86 @@ export function TagMenu({
                         <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
                           {displayTag}
                         </Text>{' '}
-                        posts
+                        posts by this user
                       </Trans>
                     </Text>
                   </View>
                 </Link>
+              </>
+            )}
 
-                {authorHandle && !isInvalidHandle(authorHandle) && (
-                  <>
-                    <Divider />
-
-                    <Link
-                      label={_(
-                        msg`View all posts by @${authorHandle} with tag ${displayTag}`,
-                      )}
-                      {...createStaticClick(() => {
-                        control.close(() => {
-                          navigation.push('Hashtag', {
-                            tag: encodeURIComponent(tag),
-                            author: authorHandle,
-                          })
-                        })
-                      })}>
-                      <View
-                        style={[
-                          a.w_full,
-                          a.flex_row,
-                          a.align_center,
-                          a.justify_start,
-                          a.gap_md,
-                          a.px_lg,
-                          a.py_md,
-                        ]}>
-                        <Person
-                          size="lg"
-                          style={[t.atoms.text_contrast_medium]}
-                        />
-                        <Text
-                          numberOfLines={1}
-                          ellipsizeMode="middle"
-                          style={[
-                            a.flex_1,
-                            a.text_md,
-                            a.font_bold,
-                            native({top: 2}),
-                            t.atoms.text_contrast_medium,
-                          ]}>
-                          <Trans>
-                            See{' '}
-                            <Text
-                              style={[a.text_md, a.font_bold, t.atoms.text]}>
-                              {displayTag}
-                            </Text>{' '}
-                            posts by this user
-                          </Trans>
-                        </Text>
-                      </View>
-                    </Link>
-                  </>
-                )}
-
-                {preferences ? (
-                  <>
-                    <Divider />
+            {preferences ? (
+              <>
+                <Divider />
 
-                    <Button
-                      label={
-                        isMuted
-                          ? _(msg`Unmute all ${displayTag} posts`)
-                          : _(msg`Mute all ${displayTag} posts`)
+                <Button
+                  label={
+                    isMuted
+                      ? _(msg`Unmute all ${displayTag} posts`)
+                      : _(msg`Mute all ${displayTag} posts`)
+                  }
+                  onPress={() => {
+                    control.close(() => {
+                      if (isMuted) {
+                        resetUpsert()
+                        removeMutedWords(removeableMuteWords)
+                      } else {
+                        resetRemove()
+                        upsertMutedWord([
+                          {
+                            value: tag,
+                            targets: ['tag'],
+                            actorTarget: 'all',
+                          },
+                        ])
                       }
-                      onPress={() => {
-                        control.close(() => {
-                          if (isMuted) {
-                            resetUpsert()
-                            removeMutedWords(removeableMuteWords)
-                          } else {
-                            resetRemove()
-                            upsertMutedWord([
-                              {
-                                value: tag,
-                                targets: ['tag'],
-                                actorTarget: 'all',
-                              },
-                            ])
-                          }
-                        })
-                      }}>
-                      <View
-                        style={[
-                          a.w_full,
-                          a.flex_row,
-                          a.align_center,
-                          a.justify_start,
-                          a.gap_md,
-                          a.px_lg,
-                          a.py_md,
-                        ]}>
-                        <Mute
-                          size="lg"
-                          style={[t.atoms.text_contrast_medium]}
-                        />
-                        <Text
-                          numberOfLines={1}
-                          ellipsizeMode="middle"
-                          style={[
-                            a.flex_1,
-                            a.text_md,
-                            a.font_bold,
-                            native({top: 2}),
-                            t.atoms.text_contrast_medium,
-                          ]}>
-                          {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
-                          <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                            {displayTag}
-                          </Text>{' '}
-                          <Trans>posts</Trans>
-                        </Text>
-                      </View>
-                    </Button>
-                  </>
-                ) : null}
-              </View>
+                    })
+                  }}>
+                  <View
+                    style={[
+                      a.w_full,
+                      a.flex_row,
+                      a.align_center,
+                      a.justify_start,
+                      a.gap_md,
+                      a.px_lg,
+                      a.py_md,
+                    ]}>
+                    <Mute size="lg" style={[t.atoms.text_contrast_medium]} />
+                    <Text
+                      numberOfLines={1}
+                      ellipsizeMode="middle"
+                      style={[
+                        a.flex_1,
+                        a.text_md,
+                        a.font_bold,
+                        native({top: 2}),
+                        t.atoms.text_contrast_medium,
+                      ]}>
+                      {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
+                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
+                        {displayTag}
+                      </Text>{' '}
+                      <Trans>posts</Trans>
+                    </Text>
+                  </View>
+                </Button>
+              </>
+            ) : null}
+          </View>
 
-              <Button
-                label={_(msg`Close this dialog`)}
-                size="small"
-                variant="ghost"
-                color="secondary"
-                onPress={() => control.close()}>
-                <ButtonText>
-                  <Trans>Cancel</Trans>
-                </ButtonText>
-              </Button>
-            </>
-          )}
-        </Dialog.Inner>
-      </Dialog.Outer>
-    </>
+          <Button
+            label={_(msg`Close this dialog`)}
+            size="small"
+            variant="ghost"
+            color="secondary"
+            onPress={() => control.close()}>
+            <ButtonText>
+              <Trans>Cancel</Trans>
+            </ButtonText>
+          </Button>
+        </>
+      )}
+    </Dialog.Inner>
   )
 }
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index 69e073271..3e202cb8f 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -1,140 +1,15 @@
-import React from 'react'
-import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
 import {UITextView} from 'react-native-uitextview'
-import createEmojiRegex from 'emoji-regex'
 
 import {logger} from '#/logger'
-import {isIOS, isNative} from '#/platform/detection'
-import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
+import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
+import {
+  childHasEmoji,
+  normalizeTextStyles,
+  renderChildrenWithEmoji,
+  TextProps,
+} from '#/alf/typography'
 import {IS_DEV} from '#/env'
-
-export type StringChild = string | (string | null)[]
-
-export type TextProps = Omit<RNTextProps, 'children'> & {
-  /**
-   * Lets the user select text, to use the native copy and paste functionality.
-   */
-  selectable?: boolean
-  /**
-   * Provides `data-*` attributes to the underlying `UITextView` component on
-   * web only.
-   */
-  dataSet?: Record<string, string | number | undefined>
-  /**
-   * Appears as a small tooltip on web hover.
-   */
-  title?: string
-} & (
-    | {
-        emoji?: true
-        children: StringChild
-      }
-    | {
-        emoji?: false
-        children: RNTextProps['children']
-      }
-  )
-
-const EMOJI = createEmojiRegex()
-
-export function childHasEmoji(children: React.ReactNode) {
-  return (Array.isArray(children) ? children : [children]).some(
-    child => typeof child === 'string' && createEmojiRegex().test(child),
-  )
-}
-
-export function childIsString(
-  children: React.ReactNode,
-): children is StringChild {
-  return (
-    typeof children === 'string' ||
-    (Array.isArray(children) &&
-      children.every(child => typeof child === 'string' || child === null))
-  )
-}
-
-export function renderChildrenWithEmoji(
-  children: StringChild,
-  props: Omit<TextProps, 'children'> = {},
-) {
-  const normalized = Array.isArray(children) ? children : [children]
-
-  return (
-    <UITextView {...props}>
-      {normalized.map(child => {
-        if (typeof child !== 'string') return child
-
-        const emojis = child.match(EMOJI)
-
-        if (emojis === null) {
-          return child
-        }
-
-        return child.split(EMOJI).map((stringPart, index) => (
-          <UITextView key={index} {...props}>
-            {stringPart}
-            {emojis[index] ? (
-              <UITextView
-                {...props}
-                style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
-                {emojis[index]}
-              </UITextView>
-            ) : null}
-          </UITextView>
-        ))
-      })}
-    </UITextView>
-  )
-}
-
-/**
- * Util to calculate lineHeight from a text size atom and a leading atom
- *
- * Example:
- *   `leading(atoms.text_md, atoms.leading_normal)` // => 24
- */
-export function leading<
-  Size extends {fontSize?: number},
-  Leading extends {lineHeight?: number},
->(textSize: Size, leading: Leading) {
-  const size = textSize?.fontSize || atoms.text_md.fontSize
-  const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
-  return Math.round(size * lineHeight)
-}
-
-/**
- * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
- * other relative leading atoms.
- *
- * If the `lineHeight` value is > 2, we assume it's an absolute value and
- * returns it as-is.
- */
-export function normalizeTextStyles(
-  styles: StyleProp<TextStyle>,
-  {
-    fontScale,
-    fontFamily,
-  }: {
-    fontScale: number
-    fontFamily: Alf['fonts']['family']
-  } & Pick<Alf, 'flags'>,
-) {
-  const s = flatten(styles)
-  // should always be defined on these components
-  s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
-
-  if (s?.lineHeight) {
-    if (s.lineHeight !== 0 && s.lineHeight <= 2) {
-      s.lineHeight = Math.round(s.fontSize * s.lineHeight)
-    }
-  } else if (!isNative) {
-    s.lineHeight = s.fontSize
-  }
-
-  applyFonts(s, fontFamily)
-
-  return s
-}
+export type {TextProps}
 
 /**
  * Our main text component. Use this most of the time.
@@ -162,10 +37,6 @@ export function Text({
         `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
       )
     }
-
-    if (emoji && !childIsString(children)) {
-      logger.error('Text: when <Text emoji />, children can only be strings.')
-    }
   }
 
   const shared = {
@@ -178,12 +49,12 @@ export function Text({
 
   return (
     <UITextView {...shared}>
-      {isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
+      {renderChildrenWithEmoji(children, shared, emoji ?? false)}
     </UITextView>
   )
 }
 
-export function createHeadingElement({level}: {level: number}) {
+function createHeadingElement({level}: {level: number}) {
   return function HeadingElement({style, ...rest}: TextProps) {
     const attr =
       web({
diff --git a/src/components/anim/AnimatedCheck.tsx b/src/components/anim/AnimatedCheck.tsx
index 7fdfc14cf..60407274e 100644
--- a/src/components/anim/AnimatedCheck.tsx
+++ b/src/components/anim/AnimatedCheck.tsx
@@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef<
   const checkAnim = useSharedValue(0)
 
   const circleAnimatedProps = useAnimatedProps(() => ({
-    strokeDashoffset: 166 - circleAnim.value * 166,
+    strokeDashoffset: 166 - circleAnim.get() * 166,
   }))
   const checkAnimatedProps = useAnimatedProps(() => ({
-    strokeDashoffset: 48 - 48 * checkAnim.value,
+    strokeDashoffset: 48 - 48 * checkAnim.get(),
   }))
 
   const play = React.useCallback(
     (cb?: () => void) => {
-      circleAnim.value = 0
-      checkAnim.value = 0
+      circleAnim.set(0)
+      checkAnim.set(0)
 
-      circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
-      checkAnim.value = withDelay(
-        500,
-        withTiming(1, {duration: 300, easing: Easing.linear}, cb),
+      circleAnim.set(() =>
+        withTiming(1, {duration: 500, easing: Easing.linear}),
+      )
+      checkAnim.set(() =>
+        withDelay(
+          500,
+          withTiming(1, {duration: 300, easing: Easing.linear}, cb),
+        ),
       )
     },
     [circleAnim, checkAnim],
diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx
index 824155d8b..086d43f95 100644
--- a/src/components/dialogs/EmbedConsent.tsx
+++ b/src/components/dialogs/EmbedConsent.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react'
+import {useCallback} from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx
index 0b8b386d3..8536001da 100644
--- a/src/components/dialogs/PostInteractionSettingsDialog.tsx
+++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx
@@ -256,6 +256,9 @@ export function PostInteractionSettingsForm({
     } else {
       newSelected.splice(i, 1)
     }
+    if (newSelected.length === 0) {
+      newSelected.push({type: 'everybody'})
+    }
 
     onChangeThreadgateAllowUISettings(newSelected)
   }
@@ -306,7 +309,7 @@ export function PostInteractionSettingsForm({
               }
               value={quotesEnabled}
               onChange={onChangeQuotesEnabled}
-              style={[, a.justify_between, a.pt_xs]}>
+              style={[a.justify_between, a.pt_xs]}>
               <Text style={[t.atoms.text_contrast_medium]}>
                 {quotesEnabled ? (
                   <Trans>Quote posts enabled</Trans>
@@ -483,7 +486,7 @@ function Selectable({
             a.justify_between,
             a.rounded_sm,
             a.p_md,
-            {height: 40}, // for consistency with checkmark icon visible or not
+            {minHeight: 40}, // for consistency with checkmark icon visible or not
             t.atoms.bg_contrast_50,
             (hovered || focused) && t.atoms.bg_contrast_100,
             isSelected && {
diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx
index daad01d2a..9acefa8fc 100644
--- a/src/components/dialogs/SwitchAccount.tsx
+++ b/src/components/dialogs/SwitchAccount.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react'
+import {useCallback} from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx
index 8dfb9bc49..ced9171ce 100644
--- a/src/components/dialogs/VerifyEmailDialog.tsx
+++ b/src/components/dialogs/VerifyEmailDialog.tsx
@@ -18,8 +18,14 @@ import {Text} from '#/components/Typography'
 
 export function VerifyEmailDialog({
   control,
+  onCloseWithoutVerifying,
+  onCloseAfterVerifying,
+  reasonText,
 }: {
   control: Dialog.DialogControlProps
+  onCloseWithoutVerifying?: () => void
+  onCloseAfterVerifying?: () => void
+  reasonText?: string
 }) {
   const agent = useAgent()
 
@@ -30,18 +36,24 @@ export function VerifyEmailDialog({
       control={control}
       onClose={async () => {
         if (!didVerify) {
+          onCloseWithoutVerifying?.()
           return
         }
 
         try {
           await agent.resumeSession(agent.session!)
+          onCloseAfterVerifying?.()
         } catch (e: unknown) {
           logger.error(String(e))
           return
         }
       }}>
       <Dialog.Handle />
-      <Inner control={control} setDidVerify={setDidVerify} />
+      <Inner
+        control={control}
+        setDidVerify={setDidVerify}
+        reasonText={reasonText}
+      />
     </Dialog.Outer>
   )
 }
@@ -49,9 +61,11 @@ export function VerifyEmailDialog({
 export function Inner({
   control,
   setDidVerify,
+  reasonText,
 }: {
   control: Dialog.DialogControlProps
   setDidVerify: (value: boolean) => void
+  reasonText?: string
 }) {
   const {_} = useLingui()
   const {currentAccount} = useSession()
@@ -132,34 +146,63 @@ export function Inner({
               <ErrorMessage message={error} />
             </View>
           ) : null}
-          <Text style={[a.text_md, a.leading_snug]}>
-            {currentStep === 'StepOne' ? (
-              <>
-                <Trans>
-                  You'll receive an email at{' '}
-                  <Text style={[a.text_md, a.leading_snug, a.font_bold]}>
-                    {currentAccount?.email}
-                  </Text>{' '}
-                  to verify it's you.
-                </Trans>{' '}
-                <InlineLinkText
-                  to="#"
-                  label={_(msg`Change email address`)}
-                  style={[a.text_md, a.leading_snug]}
-                  onPress={e => {
-                    e.preventDefault()
-                    control.close(() => {
-                      openModal({name: 'change-email'})
-                    })
-                    return false
-                  }}>
-                  <Trans>Need to change it?</Trans>
-                </InlineLinkText>
-              </>
-            ) : (
-              uiStrings[currentStep].message
-            )}
-          </Text>
+          {currentStep === 'StepOne' ? (
+            <View>
+              {reasonText ? (
+                <View style={[a.gap_sm]}>
+                  <Text style={[a.text_md, a.leading_snug]}>{reasonText}</Text>
+                  <Text style={[a.text_md, a.leading_snug]}>
+                    Don't have access to{' '}
+                    <Text style={[a.text_md, a.leading_snug, a.font_bold]}>
+                      {currentAccount?.email}
+                    </Text>
+                    ?{' '}
+                    <InlineLinkText
+                      to="#"
+                      label={_(msg`Change email address`)}
+                      style={[a.text_md, a.leading_snug]}
+                      onPress={e => {
+                        e.preventDefault()
+                        control.close(() => {
+                          openModal({name: 'change-email'})
+                        })
+                        return false
+                      }}>
+                      <Trans>Change your email address</Trans>
+                    </InlineLinkText>
+                    .
+                  </Text>
+                </View>
+              ) : (
+                <Text style={[a.text_md, a.leading_snug]}>
+                  <Trans>
+                    You'll receive an email at{' '}
+                    <Text style={[a.text_md, a.leading_snug, a.font_bold]}>
+                      {currentAccount?.email}
+                    </Text>{' '}
+                    to verify it's you.
+                  </Trans>{' '}
+                  <InlineLinkText
+                    to="#"
+                    label={_(msg`Change email address`)}
+                    style={[a.text_md, a.leading_snug]}
+                    onPress={e => {
+                      e.preventDefault()
+                      control.close(() => {
+                        openModal({name: 'change-email'})
+                      })
+                      return false
+                    }}>
+                    <Trans>Need to change it?</Trans>
+                  </InlineLinkText>
+                </Text>
+              )}
+            </View>
+          ) : (
+            <Text style={[a.text_md, a.leading_snug]}>
+              {uiStrings[currentStep].message}
+            </Text>
+          )}
         </View>
         {currentStep === 'StepTwo' ? (
           <View>
diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx
deleted file mode 100644
index f29dc356d..000000000
--- a/src/components/dialogs/nuxs/NeueTypography.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
-import {atoms as a, useAlf, useTheme} from '#/alf'
-import * as Dialog from '#/components/Dialog'
-import {useNuxDialogContext} from '#/components/dialogs/nuxs'
-import {Divider} from '#/components/Divider'
-import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
-import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
-import {Text} from '#/components/Typography'
-
-export function NeueTypography() {
-  const t = useTheme()
-  const {_} = useLingui()
-  const nuxDialogs = useNuxDialogContext()
-  const control = Dialog.useDialogControl()
-  const {fonts} = useAlf()
-
-  Dialog.useAutoOpen(control, 3e3)
-
-  const onClose = React.useCallback(() => {
-    nuxDialogs.dismissActiveNux()
-  }, [nuxDialogs])
-
-  const onChangeFontFamily = React.useCallback(
-    (values: string[]) => {
-      const next = values[0] === 'system' ? 'system' : 'theme'
-      fonts.setFontFamily(next)
-    },
-    [fonts],
-  )
-
-  const onChangeFontScale = React.useCallback(
-    (values: string[]) => {
-      const next = values[0] || ('0' as any)
-      fonts.setFontScale(next)
-    },
-    [fonts],
-  )
-
-  return (
-    <Dialog.Outer control={control} onClose={onClose}>
-      <Dialog.Handle />
-      <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
-        <View style={[a.gap_xl]}>
-          <View style={[a.gap_md]}>
-            <Text style={[a.text_3xl, a.font_heavy]}>
-              <Trans>New font settings ✨</Trans>
-            </Text>
-            <Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
-              <Trans>
-                We're introducing a new theme font, along with adjustable font
-                sizing.
-              </Trans>
-            </Text>
-            <Text
-              style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
-              <Trans>
-                You can adjust these in your Appearance Settings later.
-              </Trans>
-            </Text>
-          </View>
-
-          <Divider />
-
-          <View style={[a.gap_lg]}>
-            <AppearanceToggleButtonGroup
-              title={_(msg`Font`)}
-              description={_(
-                msg`For the best experience, we recommend using the theme font.`,
-              )}
-              icon={Aa}
-              items={[
-                {
-                  label: _(msg`System`),
-                  name: 'system',
-                },
-                {
-                  label: _(msg`Theme`),
-                  name: 'theme',
-                },
-              ]}
-              values={[fonts.family]}
-              onChange={onChangeFontFamily}
-            />
-
-            <AppearanceToggleButtonGroup
-              title={_(msg`Font size`)}
-              icon={TextSize}
-              items={[
-                {
-                  label: _(msg`Smaller`),
-                  name: '-1',
-                },
-                {
-                  label: _(msg`Default`),
-                  name: '0',
-                },
-                {
-                  label: _(msg`Larger`),
-                  name: '1',
-                },
-              ]}
-              values={[fonts.scale]}
-              onChange={onChangeFontScale}
-            />
-          </View>
-        </View>
-
-        <Dialog.Close />
-      </Dialog.ScrollableInner>
-    </Dialog.Outer>
-  )
-}
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index d17615aeb..701ae84e6 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -19,7 +19,6 @@ import {useOnboardingState} from '#/state/shell'
 /*
  * NUXs
  */
-import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
 import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
 import {IS_DEV} from '#/env'
 
@@ -36,19 +35,7 @@ const queuedNuxs: {
     currentProfile: AppBskyActorDefs.ProfileViewDetailed
     preferences: UsePreferencesQueryResponse
   }) => boolean
-}[] = [
-  {
-    id: Nux.NeueTypography,
-    enabled(props) {
-      if (props.currentProfile.createdAt) {
-        if (new Date(props.currentProfile.createdAt) < new Date('2024-10-09')) {
-          return true
-        }
-      }
-      return false
-    },
-  },
-]
+}[] = []
 
 const Context = React.createContext<Context>({
   activeNux: undefined,
@@ -66,7 +53,14 @@ export function NuxDialogs() {
   const onboardingActive = useOnboardingState().isActive
 
   const isLoading =
-    !currentAccount || !preferences || !profile || onboardingActive
+    onboardingActive ||
+    !currentAccount ||
+    !preferences ||
+    !profile ||
+    // Profile isn't legit ready until createdAt is a real date.
+    !profile.createdAt ||
+    profile.createdAt === '0001-01-01T00:00:00.000Z' // TODO: Fix this in AppView.
+
   return !isLoading ? (
     <Inner
       currentAccount={currentAccount}
@@ -174,7 +168,7 @@ function Inner({
 
   return (
     <Context.Provider value={ctx}>
-      {activeNux === Nux.NeueTypography && <NeueTypography />}
+      {/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
     </Context.Provider>
   )
 }
diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx
index b77516e7b..a087fed3f 100644
--- a/src/components/dms/ActionsWrapper.tsx
+++ b/src/components/dms/ActionsWrapper.tsx
@@ -34,7 +34,7 @@ export function ActionsWrapper({
   const scale = useSharedValue(1)
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   const open = React.useCallback(() => {
@@ -46,7 +46,7 @@ export function ActionsWrapper({
   const shrink = React.useCallback(() => {
     'worklet'
     cancelAnimation(scale)
-    scale.value = withTiming(1, {duration: 200})
+    scale.set(() => withTiming(1, {duration: 200}))
   }, [scale])
 
   const doubleTapGesture = Gesture.Tap()
@@ -58,11 +58,13 @@ export function ActionsWrapper({
   const pressAndHoldGesture = Gesture.LongPress()
     .onStart(() => {
       'worklet'
-      scale.value = withTiming(1.05, {duration: 200}, finished => {
-        if (!finished) return
-        runOnJS(open)()
-        shrink()
-      })
+      scale.set(() =>
+        withTiming(1.05, {duration: 200}, finished => {
+          if (!finished) return
+          runOnJS(open)()
+          shrink()
+        }),
+      )
     })
     .onTouchesUp(shrink)
     .onTouchesMove(shrink)
diff --git a/src/components/dms/ChatEmptyPill.tsx b/src/components/dms/ChatEmptyPill.tsx
index ffd022f56..042c3ad76 100644
--- a/src/components/dms/ChatEmptyPill.tsx
+++ b/src/components/dms/ChatEmptyPill.tsx
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
 
   const onPressIn = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1.075, {duration: 100})
+    scale.set(() => withTiming(1.075, {duration: 100}))
   }, [scale])
 
   const onPressOut = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1, {duration: 100})
+    scale.set(() => withTiming(1, {duration: 100}))
   }, [scale])
 
   const onPress = React.useCallback(() => {
@@ -61,7 +61,7 @@ export function ChatEmptyPill() {
   }, [playHaptic, prompts.length])
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   return (
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index affc292c1..e1f8df10b 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -115,7 +115,7 @@ let ConvoMenu = ({
                   {...props}
                   onPress={() => {
                     Keyboard.dismiss()
-                    // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props`
+
                     props.onPress()
                   }}
                   style={[
diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx
index 2baa07b46..cc18c1ab4 100644
--- a/src/components/dms/LeaveConvoPrompt.tsx
+++ b/src/components/dms/LeaveConvoPrompt.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index 52220e2ca..79f0997fd 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -19,10 +19,11 @@ import {ConvoItem} from '#/state/messages/convo/types'
 import {useSession} from '#/state/session'
 import {TimeElapsed} from '#/view/com/util/TimeElapsed'
 import {atoms as a, useTheme} from '#/alf'
+import {isOnlyEmoji} from '#/alf/typography'
 import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
-import {isOnlyEmoji, RichText} from '../RichText'
+import {RichText} from '../RichText'
 import {DateDivider} from './DateDivider'
 import {MessageItemEmbed} from './MessageItemEmbed'
 import {localDateString} from './util'
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index c1867e727..90ee5b979 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -62,7 +62,7 @@ export let MessageMenu = ({
       message.text,
       langPrefs.primaryLanguage,
     )
-    openLink(translatorUrl)
+    openLink(translatorUrl, true)
   }, [langPrefs.primaryLanguage, message.text, openLink])
 
   const onDelete = React.useCallback(() => {
diff --git a/src/components/dms/MessageProfileButton.tsx b/src/components/dms/MessageProfileButton.tsx
index 932982d05..22936b4c0 100644
--- a/src/components/dms/MessageProfileButton.tsx
+++ b/src/components/dms/MessageProfileButton.tsx
@@ -3,14 +3,18 @@ import {View} from 'react-native'
 import {AppBskyActorDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
 
+import {useEmail} from '#/lib/hooks/useEmail'
+import {NavigationProp} from '#/lib/routes/types'
 import {logEvent} from '#/lib/statsig/statsig'
 import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
 import {atoms as a, useTheme} from '#/alf'
-import {ButtonIcon} from '#/components/Button'
+import {Button, ButtonIcon} from '#/components/Button'
 import {canBeMessaged} from '#/components/dms/util'
 import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
-import {Link} from '#/components/Link'
+import {useDialogControl} from '../Dialog'
+import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
 
 export function MessageProfileButton({
   profile,
@@ -19,15 +23,29 @@ export function MessageProfileButton({
 }) {
   const {_} = useLingui()
   const t = useTheme()
+  const navigation = useNavigation<NavigationProp>()
+  const {needsEmailVerification} = useEmail()
+  const verifyEmailControl = useDialogControl()
 
   const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
 
   const onPress = React.useCallback(() => {
+    if (!convo?.id) {
+      return
+    }
+
+    if (needsEmailVerification) {
+      verifyEmailControl.open()
+      return
+    }
+
     if (convo && !convo.lastMessage) {
       logEvent('chat:create', {logContext: 'ProfileHeader'})
     }
     logEvent('chat:open', {logContext: 'ProfileHeader'})
-  }, [convo])
+
+    navigation.navigate('MessagesConversation', {conversation: convo.id})
+  }, [needsEmailVerification, verifyEmailControl, convo, navigation])
 
   if (isPending) {
     // show pending state based on declaration
@@ -53,18 +71,26 @@ export function MessageProfileButton({
 
   if (convo) {
     return (
-      <Link
-        testID="dmBtn"
-        size="small"
-        color="secondary"
-        variant="solid"
-        shape="round"
-        label={_(msg`Message ${profile.handle}`)}
-        to={`/messages/${convo.id}`}
-        style={[a.justify_center]}
-        onPress={onPress}>
-        <ButtonIcon icon={Message} size="md" />
-      </Link>
+      <>
+        <Button
+          accessibilityRole="button"
+          testID="dmBtn"
+          size="small"
+          color="secondary"
+          variant="solid"
+          shape="round"
+          label={_(msg`Message ${profile.handle}`)}
+          style={[a.justify_center]}
+          onPress={onPress}>
+          <ButtonIcon icon={Message} size="md" />
+        </Button>
+        <VerifyEmailDialog
+          reasonText={_(
+            msg`Before you may message another user, you must first verify your email.`,
+          )}
+          control={verifyEmailControl}
+        />
+      </>
     )
   } else {
     return null
diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx
index 2f7ff8f4b..e3bc0c1f8 100644
--- a/src/components/dms/NewMessagesPill.tsx
+++ b/src/components/dms/NewMessagesPill.tsx
@@ -35,12 +35,12 @@ export function NewMessagesPill({
 
   const onPressIn = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1.075, {duration: 100})
+    scale.set(() => withTiming(1.075, {duration: 100}))
   }, [scale])
 
   const onPressOut = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1, {duration: 100})
+    scale.set(() => withTiming(1, {duration: 100}))
   }, [scale])
 
   const onPress = React.useCallback(() => {
@@ -49,7 +49,7 @@ export function NewMessagesPill({
   }, [onPressInner, playHaptic])
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   return (
diff --git a/src/components/dms/ReportConversationPrompt.tsx b/src/components/dms/ReportConversationPrompt.tsx
index 610cfbcf9..6bb26a60f 100644
--- a/src/components/dms/ReportConversationPrompt.tsx
+++ b/src/components/dms/ReportConversationPrompt.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx
index e80fef2d7..c7fedb488 100644
--- a/src/components/dms/dialogs/NewChatDialog.tsx
+++ b/src/components/dms/dialogs/NewChatDialog.tsx
@@ -1,7 +1,8 @@
-import React, {useCallback} from 'react'
+import {useCallback} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {useEmail} from '#/lib/hooks/useEmail'
 import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
@@ -9,6 +10,8 @@ import {FAB} from '#/view/com/util/fab/FAB'
 import * as Toast from '#/view/com/util/Toast'
 import {useTheme} from '#/alf'
 import * as Dialog from '#/components/Dialog'
+import {useDialogControl} from '#/components/Dialog'
+import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
 import {SearchablePeopleList} from './SearchablePeopleList'
 
@@ -21,6 +24,8 @@ export function NewChat({
 }) {
   const t = useTheme()
   const {_} = useLingui()
+  const {needsEmailVerification} = useEmail()
+  const verifyEmailControl = useDialogControl()
 
   const {mutate: createChat} = useGetConvoForMembers({
     onSuccess: data => {
@@ -48,7 +53,13 @@ export function NewChat({
     <>
       <FAB
         testID="newChatFAB"
-        onPress={control.open}
+        onPress={() => {
+          if (needsEmailVerification) {
+            verifyEmailControl.open()
+          } else {
+            control.open()
+          }
+        }}
         icon={<Plus size="lg" fill={t.palette.white} />}
         accessibilityRole="button"
         accessibilityLabel={_(msg`New chat`)}
@@ -62,6 +73,13 @@ export function NewChat({
           onSelectChat={onCreateChat}
         />
       </Dialog.Outer>
+
+      <VerifyEmailDialog
+        reasonText={_(
+          msg`Before you may message another user, you must first verify your email.`,
+        )}
+        control={verifyEmailControl}
+      />
     </>
   )
 }
diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx
index a5687a096..bc7fcbe56 100644
--- a/src/components/dms/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dms/dialogs/SearchablePeopleList.tsx
@@ -278,7 +278,7 @@ export function SearchablePeopleList({
           ) : null}
         </View>
 
-        <View style={[, web([a.pt_xs])]}>
+        <View style={web([a.pt_xs])}>
           <SearchInput
             inputRef={inputRef}
             value={searchText}
@@ -313,6 +313,7 @@ export function SearchablePeopleList({
         web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
         native({height: '100%'}),
       ]}
+      webInnerContentContainerStyle={a.py_0}
       webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
       keyboardDismissMode="on-drag"
     />
diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx
index 38b558343..4bb27ae69 100644
--- a/src/components/dms/dialogs/ShareViaChatDialog.tsx
+++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react'
+import {useCallback} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
diff --git a/src/components/forms/DateField/index.shared.tsx b/src/components/forms/DateField/index.shared.tsx
index 814bbed7c..7438f5622 100644
--- a/src/components/forms/DateField/index.shared.tsx
+++ b/src/components/forms/DateField/index.shared.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {Pressable, View} from 'react-native'
 import {useLingui} from '@lingui/react'
 
diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx
index 8ab6e3f35..d51243d50 100644
--- a/src/components/forms/FormError.tsx
+++ b/src/components/forms/FormError.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 
 import {atoms as a, useTheme} from '#/alf'
diff --git a/src/components/hooks/dates.ts b/src/components/hooks/dates.ts
index 89c483d3c..28bb7635c 100644
--- a/src/components/hooks/dates.ts
+++ b/src/components/hooks/dates.ts
@@ -16,6 +16,7 @@ import {
   es,
   fi,
   fr,
+  gl,
   hi,
   hu,
   id,
@@ -23,11 +24,13 @@ import {
   ja,
   ko,
   nl,
+  pl,
   ptBR,
   ru,
   th,
   tr,
   uk,
+  vi,
   zhCN,
   zhHK,
   zhTW,
@@ -48,6 +51,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
   fi,
   fr,
   ga: undefined,
+  gl,
   hi,
   hu,
   id,
@@ -55,11 +59,13 @@ const locales: Record<AppLanguage, Locale | undefined> = {
   ja,
   ko,
   nl,
+  pl,
   ['pt-BR']: ptBR,
   ru,
   th,
   tr,
   uk,
+  vi,
   ['zh-CN']: zhCN,
   ['zh-HK']: zhHK,
   ['zh-TW']: zhTW,
diff --git a/src/components/hooks/useFollowMethods.ts b/src/components/hooks/useFollowMethods.ts
index 31a1e43da..d67c3690f 100644
--- a/src/components/hooks/useFollowMethods.ts
+++ b/src/components/hooks/useFollowMethods.ts
@@ -15,8 +15,8 @@ export function useFollowMethods({
   logContext,
 }: {
   profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
-  logContext: LogEvents['profile:follow:sampled']['logContext'] &
-    LogEvents['profile:unfollow:sampled']['logContext']
+  logContext: LogEvents['profile:follow']['logContext'] &
+    LogEvents['profile:unfollow']['logContext']
 }) {
   const {_} = useLingui()
   const requireAuth = useRequireAuth()
diff --git a/src/components/icons/CalendarClock.tsx b/src/components/icons/CalendarClock.tsx
new file mode 100644
index 000000000..52ba8094e
--- /dev/null
+++ b/src/components/icons/CalendarClock.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z',
+})
diff --git a/src/components/icons/ChainLink.tsx b/src/components/icons/ChainLink.tsx
new file mode 100644
index 000000000..ba0b417a9
--- /dev/null
+++ b/src/components/icons/ChainLink.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({
+  path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
+})
diff --git a/src/components/icons/common.tsx b/src/components/icons/common.tsx
index e83f96f0b..996ecb626 100644
--- a/src/components/icons/common.tsx
+++ b/src/components/icons/common.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {StyleSheet, TextProps} from 'react-native'
 import type {PathProps, SvgProps} from 'react-native-svg'
 import {Defs, LinearGradient, Stop} from 'react-native-svg'
diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx
index 67aef67b4..69193592a 100644
--- a/src/components/moderation/ContentHider.tsx
+++ b/src/components/moderation/ContentHider.tsx
@@ -32,6 +32,37 @@ export function ContentHider({
   style?: StyleProp<ViewStyle>
   childContainerStyle?: StyleProp<ViewStyle>
 }>) {
+  const blur = modui?.blurs[0]
+  if (!blur || (ignoreMute && isJustAMute(modui))) {
+    return (
+      <View testID={testID} style={style}>
+        {children}
+      </View>
+    )
+  }
+  return (
+    <ContentHiderActive
+      testID={testID}
+      modui={modui}
+      style={style}
+      childContainerStyle={childContainerStyle}>
+      {children}
+    </ContentHiderActive>
+  )
+}
+
+function ContentHiderActive({
+  testID,
+  modui,
+  style,
+  childContainerStyle,
+  children,
+}: React.PropsWithChildren<{
+  testID?: string
+  modui: ModerationUI
+  style?: StyleProp<ViewStyle>
+  childContainerStyle?: StyleProp<ViewStyle>
+}>) {
   const t = useTheme()
   const {_} = useLingui()
   const {gtMobile} = useBreakpoints()
@@ -40,7 +71,6 @@ export function ContentHider({
   const {labelDefs} = useLabelDefinitions()
   const globalLabelStrings = useGlobalLabelStrings()
   const {i18n} = useLingui()
-
   const blur = modui?.blurs[0]
   const desc = useModerationCauseDescription(blur)
 
@@ -99,14 +129,6 @@ export function ContentHider({
     globalLabelStrings,
   ])
 
-  if (!blur || (ignoreMute && isJustAMute(modui))) {
-    return (
-      <View testID={testID} style={style}>
-        {children}
-      </View>
-    )
-  }
-
   return (
     <View testID={testID} style={[a.overflow_hidden, style]}>
       <ModerationDetailsDialog control={control} modcause={blur} />
diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx
index ecdbcfd25..e6f18f1d6 100644
--- a/src/components/moderation/LabelPreference.tsx
+++ b/src/components/moderation/LabelPreference.tsx
@@ -22,7 +22,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
     <View
       style={[
         a.flex_row,
-        a.gap_md,
+        a.gap_sm,
         a.px_lg,
         a.py_lg,
         a.justify_between,
@@ -74,10 +74,9 @@ export function Buttons({
   hideLabel?: string
 }) {
   const {_} = useLingui()
-  const {gtPhone} = useBreakpoints()
 
   return (
-    <View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}>
+    <View style={[{minHeight: 35}, a.w_full]}>
       <ToggleButton.Group
         label={_(
           msg`Configure content filtering setting for category: ${name}`,
@@ -259,7 +258,7 @@ export function LabelerLabelPreference({
       </Content>
 
       {showConfig && (
-        <View style={[gtPhone ? undefined : a.w_full]}>
+        <>
           {cantConfigure ? (
             <View
               style={[
@@ -290,7 +289,7 @@ export function LabelerLabelPreference({
               hideLabel={hideLabel}
             />
           )}
-        </View>
+        </>
       )}
     </Outer>
   )
diff --git a/src/components/moderation/LabelsOnMe.tsx b/src/components/moderation/LabelsOnMe.tsx
index 33ede3ed2..681599807 100644
--- a/src/components/moderation/LabelsOnMe.tsx
+++ b/src/components/moderation/LabelsOnMe.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {StyleProp, View, ViewStyle} from 'react-native'
 import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
 import {msg, Plural} from '@lingui/macro'
diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx
index ef40a7996..bdbb2daa5 100644
--- a/src/components/moderation/ModerationDetailsDialog.tsx
+++ b/src/components/moderation/ModerationDetailsDialog.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 import {ModerationCause} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx
index 6c4e5f8c8..a68a650d6 100644
--- a/src/components/moderation/PostAlerts.tsx
+++ b/src/components/moderation/PostAlerts.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {StyleProp, ViewStyle} from 'react-native'
 import {ModerationCause, ModerationUI} from '@atproto/api'
 
diff --git a/src/components/moderation/ProfileHeaderAlerts.tsx b/src/components/moderation/ProfileHeaderAlerts.tsx
index 891caec18..4ac561fd9 100644
--- a/src/components/moderation/ProfileHeaderAlerts.tsx
+++ b/src/components/moderation/ProfileHeaderAlerts.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {StyleProp, ViewStyle} from 'react-native'
 import {ModerationDecision} from '@atproto/api'
 
diff --git a/src/components/video/PlayButtonIcon.tsx b/src/components/video/PlayButtonIcon.tsx
index 8e0a6bb7a..801cad9b9 100644
--- a/src/components/video/PlayButtonIcon.tsx
+++ b/src/components/video/PlayButtonIcon.tsx
@@ -1,4 +1,3 @@
-import React from 'react'
 import {View} from 'react-native'
 
 import {atoms as a, useTheme} from '#/alf'
@@ -10,39 +9,23 @@ export function PlayButtonIcon({size = 32}: {size?: number}) {
   const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25
 
   return (
-    <View
-      style={[
-        a.rounded_full,
-        a.overflow_hidden,
-        a.align_center,
-        a.justify_center,
-        t.atoms.shadow_lg,
-        {
-          width: size + size / 1.5,
-          height: size + size / 1.5,
-        },
-      ]}>
+    <>
       <View
         style={[
-          a.absolute,
-          a.inset_0,
+          a.rounded_full,
           {
             backgroundColor: bg,
+            shadowColor: 'black',
+            shadowRadius: 32,
+            shadowOpacity: 0.5,
+            elevation: 24,
+            width: size + size / 1.5,
+            height: size + size / 1.5,
             opacity: 0.7,
           },
         ]}
       />
-      <PlayIcon
-        width={size}
-        fill={fg}
-        style={[
-          a.relative,
-          a.z_10,
-          {
-            left: size / 50,
-          },
-        ]}
-      />
-    </View>
+      <PlayIcon width={size} fill={fg} style={a.absolute} />
+    </>
   )
 }