diff options
Diffstat (limited to 'src')
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 |