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/FeedInterstitials.tsx201
-rw-r--r--src/components/KnownFollowers.tsx16
-rw-r--r--src/components/ProfileCard.tsx45
-rw-r--r--src/components/SearchError.tsx45
-rw-r--r--src/components/VideoPostCard.tsx80
-rw-r--r--src/components/dialogs/EmailDialog/data/useAccountEmailState.ts65
-rw-r--r--src/components/dialogs/EmailDialog/data/useConfirmEmail.ts10
-rw-r--r--src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts10
-rw-r--r--src/components/interstitials/TrendingVideos.tsx31
9 files changed, 305 insertions, 198 deletions
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index a92e7be7f..2a3a00ba7 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -25,18 +25,17 @@ import {
   type ViewStyleProp,
   web,
 } from '#/alf'
-import {Button} from '#/components/Button'
+import {Button, ButtonText} from '#/components/Button'
 import * as FeedCard from '#/components/FeedCard'
 import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
-import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
 import {InlineLinkText} from '#/components/Link'
 import * as ProfileCard from '#/components/ProfileCard'
 import {Text} from '#/components/Typography'
 import type * as bsky from '#/types/bsky'
 import {ProgressGuideList} from './ProgressGuide/List'
 
-const MOBILE_CARD_WIDTH = 300
+const MOBILE_CARD_WIDTH = 165
 
 function CardOuter({
   children,
@@ -48,8 +47,8 @@ function CardOuter({
     <View
       style={[
         a.w_full,
-        a.p_lg,
-        a.rounded_md,
+        a.p_md,
+        a.rounded_lg,
         a.border,
         t.atoms.bg,
         t.atoms.border_contrast_low,
@@ -65,14 +64,30 @@ function CardOuter({
 
 export function SuggestedFollowPlaceholder() {
   const t = useTheme()
+
   return (
-    <CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
-      <ProfileCard.Header>
-        <ProfileCard.AvatarPlaceholder />
-        <ProfileCard.NameAndHandlePlaceholder />
-      </ProfileCard.Header>
+    <CardOuter
+      style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}>
+      <ProfileCard.Outer>
+        <View
+          style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}>
+          <ProfileCard.AvatarPlaceholder size={88} />
+          <ProfileCard.NamePlaceholder />
+          <View style={[a.w_full]}>
+            <ProfileCard.DescriptionPlaceholder numberOfLines={2} />
+          </View>
+        </View>
 
-      <ProfileCard.DescriptionPlaceholder numberOfLines={2} />
+        <Button
+          label=""
+          size="small"
+          variant="solid"
+          color="secondary"
+          disabled
+          style={[a.w_full, a.rounded_sm]}>
+          <ButtonText>Follow</ButtonText>
+        </Button>
+      </ProfileCard.Outer>
     </CardOuter>
   )
 }
@@ -243,10 +258,9 @@ export function ProfileGrid({
   const t = useTheme()
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
-  const navigation = useNavigation<NavigationProp>()
   const {gtMobile} = useBreakpoints()
   const isLoading = isSuggestionsLoading || !moderationOpts
-  const maxLength = gtMobile ? 4 : 6
+  const maxLength = gtMobile ? 3 : 6
 
   const content = isLoading ? (
     Array(maxLength)
@@ -254,7 +268,14 @@ export function ProfileGrid({
       .map((_, i) => (
         <View
           key={i}
-          style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}>
+          style={[
+            gtMobile &&
+              web([
+                a.flex_0,
+                a.flex_grow,
+                {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
+              ]),
+          ]}>
           <SuggestedFollowPlaceholder />
         </View>
       ))
@@ -276,44 +297,69 @@ export function ProfileGrid({
           }}
           style={[
             a.flex_1,
-            gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]),
+            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>
-                <ProfileCard.Header>
+                <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}
                   />
-                  <ProfileCard.NameAndHandle
-                    profile={profile}
-                    moderationOpts={moderationOpts}
-                  />
-                  <ProfileCard.FollowButton
-                    profile={profile}
-                    moderationOpts={moderationOpts}
-                    logContext="FeedInterstitial"
-                    shape="round"
-                    colorInverted
-                    onFollow={() => {
-                      logEvent('suggestedUser:follow', {
-                        logContext:
-                          viewContext === 'feed'
-                            ? 'InterstitialDiscover'
-                            : 'InterstitialProfile',
-                        location: 'Card',
-                        recId,
-                        position: index,
-                      })
-                    }}
-                  />
-                </ProfileCard.Header>
-                <ProfileCard.Description profile={profile} numberOfLines={2} />
+                  <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>
+
+                <ProfileCard.FollowButton
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                  logContext="FeedInterstitial"
+                  withIcon={false}
+                  style={[a.rounded_sm]}
+                  onFollow={() => {
+                    logEvent('suggestedUser:follow', {
+                      logContext:
+                        viewContext === 'feed'
+                          ? 'InterstitialDiscover'
+                          : 'InterstitialProfile',
+                      location: 'Card',
+                      recId,
+                      position: index,
+                    })
+                  }}
+                />
               </ProfileCard.Outer>
             </CardOuter>
           )}
@@ -333,36 +379,30 @@ export function ProfileGrid({
       <View
         style={[
           a.p_lg,
-          a.pb_xs,
+          a.py_md,
           a.flex_row,
           a.align_center,
           a.justify_between,
         ]}>
-        <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
+        <Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
           {viewContext === 'profile' ? (
             <Trans>Similar accounts</Trans>
           ) : (
             <Trans>Suggested for you</Trans>
           )}
         </Text>
-        <Person fill={t.atoms.text_contrast_low.color} size="sm" />
+        <InlineLinkText
+          label={_(msg`See more suggested profiles on the Explore page`)}
+          to="/search">
+          <Trans>See more</Trans>
+        </InlineLinkText>
       </View>
 
       {gtMobile ? (
-        <View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
-          <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
+        <View style={[a.px_lg, a.pb_lg]}>
+          <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
             {content}
           </View>
-
-          <View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
-            <InlineLinkText
-              label={_(msg`Browse more suggestions`)}
-              to="/search"
-              style={[t.atoms.text_contrast_medium]}>
-              <Trans>Browse more suggestions</Trans>
-            </InlineLinkText>
-            <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
-          </View>
         </View>
       ) : (
         <BlockDrawerGesture>
@@ -371,29 +411,12 @@ export function ProfileGrid({
               horizontal
               showsHorizontalScrollIndicator={false}
               snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
-              decelerationRate="fast">
-              <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
+              decelerationRate="fast"
+              style={[a.overflow_visible]}>
+              <View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
                 {content}
 
-                <Button
-                  label={_(msg`Browse more accounts on the Explore page`)}
-                  onPress={() => {
-                    navigation.navigate('SearchTab')
-                  }}>
-                  <CardOuter style={[a.flex_1, {borderWidth: 0}]}>
-                    <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]}>
-                          <Trans>
-                            Browse more suggestions on the Explore page
-                          </Trans>
-                        </Text>
-
-                        <Arrow size="xl" />
-                      </View>
-                    </View>
-                  </CardOuter>
-                </Button>
+                <SeeMoreSuggestedProfilesCard />
               </View>
             </ScrollView>
           </View>
@@ -403,6 +426,32 @@ export function ProfileGrid({
   )
 }
 
+function SeeMoreSuggestedProfilesCard() {
+  const navigation = useNavigation<NavigationProp>()
+  const t = useTheme()
+  const {_} = useLingui()
+
+  return (
+    <Button
+      label={_(msg`Browse more accounts on the Explore page`)}
+      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>
+    </Button>
+  )
+}
+
 export function SuggestedFeeds() {
   const numFeedsToDisplay = 3
   const t = useTheme()
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index c60d0f92e..28fb8f1f1 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -1,6 +1,10 @@
 import React from 'react'
 import {View} from 'react-native'
-import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
+import {
+  type AppBskyActorDefs,
+  moderateProfile,
+  type ModerationOpts,
+} from '@atproto/api'
 import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -8,9 +12,9 @@ import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useTheme} from '#/alf'
-import {Link, LinkProps} from '#/components/Link'
+import {Link, type LinkProps} from '#/components/Link'
 import {Text} from '#/components/Typography'
-import * as bsky from '#/types/bsky'
+import type * as bsky from '#/types/bsky'
 
 const AVI_SIZE = 30
 const AVI_SIZE_SMALL = 20
@@ -137,9 +141,9 @@ function KnownFollowersInner({
         <>
           <View
             style={[
+              a.flex_row,
               {
                 height: SIZE,
-                width: SIZE + (slice.length - 1) * a.gap_md.gap,
               },
               pressed && {
                 opacity: 0.5,
@@ -149,15 +153,14 @@ function KnownFollowersInner({
               <View
                 key={prof.did}
                 style={[
-                  a.absolute,
                   a.rounded_full,
                   {
                     borderWidth: AVI_BORDER,
                     borderColor: t.atoms.bg.backgroundColor,
                     width: SIZE + AVI_BORDER * 2,
                     height: SIZE + AVI_BORDER * 2,
-                    left: i * a.gap_md.gap,
                     zIndex: AVI_BORDER - i,
+                    marginLeft: i > 0 ? -8 : 0,
                   },
                 ]}>
                 <UserAvatar
@@ -165,6 +168,7 @@ function KnownFollowersInner({
                   avatar={prof.avatar}
                   moderation={moderation.ui('avatar')}
                   type={prof.associated?.labeler ? 'labeler' : 'user'}
+                  noBorder
                 />
               </View>
             ))}
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index e01c27655..f12d922fd 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -20,7 +20,13 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
 import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, platform, useTheme} from '#/alf'
+import {
+  atoms as a,
+  platform,
+  type TextStyleProp,
+  useTheme,
+  type ViewStyleProp,
+} from '#/alf'
 import {
   Button,
   ButtonIcon,
@@ -136,12 +142,14 @@ export function Avatar({
   onPress,
   disabledPreview,
   liveOverride,
+  size = 40,
 }: {
   profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   onPress?: () => void
   disabledPreview?: boolean
   liveOverride?: boolean
+  size?: number
 }) {
   const moderation = moderateProfile(profile, moderationOpts)
 
@@ -149,7 +157,7 @@ export function Avatar({
 
   return disabledPreview ? (
     <UserAvatar
-      size={40}
+      size={size}
       avatar={profile.avatar}
       type={profile.associated?.labeler ? 'labeler' : 'user'}
       moderation={moderation.ui('avatar')}
@@ -157,7 +165,7 @@ export function Avatar({
     />
   ) : (
     <PreviewableUserAvatar
-      size={40}
+      size={size}
       profile={profile}
       moderation={moderation.ui('avatar')}
       onBeforePress={onPress}
@@ -166,7 +174,7 @@ export function Avatar({
   )
 }
 
-export function AvatarPlaceholder() {
+export function AvatarPlaceholder({size = 40}: {size?: number}) {
   const t = useTheme()
   return (
     <View
@@ -174,8 +182,8 @@ export function AvatarPlaceholder() {
         a.rounded_full,
         t.atoms.bg_contrast_25,
         {
-          width: 40,
-          height: 40,
+          width: size,
+          height: size,
         },
       ]}
     />
@@ -274,7 +282,7 @@ export function Name({
   )
   const verification = useSimpleVerificationState({profile})
   return (
-    <View style={[a.flex_row, a.align_center]}>
+    <View style={[a.flex_row, a.align_center, a.max_w_full]}>
       <Text
         emoji
         style={[
@@ -343,13 +351,32 @@ export function NameAndHandlePlaceholder() {
   )
 }
 
+export function NamePlaceholder({style}: ViewStyleProp) {
+  const t = useTheme()
+
+  return (
+    <View
+      style={[
+        a.rounded_xs,
+        t.atoms.bg_contrast_25,
+        {
+          width: '60%',
+          height: 14,
+        },
+        style,
+      ]}
+    />
+  )
+}
+
 export function Description({
   profile: profileUnshadowed,
   numberOfLines = 3,
+  style,
 }: {
   profile: bsky.profile.AnyProfileView
   numberOfLines?: number
-}) {
+} & TextStyleProp) {
   const profile = useProfileShadow(profileUnshadowed)
   const rt = useMemo(() => {
     if (!('description' in profile)) return
@@ -369,7 +396,7 @@ export function Description({
     <View style={[a.pt_xs]}>
       <RichText
         value={rt}
-        style={[a.leading_snug]}
+        style={[a.leading_snug, style]}
         numberOfLines={numberOfLines}
         disableLinks
       />
diff --git a/src/components/SearchError.tsx b/src/components/SearchError.tsx
new file mode 100644
index 000000000..443bbab8f
--- /dev/null
+++ b/src/components/SearchError.tsx
@@ -0,0 +1,45 @@
+import {View} from 'react-native'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {atoms as a, useBreakpoints} from '#/alf'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+import {TimesLarge_Stroke2_Corner0_Rounded} from './icons/Times'
+
+export function SearchError({
+  title,
+  children,
+}: {
+  title?: string
+  children?: React.ReactNode
+}) {
+  const {gtMobile} = useBreakpoints()
+  const pal = usePalette('default')
+
+  return (
+    <Layout.Content>
+      <View
+        style={[
+          a.align_center,
+          a.gap_4xl,
+          a.px_xl,
+          {
+            paddingVertical: 150,
+          },
+        ]}>
+        <TimesLarge_Stroke2_Corner0_Rounded width={32} fill={pal.colors.icon} />
+        <View
+          style={[
+            a.align_center,
+            {maxWidth: gtMobile ? 394 : 294},
+            gtMobile ? a.gap_md : a.gap_sm,
+          ]}>
+          <Text style={[a.font_bold, a.text_lg, a.text_center, a.leading_snug]}>
+            {title}
+          </Text>
+          {children}
+        </View>
+      </View>
+    </Layout.Content>
+  )
+}
diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx
index c28adad8b..a1bdd29b4 100644
--- a/src/components/VideoPostCard.tsx
+++ b/src/components/VideoPostCard.tsx
@@ -390,6 +390,7 @@ export function CompactVideoPostCard({
   if (!AppBskyEmbedVideo.isView(embed)) return null
 
   const likeCount = post?.likeCount ?? 0
+  const showLikeCount = false
   const {thumbnail} = embed
   const black = getBlackColor(t)
 
@@ -410,6 +411,7 @@ export function CompactVideoPostCard({
       onPressOut={onPressOut}
       style={[
         a.flex_col,
+        t.atoms.shadow_sm,
         {
           alignItems: undefined,
           justifyContent: undefined,
@@ -420,8 +422,10 @@ export function CompactVideoPostCard({
           <View
             style={[
               a.justify_center,
-              a.rounded_md,
+              a.rounded_lg,
               a.overflow_hidden,
+              a.border,
+              t.atoms.border_contrast_low,
               {
                 backgroundColor: black,
                 aspectRatio: 9 / 16,
@@ -442,6 +446,8 @@ export function CompactVideoPostCard({
                   a.inset_0,
                   a.justify_center,
                   a.align_center,
+                  a.border,
+                  t.atoms.border_contrast_low,
                   {
                     backgroundColor: 'black',
                     opacity: 0.2,
@@ -461,8 +467,10 @@ export function CompactVideoPostCard({
           <View
             style={[
               a.justify_center,
-              a.rounded_md,
+              a.rounded_lg,
               a.overflow_hidden,
+              a.border,
+              t.atoms.border_contrast_low,
               {
                 backgroundColor: black,
                 aspectRatio: 9 / 16,
@@ -475,47 +483,51 @@ export function CompactVideoPostCard({
             />
             <MediaInsetBorder />
 
-            <View style={[a.absolute, a.inset_0]}>
+            <View style={[a.absolute, a.inset_0, t.atoms.shadow_sm]}>
               <View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}>
                 <View
-                  style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
+                  style={[a.relative, a.rounded_full, {width: 24, height: 24}]}>
                   <UserAvatar
                     type="user"
-                    size={20}
+                    size={24}
                     avatar={post.author.avatar}
                   />
                   <MediaInsetBorder />
                 </View>
               </View>
-              <View
-                style={[
-                  a.absolute,
-                  a.inset_0,
-                  a.pt_2xl,
-                  {
-                    top: 'auto',
-                  },
-                ]}>
-                <LinearGradient
-                  colors={[black, 'rgba(0, 0, 0, 0)']}
-                  locations={[0.02, 1]}
-                  start={{x: 0, y: 1}}
-                  end={{x: 0, y: 0}}
-                  style={[a.absolute, a.inset_0, {opacity: 0.9}]}
-                />
 
+              {showLikeCount && (
                 <View
-                  style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
-                  {likeCount > 0 && (
-                    <View style={[a.flex_row, a.align_center, a.gap_xs]}>
-                      <Heart size="sm" fill="white" />
-                      <Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
-                        {formatCount(i18n, likeCount)}
-                      </Text>
-                    </View>
-                  )}
+                  style={[
+                    a.absolute,
+                    a.inset_0,
+                    a.pt_2xl,
+                    {
+                      top: 'auto',
+                    },
+                  ]}>
+                  <LinearGradient
+                    colors={[black, 'rgba(0, 0, 0, 0)']}
+                    locations={[0.02, 1]}
+                    start={{x: 0, y: 1}}
+                    end={{x: 0, y: 0}}
+                    style={[a.absolute, a.inset_0, {opacity: 0.9}]}
+                  />
+
+                  <View
+                    style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
+                    {likeCount > 0 && (
+                      <View style={[a.flex_row, a.align_center, a.gap_xs]}>
+                        <Heart size="sm" fill="white" />
+                        <Text
+                          style={[a.text_sm, a.font_bold, {color: 'white'}]}>
+                          {formatCount(i18n, likeCount)}
+                        </Text>
+                      </View>
+                    )}
+                  </View>
                 </View>
-              </View>
+              )}
             </View>
           </View>
         </Hider.Content>
@@ -529,11 +541,13 @@ export function CompactVideoPostCardPlaceholder() {
   const black = getBlackColor(t)
 
   return (
-    <View style={[a.flex_1]}>
+    <View style={[a.flex_1, t.atoms.shadow_sm]}>
       <View
         style={[
-          a.rounded_md,
+          a.rounded_lg,
           a.overflow_hidden,
+          a.border,
+          t.atoms.border_contrast_low,
           {
             backgroundColor: black,
             aspectRatio: 9 / 16,
diff --git a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts
index 377411107..f25369f8d 100644
--- a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts
+++ b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts
@@ -1,7 +1,7 @@
-import {useCallback, useEffect, useState} from 'react'
-import {useQuery, useQueryClient} from '@tanstack/react-query'
+import {useEffect, useMemo, useState} from 'react'
+import {useQuery} from '@tanstack/react-query'
 
-import {useAgent} from '#/state/session'
+import {useAgent, useSessionApi} from '#/state/session'
 import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events'
 
 export type AccountEmailState = {
@@ -11,57 +11,36 @@ export type AccountEmailState = {
 
 export const accountEmailStateQueryKey = ['accountEmailState'] as const
 
-export function useInvalidateAccountEmailState() {
-  const qc = useQueryClient()
-
-  return useCallback(() => {
-    return qc.invalidateQueries({
-      queryKey: accountEmailStateQueryKey,
-    })
-  }, [qc])
-}
-
-export function useUpdateAccountEmailStateQueryCache() {
-  const qc = useQueryClient()
-
-  return useCallback(
-    (data: AccountEmailState) => {
-      return qc.setQueriesData(
-        {
-          queryKey: accountEmailStateQueryKey,
-        },
-        data,
-      )
-    },
-    [qc],
-  )
-}
-
 export function useAccountEmailState() {
   const agent = useAgent()
+  const {partialRefreshSession} = useSessionApi()
   const [prevIsEmailVerified, setPrevEmailIsVerified] = useState(
     !!agent.session?.emailConfirmed,
   )
-  const fallbackData: AccountEmailState = {
-    isEmailVerified: !!agent.session?.emailConfirmed,
-    email2FAEnabled: !!agent.session?.emailAuthFactor,
-  }
-  const query = useQuery<AccountEmailState>({
+  const state: AccountEmailState = useMemo(
+    () => ({
+      isEmailVerified: !!agent.session?.emailConfirmed,
+      email2FAEnabled: !!agent.session?.emailAuthFactor,
+    }),
+    [agent.session],
+  )
+
+  /**
+   * Only here to refetch on focus, when necessary
+   */
+  useQuery({
     enabled: !!agent.session,
-    refetchOnWindowFocus: true,
+    /**
+     * Only refetch if the email verification s incomplete.
+     */
+    refetchOnWindowFocus: !prevIsEmailVerified,
     queryKey: accountEmailStateQueryKey,
     queryFn: async () => {
-      // will also trigger updates to `#/state/session` data
-      const {data} = await agent.resumeSession(agent.session!)
-      return {
-        isEmailVerified: !!data.emailConfirmed,
-        email2FAEnabled: !!data.emailAuthFactor,
-      }
+      await partialRefreshSession()
+      return null
     },
   })
 
-  const state = query.data ?? fallbackData
-
   /*
    * This will emit `n` times for each instance of this hook. So the listeners
    * all use `once` to prevent multiple handlers firing.
diff --git a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts
index 73f824fcc..475a8cbfb 100644
--- a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts
+++ b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts
@@ -1,13 +1,10 @@
 import {useMutation} from '@tanstack/react-query'
 
 import {useAgent, useSession} from '#/state/session'
-import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
 
 export function useConfirmEmail() {
   const agent = useAgent()
   const {currentAccount} = useSession()
-  const updateAccountEmailStateQueryCache =
-    useUpdateAccountEmailStateQueryCache()
 
   return useMutation({
     mutationFn: async ({token}: {token: string}) => {
@@ -19,11 +16,8 @@ export function useConfirmEmail() {
         email: currentAccount.email,
         token: token.trim(),
       })
-      const {data} = await agent.resumeSession(agent.session!)
-      updateAccountEmailStateQueryCache({
-        isEmailVerified: !!data.emailConfirmed,
-        email2FAEnabled: !!data.emailAuthFactor,
-      })
+      // will update session state at root of app
+      await agent.resumeSession(agent.session!)
     },
   })
 }
diff --git a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts
index 39f5fd2d9..358bf8654 100644
--- a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts
+++ b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts
@@ -1,13 +1,10 @@
 import {useMutation} from '@tanstack/react-query'
 
 import {useAgent, useSession} from '#/state/session'
-import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
 
 export function useManageEmail2FA() {
   const agent = useAgent()
   const {currentAccount} = useSession()
-  const updateAccountEmailStateQueryCache =
-    useUpdateAccountEmailStateQueryCache()
 
   return useMutation({
     mutationFn: async ({
@@ -25,11 +22,8 @@ export function useManageEmail2FA() {
         emailAuthFactor: enabled,
         token,
       })
-      const {data} = await agent.resumeSession(agent.session!)
-      updateAccountEmailStateQueryCache({
-        isEmailVerified: !!data.emailConfirmed,
-        email2FAEnabled: !!data.emailAuthFactor,
-      })
+      // will update session state at root of app
+      await agent.resumeSession(agent.session!)
     },
   })
 }
diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx
index fab738b9c..4d59e2fb5 100644
--- a/src/components/interstitials/TrendingVideos.tsx
+++ b/src/components/interstitials/TrendingVideos.tsx
@@ -16,7 +16,6 @@ import {atoms as a, useGutters, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
-import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
 import {Link} from '#/components/Link'
 import * as Prompt from '#/components/Prompt'
 import {Text} from '#/components/Typography'
@@ -25,7 +24,7 @@ import {
   CompactVideoPostCardPlaceholder,
 } from '#/components/VideoPostCard'
 
-const CARD_WIDTH = 100
+const CARD_WIDTH = 108
 
 const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
 const FEED_PARAMS: {
@@ -68,9 +67,10 @@ export function TrendingVideos() {
   return (
     <View
       style={[
-        a.pt_lg,
+        a.pt_sm,
         a.pb_lg,
         a.border_t,
+        a.overflow_hidden,
         t.atoms.border_contrast_low,
         t.atoms.bg_contrast_25,
       ]}>
@@ -82,20 +82,17 @@ export function TrendingVideos() {
           a.align_center,
           a.justify_between,
         ]}>
-        <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_xs]}>
-          <Graph />
-          <Text style={[a.text_md, a.font_bold, a.leading_snug]}>
-            <Trans>Trending Videos</Trans>
-          </Text>
-        </View>
+        <Text style={[a.text_sm, a.font_bold, a.leading_snug]}>
+          <Trans>Trending Videos</Trans>
+        </Text>
         <Button
           label={_(msg`Dismiss this section`)}
           size="tiny"
-          variant="ghost"
+          variant="solid"
           color="secondary"
-          shape="round"
+          shape="square"
           onPress={() => trendingPrompt.open()}>
-          <ButtonIcon icon={X} />
+          <ButtonIcon icon={X} size="sm" />
         </Button>
       </View>
 
@@ -104,11 +101,12 @@ export function TrendingVideos() {
           horizontal
           showsHorizontalScrollIndicator={false}
           decelerationRate="fast"
-          snapToInterval={CARD_WIDTH + a.gap_sm.gap}>
+          snapToInterval={CARD_WIDTH + a.gap_md.gap}
+          style={[a.overflow_visible]}>
           <View
             style={[
               a.flex_row,
-              a.gap_sm,
+              a.gap_md,
               {
                 paddingLeft: gutters.paddingLeft,
                 paddingRight: gutters.paddingRight,
@@ -193,8 +191,11 @@ function VideoCards({
             a.justify_center,
             a.align_center,
             a.flex_1,
-            a.rounded_md,
+            a.rounded_lg,
+            a.border,
+            t.atoms.border_contrast_low,
             t.atoms.bg,
+            t.atoms.shadow_sm,
           ]}>
           {({pressed}) => (
             <View