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/BlockedGeoOverlay.tsx109
-rw-r--r--src/components/Dialog/index.web.tsx6
-rw-r--r--src/components/Dialog/shared.tsx5
-rw-r--r--src/components/FeedInterstitials.tsx323
-rw-r--r--src/components/Layout/const.ts2
-rw-r--r--src/components/Link.tsx88
-rw-r--r--src/components/MediaPreview.tsx8
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx2
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx16
-rw-r--r--src/components/Post/Embed/index.tsx12
-rw-r--r--src/components/ProfileCard.tsx37
-rw-r--r--src/components/dialogs/GifSelect.tsx14
-rw-r--r--src/components/dms/ConvoMenu.tsx40
-rw-r--r--src/components/dms/MessagesListHeader.tsx171
-rw-r--r--src/components/forms/TextField.tsx15
-rw-r--r--src/components/icons/Logo.tsx37
-rw-r--r--src/components/interstitials/TrendingVideos.tsx108
17 files changed, 612 insertions, 381 deletions
diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx
new file mode 100644
index 000000000..ae5790da9
--- /dev/null
+++ b/src/components/BlockedGeoOverlay.tsx
@@ -0,0 +1,109 @@
+import {useEffect} from 'react'
+import {ScrollView, View} from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {Full as Logo, Mark} from '#/components/icons/Logo'
+import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link'
+import {Text} from '#/components/Typography'
+
+export function BlockedGeoOverlay() {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {gtPhone} = useBreakpoints()
+  const insets = useSafeAreaInsets()
+
+  useEffect(() => {
+    // just counting overall hits here
+    logger.metric(`blockedGeoOverlay:shown`, {})
+  }, [])
+
+  const textStyles = [a.text_md, a.leading_normal]
+  const links = {
+    blog: {
+      to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`,
+      label: _(msg`Read our blog post`),
+      overridePresentation: false,
+      disableMismatchWarning: true,
+      style: textStyles,
+    },
+  }
+
+  const blocks = [
+    _(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`),
+    _(
+      msg`A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies.`,
+    ),
+    _(
+      msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`,
+    ),
+    _(
+      msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`,
+    ),
+    <>
+      To learn more, read our{' '}
+      <InlineLinkText {...links.blog}>blog post</InlineLinkText>.
+    </>,
+  ]
+
+  return (
+    <ScrollView
+      contentContainerStyle={[
+        a.px_2xl,
+        {
+          paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
+          paddingBottom: 100,
+        },
+      ]}>
+      <View
+        style={[
+          a.mx_auto,
+          web({
+            maxWidth: 440,
+            paddingTop: gtPhone ? '8vh' : undefined,
+          }),
+        ]}>
+        <View style={[a.align_start]}>
+          <View
+            style={[
+              a.pl_md,
+              a.pr_lg,
+              a.py_sm,
+              a.rounded_full,
+              a.flex_row,
+              a.align_center,
+              a.gap_xs,
+              {
+                backgroundColor: t.palette.primary_25,
+              },
+            ]}>
+            <Mark fill={t.palette.primary_600} width={14} />
+            <Text
+              style={[
+                a.font_bold,
+                {
+                  color: t.palette.primary_600,
+                },
+              ]}>
+              <Trans>Announcement</Trans>
+            </Text>
+          </View>
+        </View>
+
+        <View style={[a.gap_lg, {paddingTop: 32, paddingBottom: 48}]}>
+          {blocks.map((block, index) => (
+            <Text key={index} style={[textStyles]}>
+              {block}
+            </Text>
+          ))}
+        </View>
+
+        <Logo width={120} textFill={t.atoms.text.color} />
+      </View>
+    </ScrollView>
+  )
+}
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 12bd8819b..1417e9e91 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -193,7 +193,7 @@ export function Inner({
           onInteractOutside={preventDefault}
           onFocusOutside={preventDefault}
           onDismiss={close}
-          style={{display: 'flex', flexDirection: 'column'}}>
+          style={{height: '100%', display: 'flex', flexDirection: 'column'}}>
           {header}
           <View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}>
             {children}
@@ -227,10 +227,10 @@ export const InnerFlatList = React.forwardRef<
         web({maxHeight: '80vh'}),
         webInnerStyle,
       ]}
-      contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}>
+      contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
       <FlatList
         ref={ref}
-        style={[gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
+        style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
         {...props}
       />
     </Inner>
diff --git a/src/components/Dialog/shared.tsx b/src/components/Dialog/shared.tsx
index 40d040878..b5513b19c 100644
--- a/src/components/Dialog/shared.tsx
+++ b/src/components/Dialog/shared.tsx
@@ -5,7 +5,6 @@ import {
   View,
   type ViewStyle,
 } from 'react-native'
-import type React from 'react'
 
 import {atoms as a, useTheme} from '#/alf'
 import {Text} from '#/components/Typography'
@@ -28,6 +27,8 @@ export function Header({
     <View
       onLayout={onLayout}
       style={[
+        a.sticky,
+        a.top_0,
         a.relative,
         a.w_full,
         a.py_sm,
@@ -61,7 +62,7 @@ export function HeaderText({
   style?: StyleProp<TextStyle>
 }) {
   return (
-    <Text style={[a.text_lg, a.text_center, a.font_bold, style]}>
+    <Text style={[a.text_lg, a.text_center, a.font_heavy, style]}>
       {children}
     </Text>
   )
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 18da12b22..7debbf5e1 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
-import {View} from 'react-native'
-import {ScrollView} from 'react-native-gesture-handler'
+import {ScrollView, View} from 'react-native'
 import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -9,6 +8,7 @@ import {useNavigation} from '@react-navigation/native'
 import {type NavigationProp} from '#/lib/routes/types'
 import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
+import {isIOS} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useGetPopularFeedsQuery} from '#/state/queries/feed'
 import {type FeedDescriptor} from '#/state/queries/post-feed'
@@ -25,9 +25,9 @@ import {
   type ViewStyleProp,
   web,
 } from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import * as FeedCard from '#/components/FeedCard'
-import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
+import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
 import {InlineLinkText} from '#/components/Link'
 import * as ProfileCard from '#/components/ProfileCard'
@@ -36,6 +36,7 @@ import type * as bsky from '#/types/bsky'
 import {ProgressGuideList} from './ProgressGuide/List'
 
 const MOBILE_CARD_WIDTH = 165
+const FINAL_CARD_WIDTH = 120
 
 function CardOuter({
   children,
@@ -46,11 +47,13 @@ function CardOuter({
   return (
     <View
       style={[
+        a.flex_1,
         a.w_full,
         a.p_md,
         a.rounded_lg,
         a.border,
         t.atoms.bg,
+        t.atoms.shadow_sm,
         t.atoms.border_contrast_low,
         !gtMobile && {
           width: MOBILE_CARD_WIDTH,
@@ -63,11 +66,8 @@ function CardOuter({
 }
 
 export function SuggestedFollowPlaceholder() {
-  const t = useTheme()
-
   return (
-    <CardOuter
-      style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}>
+    <CardOuter>
       <ProfileCard.Outer>
         <View
           style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}>
@@ -78,24 +78,15 @@ export function SuggestedFollowPlaceholder() {
           </View>
         </View>
 
-        <Button
-          label=""
-          size="small"
-          variant="solid"
-          color="secondary"
-          disabled
-          style={[a.w_full, a.rounded_sm]}>
-          <ButtonText>Follow</ButtonText>
-        </Button>
+        <ProfileCard.FollowButtonPlaceholder />
       </ProfileCard.Outer>
     </CardOuter>
   )
 }
 
 export function SuggestedFeedsCardPlaceholder() {
-  const t = useTheme()
   return (
-    <CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}>
+    <CardOuter style={[a.gap_sm]}>
       <FeedCard.Header>
         <FeedCard.AvatarPlaceholder />
         <FeedCard.TitleAndBylinePlaceholder creator />
@@ -253,129 +244,133 @@ export function ProfileGrid({
   profiles: bsky.profile.AnyProfileView[]
   recId?: number
   error: Error | null
-  viewContext: 'profile' | 'feed'
+  viewContext: 'profile' | 'profileHeader' | 'feed'
 }) {
   const t = useTheme()
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
   const {gtMobile} = useBreakpoints()
+
   const isLoading = isSuggestionsLoading || !moderationOpts
-  const maxLength = gtMobile ? 3 : 6
+  const isProfileHeaderContext = viewContext === 'profileHeader'
+  const isFeedContext = viewContext === 'feed'
 
-  const content = isLoading ? (
-    Array(maxLength)
-      .fill(0)
-      .map((_, i) => (
-        <View
-          key={i}
-          style={[
-            gtMobile &&
-              web([
-                a.flex_0,
-                a.flex_grow,
-                {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
-              ]),
-          ]}>
-          <SuggestedFollowPlaceholder />
-        </View>
-      ))
-  ) : error || !profiles.length ? null : (
-    <>
-      {profiles.slice(0, maxLength).map((profile, index) => (
-        <ProfileCard.Link
-          key={profile.did}
-          profile={profile}
-          onPress={() => {
-            logEvent('suggestedUser:press', {
-              logContext:
-                viewContext === 'feed'
+  const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6
+  const minLength = gtMobile ? 3 : 4
+
+  const content = isLoading
+    ? Array(maxLength)
+        .fill(0)
+        .map((_, i) => (
+          <View
+            key={i}
+            style={[
+              a.flex_1,
+              gtMobile &&
+                web([
+                  a.flex_0,
+                  a.flex_grow,
+                  {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
+                ]),
+            ]}>
+            <SuggestedFollowPlaceholder />
+          </View>
+        ))
+    : error || !profiles.length
+      ? null
+      : profiles.slice(0, maxLength).map((profile, index) => (
+          <ProfileCard.Link
+            key={profile.did}
+            profile={profile}
+            onPress={() => {
+              logEvent('suggestedUser:press', {
+                logContext: isFeedContext
                   ? 'InterstitialDiscover'
                   : 'InterstitialProfile',
-              recId,
-              position: index,
-            })
-          }}
-          style={[
-            a.flex_1,
-            gtMobile &&
-              web([
-                a.flex_0,
-                a.flex_grow,
-                {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
-              ]),
-          ]}>
-          {({hovered, pressed}) => (
-            <CardOuter
-              style={[
-                a.flex_1,
-                t.atoms.shadow_sm,
-                (hovered || pressed) && t.atoms.border_contrast_high,
-              ]}>
-              <ProfileCard.Outer>
-                <View
-                  style={[
-                    a.flex_col,
-                    a.align_center,
-                    a.gap_sm,
-                    a.pb_sm,
-                    a.mb_auto,
-                  ]}>
-                  <ProfileCard.Avatar
-                    profile={profile}
-                    moderationOpts={moderationOpts}
-                    size={88}
-                  />
-                  <View style={[a.flex_col, a.align_center, a.max_w_full]}>
-                    <ProfileCard.Name
+                recId,
+                position: index,
+              })
+            }}
+            style={[
+              a.flex_1,
+              gtMobile &&
+                web([
+                  a.flex_0,
+                  a.flex_grow,
+                  {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
+                ]),
+            ]}>
+            {({hovered, pressed}) => (
+              <CardOuter
+                style={[(hovered || pressed) && t.atoms.border_contrast_high]}>
+                <ProfileCard.Outer>
+                  <View
+                    style={[
+                      a.flex_col,
+                      a.align_center,
+                      a.gap_sm,
+                      a.pb_sm,
+                      a.mb_auto,
+                    ]}>
+                    <ProfileCard.Avatar
                       profile={profile}
                       moderationOpts={moderationOpts}
+                      disabledPreview
+                      size={88}
                     />
-                    <ProfileCard.Description
-                      profile={profile}
-                      numberOfLines={2}
-                      style={[
-                        t.atoms.text_contrast_medium,
-                        a.text_center,
-                        a.text_xs,
-                      ]}
-                    />
+                    <View style={[a.flex_col, a.align_center, a.max_w_full]}>
+                      <ProfileCard.Name
+                        profile={profile}
+                        moderationOpts={moderationOpts}
+                      />
+                      <ProfileCard.Description
+                        profile={profile}
+                        numberOfLines={2}
+                        style={[
+                          t.atoms.text_contrast_medium,
+                          a.text_center,
+                          a.text_xs,
+                        ]}
+                      />
+                    </View>
                   </View>
-                </View>
-
-                <ProfileCard.FollowButton
-                  profile={profile}
-                  moderationOpts={moderationOpts}
-                  logContext="FeedInterstitial"
-                  withIcon={false}
-                  style={[a.rounded_sm]}
-                  onFollow={() => {
-                    logEvent('suggestedUser:follow', {
-                      logContext:
-                        viewContext === 'feed'
+
+                  <ProfileCard.FollowButton
+                    profile={profile}
+                    moderationOpts={moderationOpts}
+                    logContext="FeedInterstitial"
+                    withIcon={false}
+                    style={[a.rounded_sm]}
+                    onFollow={() => {
+                      logEvent('suggestedUser:follow', {
+                        logContext: isFeedContext
                           ? 'InterstitialDiscover'
                           : 'InterstitialProfile',
-                      location: 'Card',
-                      recId,
-                      position: index,
-                    })
-                  }}
-                />
-              </ProfileCard.Outer>
-            </CardOuter>
-          )}
-        </ProfileCard.Link>
-      ))}
-    </>
-  )
+                        location: 'Card',
+                        recId,
+                        position: index,
+                      })
+                    }}
+                  />
+                </ProfileCard.Outer>
+              </CardOuter>
+            )}
+          </ProfileCard.Link>
+        ))
 
-  if (error || (!isLoading && profiles.length < 4)) {
+  if (error || (!isLoading && profiles.length < minLength)) {
     logger.debug(`Not enough profiles to show suggested follows`)
     return null
   }
 
   return (
     <View
-      style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
+      style={[
+        !isProfileHeaderContext && a.border_t,
+        t.atoms.border_contrast_low,
+        t.atoms.bg_contrast_25,
+      ]}
+      pointerEvents={isIOS ? 'auto' : 'box-none'}>
       <View
         style={[
           a.px_lg,
@@ -383,19 +378,22 @@ export function ProfileGrid({
           a.flex_row,
           a.align_center,
           a.justify_between,
-        ]}>
+        ]}
+        pointerEvents={isIOS ? 'auto' : 'box-none'}>
         <Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
-          {viewContext === 'profile' ? (
-            <Trans>Similar accounts</Trans>
-          ) : (
+          {isFeedContext ? (
             <Trans>Suggested for you</Trans>
+          ) : (
+            <Trans>Similar accounts</Trans>
           )}
         </Text>
-        <InlineLinkText
-          label={_(msg`See more suggested profiles on the Explore page`)}
-          to="/search">
-          <Trans>See more</Trans>
-        </InlineLinkText>
+        {!isProfileHeaderContext && (
+          <InlineLinkText
+            label={_(msg`See more suggested profiles on the Explore page`)}
+            to="/search">
+            <Trans>See more</Trans>
+          </InlineLinkText>
+        )}
       </View>
 
       {gtMobile ? (
@@ -406,19 +404,16 @@ export function ProfileGrid({
         </View>
       ) : (
         <BlockDrawerGesture>
-          <View>
-            <ScrollView
-              horizontal
-              showsHorizontalScrollIndicator={false}
-              snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
-              decelerationRate="fast">
-              <View style={[a.p_lg, a.pt_md, a.flex_row, a.gap_md]}>
-                {content}
-
-                <SeeMoreSuggestedProfilesCard />
-              </View>
-            </ScrollView>
-          </View>
+          <ScrollView
+            horizontal
+            showsHorizontalScrollIndicator={false}
+            contentContainerStyle={[a.p_lg, a.pt_md, a.flex_row, a.gap_md]}
+            snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
+            decelerationRate="fast">
+            {content}
+
+            {!isProfileHeaderContext && <SeeMoreSuggestedProfilesCard />}
+          </ScrollView>
         </BlockDrawerGesture>
       )}
     </View>
@@ -426,28 +421,29 @@ export function ProfileGrid({
 }
 
 function SeeMoreSuggestedProfilesCard() {
-  const navigation = useNavigation<NavigationProp>()
   const t = useTheme()
   const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
 
   return (
     <Button
+      color="primary"
       label={_(msg`Browse more accounts on the Explore page`)}
-      style={[a.flex_col]}
-      onPress={() => {
-        navigation.navigate('SearchTab')
-      }}>
-      <CardOuter style={[a.flex_1, t.atoms.shadow_sm]}>
-        <View style={[a.flex_1, a.justify_center]}>
-          <View style={[a.flex_col, a.align_center, a.gap_md]}>
-            <Text style={[a.leading_snug, a.text_center]}>
-              <Trans>See more accounts you might like</Trans>
-            </Text>
-
-            <Arrow size="xl" />
-          </View>
-        </View>
-      </CardOuter>
+      style={[
+        a.flex_col,
+        a.align_center,
+        a.gap_xs,
+        a.p_md,
+        a.rounded_lg,
+        t.atoms.shadow_sm,
+        {width: FINAL_CARD_WIDTH},
+      ]}
+      onPress={() => navigation.navigate('SearchTab')}>
+      <ButtonIcon icon={ArrowRight} size="lg" />
+      <ButtonText
+        style={[a.text_md, a.font_medium, a.leading_snug, a.text_center]}>
+        <Trans>See more</Trans>
+      </ButtonText>
     </Button>
   )
 }
@@ -491,10 +487,7 @@ export function SuggestedFeeds() {
           }}>
           {({hovered, pressed}) => (
             <CardOuter
-              style={[
-                a.flex_1,
-                (hovered || pressed) && t.atoms.border_contrast_high,
-              ]}>
+              style={[(hovered || pressed) && t.atoms.border_contrast_high]}>
               <FeedCard.Outer>
                 <FeedCard.Header>
                   <FeedCard.Avatar src={feed.avatar} />
@@ -549,7 +542,7 @@ export function SuggestedFeeds() {
               style={[t.atoms.text_contrast_medium]}>
               <Trans>Browse more suggestions</Trans>
             </InlineLinkText>
-            <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
+            <ArrowRight size="sm" fill={t.atoms.text_contrast_medium.color} />
           </View>
         </View>
       ) : (
@@ -568,7 +561,7 @@ export function SuggestedFeeds() {
                   navigation.navigate('SearchTab')
                 }}
                 style={[a.flex_col]}>
-                <CardOuter style={[a.flex_1]}>
+                <CardOuter>
                   <View style={[a.flex_1, a.justify_center]}>
                     <View style={[a.flex_row, a.px_lg]}>
                       <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
@@ -577,7 +570,7 @@ export function SuggestedFeeds() {
                         </Trans>
                       </Text>
 
-                      <Arrow size="xl" />
+                      <ArrowRight size="xl" />
                     </View>
                   </View>
                 </CardOuter>
diff --git a/src/components/Layout/const.ts b/src/components/Layout/const.ts
index 2b5d3a1fc..2721bed21 100644
--- a/src/components/Layout/const.ts
+++ b/src/components/Layout/const.ts
@@ -13,7 +13,7 @@ export const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3
 /**
  * Corresponds to the width of a small square or round button
  */
-export const HEADER_SLOT_SIZE = 34
+export const HEADER_SLOT_SIZE = 33
 
 /**
  * How far to shift the center column when in the tablet breakpoint
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 6954be6a8..421a7fe9d 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -1,5 +1,5 @@
 import React, {useMemo} from 'react'
-import {type GestureResponderEvent} from 'react-native'
+import {type GestureResponderEvent, Linking} from 'react-native'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 import {
   type LinkProps as RNLinkProps,
@@ -13,6 +13,7 @@ import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types'
 import {shareUrl} from '#/lib/sharing'
 import {
   convertBskyAppUrlIfNeeded,
+  createProxiedUrl,
   isBskyDownloadUrl,
   isExternalUrl,
   linkRequiresWarning,
@@ -407,6 +408,91 @@ export function InlineLinkText({
   )
 }
 
+/**
+ * A barebones version of `InlineLinkText`, for use outside a
+ * `react-navigation` context.
+ */
+export function SimpleInlineLinkText({
+  children,
+  to,
+  style,
+  download,
+  selectable,
+  label,
+  disableUnderline,
+  shouldProxy,
+  ...rest
+}: Omit<
+  InlineLinkProps,
+  | 'to'
+  | 'action'
+  | 'disableMismatchWarning'
+  | 'overridePresentation'
+  | 'onPress'
+  | 'onLongPress'
+  | 'shareOnLongPress'
+> & {
+  to: string
+}) {
+  const t = useTheme()
+  const {
+    state: hovered,
+    onIn: onHoverIn,
+    onOut: onHoverOut,
+  } = useInteractionState()
+  const flattenedStyle = flatten(style) || {}
+  const isExternal = isExternalUrl(to)
+
+  let href = to
+  if (shouldProxy) {
+    href = createProxiedUrl(href)
+  }
+
+  const onPress = () => {
+    Linking.openURL(href)
+  }
+
+  return (
+    <Text
+      selectable={selectable}
+      accessibilityHint=""
+      accessibilityLabel={label}
+      {...rest}
+      style={[
+        {color: t.palette.primary_500},
+        hovered &&
+          !disableUnderline && {
+            ...web({
+              outline: 0,
+              textDecorationLine: 'underline',
+              textDecorationColor:
+                flattenedStyle.color ?? t.palette.primary_500,
+            }),
+          },
+        flattenedStyle,
+      ]}
+      role="link"
+      onPress={onPress}
+      onMouseEnter={onHoverIn}
+      onMouseLeave={onHoverOut}
+      accessibilityRole="link"
+      href={href}
+      {...web({
+        hrefAttrs: {
+          target: download ? undefined : isExternal ? 'blank' : undefined,
+          rel: isExternal ? 'noopener noreferrer' : undefined,
+          download,
+        },
+        dataSet: {
+          // default to no underline, apply this ourselves
+          noUnderline: '1',
+        },
+      })}>
+      {children}
+    </Text>
+  )
+}
+
 export function WebOnlyInlineLinkText({
   children,
   to,
diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx
index 208973cc9..c2603a4d7 100644
--- a/src/components/MediaPreview.tsx
+++ b/src/components/MediaPreview.tsx
@@ -1,7 +1,6 @@
-import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
 import {Image} from 'expo-image'
-import {AppBskyFeedDefs} from '@atproto/api'
+import {type AppBskyFeedDefs} from '@atproto/api'
 import {Trans} from '@lingui/macro'
 
 import {isTenorGifUri} from '#/lib/strings/embed-player'
@@ -92,12 +91,11 @@ export function ImageItem({
       <Image
         key={thumbnail}
         source={{uri: thumbnail}}
+        alt={alt}
         style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]}
         contentFit="cover"
         accessible={true}
         accessibilityIgnoresInvertColors
-        accessibilityHint={alt}
-        accessibilityLabel=""
       />
       <MediaInsetBorder style={[a.rounded_xs]} />
       {children}
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
index d84a90fa6..e4814462f 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
@@ -146,6 +146,8 @@ export function Scrubber({
   const progress = scrubberActive ? seekPosition : currentTime
   const progressPercent = (progress / duration) * 100
 
+  if (duration < 3) return null
+
   return (
     <View
       testID="scrubber"
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
index 676b52661..7a54ef486 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
@@ -373,13 +373,15 @@ export function Controls({
             onPress={onPressPlayPause}
           />
           <View style={a.flex_1} />
-          <Text
-            style={[
-              a.px_xs,
-              {color: t.palette.white, fontVariant: ['tabular-nums']},
-            ]}>
-            {formatTime(currentTime)} / {formatTime(duration)}
-          </Text>
+          {Math.round(duration) > 0 && (
+            <Text
+              style={[
+                a.px_xs,
+                {color: t.palette.white, fontVariant: ['tabular-nums']},
+              ]}>
+              {formatTime(currentTime)} / {formatTime(duration)}
+            </Text>
+          )}
           {hasSubtitleTrack && (
             <ControlButton
               active={subtitlesEnabled}
diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx
index 9c5444b27..8566c2fe6 100644
--- a/src/components/Post/Embed/index.tsx
+++ b/src/components/Post/Embed/index.tsx
@@ -87,14 +87,18 @@ function MediaEmbed({
   switch (embed.type) {
     case 'images': {
       return (
-        <ContentHider modui={rest.moderation?.ui('contentMedia')}>
+        <ContentHider
+          modui={rest.moderation?.ui('contentMedia')}
+          activeStyle={[a.mt_sm]}>
           <ImageEmbed embed={embed} {...rest} />
         </ContentHider>
       )
     }
     case 'link': {
       return (
-        <ContentHider modui={rest.moderation?.ui('contentMedia')}>
+        <ContentHider
+          modui={rest.moderation?.ui('contentMedia')}
+          activeStyle={[a.mt_sm]}>
           <ExternalEmbed
             link={embed.view.external}
             onOpen={rest.onOpen}
@@ -105,7 +109,9 @@ function MediaEmbed({
     }
     case 'video': {
       return (
-        <ContentHider modui={rest.moderation?.ui('contentMedia')}>
+        <ContentHider
+          modui={rest.moderation?.ui('contentMedia')}
+          activeStyle={[a.mt_sm]}>
           <VideoEmbed embed={embed.view} />
         </ContentHider>
       )
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index f12d922fd..095b62167 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -513,12 +513,19 @@ export function FollowButtonInner({
       comment: 'User is following this account, click to unfollow',
     }),
   )
-  const followLabel = _(
-    msg({
-      message: 'Follow',
-      comment: 'User is not following this account, click to follow',
-    }),
-  )
+  const followLabel = profile.viewer?.followedBy
+    ? _(
+        msg({
+          message: 'Follow back',
+          comment: 'User is not following this account, click to follow back',
+        }),
+      )
+    : _(
+        msg({
+          message: 'Follow',
+          comment: 'User is not following this account, click to follow',
+        }),
+      )
 
   if (!profile.viewer) return null
   if (
@@ -561,6 +568,24 @@ export function FollowButtonInner({
   )
 }
 
+export function FollowButtonPlaceholder({style}: ViewStyleProp) {
+  const t = useTheme()
+
+  return (
+    <View
+      style={[
+        a.rounded_sm,
+        t.atoms.bg_contrast_25,
+        a.w_full,
+        {
+          height: 33,
+        },
+        style,
+      ]}
+    />
+  )
+}
+
 export function Labels({
   profile,
   moderationOpts,
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx
index e18fdf2db..15c1ba26e 100644
--- a/src/components/dialogs/GifSelect.tsx
+++ b/src/components/dialogs/GifSelect.tsx
@@ -1,4 +1,4 @@
-import React, {
+import {
   useCallback,
   useImperativeHandle,
   useMemo,
@@ -119,7 +119,7 @@ function GifList({
     [onSelectGif],
   )
 
-  const onEndReached = React.useCallback(() => {
+  const onEndReached = useCallback(() => {
     if (isFetchingNextPage || !hasNextPage || error) return
     fetchNextPage()
   }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
@@ -172,7 +172,7 @@ function GifList({
           </Button>
         )}
 
-        <TextField.Root>
+        <TextField.Root style={[!gtMobile && isWeb && a.flex_1]}>
           <TextField.Icon icon={Search} />
           <TextField.Input
             label={_(msg`Search GIFs`)}
@@ -206,11 +206,9 @@ function GifList({
         renderItem={renderItem}
         numColumns={gtMobile ? 3 : 2}
         columnWrapperStyle={[a.gap_sm]}
-        contentContainerStyle={[
-          native([a.px_xl, {minHeight: height}]),
-          web(a.h_full_vh),
-        ]}
-        style={[web(a.h_full_vh)]}
+        contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
+        webInnerStyle={[web({minHeight: '80vh'})]}
+        webInnerContentContainerStyle={[web(a.pb_0)]}
         ListHeaderComponent={
           <>
             {listHeader}
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index 8aa2335d0..1b1ebbcd5 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -1,12 +1,12 @@
 import React, {useCallback} from 'react'
-import {Keyboard, Pressable, View} from 'react-native'
-import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api'
+import {Keyboard, View} from 'react-native'
+import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
-import {NavigationProp} from '#/lib/routes/types'
-import {Shadow} from '#/state/cache/types'
+import {type NavigationProp} from '#/lib/routes/types'
+import {type Shadow} from '#/state/cache/types'
 import {
   useConvoQuery,
   useMarkAsReadMutation,
@@ -14,11 +14,15 @@ import {
 import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
 import {useProfileBlockMutationQueue} from '#/state/queries/profile'
 import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
+import {type ViewStyleProp} from '#/alf'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
 import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
 import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
 import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
+import {ReportDialog} from '#/components/dms/ReportDialog'
 import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
+import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
 import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
 import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
@@ -30,9 +34,7 @@ import {
 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
 import * as Menu from '#/components/Menu'
 import * as Prompt from '#/components/Prompt'
-import * as bsky from '#/types/bsky'
-import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
-import {ReportDialog} from './ReportDialog'
+import type * as bsky from '#/types/bsky'
 
 let ConvoMenu = ({
   convo,
@@ -59,7 +61,6 @@ let ConvoMenu = ({
   style?: ViewStyleProp['style']
 }): React.ReactNode => {
   const {_} = useLingui()
-  const t = useTheme()
 
   const leaveConvoControl = Prompt.usePromptControl()
   const reportControl = Prompt.usePromptControl()
@@ -73,22 +74,21 @@ let ConvoMenu = ({
         {!hideTrigger && (
           <View style={[style]}>
             <Menu.Trigger label={_(msg`Chat settings`)}>
-              {({props, state}) => (
-                <Pressable
+              {({props}) => (
+                <Button
+                  label={props.accessibilityLabel}
                   {...props}
                   onPress={() => {
                     Keyboard.dismiss()
                     props.onPress()
                   }}
-                  style={[
-                    a.p_sm,
-                    a.rounded_full,
-                    (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
-                    // make sure pfp is in the middle
-                    {marginLeft: -10},
-                  ]}>
-                  <DotsHorizontal size="md" style={t.atoms.text} />
-                </Pressable>
+                  size="small"
+                  color="secondary"
+                  shape="round"
+                  variant="ghost"
+                  style={[a.bg_transparent]}>
+                  <ButtonIcon icon={DotsHorizontal} size="md" />
+                </Button>
               )}
             </Menu.Trigger>
           </View>
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index c8ed98f88..d37e4a34a 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -1,48 +1,42 @@
-import React, {useCallback} from 'react'
-import {TouchableOpacity, View} from 'react-native'
+import {useMemo} from 'react'
+import {View} from 'react-native'
 import {
   type AppBskyActorDefs,
   type ModerationCause,
   type ModerationDecision,
 } from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
 
-import {BACK_HITSLOP} from '#/lib/constants'
 import {makeProfileLink} from '#/lib/routes/links'
-import {type NavigationProp} from '#/lib/routes/types'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {isWeb} from '#/platform/detection'
 import {type Shadow} from '#/state/cache/profile-shadow'
 import {isConvoActive, useConvo} from '#/state/messages/convo'
 import {type ConvoItem} from '#/state/messages/convo/types'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {atoms as a, useTheme, web} from '#/alf'
 import {ConvoMenu} from '#/components/dms/ConvoMenu'
 import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
+import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {Text} from '#/components/Typography'
 import {useSimpleVerificationState} from '#/components/verification'
 import {VerificationCheck} from '#/components/verification/VerificationCheck'
 
-const PFP_SIZE = isWeb ? 40 : 34
+const PFP_SIZE = isWeb ? 40 : Layout.HEADER_SLOT_SIZE
 
-export let MessagesListHeader = ({
+export function MessagesListHeader({
   profile,
   moderation,
 }: {
   profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
   moderation?: ModerationDecision
-}): React.ReactNode => {
+}) {
   const t = useTheme()
-  const {_} = useLingui()
-  const {gtTablet} = useBreakpoints()
-  const navigation = useNavigation<NavigationProp>()
 
-  const blockInfo = React.useMemo(() => {
+  const blockInfo = useMemo(() => {
     if (!moderation) return
     const modui = moderation.ui('profileView')
     const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
@@ -54,87 +48,54 @@ export let MessagesListHeader = ({
     }
   }, [moderation])
 
-  const onPressBack = useCallback(() => {
-    if (navigation.canGoBack()) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('Messages', {})
-    }
-  }, [navigation])
-
   return (
-    <View
-      style={[
-        t.atoms.bg,
-        t.atoms.border_contrast_low,
-        a.border_b,
-        a.flex_row,
-        a.align_start,
-        a.gap_sm,
-        gtTablet ? a.pl_lg : a.pl_xl,
-        a.pr_lg,
-        a.py_sm,
-      ]}>
-      <TouchableOpacity
-        testID="conversationHeaderBackBtn"
-        onPress={onPressBack}
-        hitSlop={BACK_HITSLOP}
-        style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Back`)}
-        accessibilityHint="">
-        <FontAwesomeIcon
-          size={18}
-          icon="angle-left"
-          style={{
-            marginTop: 6,
-          }}
-          color={t.atoms.text.color}
-        />
-      </TouchableOpacity>
-
-      {profile && moderation && blockInfo ? (
-        <HeaderReady
-          profile={profile}
-          moderation={moderation}
-          blockInfo={blockInfo}
-        />
-      ) : (
-        <>
-          <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
-            <View
-              style={[
-                {width: PFP_SIZE, height: PFP_SIZE},
-                a.rounded_full,
-                t.atoms.bg_contrast_25,
-              ]}
-            />
-            <View style={a.gap_xs}>
-              <View
-                style={[
-                  {width: 120, height: 16},
-                  a.rounded_xs,
-                  t.atoms.bg_contrast_25,
-                  a.mt_xs,
-                ]}
-              />
+    <Layout.Header.Outer>
+      <View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}>
+        <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
+          <Layout.Header.BackButton />
+        </View>
+        {profile && moderation && blockInfo ? (
+          <HeaderReady
+            profile={profile}
+            moderation={moderation}
+            blockInfo={blockInfo}
+          />
+        ) : (
+          <>
+            <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
               <View
                 style={[
-                  {width: 175, height: 12},
-                  a.rounded_xs,
+                  {width: PFP_SIZE, height: PFP_SIZE},
+                  a.rounded_full,
                   t.atoms.bg_contrast_25,
                 ]}
               />
+              <View style={a.gap_xs}>
+                <View
+                  style={[
+                    {width: 120, height: 16},
+                    a.rounded_xs,
+                    t.atoms.bg_contrast_25,
+                    a.mt_xs,
+                  ]}
+                />
+                <View
+                  style={[
+                    {width: 175, height: 12},
+                    a.rounded_xs,
+                    t.atoms.bg_contrast_25,
+                  ]}
+                />
+              </View>
             </View>
-          </View>
 
-          <View style={{width: 30}} />
-        </>
-      )}
-    </View>
+            <Layout.Header.Slot />
+          </>
+        )}
+      </View>
+    </Layout.Header.Outer>
   )
 }
-MessagesListHeader = React.memo(MessagesListHeader)
 
 function HeaderReady({
   profile,
@@ -181,15 +142,13 @@ function HeaderReady({
           label={_(msg`View ${displayName}'s profile`)}
           style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
           to={makeProfileLink(profile)}>
-          <View style={[a.pt_2xs]}>
-            <PreviewableUserAvatar
-              size={PFP_SIZE}
-              profile={profile}
-              moderation={moderation.ui('avatar')}
-              disableHoverCard={moderation.blocked}
-            />
-          </View>
-          <View style={a.flex_1}>
+          <PreviewableUserAvatar
+            size={PFP_SIZE}
+            profile={profile}
+            moderation={moderation.ui('avatar')}
+            disableHoverCard={moderation.blocked}
+          />
+          <View style={[a.flex_1]}>
             <View style={[a.flex_row, a.align_center]}>
               <Text
                 emoji
@@ -215,7 +174,7 @@ function HeaderReady({
               <Text
                 style={[
                   t.atoms.text_contrast_medium,
-                  a.text_sm,
+                  a.text_xs,
                   web([a.leading_normal, {marginTop: -2}]),
                 ]}
                 numberOfLines={1}>
@@ -235,15 +194,19 @@ function HeaderReady({
           </View>
         </Link>
 
-        {isConvoActive(convoState) && (
-          <ConvoMenu
-            convo={convoState.convo}
-            profile={profile}
-            currentScreen="conversation"
-            blockInfo={blockInfo}
-            latestReportableMessage={latestReportableMessage}
-          />
-        )}
+        <View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
+          <Layout.Header.Slot>
+            {isConvoActive(convoState) && (
+              <ConvoMenu
+                convo={convoState.convo}
+                profile={profile}
+                currentScreen="conversation"
+                blockInfo={blockInfo}
+                latestReportableMessage={latestReportableMessage}
+              />
+            )}
+          </Layout.Header.Slot>
+        </View>
       </View>
 
       <View
diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index 9993317d6..3d4caa93b 100644
--- a/src/components/forms/TextField.tsx
+++ b/src/components/forms/TextField.tsx
@@ -48,9 +48,11 @@ const Context = createContext<{
 })
 Context.displayName = 'TextFieldContext'
 
-export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
+export type RootProps = React.PropsWithChildren<
+  {isInvalid?: boolean} & TextStyleProp
+>
 
-export function Root({children, isInvalid = false}: RootProps) {
+export function Root({children, isInvalid = false, style}: RootProps) {
   const inputRef = useRef<TextInput>(null)
   const {
     state: hovered,
@@ -85,7 +87,14 @@ export function Root({children, isInvalid = false}: RootProps) {
   return (
     <Context.Provider value={context}>
       <View
-        style={[a.flex_row, a.align_center, a.relative, a.w_full, a.px_md]}
+        style={[
+          a.flex_row,
+          a.align_center,
+          a.relative,
+          a.w_full,
+          a.px_md,
+          style,
+        ]}
         {...web({
           onClick: () => inputRef.current?.focus(),
           onMouseOver: onHoverIn,
diff --git a/src/components/icons/Logo.tsx b/src/components/icons/Logo.tsx
index 6f16d8a44..75c5cb420 100644
--- a/src/components/icons/Logo.tsx
+++ b/src/components/icons/Logo.tsx
@@ -1,5 +1,42 @@
+import Svg, {Path} from 'react-native-svg'
+
+import {type Props, useCommonSVGProps} from './common'
 import {createSinglePathSVG} from './TEMPLATE'
 
 export const Mark = createSinglePathSVG({
   path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z',
 })
+
+export function Full(
+  props: Omit<Props, 'fill' | 'size' | 'height'> & {
+    markFill?: Props['fill']
+    textFill?: Props['fill']
+  },
+) {
+  const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
+  const ratio = 123 / 555
+
+  return (
+    <Svg
+      fill="none"
+      {...rest}
+      viewBox="0 0 555 123"
+      width={size}
+      height={size * ratio}
+      style={[style]}>
+      {gradient}
+      <Path
+        fill={props.markFill ?? fill}
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M101.821 7.673C112.575-.367 130-6.589 130 13.21c0 3.953-2.276 33.214-3.611 37.965-4.641 16.516-21.549 20.729-36.591 18.179 26.292 4.457 32.979 19.218 18.535 33.98-27.433 28.035-39.428-7.034-42.502-16.02-.563-1.647-.827-2.418-.831-1.763-.004-.655-.268.116-.831 1.763-3.074 8.986-15.07 44.055-42.502 16.02C7.223 88.571 13.91 73.81 40.202 69.353c-15.041 2.55-31.95-1.663-36.59-18.179C2.275 46.424 0 17.162 0 13.21 0-6.59 17.426-.368 28.18 7.673 43.084 18.817 59.114 41.413 65 53.54c5.886-12.125 21.917-34.722 36.821-45.866Z"
+      />
+      <Path
+        fill={props.textFill ?? fill}
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="m454.459 63.823 24.128-25.056h32.638l4.825 15.104c3.561 11.357 6.664 22.598 9.422 33.72 2.527-9.6 5.744-20.84 9.536-33.603l4.826-15.221H555l-22.864 65.335c-2.413 6.673-5.4 11.475-9.192 14.168-3.791 2.693-9.192 3.98-16.315 3.98-2.413 0-4.481-.117-6.319-.352v-11.59h5.514c6.549 0 9.767-4.099 9.767-9.719 0-2.81-.92-6.908-2.758-12.177l-17.177-49.478-22.239 22.665L497.2 99.184h-16.545l-17.234-28.101-8.962 9.133v18.968h-14.246V15.817h14.246v48.006Zm-48.373-26.46c16.889 0 25.622 6.79 26.196 20.49h-13.673c-.344-7.377-4.595-9.954-12.523-9.954-6.894 0-10.341 2.342-10.341 7.026 0 4.215 2.987 6.089 9.881 7.377l7.469 1.17c14.361 2.694 20.566 8.08 20.566 18.384 0 12.176-9.652 18.967-26.311 18.967-17.235 0-26.311-6.908-27.116-20.842h14.132c.804 7.494 4.481 10.304 13.213 10.304 7.813 0 11.72-2.459 11.72-7.26 0-4.332-2.758-6.44-11.605-7.962l-6.778-1.17c-12.983-2.224-19.418-8.313-19.418-18.265 0-11.358 8.847-18.266 24.588-18.266ZM270.534 76.351c0 7.61 3.677 11.474 11.145 11.474 7.008 0 13.212-5.268 13.213-15.22v-33.84h14.476v60.418h-14.016v-8.782c-4.481 6.791-10.686 10.187-18.614 10.187-12.523 0-20.68-7.728-20.68-21.778V38.767h14.476v37.585Zm75.432-38.99c8.961 0 16.085 3.045 21.37 9.016s7.928 13.933 7.928 23.651v3.513h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.663 0 11.144-2.927 13.557-8.664h13.903c-3.103 12.294-13.443 20.139-27.575 20.139-8.847 0-15.971-2.927-21.371-8.664-5.4-5.737-8.157-13.348-8.157-22.95 0-9.483 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664ZM195.628 15.817c17.809 0 26.426 9.251 26.426 21.545 0 8.196-3.677 14.168-10.915 17.914 9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.767 23.886-28.494 23.886h-38.26V15.817h36.996Zm51.264 83.367h-14.477V15.817h14.477v83.367ZM174.143 86.07h21.944c8.732 0 13.443-4.098 13.443-11.474 0-7.728-4.481-11.592-13.443-11.592h-21.944V86.07Zm171.708-37.233c-7.928 0-13.443 4.683-14.822 14.401h29.758c-1.264-8.781-6.549-14.401-14.936-14.401Zm-171.708 1.756h20.336c7.927 0 12.178-4.215 12.178-11.24 0-6.44-4.366-10.539-12.178-10.539h-20.336v21.779Z"
+      />
+    </Svg>
+  )
+}
diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx
index 4d59e2fb5..6be64335a 100644
--- a/src/components/interstitials/TrendingVideos.tsx
+++ b/src/components/interstitials/TrendingVideos.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect} from 'react'
+import {useCallback, useEffect, useMemo} from 'react'
 import {ScrollView, View} from 'react-native'
 import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
@@ -55,7 +55,7 @@ export function TrendingVideos() {
   const {setTrendingVideoDisabled} = useTrendingSettingsApi()
   const trendingPrompt = Prompt.usePromptControl()
 
-  const onConfirmHide = React.useCallback(() => {
+  const onConfirmHide = useCallback(() => {
     setTrendingVideoDisabled(true)
     logEvent('trendingVideos:hide', {context: 'interstitial:discover'})
   }, [setTrendingVideoDisabled])
@@ -147,9 +147,7 @@ function VideoCards({
 }: {
   data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
 }) {
-  const t = useTheme()
-  const {_} = useLingui()
-  const items = React.useMemo(() => {
+  const items = useMemo(() => {
     return data.pages
       .flatMap(page => page.slices)
       .map(slice => slice.items[0])
@@ -157,10 +155,6 @@ function VideoCards({
       .filter(item => AppBskyEmbedVideo.isView(item.post.embed))
       .slice(0, 8)
   }, [data])
-  const href = React.useMemo(() => {
-    const urip = new AtUri(VIDEO_FEED_URI)
-    return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover')
-  }, [])
 
   return (
     <>
@@ -183,50 +177,58 @@ function VideoCards({
         </View>
       ))}
 
-      <View style={[{width: CARD_WIDTH * 2}]}>
-        <Link
-          to={href}
-          label={_(msg`View more`)}
-          style={[
-            a.justify_center,
-            a.align_center,
-            a.flex_1,
-            a.rounded_lg,
-            a.border,
-            t.atoms.border_contrast_low,
-            t.atoms.bg,
-            t.atoms.shadow_sm,
-          ]}>
-          {({pressed}) => (
-            <View
-              style={[
-                a.flex_row,
-                a.align_center,
-                a.gap_md,
-                {
-                  opacity: pressed ? 0.6 : 1,
-                },
-              ]}>
-              <Text style={[a.text_md]}>
-                <Trans>View more</Trans>
-              </Text>
-              <View
-                style={[
-                  a.align_center,
-                  a.justify_center,
-                  a.rounded_full,
-                  {
-                    width: 34,
-                    height: 34,
-                    backgroundColor: t.palette.primary_500,
-                  },
-                ]}>
-                <ButtonIcon icon={ChevronRight} />
-              </View>
-            </View>
-          )}
-        </Link>
-      </View>
+      <ViewMoreCard />
     </>
   )
 }
+
+function ViewMoreCard() {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const href = useMemo(() => {
+    const urip = new AtUri(VIDEO_FEED_URI)
+    return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover')
+  }, [])
+
+  return (
+    <View style={[{width: CARD_WIDTH * 2}]}>
+      <Link
+        to={href}
+        label={_(msg`View more`)}
+        style={[
+          a.justify_center,
+          a.align_center,
+          a.flex_1,
+          a.rounded_lg,
+          a.border,
+          t.atoms.border_contrast_low,
+          t.atoms.bg,
+          t.atoms.shadow_sm,
+        ]}>
+        {({pressed}) => (
+          <View
+            style={[
+              a.flex_row,
+              a.align_center,
+              a.gap_md,
+              {
+                opacity: pressed ? 0.6 : 1,
+              },
+            ]}>
+            <Text style={[a.text_md]}>
+              <Trans>View more</Trans>
+            </Text>
+            <Button
+              color="primary"
+              size="small"
+              shape="round"
+              label={_(msg`View more trending videos`)}>
+              <ButtonIcon icon={ChevronRight} />
+            </Button>
+          </View>
+        )}
+      </Link>
+    </View>
+  )
+}