about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/FeedCard.tsx4
-rw-r--r--src/components/FeedInterstitials.tsx8
-rw-r--r--src/components/KnownFollowers.tsx5
-rw-r--r--src/components/ListCard.tsx4
-rw-r--r--src/components/MediaPreview.tsx58
-rw-r--r--src/components/ProfileCard.tsx23
-rw-r--r--src/components/StarterPack/QrCode.tsx8
-rw-r--r--src/components/StarterPack/QrCodeDialog.tsx8
-rw-r--r--src/components/StarterPack/StarterPackCard.tsx20
-rw-r--r--src/components/StarterPack/Wizard/WizardEditListDialog.tsx2
-rw-r--r--src/components/StarterPack/Wizard/WizardListCard.tsx3
-rw-r--r--src/components/VideoPostCard.tsx8
-rw-r--r--src/components/WhoCanReply.tsx6
-rw-r--r--src/components/dms/ConvoMenu.tsx11
-rw-r--r--src/components/dms/MessageProfileButton.tsx2
-rw-r--r--src/components/dms/MessagesListBlockedFooter.tsx5
-rw-r--r--src/components/dms/MessagesListHeader.tsx9
-rw-r--r--src/components/dms/ReportDialog.tsx16
-rw-r--r--src/components/dms/dialogs/SearchablePeopleList.tsx7
-rw-r--r--src/components/dms/util.ts4
-rw-r--r--src/components/hooks/useFollowMethods.ts4
-rw-r--r--src/lib/api/feed-manip.ts11
-rw-r--r--src/lib/api/feed/merge.ts1
-rw-r--r--src/lib/api/hack-add-deleted-embed.ts24
-rw-r--r--src/lib/api/index.ts21
-rw-r--r--src/lib/embeds.ts24
-rw-r--r--src/lib/generate-starterpack.ts17
-rw-r--r--src/lib/moderation/blocked-and-muted.ts14
-rw-r--r--src/lib/strings/embed-player.ts9
-rw-r--r--src/lib/strings/starter-pack.ts6
-rw-r--r--src/screens/Messages/Conversation.tsx2
-rw-r--r--src/screens/Messages/components/ChatListItem.tsx4
-rw-r--r--src/screens/Messages/components/MessageInputEmbed.tsx7
-rw-r--r--src/screens/Messages/components/MessagesList.tsx9
-rw-r--r--src/screens/Onboarding/StepFinished.tsx31
-rw-r--r--src/screens/Onboarding/util.ts17
-rw-r--r--src/screens/Profile/KnownFollowers.tsx2
-rw-r--r--src/screens/Settings/components/PwiOptOut.tsx19
-rw-r--r--src/screens/Signup/index.tsx6
-rw-r--r--src/screens/StarterPack/StarterPackLandingScreen.tsx8
-rw-r--r--src/screens/StarterPack/StarterPackScreen.tsx8
-rw-r--r--src/screens/StarterPack/Wizard/State.tsx17
-rw-r--r--src/screens/StarterPack/Wizard/StepProfiles.tsx3
-rw-r--r--src/screens/StarterPack/Wizard/index.tsx9
-rw-r--r--src/screens/VideoFeed/index.tsx8
-rw-r--r--src/state/cache/profile-shadow.ts13
-rw-r--r--src/state/cache/thread-mutes.tsx1
-rw-r--r--src/state/messages/convo/agent.ts23
-rw-r--r--src/state/messages/convo/types.ts26
-rw-r--r--src/state/messages/events/agent.ts7
-rw-r--r--src/state/queries/list.ts9
-rw-r--r--src/state/queries/messages/actor-declaration.ts2
-rw-r--r--src/state/queries/messages/list-conversations.tsx50
-rw-r--r--src/state/queries/notifications/feed.ts18
-rw-r--r--src/state/queries/notifications/util.ts18
-rw-r--r--src/state/queries/post-feed.ts2
-rw-r--r--src/state/queries/post-quotes.ts2
-rw-r--r--src/state/queries/post-thread.ts11
-rw-r--r--src/state/queries/postgate/index.ts6
-rw-r--r--src/state/queries/postgate/util.ts11
-rw-r--r--src/state/queries/profile.ts74
-rw-r--r--src/state/queries/resolve-uri.ts19
-rw-r--r--src/state/queries/search-posts.ts2
-rw-r--r--src/state/queries/service-config.ts1
-rw-r--r--src/state/queries/starter-packs.ts11
-rw-r--r--src/state/queries/threadgate/index.ts6
-rw-r--r--src/state/queries/threadgate/types.ts2
-rw-r--r--src/state/queries/threadgate/util.ts21
-rw-r--r--src/state/queries/unstable-profile-cache.ts51
-rw-r--r--src/state/queries/util.ts13
-rw-r--r--src/state/shell/composer/index.tsx3
-rw-r--r--src/types/bsky/index.ts51
-rw-r--r--src/types/bsky/post.ts148
-rw-r--r--src/types/bsky/profile.ts10
-rw-r--r--src/types/bsky/starterPack.ts11
-rw-r--r--src/view/com/lists/ListMembers.tsx7
-rw-r--r--src/view/com/notifications/NotificationFeedItem.tsx18
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx2
-rw-r--r--src/view/com/post-thread/PostThreadFollowBtn.tsx5
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx6
-rw-r--r--src/view/com/post/Post.tsx4
-rw-r--r--src/view/com/posts/PostFeedItem.tsx9
-rw-r--r--src/view/com/profile/FollowButton.tsx4
-rw-r--r--src/view/com/profile/ProfileCard.tsx12
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx2
-rw-r--r--src/view/com/profile/ProfileFollows.tsx2
-rw-r--r--src/view/com/util/UserAvatar.tsx5
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx13
-rw-r--r--src/view/screens/DebugMod.tsx2
-rw-r--r--src/view/screens/Search/Search.tsx13
-rw-r--r--src/view/shell/desktop/LeftNav.tsx2
91 files changed, 801 insertions, 419 deletions
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index de94d7e19..709d0631d 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {GestureResponderEvent, View} from 'react-native'
 import {
-  AppBskyActorDefs,
   AppBskyFeedDefs,
   AppBskyGraphDefs,
   AtUri,
@@ -32,6 +31,7 @@ import {Loader} from '#/components/Loader'
 import * as Prompt from '#/components/Prompt'
 import {RichText, RichTextProps} from '#/components/RichText'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 type Props = {
   view: AppBskyFeedDefs.GeneratorView
@@ -115,7 +115,7 @@ export function TitleAndByline({
   creator,
 }: {
   title: string
-  creator?: AppBskyActorDefs.ProfileViewBasic
+  creator?: bsky.profile.AnyProfileView
 }) {
   const t = useTheme()
 
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 926d27baa..eafed25e5 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
-import {ScrollView, View} from 'react-native'
-import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
+import {View} from 'react-native'
+import {ScrollView} from 'react-native-gesture-handler'
+import {AppBskyFeedDefs, AtUri} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
@@ -26,6 +27,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
 import {InlineLinkText} from '#/components/Link'
 import * as ProfileCard from '#/components/ProfileCard'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 import {ProgressGuideList} from './ProgressGuide/List'
 
 const MOBILE_CARD_WIDTH = 300
@@ -227,7 +229,7 @@ export function ProfileGrid({
   viewContext = 'feed',
 }: {
   isSuggestionsLoading: boolean
-  profiles: AppBskyActorDefs.ProfileViewDetailed[]
+  profiles: bsky.profile.AnyProfileView[]
   recId?: number
   error: Error | null
   viewContext: 'profile' | 'feed'
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index b5c501039..1e7cf448a 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -10,6 +10,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useTheme} from '#/alf'
 import {Link, LinkProps} from '#/components/Link'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 const AVI_SIZE = 30
 const AVI_SIZE_SMALL = 20
@@ -33,7 +34,7 @@ export function KnownFollowers({
   onLinkPress,
   minimal,
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   onLinkPress?: LinkProps['onPress']
   minimal?: boolean
@@ -77,7 +78,7 @@ function KnownFollowersInner({
   onLinkPress,
   minimal,
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
   onLinkPress?: LinkProps['onPress']
diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx
index ed5838fb0..30156ee0d 100644
--- a/src/components/ListCard.tsx
+++ b/src/components/ListCard.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {View} from 'react-native'
 import {
-  AppBskyActorDefs,
   AppBskyGraphDefs,
   AtUri,
   moderateUserList,
@@ -26,6 +25,7 @@ import {
 import {Link as InternalLink, LinkProps} from '#/components/Link'
 import * as Hider from '#/components/moderation/Hider'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 /*
  * This component is based on `FeedCard` and is tightly coupled with that
@@ -107,7 +107,7 @@ export function TitleAndByline({
   modUi,
 }: {
   title: string
-  creator?: AppBskyActorDefs.ProfileViewBasic
+  creator?: bsky.profile.AnyProfileView
   purpose?: AppBskyGraphDefs.ListView['purpose']
   modUi?: ModerationUI
 }) {
diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx
index 9a05b54df..6e368e7dc 100644
--- a/src/components/MediaPreview.tsx
+++ b/src/components/MediaPreview.tsx
@@ -1,19 +1,15 @@
 import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {Image} from 'expo-image'
-import {
-  AppBskyEmbedExternal,
-  AppBskyEmbedImages,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyEmbedVideo,
-} from '@atproto/api'
+import {AppBskyFeedDefs} from '@atproto/api'
 import {Trans} from '@lingui/macro'
 
-import {parseTenorGif} from '#/lib/strings/embed-player'
+import {isTenorGifUri} from '#/lib/strings/embed-player'
 import {atoms as a, useTheme} from '#/alf'
 import {MediaInsetBorder} from '#/components/MediaInsetBorder'
 import {Text} from '#/components/Typography'
 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
+import * as bsky from '#/types/bsky'
 
 /**
  * Streamlined MediaPreview component which just handles images, gifs, and videos
@@ -22,20 +18,17 @@ export function Embed({
   embed,
   style,
 }: {
-  embed?:
-    | AppBskyEmbedImages.View
-    | AppBskyEmbedRecordWithMedia.View
-    | AppBskyEmbedExternal.View
-    | AppBskyEmbedVideo.View
-    | {[k: string]: unknown}
+  embed: AppBskyFeedDefs.PostView['embed']
   style?: StyleProp<ViewStyle>
 }) {
-  let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
+  const e = bsky.post.parseEmbed(embed)
 
-  if (AppBskyEmbedImages.isView(media)) {
+  if (!e) return null
+
+  if (e.type === 'images') {
     return (
       <Outer style={style}>
-        {media.images.map(image => (
+        {e.view.images.map(image => (
           <ImageItem
             key={image.thumb}
             thumbnail={image.thumb}
@@ -44,28 +37,21 @@ export function Embed({
         ))}
       </Outer>
     )
-  } else if (AppBskyEmbedExternal.isView(media) && media.external.thumb) {
-    let url: URL | undefined
-    try {
-      url = new URL(media.external.uri)
-    } catch {}
-    if (url) {
-      const {success} = parseTenorGif(url)
-      if (success) {
-        return (
-          <Outer style={style}>
-            <GifItem
-              thumbnail={media.external.thumb}
-              alt={media.external.title}
-            />
-          </Outer>
-        )
-      }
-    }
-  } else if (AppBskyEmbedVideo.isView(media)) {
+  } else if (e.type === 'link') {
+    if (!e.view.external.thumb) return null
+    if (!isTenorGifUri(e.view.external.uri)) return null
+    return (
+      <Outer style={style}>
+        <GifItem
+          thumbnail={e.view.external.thumb}
+          alt={e.view.external.title}
+        />
+      </Outer>
+    )
+  } else if (e.type === 'video') {
     return (
       <Outer style={style}>
-        <VideoItem thumbnail={media.thumbnail} alt={media.alt} />
+        <VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
       </Outer>
     )
   }
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index 78d86ab36..b56112dcf 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {GestureResponderEvent, View} from 'react-native'
 import {
-  AppBskyActorDefs,
   moderateProfile,
   ModerationOpts,
   RichText as RichTextApi,
@@ -25,13 +24,14 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
 import {Link as InternalLink, LinkProps} from '#/components/Link'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function Default({
   profile,
   moderationOpts,
   logContext = 'ProfileCard',
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   logContext?: 'ProfileCard' | 'StarterPackProfilesList'
 }) {
@@ -51,7 +51,7 @@ export function Card({
   moderationOpts,
   logContext = 'ProfileCard',
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   logContext?: 'ProfileCard' | 'StarterPackProfilesList'
 }) {
@@ -101,7 +101,7 @@ export function Link({
   style,
   ...rest
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
 } & Omit<LinkProps, 'to' | 'label'>) {
   const {_} = useLingui()
   return (
@@ -126,7 +126,7 @@ export function Avatar({
   profile,
   moderationOpts,
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
 }) {
   const moderation = moderateProfile(profile, moderationOpts)
@@ -161,7 +161,7 @@ export function NameAndHandle({
   profile,
   moderationOpts,
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
 }) {
   const t = useTheme()
@@ -224,17 +224,16 @@ export function Description({
   profile: profileUnshadowed,
   numberOfLines = 3,
 }: {
-  profile: AppBskyActorDefs.ProfileViewDetailed
+  profile: bsky.profile.AnyProfileView
   numberOfLines?: number
 }) {
   const profile = useProfileShadow(profileUnshadowed)
-  const {description} = profile
   const rt = React.useMemo(() => {
-    if (!description) return
-    const rt = new RichTextApi({text: description || ''})
+    if (!('description' in profile)) return
+    const rt = new RichTextApi({text: profile.description || ''})
     rt.detectFacetsWithoutResolution()
     return rt
-  }, [description])
+  }, [profile])
   if (!rt) return null
   if (
     profile.viewer &&
@@ -281,7 +280,7 @@ export function DescriptionPlaceholder({
 }
 
 export type FollowButtonProps = {
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   logContext: LogEvents['profile:follow']['logContext'] &
     LogEvents['profile:unfollow']['logContext']
diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx
index 515a9059a..6443ec694 100644
--- a/src/components/StarterPack/QrCode.tsx
+++ b/src/components/StarterPack/QrCode.tsx
@@ -13,6 +13,7 @@ import {useTheme} from '#/alf'
 import {atoms as a} from '#/alf'
 import {LinearGradientBackground} from '#/components/LinearGradientBackground'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 const LazyViewShot = React.lazy(
   // @ts-expect-error dynamic import
@@ -30,7 +31,12 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
 ) {
   const {record} = starterPack
 
-  if (!AppBskyGraphStarterpack.isRecord(record)) {
+  if (
+    !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+      record,
+      AppBskyGraphStarterpack.isRecord,
+    )
+  ) {
     return null
   }
 
diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx
index 2feea0973..43d8b72da 100644
--- a/src/components/StarterPack/QrCodeDialog.tsx
+++ b/src/components/StarterPack/QrCodeDialog.tsx
@@ -18,6 +18,7 @@ import * as Dialog from '#/components/Dialog'
 import {DialogControlProps} from '#/components/Dialog'
 import {Loader} from '#/components/Loader'
 import {QrCode} from '#/components/StarterPack/QrCode'
+import * as bsky from '#/types/bsky'
 
 export function QrCodeDialog({
   starterPack,
@@ -77,7 +78,12 @@ export function QrCodeDialog({
       } else {
         setIsProcessing(true)
 
-        if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
+        if (
+          !bsky.validate(
+            starterPack.record,
+            AppBskyGraphStarterpack.validateRecord,
+          )
+        ) {
           return
         }
 
diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx
index 2a9da509d..caa052726 100644
--- a/src/components/StarterPack/StarterPackCard.tsx
+++ b/src/components/StarterPack/StarterPackCard.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {View} from 'react-native'
 import {Image} from 'expo-image'
-import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
+import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
@@ -15,11 +15,12 @@ import {atoms as a, useTheme} from '#/alf'
 import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
 import {Link as BaseLink, LinkProps as BaseLinkProps} from '#/components/Link'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function Default({
   starterPack,
 }: {
-  starterPack?: AppBskyGraphDefs.StarterPackViewBasic
+  starterPack?: bsky.starterPack.AnyStarterPackView
 }) {
   if (!starterPack) return null
   return (
@@ -32,7 +33,7 @@ export function Default({
 export function Notification({
   starterPack,
 }: {
-  starterPack?: AppBskyGraphDefs.StarterPackViewBasic
+  starterPack?: bsky.starterPack.AnyStarterPackView
 }) {
   if (!starterPack) return null
   return (
@@ -47,7 +48,7 @@ export function Card({
   noIcon,
   noDescription,
 }: {
-  starterPack: AppBskyGraphDefs.StarterPackViewBasic
+  starterPack: bsky.starterPack.AnyStarterPackView
   noIcon?: boolean
   noDescription?: boolean
 }) {
@@ -57,7 +58,12 @@ export function Card({
   const t = useTheme()
   const {currentAccount} = useSession()
 
-  if (!AppBskyGraphStarterpack.isRecord(record)) {
+  if (
+    !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+      record,
+      AppBskyGraphStarterpack.isRecord,
+    )
+  ) {
     return null
   }
 
@@ -100,7 +106,7 @@ export function Link({
   starterPack,
   children,
 }: {
-  starterPack: AppBskyGraphDefs.StarterPackViewBasic
+  starterPack: bsky.starterPack.AnyStarterPackView
   onPress?: () => void
   children: BaseLinkProps['children']
 }) {
@@ -139,7 +145,7 @@ export function Link({
 export function Embed({
   starterPack,
 }: {
-  starterPack: AppBskyGraphDefs.StarterPackViewBasic
+  starterPack: bsky.starterPack.AnyStarterPackView
 }) {
   const t = useTheme()
   const imageUri = getStarterPackOgCard(starterPack)
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index b67a8d302..5ce298842 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -38,7 +38,7 @@ export function WizardEditListDialog({
   state: WizardState
   dispatch: (action: WizardAction) => void
   moderationOpts: ModerationOpts
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: AppBskyActorDefs.ProfileViewDetailed
 }) {
   const {_} = useLingui()
   const t = useTheme()
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index 75d2bff60..e1a70a0b7 100644
--- a/src/components/StarterPack/Wizard/WizardListCard.tsx
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -22,6 +22,7 @@ import {Button, ButtonText} from '#/components/Button'
 import * as Toggle from '#/components/forms/Toggle'
 import {Checkbox} from '#/components/forms/Toggle'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 function WizardListCard({
   type,
@@ -123,7 +124,7 @@ export function WizardProfileCard({
   btnType: 'checkbox' | 'remove'
   state: WizardState
   dispatch: (action: WizardAction) => void
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
 }) {
   const {currentAccount} = useSession()
diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx
index cad5eb234..c28adad8b 100644
--- a/src/components/VideoPostCard.tsx
+++ b/src/components/VideoPostCard.tsx
@@ -27,6 +27,7 @@ import {Link} from '#/components/Link'
 import {MediaInsetBorder} from '#/components/MediaInsetBorder'
 import * as Hider from '#/components/moderation/Hider'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 function getBlackColor(t: ReturnType<typeof useTheme>) {
   return select(t.name, {
@@ -78,7 +79,12 @@ export function VideoPostCard({
   if (!AppBskyEmbedVideo.isView(embed)) return null
 
   const author = post.author
-  const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
+  const text = bsky.dangerousIsType<AppBskyFeedPost.Record>(
+    post.record,
+    AppBskyFeedPost.isRecord,
+  )
+    ? post.record?.text
+    : ''
   const likeCount = post?.likeCount ?? 0
   const repostCount = post?.repostCount ?? 0
   const {thumbnail} = embed
diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx
index 7d74a50c6..29f4ac5bc 100644
--- a/src/components/WhoCanReply.tsx
+++ b/src/components/WhoCanReply.tsx
@@ -29,6 +29,7 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
 
 interface WhoCanReplyProps {
@@ -48,7 +49,10 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
    * unexpectedly, we should check to make sure it's for sure the root URI.
    */
   const rootUri =
-    AppBskyFeedPost.isRecord(post.record) && post.record.reply?.root
+    bsky.dangerousIsType<AppBskyFeedPost.Record>(
+      post.record,
+      AppBskyFeedPost.isRecord,
+    ) && post.record.reply?.root
       ? post.record.reply.root.uri
       : post.uri
   const settings = React.useMemo(() => {
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index f44692a2e..590f25dd3 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -1,10 +1,6 @@
 import React, {useCallback} from 'react'
 import {Keyboard, Pressable, View} from 'react-native'
-import {
-  AppBskyActorDefs,
-  ChatBskyConvoDefs,
-  ModerationCause,
-} from '@atproto/api'
+import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
@@ -34,6 +30,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'
 
@@ -49,7 +46,7 @@ let ConvoMenu = ({
   style,
 }: {
   convo: ChatBskyConvoDefs.ConvoView
-  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  profile: Shadow<bsky.profile.AnyProfileView>
   control?: Menu.MenuControlProps
   currentScreen: 'list' | 'conversation'
   showMarkAsRead?: boolean
@@ -148,7 +145,7 @@ function MenuContent({
   blockedByListControl,
 }: {
   convo: ChatBskyConvoDefs.ConvoView
-  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  profile: Shadow<bsky.profile.AnyProfileView>
   showMarkAsRead?: boolean
   blockInfo: {
     listBlocks: ModerationCause[]
diff --git a/src/components/dms/MessageProfileButton.tsx b/src/components/dms/MessageProfileButton.tsx
index 22936b4c0..5eac7f5c5 100644
--- a/src/components/dms/MessageProfileButton.tsx
+++ b/src/components/dms/MessageProfileButton.tsx
@@ -19,7 +19,7 @@ import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
 export function MessageProfileButton({
   profile,
 }: {
-  profile: AppBskyActorDefs.ProfileView
+  profile: AppBskyActorDefs.ProfileViewDetailed
 }) {
   const {_} = useLingui()
   const t = useTheme()
diff --git a/src/components/dms/MessagesListBlockedFooter.tsx b/src/components/dms/MessagesListBlockedFooter.tsx
index 19a7cc9c2..9c63ef2c7 100644
--- a/src/components/dms/MessagesListBlockedFooter.tsx
+++ b/src/components/dms/MessagesListBlockedFooter.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {View} from 'react-native'
-import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
+import {ModerationDecision} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -14,6 +14,7 @@ import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
 import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
 import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function MessagesListBlockedFooter({
   recipient: initialRecipient,
@@ -21,7 +22,7 @@ export function MessagesListBlockedFooter({
   hasMessages,
   moderation,
 }: {
-  recipient: AppBskyActorDefs.ProfileViewBasic
+  recipient: bsky.profile.AnyProfileView
   convoId: string
   hasMessages: boolean
   moderation: ModerationDecision
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index f8d9b290d..7c35c30ba 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -17,6 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {isWeb} from '#/platform/detection'
 import {Shadow} from '#/state/cache/profile-shadow'
 import {isConvoActive, useConvo} from '#/state/messages/convo'
+import {ConvoItem} from '#/state/messages/convo/types'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
 import {ConvoMenu} from '#/components/dms/ConvoMenu'
@@ -31,7 +32,7 @@ export let MessagesListHeader = ({
   profile,
   moderation,
 }: {
-  profile?: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
   moderation?: ModerationDecision
 }): React.ReactNode => {
   const t = useTheme()
@@ -138,7 +139,7 @@ function HeaderReady({
   moderation,
   blockInfo,
 }: {
-  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
   moderation: ModerationDecision
   blockInfo: {
     listBlocks: ModerationCause[]
@@ -157,8 +158,10 @@ function HeaderReady({
         moderation.ui('displayName'),
       )
 
+  // @ts-ignore findLast is polyfilled - esb
   const latestMessageFromOther = convoState.items.findLast(
-    item => item.type === 'message' && item.message.sender.did === profile.did,
+    (item: ConvoItem) =>
+      item.type === 'message' && item.message.sender.did === profile.did,
   )
 
   const latestReportableMessage =
diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx
index af24a7246..71cca897a 100644
--- a/src/components/dms/ReportDialog.tsx
+++ b/src/components/dms/ReportDialog.tsx
@@ -1,6 +1,7 @@
 import React, {memo, useMemo, useState} from 'react'
 import {View} from 'react-native'
 import {
+  $Typed,
   AppBskyActorDefs,
   ChatBskyConvoDefs,
   ComAtprotoModerationCreateReport,
@@ -154,15 +155,16 @@ function SubmitStep({
     mutationFn: async () => {
       if (params.type === 'convoMessage') {
         const {convoId, message} = params
+        const subject: $Typed<ChatBskyConvoDefs.MessageRef> = {
+          $type: 'chat.bsky.convo.defs#messageRef',
+          messageId: message.id,
+          convoId,
+          did: message.sender.did,
+        }
 
         const report = {
           reasonType: reportOption.reason,
-          subject: {
-            $type: 'chat.bsky.convo.defs#messageRef',
-            messageId: message.id,
-            convoId,
-            did: message.sender.did,
-          } satisfies ChatBskyConvoDefs.MessageRef,
+          subject,
           reason: details,
         } satisfies ComAtprotoModerationCreateReport.InputSchema
 
@@ -285,7 +287,7 @@ function DoneStep({
 }: {
   convoId: string
   currentScreen: 'list' | 'conversation'
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: AppBskyActorDefs.ProfileViewDetailed
 }) {
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx
index 9e15e2ba8..3ac0b3ab0 100644
--- a/src/components/dms/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dms/dialogs/SearchablePeopleList.tsx
@@ -6,7 +6,7 @@ import React, {
   useState,
 } from 'react'
 import {TextInput, View} from 'react-native'
-import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
+import {moderateProfile, ModerationOpts} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -28,13 +28,14 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 type Item =
   | {
       type: 'profile'
       key: string
       enabled: boolean
-      profile: AppBskyActorDefs.ProfileView
+      profile: bsky.profile.AnyProfileView
     }
   | {
       type: 'empty'
@@ -330,7 +331,7 @@ function ProfileCard({
   onPress,
 }: {
   enabled: boolean
-  profile: AppBskyActorDefs.ProfileView
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   onPress: (did: string) => void
 }) {
diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts
index 003532d0c..7315f5fc9 100644
--- a/src/components/dms/util.ts
+++ b/src/components/dms/util.ts
@@ -1,6 +1,6 @@
-import {AppBskyActorDefs} from '@atproto/api'
+import * as bsky from '#/types/bsky'
 
-export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) {
+export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
   switch (profile.associated?.chat?.allowIncoming) {
     case 'none':
       return false
diff --git a/src/components/hooks/useFollowMethods.ts b/src/components/hooks/useFollowMethods.ts
index d67c3690f..e6b3f2c47 100644
--- a/src/components/hooks/useFollowMethods.ts
+++ b/src/components/hooks/useFollowMethods.ts
@@ -1,5 +1,4 @@
 import React from 'react'
-import {AppBskyActorDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -9,12 +8,13 @@ import {Shadow} from '#/state/cache/types'
 import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useRequireAuth} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
+import * as bsky from '#/types/bsky'
 
 export function useFollowMethods({
   profile,
   logContext,
 }: {
-  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  profile: Shadow<bsky.profile.AnyProfileView>
   logContext: LogEvents['profile:follow']['logContext'] &
     LogEvents['profile:unfollow']['logContext']
 }) {
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index 4aa20fd12..a1b2e2bc9 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -6,6 +6,7 @@ import {
   AppBskyFeedPost,
 } from '@atproto/api'
 
+import * as bsky from '#/types/bsky'
 import {isPostInLanguage} from '../../locale/helpers'
 import {FALLBACK_MARKER_POST} from './feed/home'
 import {ReasonFeedSource} from './feed/types'
@@ -57,7 +58,9 @@ export class FeedViewPostsSlice {
     }
     this._feedPost = feedPost
     this._reactKey = `slice-${post.uri}-${
-      feedPost.reason?.indexedAt || post.indexedAt
+      feedPost.reason && 'indexedAt' in feedPost.reason
+        ? feedPost.reason.indexedAt
+        : post.indexedAt
     }`
     if (feedPost.post.uri === FALLBACK_MARKER_POST.post.uri) {
       this.isFallbackMarker = true
@@ -65,7 +68,7 @@ export class FeedViewPostsSlice {
     }
     if (
       !AppBskyFeedPost.isRecord(post.record) ||
-      !AppBskyFeedPost.validateRecord(post.record).success
+      !bsky.validate(post.record, AppBskyFeedPost.validateRecord)
     ) {
       return
     }
@@ -97,7 +100,7 @@ export class FeedViewPostsSlice {
     if (
       !AppBskyFeedDefs.isPostView(parent) ||
       !AppBskyFeedPost.isRecord(parent.record) ||
-      !AppBskyFeedPost.validateRecord(parent.record).success
+      !bsky.validate(parent.record, AppBskyFeedPost.validateRecord)
     ) {
       this.isOrphan = true
       return
@@ -139,7 +142,7 @@ export class FeedViewPostsSlice {
     if (
       !AppBskyFeedDefs.isPostView(root) ||
       !AppBskyFeedPost.isRecord(root.record) ||
-      !AppBskyFeedPost.validateRecord(root.record).success
+      !bsky.validate(root.record, AppBskyFeedPost.validateRecord)
     ) {
       this.isOrphan = true
       return
diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts
index 35c344055..7f8c1c275 100644
--- a/src/lib/api/feed/merge.ts
+++ b/src/lib/api/feed/merge.ts
@@ -311,6 +311,7 @@ class MergeFeedSource_Custom extends MergeFeedSource {
       )
       // attach source info
       for (const post of res.data.feed) {
+        // @ts-ignore
         post.__source = this.sourceInfo
       }
       return res
diff --git a/src/lib/api/hack-add-deleted-embed.ts b/src/lib/api/hack-add-deleted-embed.ts
deleted file mode 100644
index 59aad21a2..000000000
--- a/src/lib/api/hack-add-deleted-embed.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import {
-  AppBskyFeedDefs,
-  AppBskyFeedPost,
-  ComAtprotoRepoStrongRef,
-} from '@atproto/api'
-
-/**
- * HACK
- * The server doesnt seem to be correctly giving the notFound view yet
- * so I'm adding it manually for now
- * -prf
- */
-export function hackAddDeletedEmbed(post: AppBskyFeedDefs.PostView) {
-  const record = post.record as AppBskyFeedPost.Record
-  if (record.embed?.$type === 'app.bsky.embed.record' && !post.embed) {
-    post.embed = {
-      $type: 'app.bsky.embed.record#view',
-      record: {
-        $type: 'app.bsky.embed.record#viewNotFound',
-        uri: (record.embed.record as ComAtprotoRepoStrongRef.Main).uri,
-      },
-    }
-  }
-}
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 5cc0d6336..d1f304d4a 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -1,4 +1,5 @@
 import {
+  $Typed,
   AppBskyEmbedExternal,
   AppBskyEmbedImages,
   AppBskyEmbedRecord,
@@ -74,7 +75,7 @@ export async function post(
   }
 
   const did = agent.assertDid
-  const writes: ComAtprotoRepoApplyWrites.Create[] = []
+  const writes: $Typed<ComAtprotoRepoApplyWrites.Create>[] = []
   const uris: string[] = []
 
   let now = new Date()
@@ -91,7 +92,7 @@ export async function post(
       draft,
       opts.onStateChange,
     )
-    let labels: ComAtprotoLabelDefs.SelfLabels | undefined
+    let labels: $Typed<ComAtprotoLabelDefs.SelfLabels> | undefined
     if (draft.labels.length) {
       labels = {
         $type: 'com.atproto.label.defs#selfLabels',
@@ -230,11 +231,11 @@ async function resolveEmbed(
   draft: PostDraft,
   onStateChange: ((state: string) => void) | undefined,
 ): Promise<
-  | AppBskyEmbedImages.Main
-  | AppBskyEmbedVideo.Main
-  | AppBskyEmbedExternal.Main
-  | AppBskyEmbedRecord.Main
-  | AppBskyEmbedRecordWithMedia.Main
+  | $Typed<AppBskyEmbedImages.Main>
+  | $Typed<AppBskyEmbedVideo.Main>
+  | $Typed<AppBskyEmbedExternal.Main>
+  | $Typed<AppBskyEmbedRecord.Main>
+  | $Typed<AppBskyEmbedRecordWithMedia.Main>
   | undefined
 > {
   if (draft.embed.quote) {
@@ -288,9 +289,9 @@ async function resolveMedia(
   embedDraft: EmbedDraft,
   onStateChange: ((state: string) => void) | undefined,
 ): Promise<
-  | AppBskyEmbedExternal.Main
-  | AppBskyEmbedImages.Main
-  | AppBskyEmbedVideo.Main
+  | $Typed<AppBskyEmbedExternal.Main>
+  | $Typed<AppBskyEmbedImages.Main>
+  | $Typed<AppBskyEmbedVideo.Main>
   | undefined
 > {
   if (embedDraft.media?.type === 'images') {
diff --git a/src/lib/embeds.ts b/src/lib/embeds.ts
deleted file mode 100644
index 2904f1cc3..000000000
--- a/src/lib/embeds.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import {
-  AppBskyEmbedRecord,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyFeedDefs,
-} from '@atproto/api'
-
-export function isEmbedByEmbedder(
-  embed: AppBskyFeedDefs.PostView['embed'],
-  did: string,
-): boolean {
-  if (!embed) {
-    return false
-  }
-  if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
-    return embed.record.author.did === did
-  }
-  if (
-    AppBskyEmbedRecordWithMedia.isView(embed) &&
-    AppBskyEmbedRecord.isViewRecord(embed.record.record)
-  ) {
-    return embed.record.record.author.did === did
-  }
-  return true
-}
diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts
index 3be338ac8..11e334329 100644
--- a/src/lib/generate-starterpack.ts
+++ b/src/lib/generate-starterpack.ts
@@ -1,7 +1,9 @@
 import {
+  $Typed,
   AppBskyActorDefs,
   AppBskyGraphGetStarterPack,
   BskyAgent,
+  ComAtprotoRepoApplyWrites,
   Facet,
 } from '@atproto/api'
 import {msg} from '@lingui/macro'
@@ -13,6 +15,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {enforceLen} from '#/lib/strings/helpers'
 import {useAgent} from '#/state/session'
+import * as bsky from '#/types/bsky'
 
 export const createStarterPackList = async ({
   name,
@@ -24,7 +27,7 @@ export const createStarterPackList = async ({
   name: string
   description?: string
   descriptionFacets?: Facet[]
-  profiles: AppBskyActorDefs.ProfileViewBasic[]
+  profiles: bsky.profile.AnyProfileView[]
   agent: BskyAgent
 }): Promise<{uri: string; cid: string}> => {
   if (profiles.length === 0) throw new Error('No profiles given')
@@ -68,8 +71,8 @@ export function useGenerateStarterPackMutation({
 
   return useMutation<{uri: string; cid: string}, Error, void>({
     mutationFn: async () => {
-      let profile: AppBskyActorDefs.ProfileViewBasic | undefined
-      let profiles: AppBskyActorDefs.ProfileViewBasic[] | undefined
+      let profile: AppBskyActorDefs.ProfileViewDetailed | undefined
+      let profiles: AppBskyActorDefs.ProfileView[] | undefined
 
       await Promise.all([
         (async () => {
@@ -136,7 +139,13 @@ export function useGenerateStarterPackMutation({
   })
 }
 
-function createListItem({did, listUri}: {did: string; listUri: string}) {
+function createListItem({
+  did,
+  listUri,
+}: {
+  did: string
+  listUri: string
+}): $Typed<ComAtprotoRepoApplyWrites.Create> {
   return {
     $type: 'com.atproto.repo.applyWrites#create',
     collection: 'app.bsky.graph.listitem',
diff --git a/src/lib/moderation/blocked-and-muted.ts b/src/lib/moderation/blocked-and-muted.ts
index 18e6ef3e3..27c461a3d 100644
--- a/src/lib/moderation/blocked-and-muted.ts
+++ b/src/lib/moderation/blocked-and-muted.ts
@@ -1,17 +1,9 @@
-import {AppBskyActorDefs} from '@atproto/api'
+import * as bsky from '#/types/bsky'
 
-export function isBlockedOrBlocking(
-  profile:
-    | AppBskyActorDefs.ProfileViewBasic
-    | AppBskyActorDefs.ProfileViewDetailed,
-) {
+export function isBlockedOrBlocking(profile: bsky.profile.AnyProfileView) {
   return profile.viewer?.blockedBy || profile.viewer?.blocking
 }
 
-export function isMuted(
-  profile:
-    | AppBskyActorDefs.ProfileViewBasic
-    | AppBskyActorDefs.ProfileViewDetailed,
-) {
+export function isMuted(profile: bsky.profile.AnyProfileView) {
   return profile.viewer?.muted || profile.viewer?.mutedByList
 }
diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts
index 9ee5128c8..0b3073b95 100644
--- a/src/lib/strings/embed-player.ts
+++ b/src/lib/strings/embed-player.ts
@@ -568,3 +568,12 @@ export function parseTenorGif(urlp: URL):
     dimensions,
   }
 }
+
+export function isTenorGifUri(url: URL | string) {
+  try {
+    return parseTenorGif(typeof url === 'string' ? new URL(url) : url).success
+  } catch {
+    // Invalid URL
+    return false
+  }
+}
diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts
index ca3410015..ced947b59 100644
--- a/src/lib/strings/starter-pack.ts
+++ b/src/lib/strings/starter-pack.ts
@@ -1,4 +1,6 @@
-import {AppBskyGraphDefs, AtUri} from '@atproto/api'
+import {AtUri} from '@atproto/api'
+
+import * as bsky from '#/types/bsky'
 
 export function createStarterPackLinkFromAndroidReferrer(
   referrerQueryString: string,
@@ -79,7 +81,7 @@ export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
 }
 
 export function getStarterPackOgCard(
-  didOrStarterPack: AppBskyGraphDefs.StarterPackView | string,
+  didOrStarterPack: bsky.starterPack.AnyStarterPackView | string,
   rkey?: string,
 ) {
   if (typeof didOrStarterPack === 'string') {
diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx
index f51822952..69af0ea58 100644
--- a/src/screens/Messages/Conversation.tsx
+++ b/src/screens/Messages/Conversation.tsx
@@ -165,7 +165,7 @@ function InnerReady({
   setHasScrolled,
 }: {
   moderation: ModerationDecision
-  recipient: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed>
   hasScrolled: boolean
   setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
 }) {
diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx
index ad97497f4..501ab2374 100644
--- a/src/screens/Messages/components/ChatListItem.tsx
+++ b/src/screens/Messages/components/ChatListItem.tsx
@@ -1,7 +1,6 @@
 import React, {useCallback, useMemo, useState} from 'react'
 import {GestureResponderEvent, View} from 'react-native'
 import {
-  AppBskyActorDefs,
   AppBskyEmbedRecord,
   ChatBskyConvoDefs,
   moderateProfile,
@@ -44,6 +43,7 @@ import {Link} from '#/components/Link'
 import {useMenuControl} from '#/components/Menu'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export let ChatListItem = ({
   convo,
@@ -78,7 +78,7 @@ function ChatListItemReady({
   moderationOpts,
 }: {
   convo: ChatBskyConvoDefs.ConvoView
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
 }) {
   const t = useTheme()
diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx
index 6df0ef2fc..d368f05b6 100644
--- a/src/screens/Messages/components/MessageInputEmbed.tsx
+++ b/src/screens/Messages/components/MessageInputEmbed.tsx
@@ -30,6 +30,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function useMessageEmbed() {
   const route =
@@ -113,8 +114,10 @@ export function MessageInputEmbed({
   const {rt, record} = useMemo(() => {
     if (
       post &&
-      AppBskyFeedPost.isRecord(post.record) &&
-      AppBskyFeedPost.validateRecord(post.record).success
+      bsky.dangerousIsType<AppBskyFeedPost.Record>(
+        post.record,
+        AppBskyFeedPost.isRecord,
+      )
     ) {
       return {
         rt: new RichTextAPI({
diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx
index 071ce1cd7..10a2b1d37 100644
--- a/src/screens/Messages/components/MessagesList.tsx
+++ b/src/screens/Messages/components/MessagesList.tsx
@@ -10,7 +10,12 @@ import Animated, {
 } from 'react-native-reanimated'
 import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
+import {
+  $Typed,
+  AppBskyEmbedRecord,
+  AppBskyRichtextFacet,
+  RichText,
+} from '@atproto/api'
 
 import {clamp} from '#/lib/numbers'
 import {ScrollProvider} from '#/lib/ScrollContext'
@@ -297,7 +302,7 @@ export function MessagesList({
       // we want to remove the post link from the text, re-trim, then detect facets
       rt.detectFacetsWithoutResolution()
 
-      let embed: AppBskyEmbedRecord.Main | undefined
+      let embed: $Typed<AppBskyEmbedRecord.Main> | undefined
 
       if (embedUri) {
         try {
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index 33d27cf66..d0b0cacca 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -1,6 +1,11 @@
 import React from 'react'
 import {View} from 'react-native'
-import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
+import {
+  AppBskyActorProfile,
+  AppBskyGraphDefs,
+  AppBskyGraphStarterpack,
+  Un$Typed,
+} from '@atproto/api'
 import {SavedFeed} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
 import {TID} from '@atproto/common-web'
 import {msg, Trans} from '@lingui/macro'
@@ -44,6 +49,7 @@ import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2'
 import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function StepFinished() {
   const {_} = useLingui()
@@ -141,29 +147,29 @@ export function StepFinished() {
               : undefined
 
           await agent.upsertProfile(async existing => {
-            existing = existing ?? {}
+            let next: Un$Typed<AppBskyActorProfile.Record> = existing ?? {}
 
             if (blobPromise) {
               const res = await blobPromise
               if (res.data.blob) {
-                existing.avatar = res.data.blob
+                next.avatar = res.data.blob
               }
             }
 
             if (starterPack) {
-              existing.joinedViaStarterPack = {
+              next.joinedViaStarterPack = {
                 uri: starterPack.uri,
                 cid: starterPack.cid,
               }
             }
 
-            existing.displayName = ''
+            next.displayName = ''
             // HACKFIX
             // creating a bunch of identical profile objects is breaking the relay
             // tossing this unspecced field onto it to reduce the size of the problem
             // -prf
-            existing.createdAt = new Date().toISOString()
-            return existing
+            next.createdAt = new Date().toISOString()
+            return next
           })
 
           logEvent('onboarding:finished:avatarResult', {
@@ -205,9 +211,14 @@ export function StepFinished() {
     onboardDispatch({type: 'finish'})
     logEvent('onboarding:finished:nextPressed', {
       usedStarterPack: Boolean(starterPack),
-      starterPackName: AppBskyGraphStarterpack.isRecord(starterPack?.record)
-        ? starterPack.record.name
-        : undefined,
+      starterPackName:
+        starterPack &&
+        bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+          starterPack.record,
+          AppBskyGraphStarterpack.isRecord,
+        )
+          ? starterPack.record.name
+          : undefined,
       starterPackCreator: starterPack?.creator.did,
       starterPackUri: starterPack?.uri,
       profilesFollowed: listItems?.length ?? 0,
diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts
index 14750f34c..d14c9562e 100644
--- a/src/screens/Onboarding/util.ts
+++ b/src/screens/Onboarding/util.ts
@@ -1,7 +1,9 @@
 import {
+  $Typed,
   AppBskyGraphFollow,
   AppBskyGraphGetFollows,
   BskyAgent,
+  ComAtprotoRepoApplyWrites,
 } from '@atproto/api'
 import {TID} from '@atproto/common-web'
 import chunk from 'lodash.chunk'
@@ -15,7 +17,7 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
     throw new Error(`bulkWriteFollows failed: no session`)
   }
 
-  const followRecords: AppBskyGraphFollow.Record[] = dids.map(did => {
+  const followRecords: $Typed<AppBskyGraphFollow.Record>[] = dids.map(did => {
     return {
       $type: 'app.bsky.graph.follow',
       subject: did,
@@ -23,12 +25,13 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
     }
   })
 
-  const followWrites = followRecords.map(r => ({
-    $type: 'com.atproto.repo.applyWrites#create',
-    collection: 'app.bsky.graph.follow',
-    rkey: TID.nextStr(),
-    value: r,
-  }))
+  const followWrites: $Typed<ComAtprotoRepoApplyWrites.Create>[] =
+    followRecords.map(r => ({
+      $type: 'com.atproto.repo.applyWrites#create',
+      collection: 'app.bsky.graph.follow',
+      rkey: TID.nextStr(),
+      value: r,
+    }))
 
   const chunks = chunk(followWrites, 50)
   for (const chunk of chunks) {
diff --git a/src/screens/Profile/KnownFollowers.tsx b/src/screens/Profile/KnownFollowers.tsx
index d6dd15c69..6b22a0add 100644
--- a/src/screens/Profile/KnownFollowers.tsx
+++ b/src/screens/Profile/KnownFollowers.tsx
@@ -21,7 +21,7 @@ function renderItem({
   item,
   index,
 }: {
-  item: AppBskyActorDefs.ProfileViewBasic
+  item: AppBskyActorDefs.ProfileView
   index: number
 }) {
   return (
diff --git a/src/screens/Settings/components/PwiOptOut.tsx b/src/screens/Settings/components/PwiOptOut.tsx
index 4339ade9b..e58514976 100644
--- a/src/screens/Settings/components/PwiOptOut.tsx
+++ b/src/screens/Settings/components/PwiOptOut.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {View} from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
+import {$Typed, ComAtprotoLabelDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -12,6 +12,7 @@ import {useSession} from '#/state/session'
 import {atoms as a, useTheme} from '#/alf'
 import * as Toggle from '#/components/forms/Toggle'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function PwiOptOut() {
   const t = useTheme()
@@ -33,7 +34,10 @@ export function PwiOptOut() {
       profile,
       updates: existing => {
         // create labels attr if needed
-        existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels)
+        const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = bsky.validate(
+          existing.labels,
+          ComAtprotoLabelDefs.validateSelfLabels,
+        )
           ? existing.labels
           : {
               $type: 'com.atproto.label.defs#selfLabels',
@@ -41,23 +45,26 @@ export function PwiOptOut() {
             }
 
         // toggle the label
-        const hasLabel = existing.labels.values.some(
+        const hasLabel = labels.values.some(
           l => l.val === '!no-unauthenticated',
         )
         if (hasLabel) {
           wasAdded = false
-          existing.labels.values = existing.labels.values.filter(
+          labels.values = labels.values.filter(
             l => l.val !== '!no-unauthenticated',
           )
         } else {
           wasAdded = true
-          existing.labels.values.push({val: '!no-unauthenticated'})
+          labels.values.push({val: '!no-unauthenticated'})
         }
 
         // delete if no longer needed
-        if (existing.labels.values.length === 0) {
+        if (labels.values.length === 0) {
           delete existing.labels
+        } else {
+          existing.labels = labels
         }
+
         return existing
       },
       checkCommitted: res => {
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index 5f406eb7a..e82d0da1c 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -26,6 +26,7 @@ import {Divider} from '#/components/Divider'
 import {LinearGradientBackground} from '#/components/LinearGradientBackground'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 export function Signup({onPressBack}: {onPressBack: () => void}) {
   const {_} = useLingui()
@@ -95,7 +96,10 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
         scrollable>
         <View testID="createAccount" style={a.flex_1}>
           {showStarterPackCard &&
-          AppBskyGraphStarterpack.isRecord(starterPack.record) ? (
+          bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+            starterPack.record,
+            AppBskyGraphStarterpack.isRecord,
+          ) ? (
             <Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}>
               <LinearGradientBackground
                 style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}>
diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx
index ec31fc21d..2d9a91969 100644
--- a/src/screens/StarterPack/StarterPackLandingScreen.tsx
+++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx
@@ -38,6 +38,7 @@ import {Default as ProfileCard} from '#/components/ProfileCard'
 import * as Prompt from '#/components/Prompt'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
 
@@ -85,7 +86,12 @@ export function LandingScreen({
   }
 
   // Just for types, this cannot be hit
-  if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
+  if (
+    !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+      starterPack.record,
+      AppBskyGraphStarterpack.isRecord,
+    )
+  ) {
     return null
   }
 
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 3a3e4234f..ac61c153b 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -66,6 +66,7 @@ import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
 import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
 import {ShareDialog} from '#/components/StarterPack/ShareDialog'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 type StarterPackScreeProps = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -387,7 +388,12 @@ function Header({
     })
   }
 
-  if (!AppBskyGraphStarterpack.isRecord(record)) {
+  if (
+    !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
+      record,
+      AppBskyGraphStarterpack.isRecord,
+    )
+  ) {
     return null
   }
 
diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx
index f65933fbb..baf0195d8 100644
--- a/src/screens/StarterPack/Wizard/State.tsx
+++ b/src/screens/StarterPack/Wizard/State.tsx
@@ -1,15 +1,12 @@
 import React from 'react'
-import {
-  AppBskyActorDefs,
-  AppBskyGraphDefs,
-  AppBskyGraphStarterpack,
-} from '@atproto/api'
+import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
 import {msg} from '@lingui/macro'
 
 import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
 import {useSession} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
+import * as bsky from '#/types/bsky'
 
 const steps = ['Details', 'Profiles', 'Feeds'] as const
 type Step = (typeof steps)[number]
@@ -20,7 +17,7 @@ type Action =
   | {type: 'SetCanNext'; canNext: boolean}
   | {type: 'SetName'; name: string}
   | {type: 'SetDescription'; description: string}
-  | {type: 'AddProfile'; profile: AppBskyActorDefs.ProfileViewBasic}
+  | {type: 'AddProfile'; profile: bsky.profile.AnyProfileView}
   | {type: 'RemoveProfile'; profileDid: string}
   | {type: 'AddFeed'; feed: GeneratorView}
   | {type: 'RemoveFeed'; feedUri: string}
@@ -32,7 +29,7 @@ interface State {
   currentStep: Step
   name?: string
   description?: string
-  profiles: AppBskyActorDefs.ProfileViewBasic[]
+  profiles: bsky.profile.AnyProfileView[]
   feeds: GeneratorView[]
   processing: boolean
   error?: string
@@ -113,7 +110,6 @@ function reducer(state: State, action: Action): State {
   return updatedState
 }
 
-// TODO supply the initial state to this component
 export function Provider({
   starterPack,
   listItems,
@@ -126,7 +122,10 @@ export function Provider({
   const {currentAccount} = useSession()
 
   const createInitialState = (): State => {
-    if (starterPack && AppBskyGraphStarterpack.isRecord(starterPack.record)) {
+    if (
+      starterPack &&
+      bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
+    ) {
       return {
         canNext: true,
         currentStep: 'Details',
diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx
index e13febc75..8a9a891e1 100644
--- a/src/screens/StarterPack/Wizard/StepProfiles.tsx
+++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx
@@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader'
 import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
 import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 
 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
   return item?.did ?? ''
@@ -50,7 +51,7 @@ export function StepProfiles({
 
   const renderItem = ({
     item,
-  }: ListRenderItemInfo<AppBskyActorDefs.ProfileViewBasic>) => {
+  }: ListRenderItemInfo<bsky.profile.AnyProfileView>) => {
     return (
       <WizardProfileCard
         profile={item}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 3f8ce3c00..92cad2f65 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -54,6 +54,7 @@ import {ListMaybePlaceholder} from '#/components/Lists'
 import {Loader} from '#/components/Loader'
 import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 import {Provider} from './State'
 
 export function Wizard({
@@ -141,7 +142,7 @@ function WizardInner({
 }: {
   currentStarterPack?: AppBskyGraphDefs.StarterPackView
   currentListItems?: AppBskyGraphDefs.ListItemView[]
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: AppBskyActorDefs.ProfileViewDetailed
   moderationOpts: ModerationOpts
 }) {
   const navigation = useNavigation<NavigationProp>()
@@ -363,7 +364,7 @@ function Footer({
   onNext: () => void
   nextBtnText: string
   moderationOpts: ModerationOpts
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: AppBskyActorDefs.ProfileViewDetailed
 }) {
   const {_} = useLingui()
   const t = useTheme()
@@ -577,10 +578,10 @@ function Footer({
   )
 }
 
-function getName(item: AppBskyActorDefs.ProfileViewBasic | GeneratorView) {
+function getName(item: bsky.profile.AnyProfileView | GeneratorView) {
   if (typeof item.displayName === 'string') {
     return enforceLen(sanitizeDisplayName(item.displayName), 28, true)
-  } else if (typeof item.handle === 'string') {
+  } else if ('handle' in item && typeof item.handle === 'string') {
     return enforceLen(sanitizeHandle(item.handle), 28, true)
   }
   return ''
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index 8198d45a3..04c2d7792 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -90,6 +90,7 @@ import {ListFooter} from '#/components/Lists'
 import * as Hider from '#/components/moderation/Hider'
 import {RichText} from '#/components/RichText'
 import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
 import {Scrubber, VIDEO_PLAYER_BOTTOM_INSET} from './components/Scrubber'
 
 function createThreeVideoPlayers(
@@ -694,7 +695,12 @@ function Overlay({
   )
 
   const rkey = new AtUri(post.uri).rkey
-  const record = AppBskyFeedPost.isRecord(post.record) ? post.record : undefined
+  const record = bsky.dangerousIsType<AppBskyFeedPost.Record>(
+    post.record,
+    AppBskyFeedPost.isRecord,
+  )
+    ? post.record
+    : undefined
   const richText = new RichTextAPI({
     text: record?.text || '',
     facets: record?.facets,
diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts
index 4d823ec8e..adbff3919 100644
--- a/src/state/cache/profile-shadow.ts
+++ b/src/state/cache/profile-shadow.ts
@@ -1,9 +1,9 @@
 import {useEffect, useMemo, useState} from 'react'
-import {AppBskyActorDefs} from '@atproto/api'
 import {QueryClient} from '@tanstack/react-query'
 import EventEmitter from 'eventemitter3'
 
 import {batchedUpdates} from '#/lib/batchedUpdates'
+import * as bsky from '#/types/bsky'
 import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '../queries/actor-search'
 import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '../queries/known-followers'
 import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members'
@@ -20,6 +20,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData
 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '../queries/profile-follows'
 import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '../queries/suggested-follows'
 import {castAsShadow, Shadow} from './types'
+
 export type {Shadow} from './types'
 
 export interface ProfileShadow {
@@ -29,13 +30,13 @@ export interface ProfileShadow {
 }
 
 const shadows: WeakMap<
-  AppBskyActorDefs.ProfileView,
+  bsky.profile.AnyProfileView,
   Partial<ProfileShadow>
 > = new WeakMap()
 const emitter = new EventEmitter()
 
 export function useProfileShadow<
-  TProfileView extends AppBskyActorDefs.ProfileView,
+  TProfileView extends bsky.profile.AnyProfileView,
 >(profile: TProfileView): Shadow<TProfileView> {
   const [shadow, setShadow] = useState(() => shadows.get(profile))
   const [prevPost, setPrevPost] = useState(profile)
@@ -68,7 +69,7 @@ export function useProfileShadow<
  * This is useful for when the profile is not guaranteed to be loaded yet.
  */
 export function useMaybeProfileShadow<
-  TProfileView extends AppBskyActorDefs.ProfileView,
+  TProfileView extends bsky.profile.AnyProfileView,
 >(profile?: TProfileView): Shadow<TProfileView> | undefined {
   const [shadow, setShadow] = useState(() =>
     profile ? shadows.get(profile) : undefined,
@@ -115,7 +116,7 @@ export function updateProfileShadow(
   })
 }
 
-function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
+function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
   profile: TProfileView,
   shadow: Partial<ProfileShadow>,
 ): Shadow<TProfileView> {
@@ -137,7 +138,7 @@ function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
 function* findProfilesInCache(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileView, void> {
+): Generator<bsky.profile.AnyProfileView, void> {
   yield* findAllProfilesInListMembersQueryData(queryClient, did)
   yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
   yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx
index dc5104c14..4492977f2 100644
--- a/src/state/cache/thread-mutes.tsx
+++ b/src/state/cache/thread-mutes.tsx
@@ -69,6 +69,7 @@ function useMigrateMutes(setThreadMute: SetStateContext) {
         while (!cancelled) {
           const threads = persisted.get('mutedThreads')
 
+          // @ts-ignore findLast is polyfilled - esb
           const root = threads.findLast(uri => uri.includes(currentAccount.did))
 
           if (!root) break
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts
index 91dd59813..eed44c757 100644
--- a/src/state/messages/convo/agent.ts
+++ b/src/state/messages/convo/agent.ts
@@ -1,6 +1,6 @@
 import {
-  AppBskyActorDefs,
   BskyAgent,
+  ChatBskyActorDefs,
   ChatBskyConvoDefs,
   ChatBskyConvoGetLog,
   ChatBskyConvoSendMessage,
@@ -80,8 +80,8 @@ export class Convo {
 
   convoId: string
   convo: ChatBskyConvoDefs.ConvoView | undefined
-  sender: AppBskyActorDefs.ProfileViewBasic | undefined
-  recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined
+  sender: ChatBskyActorDefs.ProfileViewBasic | undefined
+  recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
   snapshot: ConvoState | undefined
 
   constructor(params: ConvoParams) {
@@ -463,7 +463,7 @@ export class Convo {
         throw new Error('Convo: could not find recipients in convo')
       }
 
-      const userIsDisabled = this.sender.chatDisabled as boolean
+      const userIsDisabled = Boolean(this.sender.chatDisabled)
 
       if (userIsDisabled) {
         this.dispatch({event: ConvoDispatchEvent.Disable})
@@ -529,8 +529,8 @@ export class Convo {
   private pendingFetchConvo:
     | Promise<{
         convo: ChatBskyConvoDefs.ConvoView
-        sender: AppBskyActorDefs.ProfileViewBasic | undefined
-        recipients: AppBskyActorDefs.ProfileViewBasic[]
+        sender: ChatBskyActorDefs.ProfileViewBasic | undefined
+        recipients: ChatBskyActorDefs.ProfileViewBasic[]
       }>
     | undefined
   async fetchConvo() {
@@ -538,8 +538,8 @@ export class Convo {
 
     this.pendingFetchConvo = new Promise<{
       convo: ChatBskyConvoDefs.ConvoView
-      sender: AppBskyActorDefs.ProfileViewBasic | undefined
-      recipients: AppBskyActorDefs.ProfileViewBasic[]
+      sender: ChatBskyActorDefs.ProfileViewBasic | undefined
+      recipients: ChatBskyActorDefs.ProfileViewBasic[]
     }>(async (resolve, reject) => {
       try {
         const response = await networkRetry(2, () => {
@@ -704,7 +704,7 @@ export class Convo {
        * If there's a rev, we should handle it. If there's not a rev, we don't
        * know what it is.
        */
-      if (typeof ev.rev === 'string') {
+      if ('rev' in ev && typeof ev.rev === 'string') {
         const isUninitialized = !this.latestRev
         const isNewEvent = this.latestRev && ev.rev > this.latestRev
 
@@ -1049,7 +1049,10 @@ export class Convo {
            * `getItems` is only run in "active" status states, where
            * `this.sender` is defined
            */
-          sender: this.sender!,
+          sender: {
+            $type: 'chat.bsky.convo.defs#messageViewSender',
+            did: this.sender!.did,
+          },
         },
         nextMessage: null,
         prevMessage: null,
diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts
index 9f1707c71..69e15acc4 100644
--- a/src/state/messages/convo/types.ts
+++ b/src/state/messages/convo/types.ts
@@ -1,6 +1,6 @@
 import {
-  AppBskyActorDefs,
   BskyAgent,
+  ChatBskyActorDefs,
   ChatBskyConvoDefs,
   ChatBskyConvoSendMessage,
 } from '@atproto/api'
@@ -147,8 +147,8 @@ export type ConvoStateUninitialized = {
   items: []
   convo: ChatBskyConvoDefs.ConvoView | undefined
   error: undefined
-  sender: AppBskyActorDefs.ProfileViewBasic | undefined
-  recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined
+  sender: ChatBskyActorDefs.ProfileViewBasic | undefined
+  recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
   isFetchingHistory: false
   deleteMessage: undefined
   sendMessage: undefined
@@ -159,8 +159,8 @@ export type ConvoStateInitializing = {
   items: []
   convo: ChatBskyConvoDefs.ConvoView | undefined
   error: undefined
-  sender: AppBskyActorDefs.ProfileViewBasic | undefined
-  recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined
+  sender: ChatBskyActorDefs.ProfileViewBasic | undefined
+  recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
   isFetchingHistory: boolean
   deleteMessage: undefined
   sendMessage: undefined
@@ -171,8 +171,8 @@ export type ConvoStateReady = {
   items: ConvoItem[]
   convo: ChatBskyConvoDefs.ConvoView
   error: undefined
-  sender: AppBskyActorDefs.ProfileViewBasic
-  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  sender: ChatBskyActorDefs.ProfileViewBasic
+  recipients: ChatBskyActorDefs.ProfileViewBasic[]
   isFetchingHistory: boolean
   deleteMessage: DeleteMessage
   sendMessage: SendMessage
@@ -183,8 +183,8 @@ export type ConvoStateBackgrounded = {
   items: ConvoItem[]
   convo: ChatBskyConvoDefs.ConvoView
   error: undefined
-  sender: AppBskyActorDefs.ProfileViewBasic
-  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  sender: ChatBskyActorDefs.ProfileViewBasic
+  recipients: ChatBskyActorDefs.ProfileViewBasic[]
   isFetchingHistory: boolean
   deleteMessage: DeleteMessage
   sendMessage: SendMessage
@@ -195,8 +195,8 @@ export type ConvoStateSuspended = {
   items: ConvoItem[]
   convo: ChatBskyConvoDefs.ConvoView
   error: undefined
-  sender: AppBskyActorDefs.ProfileViewBasic
-  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  sender: ChatBskyActorDefs.ProfileViewBasic
+  recipients: ChatBskyActorDefs.ProfileViewBasic[]
   isFetchingHistory: boolean
   deleteMessage: DeleteMessage
   sendMessage: SendMessage
@@ -219,8 +219,8 @@ export type ConvoStateDisabled = {
   items: ConvoItem[]
   convo: ChatBskyConvoDefs.ConvoView
   error: undefined
-  sender: AppBskyActorDefs.ProfileViewBasic
-  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  sender: ChatBskyActorDefs.ProfileViewBasic
+  recipients: ChatBskyActorDefs.ProfileViewBasic[]
   isFetchingHistory: boolean
   deleteMessage: DeleteMessage
   sendMessage: SendMessage
diff --git a/src/state/messages/events/agent.ts b/src/state/messages/events/agent.ts
index 01165256a..9244a4fa5 100644
--- a/src/state/messages/events/agent.ts
+++ b/src/state/messages/events/agent.ts
@@ -65,10 +65,7 @@ export class MessagesEventBus {
     const handle = (event: MessagesEventBusEvent) => {
       if (event.type === 'logs' && options.convoId) {
         const filteredLogs = event.logs.filter(log => {
-          if (
-            typeof log.convoId === 'string' &&
-            log.convoId === options.convoId
-          ) {
+          if ('convoId' in log && log.convoId === options.convoId) {
             return log.convoId === options.convoId
           }
           return false
@@ -355,7 +352,7 @@ export class MessagesEventBus {
          * If there's a rev, we should handle it. If there's not a rev, we don't
          * know what it is.
          */
-        if (typeof ev.rev === 'string') {
+        if ('rev' in ev && typeof ev.rev === 'string') {
           /*
            * We only care about new events
            */
diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts
index be7542880..260a0bf2c 100644
--- a/src/state/queries/list.ts
+++ b/src/state/queries/list.ts
@@ -1,11 +1,14 @@
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {
+  $Typed,
   AppBskyGraphDefs,
   AppBskyGraphGetList,
   AppBskyGraphList,
   AtUri,
   BskyAgent,
+  ComAtprotoRepoApplyWrites,
   Facet,
+  Un$Typed,
 } from '@atproto/api'
 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
 import chunk from 'lodash.chunk'
@@ -68,7 +71,7 @@ export function useListCreateMutation() {
         ) {
           throw new Error('Invalid list purpose: must be curatelist or modlist')
         }
-        const record: AppBskyGraphList.Record = {
+        const record: Un$Typed<AppBskyGraphList.Record> = {
           purpose,
           name,
           description,
@@ -212,7 +215,9 @@ export function useListDeleteMutation() {
       }
 
       // batch delete the list and listitem records
-      const createDel = (uri: string) => {
+      const createDel = (
+        uri: string,
+      ): $Typed<ComAtprotoRepoApplyWrites.Delete> => {
         const urip = new AtUri(uri)
         return {
           $type: 'com.atproto.repo.applyWrites#delete',
diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts
index 828b85d9e..34fb10935 100644
--- a/src/state/queries/messages/actor-declaration.ts
+++ b/src/state/queries/messages/actor-declaration.ts
@@ -69,12 +69,10 @@ export function useDeleteActorDeclaration() {
   return useMutation({
     mutationFn: async () => {
       if (!currentAccount) throw new Error('Not signed in')
-      // TODO(sam): remove validate: false once PDSes have the new lexicon
       const result = await agent.api.com.atproto.repo.deleteRecord({
         repo: currentAccount.did,
         collection: 'chat.bsky.actor.declaration',
         rkey: 'self',
-        validate: false,
       })
       return result
     },
diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx
index ae379f962..8c9d6c429 100644
--- a/src/state/queries/messages/list-conversations.tsx
+++ b/src/state/queries/messages/list-conversations.tsx
@@ -101,7 +101,7 @@ export function ListConvosProviderInner({
       events => {
         if (events.type !== 'logs') return
 
-        events.logs.forEach(log => {
+        for (const log of events.logs) {
           if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
             debouncedRefetch()
           } else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
@@ -110,30 +110,40 @@ export function ListConvosProviderInner({
             )
           } else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) {
             queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) =>
-              optimisticUpdate(log.convoId, old, convo =>
-                log.message.id === convo.lastMessage?.id
-                  ? {
-                      ...convo,
-                      rev: log.rev,
-                      lastMessage: log.message,
-                    }
-                  : convo,
-              ),
+              optimisticUpdate(log.convoId, old, convo => {
+                if (
+                  (ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
+                    ChatBskyConvoDefs.isMessageView(log.message)) &&
+                  (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) ||
+                    ChatBskyConvoDefs.isMessageView(convo.lastMessage))
+                ) {
+                  return log.message.id === convo.lastMessage.id
+                    ? {
+                        ...convo,
+                        rev: log.rev,
+                        lastMessage: log.message,
+                      }
+                    : convo
+                } else {
+                  return convo
+                }
+              }),
             )
           } else if (ChatBskyConvoDefs.isLogCreateMessage(log)) {
+            // Store in a new var to avoid TS errors due to closures.
+            const logRef: ChatBskyConvoDefs.LogCreateMessage = log
+
             queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => {
               if (!old) return old
 
               function updateConvo(convo: ChatBskyConvoDefs.ConvoView) {
-                if (!ChatBskyConvoDefs.isLogCreateMessage(log)) return convo
-
                 let unreadCount = convo.unreadCount
                 if (convo.id !== currentConvoId) {
                   if (
-                    ChatBskyConvoDefs.isMessageView(log.message) ||
-                    ChatBskyConvoDefs.isDeletedMessageView(log.message)
+                    ChatBskyConvoDefs.isMessageView(logRef.message) ||
+                    ChatBskyConvoDefs.isDeletedMessageView(logRef.message)
                   ) {
-                    if (log.message.sender.did !== currentAccount?.did) {
+                    if (logRef.message.sender.did !== currentAccount?.did) {
                       unreadCount++
                     }
                   }
@@ -143,8 +153,8 @@ export function ListConvosProviderInner({
 
                 return {
                   ...convo,
-                  rev: log.rev,
-                  lastMessage: log.message,
+                  rev: logRef.rev,
+                  lastMessage: logRef.message,
                   unreadCount,
                 }
               }
@@ -152,10 +162,10 @@ export function ListConvosProviderInner({
               function filterConvoFromPage(
                 convo: ChatBskyConvoDefs.ConvoView[],
               ) {
-                return convo.filter(c => c.id !== log.convoId)
+                return convo.filter(c => c.id !== logRef.convoId)
               }
 
-              const existingConvo = getConvoFromQueryData(log.convoId, old)
+              const existingConvo = getConvoFromQueryData(logRef.convoId, old)
 
               if (existingConvo) {
                 return {
@@ -186,7 +196,7 @@ export function ListConvosProviderInner({
               }
             })
           }
-        })
+        }
       },
       {
         // get events for all chats
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index 72100a624..396994110 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -295,9 +295,11 @@ export function* findAllPostsInQueryData(
           }
         }
 
-        const quotedPost = getEmbeddedPost(item.subject?.embed)
-        if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
-          yield embedViewRecordToPostView(quotedPost!)
+        if (AppBskyFeedDefs.isPostView(item.subject)) {
+          const quotedPost = getEmbeddedPost(item.subject?.embed)
+          if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
+            yield embedViewRecordToPostView(quotedPost!)
+          }
         }
       }
     }
@@ -307,7 +309,7 @@ export function* findAllPostsInQueryData(
 export function* findAllProfilesInQueryData(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileView, void> {
+): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
   const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
     queryKey: [RQKEY_ROOT],
   })
@@ -323,9 +325,11 @@ export function* findAllProfilesInQueryData(
         ) {
           yield item.subject.author
         }
-        const quotedPost = getEmbeddedPost(item.subject?.embed)
-        if (quotedPost?.author.did === did) {
-          yield quotedPost.author
+        if (AppBskyFeedDefs.isPostView(item.subject)) {
+          const quotedPost = getEmbeddedPost(item.subject?.embed)
+          if (quotedPost?.author.did === did) {
+            yield quotedPost.author
+          }
         }
       }
     }
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index 0d72e9e92..f6f53f58f 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -14,6 +14,7 @@ import {QueryClient} from '@tanstack/react-query'
 import chunk from 'lodash.chunk'
 
 import {labelIsHideableOffense} from '#/lib/moderation'
+import * as bsky from '#/types/bsky'
 import {precacheProfile} from '../profile'
 import {FeedNotification, FeedPage, NotificationType} from './types'
 
@@ -205,12 +206,9 @@ async function fetchSubjects(
     ),
   )
   const postsMap = new Map<string, AppBskyFeedDefs.PostView>()
-  const packsMap = new Map<string, AppBskyGraphDefs.StarterPackView>()
+  const packsMap = new Map<string, AppBskyGraphDefs.StarterPackViewBasic>()
   for (const post of postsChunks.flat()) {
-    if (
-      AppBskyFeedPost.isRecord(post.record) &&
-      AppBskyFeedPost.validateRecord(post.record).success
-    ) {
+    if (AppBskyFeedPost.isRecord(post.record)) {
       postsMap.set(post.uri, post)
     }
   }
@@ -255,8 +253,14 @@ function getSubjectUri(
     return notif.uri
   } else if (type === 'post-like' || type === 'repost') {
     if (
-      AppBskyFeedRepost.isRecord(notif.record) ||
-      AppBskyFeedLike.isRecord(notif.record)
+      bsky.dangerousIsType<AppBskyFeedRepost.Record>(
+        notif.record,
+        AppBskyFeedRepost.isRecord,
+      ) ||
+      bsky.dangerousIsType<AppBskyFeedLike.Record>(
+        notif.record,
+        AppBskyFeedLike.isRecord,
+      )
     ) {
       return typeof notif.record.subject?.uri === 'string'
         ? notif.record.subject?.uri
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 350970ffd..b29384e03 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -547,7 +547,7 @@ export function* findAllPostsInQueryData(
 export function* findAllProfilesInQueryData(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileView, undefined> {
+): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
   const queryDatas = queryClient.getQueriesData<
     InfiniteData<FeedPageUnselected>
   >({
diff --git a/src/state/queries/post-quotes.ts b/src/state/queries/post-quotes.ts
index be51eaab0..af9699d2b 100644
--- a/src/state/queries/post-quotes.ts
+++ b/src/state/queries/post-quotes.ts
@@ -70,7 +70,7 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) {
 export function* findAllProfilesInQueryData(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileView, void> {
+): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
   const queryDatas = queryClient.getQueriesData<
     InfiniteData<AppBskyFeedGetQuotes.OutputSchema>
   >({
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
index 79350c119..b1cd626cf 100644
--- a/src/state/queries/post-thread.ts
+++ b/src/state/queries/post-thread.ts
@@ -18,6 +18,7 @@ import {
   findAllProfilesInQueryData as findAllProfilesInSearchQueryData,
 } from '#/state/queries/search-posts'
 import {useAgent} from '#/state/session'
+import * as bsky from '#/types/bsky'
 import {
   findAllPostsInQueryData as findAllPostsInNotifsQueryData,
   findAllProfilesInQueryData as findAllProfilesInNotifsQueryData,
@@ -332,8 +333,10 @@ function responseToThreadNodes(
 ): ThreadNode {
   if (
     AppBskyFeedDefs.isThreadViewPost(node) &&
-    AppBskyFeedPost.isRecord(node.post.record) &&
-    AppBskyFeedPost.validateRecord(node.post.record).success
+    bsky.dangerousIsType<AppBskyFeedPost.Record>(
+      node.post.record,
+      AppBskyFeedPost.isRecord,
+    )
   ) {
     const post = node.post
     // These should normally be present. They're missing only for
@@ -364,7 +367,7 @@ function responseToThreadNodes(
         depth,
         isHighlightedPost: depth === 0,
         hasMore:
-          direction === 'down' && !node.replies?.length && !!node.replyCount,
+          direction === 'down' && !node.replies?.length && !!post.replyCount,
         isSelfThread: false, // populated `annotateSelfThread`
         hasMoreSelfThread: false, // populated in `annotateSelfThread`
       },
@@ -497,7 +500,7 @@ export function* findAllPostsInQueryData(
 export function* findAllProfilesInQueryData(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileView, void> {
+): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
   const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({
     queryKey: [RQKEY_ROOT],
   })
diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts
index 149b9cbe9..346e7bfe2 100644
--- a/src/state/queries/postgate/index.ts
+++ b/src/state/queries/postgate/index.ts
@@ -21,6 +21,7 @@ import {
   POSTGATE_COLLECTION,
 } from '#/state/queries/postgate/util'
 import {useAgent} from '#/state/session'
+import * as bsky from '#/types/bsky'
 
 export async function getPostgateRecord({
   agent,
@@ -60,7 +61,10 @@ export async function getPostgateRecord({
         }),
     )
 
-    if (data.value && AppBskyFeedPostgate.isRecord(data.value)) {
+    if (
+      data.value &&
+      bsky.validate(data.value, AppBskyFeedPostgate.validateRecord)
+    ) {
       return data.value
     } else {
       return undefined
diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts
index 96762d38c..c1955cc74 100644
--- a/src/state/queries/postgate/util.ts
+++ b/src/state/queries/postgate/util.ts
@@ -1,4 +1,5 @@
 import {
+  $Typed,
   AppBskyEmbedRecord,
   AppBskyEmbedRecordWithMedia,
   AppBskyFeedDefs,
@@ -45,8 +46,12 @@ export function mergePostgateRecords(
   })
 }
 
-export function createEmbedViewDetachedRecord({uri}: {uri: string}) {
-  const record: AppBskyEmbedRecord.ViewDetached = {
+export function createEmbedViewDetachedRecord({
+  uri,
+}: {
+  uri: string
+}): $Typed<AppBskyEmbedRecord.View> {
+  const record: $Typed<AppBskyEmbedRecord.ViewDetached> = {
     $type: 'app.bsky.embed.record#viewDetached',
     uri,
     detached: true,
@@ -95,7 +100,7 @@ export function createMaybeDetachedQuoteEmbed({
 
 export function createEmbedViewRecordFromPost(
   post: AppBskyFeedDefs.PostView,
-): AppBskyEmbedRecord.ViewRecord {
+): $Typed<AppBskyEmbedRecord.ViewRecord> {
   return {
     $type: 'app.bsky.embed.record#viewRecord',
     uri: post.uri,
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 291999ae1..2c98df634 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -8,6 +8,7 @@ import {
   AtUri,
   BskyAgent,
   ComAtprotoRepoUploadBlob,
+  Un$Typed,
 } from '@atproto/api'
 import {
   keepPreviousData,
@@ -24,7 +25,12 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
 import {Shadow} from '#/state/cache/types'
 import {STALE} from '#/state/queries'
 import {resetProfilePostsQueries} from '#/state/queries/post-feed'
+import {
+  unstableCacheProfileView,
+  useUnstableProfileViewCache,
+} from '#/state/queries/unstable-profile-cache'
 import * as userActionHistory from '#/state/userActionHistory'
+import * as bsky from '#/types/bsky'
 import {updateProfileShadow} from '../cache/profile-shadow'
 import {useAgent, useSession} from '../session'
 import {
@@ -35,6 +41,12 @@ import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-conversations'
 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
 import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
 
+export * from '#/state/queries/unstable-profile-cache'
+/**
+ * @deprecated use {@link unstableCacheProfileView} instead
+ */
+export const precacheProfile = unstableCacheProfileView
+
 const RQKEY_ROOT = 'profile'
 export const RQKEY = (did: string) => [RQKEY_ROOT, did]
 
@@ -44,12 +56,6 @@ export const profilesQueryKey = (handles: string[]) => [
   handles,
 ]
 
-const profileBasicQueryKeyRoot = 'profileBasic'
-export const profileBasicQueryKey = (didOrHandle: string) => [
-  profileBasicQueryKeyRoot,
-  didOrHandle,
-]
-
 export function useProfileQuery({
   did,
   staleTime = STALE.SECONDS.FIFTEEN,
@@ -57,8 +63,8 @@ export function useProfileQuery({
   did: string | undefined
   staleTime?: number
 }) {
-  const queryClient = useQueryClient()
   const agent = useAgent()
+  const {getUnstableProfile} = useUnstableProfileViewCache()
   return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
     // WARNING
     // this staleTime is load-bearing
@@ -73,10 +79,7 @@ export function useProfileQuery({
     },
     placeholderData: () => {
       if (!did) return
-
-      return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
-        profileBasicQueryKey(did),
-      )
+      return getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed
     },
     enabled: !!did,
   })
@@ -121,10 +124,12 @@ export function usePrefetchProfileQuery() {
 }
 
 interface ProfileUpdateParams {
-  profile: AppBskyActorDefs.ProfileView
+  profile: AppBskyActorDefs.ProfileViewDetailed
   updates:
-    | AppBskyActorProfile.Record
-    | ((existing: AppBskyActorProfile.Record) => AppBskyActorProfile.Record)
+    | Un$Typed<AppBskyActorProfile.Record>
+    | ((
+        existing: Un$Typed<AppBskyActorProfile.Record>,
+      ) => Un$Typed<AppBskyActorProfile.Record>)
   newUserAvatar?: RNImage | undefined | null
   newUserBanner?: RNImage | undefined | null
   checkCommitted?: (res: AppBskyActorGetProfile.Response) => boolean
@@ -161,29 +166,29 @@ export function useProfileUpdateMutation() {
         )
       }
       await agent.upsertProfile(async existing => {
-        existing = existing || {}
+        let next: Un$Typed<AppBskyActorProfile.Record> = existing || {}
         if (typeof updates === 'function') {
-          existing = updates(existing)
+          next = updates(next)
         } else {
-          existing.displayName = updates.displayName
-          existing.description = updates.description
+          next.displayName = updates.displayName
+          next.description = updates.description
           if ('pinnedPost' in updates) {
-            existing.pinnedPost = updates.pinnedPost
+            next.pinnedPost = updates.pinnedPost
           }
         }
         if (newUserAvatarPromise) {
           const res = await newUserAvatarPromise
-          existing.avatar = res.data.blob
+          next.avatar = res.data.blob
         } else if (newUserAvatar === null) {
-          existing.avatar = undefined
+          next.avatar = undefined
         }
         if (newUserBannerPromise) {
           const res = await newUserBannerPromise
-          existing.banner = res.data.blob
+          next.banner = res.data.blob
         } else if (newUserBanner === null) {
-          existing.banner = undefined
+          next.banner = undefined
         }
-        return existing
+        return next
       })
       await whenAppViewReady(
         agent,
@@ -228,7 +233,7 @@ export function useProfileUpdateMutation() {
 }
 
 export function useProfileFollowMutationQueue(
-  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
+  profile: Shadow<bsky.profile.AnyProfileView>,
   logContext: LogEvents['profile:follow']['logContext'] &
     LogEvents['profile:follow']['logContext'],
 ) {
@@ -302,7 +307,7 @@ export function useProfileFollowMutationQueue(
 
 function useProfileFollowMutation(
   logContext: LogEvents['profile:follow']['logContext'],
-  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
+  profile: Shadow<bsky.profile.AnyProfileView>,
 ) {
   const {currentAccount} = useSession()
   const agent = useAgent()
@@ -321,7 +326,10 @@ function useProfileFollowMutation(
         didBecomeMutual: profile.viewer
           ? Boolean(profile.viewer.followedBy)
           : undefined,
-        followeeClout: toClout(profile.followersCount),
+        followeeClout:
+          'followersCount' in profile
+            ? toClout(profile.followersCount)
+            : undefined,
         followerClout: toClout(ownProfile?.followersCount),
       })
       return await agent.follow(did)
@@ -342,7 +350,7 @@ function useProfileUnfollowMutation(
 }
 
 export function useProfileMuteMutationQueue(
-  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
+  profile: Shadow<bsky.profile.AnyProfileView>,
 ) {
   const queryClient = useQueryClient()
   const did = profile.did
@@ -417,7 +425,7 @@ function useProfileUnmuteMutation() {
 }
 
 export function useProfileBlockMutationQueue(
-  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
+  profile: Shadow<bsky.profile.AnyProfileView>,
 ) {
   const queryClient = useQueryClient()
   const did = profile.did
@@ -513,14 +521,6 @@ function useProfileUnblockMutation() {
   })
 }
 
-export function precacheProfile(
-  queryClient: QueryClient,
-  profile: AppBskyActorDefs.ProfileViewBasic,
-) {
-  queryClient.setQueryData(profileBasicQueryKey(profile.handle), profile)
-  queryClient.setQueryData(profileBasicQueryKey(profile.did), profile)
-}
-
 async function whenAppViewReady(
   agent: BskyAgent,
   actor: string,
diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts
index c1fd8e240..1422a2dae 100644
--- a/src/state/queries/resolve-uri.ts
+++ b/src/state/queries/resolve-uri.ts
@@ -1,14 +1,9 @@
-import {AppBskyActorDefs, AtUri} from '@atproto/api'
-import {
-  QueryClient,
-  useQuery,
-  useQueryClient,
-  UseQueryResult,
-} from '@tanstack/react-query'
+import {AtUri} from '@atproto/api'
+import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
 
 import {STALE} from '#/state/queries'
 import {useAgent} from '#/state/session'
-import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
+import {useUnstableProfileViewCache} from './profile'
 
 const RQKEY_ROOT = 'resolved-did'
 export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle]
@@ -28,8 +23,8 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
 }
 
 export function useResolveDidQuery(didOrHandle: string | undefined) {
-  const queryClient = useQueryClient()
   const agent = useAgent()
+  const {getUnstableProfile} = useUnstableProfileViewCache()
 
   return useQuery<string, Error>({
     staleTime: STALE.HOURS.ONE,
@@ -45,11 +40,7 @@ export function useResolveDidQuery(didOrHandle: string | undefined) {
     initialData: () => {
       // Return undefined if no did or handle
       if (!didOrHandle) return
-
-      const profile =
-        queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
-          RQKEY_PROFILE_BASIC(didOrHandle),
-        )
+      const profile = getUnstableProfile(didOrHandle)
       return profile?.did
     },
     enabled: !!didOrHandle,
diff --git a/src/state/queries/search-posts.ts b/src/state/queries/search-posts.ts
index 8a8a3fa52..d0bfd55df 100644
--- a/src/state/queries/search-posts.ts
+++ b/src/state/queries/search-posts.ts
@@ -174,7 +174,7 @@ export function* findAllPostsInQueryData(
 export function* findAllProfilesInQueryData(
   queryClient: QueryClient,
   did: string,
-): Generator<AppBskyActorDefs.ProfileView, undefined> {
+): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
   const queryDatas = queryClient.getQueriesData<
     InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
   >({
diff --git a/src/state/queries/service-config.ts b/src/state/queries/service-config.ts
index 9a9db7865..12d2cc6be 100644
--- a/src/state/queries/service-config.ts
+++ b/src/state/queries/service-config.ts
@@ -19,6 +19,7 @@ export function useServiceConfigQuery() {
         const {data} = await agent.api.app.bsky.unspecced.getConfig()
         return {
           checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
+          // @ts-expect-error not included in types atm
           topicsEnabled: Boolean(data.topicsEnabled),
         }
       } catch (e) {
diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts
index b90a57037..5b39fa45f 100644
--- a/src/state/queries/starter-packs.ts
+++ b/src/state/queries/starter-packs.ts
@@ -1,5 +1,4 @@
 import {
-  AppBskyActorDefs,
   AppBskyFeedDefs,
   AppBskyGraphDefs,
   AppBskyGraphGetStarterPack,
@@ -29,6 +28,7 @@ import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-pa
 import {STALE} from '#/state/queries/index'
 import {invalidateListMembersQuery} from '#/state/queries/list-members'
 import {useAgent} from '#/state/session'
+import * as bsky from '#/types/bsky'
 
 const RQKEY_ROOT = 'starter-pack'
 const RQKEY = ({
@@ -93,7 +93,7 @@ export async function invalidateStarterPack({
 interface UseCreateStarterPackMutationParams {
   name: string
   description?: string
-  profiles: AppBskyActorDefs.ProfileViewBasic[]
+  profiles: bsky.profile.AnyProfileView[]
   feeds?: AppBskyFeedDefs.GeneratorView[]
 }
 
@@ -131,7 +131,7 @@ export function useCreateStarterPackMutation({
 
       return await agent.app.bsky.graph.starterpack.create(
         {
-          repo: agent.session?.did,
+          repo: agent.assertDid,
         },
         {
           name,
@@ -366,7 +366,10 @@ export async function precacheStarterPack(
   let starterPackView: AppBskyGraphDefs.StarterPackView | undefined
   if (AppBskyGraphDefs.isStarterPackView(starterPack)) {
     starterPackView = starterPack
-  } else if (AppBskyGraphDefs.isStarterPackViewBasic(starterPack)) {
+  } else if (
+    AppBskyGraphDefs.isStarterPackViewBasic(starterPack) &&
+    bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
+  ) {
     const listView: AppBskyGraphDefs.ListViewBasic = {
       uri: starterPack.record.list,
       // This will be populated once the data from server is fetched
diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts
index 8aa932081..478658fe8 100644
--- a/src/state/queries/threadgate/index.ts
+++ b/src/state/queries/threadgate/index.ts
@@ -20,6 +20,7 @@ import {
 } from '#/state/queries/threadgate/util'
 import {useAgent} from '#/state/session'
 import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
+import * as bsky from '#/types/bsky'
 
 export * from '#/state/queries/threadgate/types'
 export * from '#/state/queries/threadgate/util'
@@ -138,7 +139,10 @@ export async function getThreadgateRecord({
         }),
     )
 
-    if (data.value && AppBskyFeedThreadgate.isRecord(data.value)) {
+    if (
+      data.value &&
+      bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord)
+    ) {
       return data.value
     } else {
       return null
diff --git a/src/state/queries/threadgate/types.ts b/src/state/queries/threadgate/types.ts
index 56eadabcd..bbe677ad4 100644
--- a/src/state/queries/threadgate/types.ts
+++ b/src/state/queries/threadgate/types.ts
@@ -4,4 +4,4 @@ export type ThreadgateAllowUISetting =
   | {type: 'mention'}
   | {type: 'following'}
   | {type: 'followers'}
-  | {type: 'list'; list: unknown}
+  | {type: 'list'; list: string}
diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts
index 4459eddbe..cbe8d4695 100644
--- a/src/state/queries/threadgate/util.ts
+++ b/src/state/queries/threadgate/util.ts
@@ -1,14 +1,15 @@
 import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
 
 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
+import * as bsky from '#/types/bsky'
 
 export function threadgateViewToAllowUISetting(
   threadgateView: AppBskyFeedDefs.ThreadgateView | undefined,
 ): ThreadgateAllowUISetting[] {
+  // Validate the record for clarity, since backwards compat code is a little confusing
   const threadgate =
     threadgateView &&
-    AppBskyFeedThreadgate.isRecord(threadgateView.record) &&
-    AppBskyFeedThreadgate.validateRecord(threadgateView.record).success
+    bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord)
       ? threadgateView.record
       : undefined
   return threadgateRecordToAllowUISetting(threadgate)
@@ -39,14 +40,14 @@ export function threadgateRecordToAllowUISetting(
   const settings: ThreadgateAllowUISetting[] = threadgate.allow
     .map(allow => {
       let setting: ThreadgateAllowUISetting | undefined
-      if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
+      if (AppBskyFeedThreadgate.isMentionRule(allow)) {
         setting = {type: 'mention'}
-      } else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
+      } else if (AppBskyFeedThreadgate.isFollowingRule(allow)) {
         setting = {type: 'following'}
-      } else if (allow.$type === 'app.bsky.feed.threadgate#followerRule') {
-        setting = {type: 'followers'}
-      } else if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
+      } else if (AppBskyFeedThreadgate.isListRule(allow)) {
         setting = {type: 'list', list: allow.list}
+      } else if (AppBskyFeedThreadgate.isFollowerRule(allow)) {
+        setting = {type: 'followers'}
       }
       return setting
     })
@@ -69,11 +70,7 @@ export function threadgateAllowUISettingToAllowRecordValue(
     return undefined
   }
 
-  let allow: (
-    | AppBskyFeedThreadgate.MentionRule
-    | AppBskyFeedThreadgate.FollowingRule
-    | AppBskyFeedThreadgate.ListRule
-  )[] = []
+  let allow: Exclude<AppBskyFeedThreadgate.Record['allow'], undefined> = []
 
   if (!threadgate.find(v => v.type === 'nobody')) {
     for (const rule of threadgate) {
diff --git a/src/state/queries/unstable-profile-cache.ts b/src/state/queries/unstable-profile-cache.ts
new file mode 100644
index 000000000..4ac5001b7
--- /dev/null
+++ b/src/state/queries/unstable-profile-cache.ts
@@ -0,0 +1,51 @@
+import {useCallback} from 'react'
+import {QueryClient, useQueryClient} from '@tanstack/react-query'
+
+import * as bsky from '#/types/bsky'
+
+const unstableProfileViewCacheQueryKeyRoot = 'unstableProfileViewCache'
+export const unstableProfileViewCacheQueryKey = (didOrHandle: string) => [
+  unstableProfileViewCacheQueryKeyRoot,
+  didOrHandle,
+]
+
+/**
+ * Used as a rough cache of profile views to make loading snappier. This method
+ * accepts and stores any profile view type by both handle and DID.
+ *
+ * Access the cache via {@link useUnstableProfileViewCache}.
+ */
+export function unstableCacheProfileView(
+  queryClient: QueryClient,
+  profile: bsky.profile.AnyProfileView,
+) {
+  queryClient.setQueryData(
+    unstableProfileViewCacheQueryKey(profile.handle),
+    profile,
+  )
+  queryClient.setQueryData(
+    unstableProfileViewCacheQueryKey(profile.did),
+    profile,
+  )
+}
+
+/**
+ * Hook to access the unstable profile view cache. This cache can return ANY
+ * profile view type, so if the object shape is important, you need to use the
+ * identity validators shipped in the atproto SDK e.g.
+ * `AppBskyActorDefs.isValidProfileViewBasic` to confirm before using.
+ *
+ * To cache a profile, use {@link unstableCacheProfileView}.
+ */
+export function useUnstableProfileViewCache() {
+  const qc = useQueryClient()
+  const getUnstableProfile = useCallback(
+    (didOrHandle: string) => {
+      return qc.getQueryData<bsky.profile.AnyProfileView>(
+        unstableProfileViewCacheQueryKey(didOrHandle),
+      )
+    },
+    [qc],
+  )
+  return {getUnstableProfile}
+}
diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts
index 887c1df0a..71d185bec 100644
--- a/src/state/queries/util.ts
+++ b/src/state/queries/util.ts
@@ -8,6 +8,8 @@ import {
 } from '@atproto/api'
 import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query'
 
+import * as bsky from '#/types/bsky'
+
 export async function truncateAndInvalidate<T = any>(
   queryClient: QueryClient,
   queryKey: QueryKey,
@@ -44,7 +46,9 @@ export function didOrHandleUriMatches(
 export function getEmbeddedPost(
   v: unknown,
 ): AppBskyEmbedRecord.ViewRecord | undefined {
-  if (AppBskyEmbedRecord.isView(v)) {
+  if (
+    bsky.dangerousIsType<AppBskyEmbedRecord.View>(v, AppBskyEmbedRecord.isView)
+  ) {
     if (
       AppBskyEmbedRecord.isViewRecord(v.record) &&
       AppBskyFeedPost.isRecord(v.record.value)
@@ -52,7 +56,12 @@ export function getEmbeddedPost(
       return v.record
     }
   }
-  if (AppBskyEmbedRecordWithMedia.isView(v)) {
+  if (
+    bsky.dangerousIsType<AppBskyEmbedRecordWithMedia.View>(
+      v,
+      AppBskyEmbedRecordWithMedia.isView,
+    )
+  ) {
     if (
       AppBskyEmbedRecord.isViewRecord(v.record.record) &&
       AppBskyFeedPost.isRecord(v.record.record.value)
diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx
index f1ea41c64..33634c047 100644
--- a/src/state/shell/composer/index.tsx
+++ b/src/state/shell/composer/index.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {
   AppBskyActorDefs,
-  AppBskyEmbedRecord,
   AppBskyFeedDefs,
   ModerationDecision,
 } from '@atproto/api'
@@ -21,7 +20,7 @@ export interface ComposerOptsPostRef {
   cid: string
   text: string
   author: AppBskyActorDefs.ProfileViewBasic
-  embed?: AppBskyEmbedRecord.ViewRecord['embed']
+  embed?: AppBskyFeedDefs.PostView['embed']
   moderation?: ModerationDecision
 }
 
diff --git a/src/types/bsky/index.ts b/src/types/bsky/index.ts
new file mode 100644
index 000000000..d5acbdbb5
--- /dev/null
+++ b/src/types/bsky/index.ts
@@ -0,0 +1,51 @@
+import {ValidationResult} from '@atproto/lexicon'
+
+export * as post from '#/types/bsky/post'
+export * as profile from '#/types/bsky/profile'
+export * as starterPack from '#/types/bsky/starterPack'
+
+/**
+ * Fast type checking without full schema validation, for use with data we
+ * trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
+ * utils do not assert the type of the entire object, only the `$type` string.
+ *
+ * For full validation of the object schema, use the `validate` export from
+ * this file.
+ *
+ * Usage:
+ * ```ts
+ * import * as bsky from '#/types/bsky'
+ *
+ * if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
+ *   // `item` has type `$Typed<AppBskyFeedPost.Record>` here
+ * }
+ * ```
+ */
+export function dangerousIsType<R extends {$type?: string}>(
+  record: unknown,
+  identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
+): record is R {
+  return identity(record)
+}
+
+/**
+ * Fully validates the object schema, which has a performance cost.
+ *
+ * For faster checks with data we trust, like that from our app view, use the
+ * `dangerousIsType` export from this same file.
+ *
+ * Usage:
+ * ```ts
+ * import * as bsky from '#/types/bsky'
+ *
+ * if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
+ *   // `item` has type `$Typed<AppBskyFeedPost.Record>` here
+ * }
+ * ```
+ */
+export function validate<R extends {$type?: string}>(
+  record: unknown,
+  validator: (v: unknown) => ValidationResult<R>,
+): record is R {
+  return validator(record).success
+}
diff --git a/src/types/bsky/post.ts b/src/types/bsky/post.ts
new file mode 100644
index 000000000..225726f41
--- /dev/null
+++ b/src/types/bsky/post.ts
@@ -0,0 +1,148 @@
+import {
+  AppBskyEmbedExternal,
+  AppBskyEmbedImages,
+  AppBskyEmbedRecord,
+  AppBskyEmbedRecordWithMedia,
+  AppBskyEmbedVideo,
+  AppBskyFeedDefs,
+  AppBskyGraphDefs,
+  AppBskyLabelerDefs,
+} from '@atproto/api'
+
+export type Embed =
+  | {
+      type: 'post'
+      view: AppBskyEmbedRecord.ViewRecord
+    }
+  | {
+      type: 'post_not_found'
+      view: AppBskyEmbedRecord.ViewNotFound
+    }
+  | {
+      type: 'post_blocked'
+      view: AppBskyEmbedRecord.ViewBlocked
+    }
+  | {
+      type: 'post_detached'
+      view: AppBskyEmbedRecord.ViewDetached
+    }
+  | {
+      type: 'feed'
+      view: AppBskyFeedDefs.GeneratorView
+    }
+  | {
+      type: 'list'
+      view: AppBskyGraphDefs.ListView
+    }
+  | {
+      type: 'labeler'
+      view: AppBskyLabelerDefs.LabelerView
+    }
+  | {
+      type: 'starter_pack'
+      view: AppBskyGraphDefs.StarterPackViewBasic
+    }
+  | {
+      type: 'images'
+      view: AppBskyEmbedImages.View
+    }
+  | {
+      type: 'link'
+      view: AppBskyEmbedExternal.View
+    }
+  | {
+      type: 'video'
+      view: AppBskyEmbedVideo.View
+    }
+  | {
+      type: 'post_with_media'
+      view: Embed
+      media: Embed
+    }
+  | {
+      type: 'unknown'
+      view: null
+    }
+
+export type EmbedType<T extends Embed['type']> = Extract<Embed, {type: T}>
+
+export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed {
+  if (AppBskyEmbedRecord.isViewRecord(record)) {
+    return {
+      type: 'post',
+      view: record,
+    }
+  } else if (AppBskyEmbedRecord.isViewNotFound(record)) {
+    return {
+      type: 'post_not_found',
+      view: record,
+    }
+  } else if (AppBskyEmbedRecord.isViewBlocked(record)) {
+    return {
+      type: 'post_blocked',
+      view: record,
+    }
+  } else if (AppBskyEmbedRecord.isViewDetached(record)) {
+    return {
+      type: 'post_detached',
+      view: record,
+    }
+  } else if (AppBskyFeedDefs.isGeneratorView(record)) {
+    return {
+      type: 'feed',
+      view: record,
+    }
+  } else if (AppBskyGraphDefs.isListView(record)) {
+    return {
+      type: 'list',
+      view: record,
+    }
+  } else if (AppBskyLabelerDefs.isLabelerView(record)) {
+    return {
+      type: 'labeler',
+      view: record,
+    }
+  } else if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
+    return {
+      type: 'starter_pack',
+      view: record,
+    }
+  } else {
+    return {
+      type: 'unknown',
+      view: null,
+    }
+  }
+}
+
+export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed {
+  if (AppBskyEmbedImages.isView(embed)) {
+    return {
+      type: 'images',
+      view: embed,
+    }
+  } else if (AppBskyEmbedExternal.isView(embed)) {
+    return {
+      type: 'link',
+      view: embed,
+    }
+  } else if (AppBskyEmbedVideo.isView(embed)) {
+    return {
+      type: 'video',
+      view: embed,
+    }
+  } else if (AppBskyEmbedRecord.isView(embed)) {
+    return parseEmbedRecordView(embed)
+  } else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
+    return {
+      type: 'post_with_media',
+      view: parseEmbedRecordView(embed.record),
+      media: parseEmbed(embed.media),
+    }
+  } else {
+    return {
+      type: 'unknown',
+      view: null,
+    }
+  }
+}
diff --git a/src/types/bsky/profile.ts b/src/types/bsky/profile.ts
new file mode 100644
index 000000000..7449f117e
--- /dev/null
+++ b/src/types/bsky/profile.ts
@@ -0,0 +1,10 @@
+import {AppBskyActorDefs, ChatBskyActorDefs} from '@atproto/api'
+
+/**
+ * Matches any profile view exported by our SDK
+ */
+export type AnyProfileView =
+  | AppBskyActorDefs.ProfileViewBasic
+  | AppBskyActorDefs.ProfileView
+  | AppBskyActorDefs.ProfileViewDetailed
+  | ChatBskyActorDefs.ProfileViewBasic
diff --git a/src/types/bsky/starterPack.ts b/src/types/bsky/starterPack.ts
new file mode 100644
index 000000000..0064e16bc
--- /dev/null
+++ b/src/types/bsky/starterPack.ts
@@ -0,0 +1,11 @@
+import {AppBskyGraphDefs} from '@atproto/api'
+
+export const isBasicView = AppBskyGraphDefs.isStarterPackViewBasic
+export const isView = AppBskyGraphDefs.isStarterPackView
+
+/**
+ * Matches any starter pack view exported by our SDK
+ */
+export type AnyStarterPackView =
+  | AppBskyGraphDefs.StarterPackViewBasic
+  | AppBskyGraphDefs.StarterPackView
diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx
index 0caae6701..31d2b5fb5 100644
--- a/src/view/com/lists/ListMembers.tsx
+++ b/src/view/com/lists/ListMembers.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback} from 'react'
 import {Dimensions, StyleProp, View, ViewStyle} from 'react-native'
-import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
+import {AppBskyGraphDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -11,6 +11,7 @@ import {useModalControls} from '#/state/modals'
 import {useListMembersQuery} from '#/state/queries/list-members'
 import {useSession} from '#/state/session'
 import {ListFooter} from '#/components/Lists'
+import * as bsky from '#/types/bsky'
 import {ProfileCard} from '../profile/ProfileCard'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {Button} from '../util/forms/Button'
@@ -116,7 +117,7 @@ export function ListMembers({
   }, [fetchNextPage])
 
   const onPressEditMembership = React.useCallback(
-    (profile: AppBskyActorDefs.ProfileViewBasic) => {
+    (profile: bsky.profile.AnyProfileView) => {
       openModal({
         name: 'user-add-remove-lists',
         subject: profile.did,
@@ -131,7 +132,7 @@ export function ListMembers({
   // =
 
   const renderMemberButton = React.useCallback(
-    (profile: AppBskyActorDefs.ProfileViewBasic) => {
+    (profile: bsky.profile.AnyProfileView) => {
       if (!isOwner) {
         return null
       }
diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx
index 1267ce089..84694fe3b 100644
--- a/src/view/com/notifications/NotificationFeedItem.tsx
+++ b/src/view/com/notifications/NotificationFeedItem.tsx
@@ -58,6 +58,7 @@ import * as MediaPreview from '#/components/MediaPreview'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
+import * as bsky from '#/types/bsky'
 import {FeedSourceCard} from '../feeds/FeedSourceCard'
 import {Post} from '../post/Post'
 import {Link, TextLink} from '../util/Link'
@@ -71,7 +72,7 @@ const MAX_AUTHORS = 5
 const EXPANDED_AUTHOR_EL_HEIGHT = 35
 
 interface Author {
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: AppBskyActorDefs.ProfileView
   href: string
   moderation: ModerationDecision
 }
@@ -264,7 +265,10 @@ let NotificationFeedItem = ({
 
     if (
       item.notification.author.viewer?.following &&
-      AppBskyGraphFollow.isRecord(item.notification.record)
+      bsky.dangerousIsType<AppBskyGraphFollow.Record>(
+        item.notification.record,
+        AppBskyGraphFollow.isRecord,
+      )
     ) {
       let followingTimestamp
       try {
@@ -521,7 +525,7 @@ function ExpandListPressable({
   }
 }
 
-function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileViewBasic}) {
+function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) {
   const {_} = useLingui()
   const agent = useAgent()
   const navigation = useNavigation<NavigationProp>()
@@ -716,7 +720,13 @@ function ExpandedAuthorsList({
 
 function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
   const pal = usePalette('default')
-  if (post && AppBskyFeedPost.isRecord(post?.record)) {
+  if (
+    post &&
+    bsky.dangerousIsType<AppBskyFeedPost.Record>(
+      post?.record,
+      AppBskyFeedPost.isRecord,
+    )
+  ) {
     const text = post.record.text
 
     return (
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 2143bd9c2..c27e37f49 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -16,7 +16,7 @@ function renderItem({
   item,
   index,
 }: {
-  item: ActorDefs.ProfileViewBasic
+  item: ActorDefs.ProfileView
   index: number
 }) {
   return (
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx
index 9dc93916a..145e919f9 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
 import {logger} from '#/logger'
-import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {
   useProfileFollowMutationQueue,
   useProfileQuery,
@@ -35,8 +35,7 @@ function PostThreadFollowBtnLoaded({
   const navigation = useNavigation()
   const {_} = useLingui()
   const {gtMobile} = useBreakpoints()
-  const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
-    useProfileShadow(profileUnshadowed)
+  const profile = useProfileShadow(profileUnshadowed)
   const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
     profile,
     'PostThreadItem',
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 928ccd783..024629198 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -58,6 +58,7 @@ import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
 import {Text} from '#/components/Typography'
 import {WhoCanReply} from '#/components/WhoCanReply'
+import * as bsky from '#/types/bsky'
 
 export function PostThreadItem({
   post,
@@ -790,7 +791,10 @@ function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
   const control = Prompt.usePromptControl()
 
   const indexedAt = new Date(post.indexedAt)
-  const createdAt = AppBskyFeedPost.isRecord(post.record)
+  const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>(
+    post.record,
+    AppBskyFeedPost.isRecord,
+  )
     ? new Date(post.record.createdAt)
     : new Date(post.indexedAt)
 
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index c87e361e1..2645237ad 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -28,6 +28,7 @@ import {atoms as a} from '#/alf'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
+import * as bsky from '#/types/bsky'
 import {ContentHider} from '../../../components/moderation/ContentHider'
 import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
 import {PostAlerts} from '../../../components/moderation/PostAlerts'
@@ -53,8 +54,7 @@ export function Post({
   const moderationOpts = useModerationOpts()
   const record = useMemo<AppBskyFeedPost.Record | undefined>(
     () =>
-      AppBskyFeedPost.isRecord(post.record) &&
-      AppBskyFeedPost.validateRecord(post.record).success
+      bsky.validate(post.record, AppBskyFeedPost.validateRecord)
         ? post.record
         : undefined,
     [post],
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index 4b18c470a..13c243c0a 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -47,6 +47,7 @@ import {AppModerationCause} from '#/components/Pills'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
+import * as bsky from '#/types/bsky'
 import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
 import {AviFollowButton} from './AviFollowButton'
 
@@ -232,8 +233,9 @@ let FeedItemInner = ({
    * If `post[0]` in this slice is the actual root post (not an orphan thread),
    * then we may have a threadgate record to reference
    */
-  const threadgateRecord = AppBskyFeedThreadgate.isRecord(
+  const threadgateRecord = bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
     rootPost.threadgate?.record,
+    AppBskyFeedThreadgate.isRecord,
   )
     ? rootPost.threadgate.record
     : undefined
@@ -461,7 +463,10 @@ let PostContent = ({
   })
   const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
     const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
-    const rootPostUri = AppBskyFeedPost.isRecord(post.record)
+    const rootPostUri = bsky.dangerousIsType<AppBskyFeedPost.Record>(
+      post.record,
+      AppBskyFeedPost.isRecord,
+    )
       ? post.record?.reply?.root?.uri || post.uri
       : undefined
     const isControlledByViewer =
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index ff58dc945..656ed914a 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -1,10 +1,10 @@
 import {StyleProp, TextStyle, View} from 'react-native'
-import {AppBskyActorDefs} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {Shadow} from '#/state/cache/types'
 import {useProfileFollowMutationQueue} from '#/state/queries/profile'
+import * as bsky from '#/types/bsky'
 import {Button, ButtonType} from '../util/forms/Button'
 import * as Toast from '../util/Toast'
 
@@ -18,7 +18,7 @@ export function FollowButton({
 }: {
   unfollowedType?: ButtonType
   followedType?: ButtonType
-  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
+  profile: Shadow<bsky.profile.AnyProfileView>
   labelStyle?: StyleProp<TextStyle>
   logContext: 'ProfileCard' | 'StarterPackProfilesList'
   onFollow?: () => void
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index f710d7b4e..bd09d6514 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -24,6 +24,7 @@ import {
   shouldShowKnownFollowers,
 } from '#/components/KnownFollowers'
 import * as Pills from '#/components/Pills'
+import * as bsky from '#/types/bsky'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
@@ -41,12 +42,12 @@ export function ProfileCard({
   showKnownFollowers,
 }: {
   testID?: string
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: bsky.profile.AnyProfileView
   noModFilter?: boolean
   noBg?: boolean
   noBorder?: boolean
   renderButton?: (
-    profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
+    profile: Shadow<bsky.profile.AnyProfileView>,
   ) => React.ReactNode
   onPress?: () => void
   style?: StyleProp<ViewStyle>
@@ -76,6 +77,7 @@ export function ProfileCard({
     showKnownFollowers &&
     shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
     moderationOpts
+  const hasDescription = 'description' in profile
 
   return (
     <Link
@@ -126,9 +128,9 @@ export function ProfileCard({
           <View style={styles.layoutButton}>{renderButton(profile)}</View>
         ) : undefined}
       </View>
-      {profile.description || knownFollowersVisible ? (
+      {hasDescription || knownFollowersVisible ? (
         <View style={styles.details}>
-          {profile.description ? (
+          {hasDescription && profile.description ? (
             <Text emoji style={pal.text} numberOfLines={4}>
               {profile.description as string}
             </Text>
@@ -139,7 +141,7 @@ export function ProfileCard({
                 a.flex_row,
                 a.align_center,
                 a.gap_sm,
-                !!profile.description && a.mt_md,
+                !!hasDescription && a.mt_md,
               ]}>
               <KnownFollowers
                 minimal
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 3c0476929..d6b764656 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -17,7 +17,7 @@ function renderItem({
   item,
   index,
 }: {
-  item: ActorDefs.ProfileViewBasic
+  item: ActorDefs.ProfileView
   index: number
 }) {
   return (
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 1cd65c74c..d67a7261a 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -17,7 +17,7 @@ function renderItem({
   item,
   index,
 }: {
-  item: ActorDefs.ProfileViewBasic
+  item: ActorDefs.ProfileView
   index: number
 }) {
   return (
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 2496f9d2a..934e8f50c 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -2,7 +2,7 @@ import React, {memo, useMemo} from 'react'
 import {Image, Pressable, StyleSheet, View} from 'react-native'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import Svg, {Circle, Path, Rect} from 'react-native-svg'
-import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
+import {ModerationUI} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -31,6 +31,7 @@ import {Link} from '#/components/Link'
 import {MediaInsetBorder} from '#/components/MediaInsetBorder'
 import * as Menu from '#/components/Menu'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
+import * as bsky from '#/types/bsky'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
 
 export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
@@ -55,7 +56,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps {
 
 interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
   moderation?: ModerationUI
-  profile: AppBskyActorDefs.ProfileViewBasic
+  profile: bsky.profile.AnyProfileView
   disableHoverCard?: boolean
   onBeforePress?: () => void
 }
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index cb549f7cd..e283a2eec 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -36,6 +36,7 @@ import {useSession} from '#/state/session'
 import {atoms as a, useTheme} from '#/alf'
 import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
+import * as bsky from '#/types/bsky'
 import {ContentHider} from '../../../../components/moderation/ContentHider'
 import {PostAlerts} from '../../../../components/moderation/PostAlerts'
 import {Link} from '../Link'
@@ -171,10 +172,14 @@ export function QuoteEmbed({
   const itemTitle = `Post by ${quote.author.handle}`
 
   const richText = React.useMemo(() => {
-    const text = AppBskyFeedPost.isRecord(quote.record) ? quote.record.text : ''
-    const facets = AppBskyFeedPost.isRecord(quote.record)
-      ? quote.record.facets
-      : undefined
+    if (
+      !bsky.dangerousIsType<AppBskyFeedPost.Record>(
+        quote.record,
+        AppBskyFeedPost.isRecord,
+      )
+    )
+      return undefined
+    const {text, facets} = quote.record
     return text.trim()
       ? new RichTextAPI({text: text, facets: facets})
       : undefined
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index 2224a4462..9774c644c 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -133,6 +133,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
     })
     mockedProfile.did = did
     mockedProfile.avatar = 'https://bsky.social/about/images/favicon-32x32.png'
+    // @ts-expect-error ProfileViewBasic is close enough -esb
     mockedProfile.banner =
       'https://bsky.social/about/images/social-card-default-gradient.png'
     return mockedProfile
@@ -922,6 +923,7 @@ function MockAccountScreen({
           // @ts-ignore ProfileViewBasic is close enough -prf
           profile={profile}
           moderationOpts={moderationOpts}
+          // @ts-ignore ProfileViewBasic is close enough -esb
           descriptionRT={new RichText({text: profile.description as string})}
         />
       </ScreenHider>
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 83503a706..b6d75b274 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -76,6 +76,7 @@ import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Glo
 import * as Layout from '#/components/Layout'
 import * as Menu from '#/components/Menu'
 import {account, useStorage} from '#/storage'
+import * as bsky from '#/types/bsky'
 
 function Loader() {
   return (
@@ -656,7 +657,7 @@ export function SearchScreenShell({
   )
 
   const updateProfileHistory = useCallback(
-    async (item: AppBskyActorDefs.ProfileViewBasic) => {
+    async (item: bsky.profile.AnyProfileView) => {
       const newAccountHistory = [
         item.did,
         ...accountHistory.filter(p => p !== item.did),
@@ -673,7 +674,7 @@ export function SearchScreenShell({
     [termHistory, setTermHistory],
   )
   const deleteProfileHistoryItem = useCallback(
-    async (item: AppBskyActorDefs.ProfileViewBasic) => {
+    async (item: AppBskyActorDefs.ProfileViewDetailed) => {
       setAccountHistory(accountHistory.filter(p => p !== item.did))
     },
     [accountHistory, setAccountHistory],
@@ -766,7 +767,7 @@ export function SearchScreenShell({
   )
 
   const handleProfileClick = React.useCallback(
-    (profile: AppBskyActorDefs.ProfileViewBasic) => {
+    (profile: bsky.profile.AnyProfileView) => {
       // Slight delay to avoid updating during push nav animation.
       setTimeout(() => {
         updateProfileHistory(profile)
@@ -1013,11 +1014,11 @@ function SearchHistory({
   onRemoveProfileClick,
 }: {
   searchHistory: string[]
-  selectedProfiles: AppBskyActorDefs.ProfileViewBasic[]
+  selectedProfiles: AppBskyActorDefs.ProfileViewDetailed[]
   onItemClick: (item: string) => void
-  onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
+  onProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
   onRemoveItemClick: (item: string) => void
-  onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
+  onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
 }) {
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index bb5de2eb4..522b51dba 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -220,7 +220,7 @@ function SwitchMenuItems({
   accounts:
     | {
         account: SessionAccount
-        profile?: AppBskyActorDefs.ProfileView
+        profile?: AppBskyActorDefs.ProfileViewDetailed
       }[]
     | undefined
   signOutPromptControl: DialogControlProps