diff options
Diffstat (limited to 'src/state')
26 files changed, 246 insertions, 156 deletions
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 } |