@atproto/api@next integration (#7344)

* Bump SDK

* Use consistent type in profile query

* Omit  from constraint for profile shadow

* Replace isRecord with isValidRecord in QuoteEmbed

* Omit type from constraint for old ProfileCard

* Omit type from constraint in profile queries where appropriate

* Use correct type for update profile mutation

* Conslidate and fix check for isValidRecord in Post.tsx

* Replace isRecord with isValidRecord in PostThreadItem

* Remove redundant cast in PostThreadFollowBtn

* Ignore errors in DebugMod screen

* Use matching type in ProfileFollows screen

* Use matching type in ProfileFollowers screen

* Migrate to isValidRecord in PostFeedItem

* Use matching type if PostRepostedBy

* Omit type from constraint in avatar props

* Use matching types in NotificationFeedItem

* Todo

* Use isValidRecord in NotfyFeedItem

* Improve MediaPreview types

* Migrate another isValidRecord in NotificationFeedItem

* Migrate to isValidView in queries/util

* Migrate to isValidRecord in threadgate/util

* Fix types in threadgates

* Fix up types in starter-packs queries

* Todo

* Specify exact types in search-posts

* Use internal type util to align types

* Ditto last

* Migrate postgate/index

* Specify exact types in post-thread

* Use correct type in post-quotes

* FIX potential bug in post-thread

* Use correct type in post-feed

* Add correct type guards to notifications/feed

* Migrate a guard in notifications/util

* Migrate guard in Wizard/State

* [@next] Profile handling, migrate `ProfileCard` (#7347)

* Introduce new utils for profiles, migrate old ProfileCard

* Rename, reorg

* Add parseEmbed utils

* Expand AnyProfileView to include chat profile view, update post shadow to reflect this

* Cast for perf reasons

* Tighten up types now that we have AnyProfileView

* Add fastIsType util

* Use `assertDid`

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use util types

* Comment

* Use fastIsType where no validation was happening before

* suggestions (#7382)

* suggestions

* Revert unneeded changes

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Use new util

* Rename to dangerousIsType

* Convert object shape

* Use dangerous util

* Use dangerous util

* Use dangerous util, we can trust post records

* Use dangerous util

* Use AnyProfileVIew

* Convert object shape

* Clean up handling

* Patch moderateProfile to accept known profile views, to discuss

* Add AnyStarterPackView and related, implement in first usage

* Remove validation, fix type, fix ref

* Migrate over list-conversations

* Clarify intent behind precacheProfile and its unstable query cache

* Clean up unstable profile cache

* Fix types during label creation in PwiOptOut (#7346)

* Tighten types in queries/list

* Chat: use correct profile views

* Chat: fix log type check

* Chat: construct lexically correct shape, even though it's only internal usage

* Chat: use correct profile types

* Chat: fix type check in logs

* Starter: use correct profile types

* Starter: use correct profile types

* Starter: tighten types to match lex

* Any profile type will work in blocked-and-muted

* Use dangerous util

* Use dangerousIsType

* Update new ProfileCard to use AnyProfileView

* Use dangerousIsType

* Remove outdated todo

* Use correct profile type

* Use correct profile types

* Tighten up types

* Use dangerousIsType

* Chat: more type fixes

* Remove unused file

* Add a few  utils

* Remove unused file

* Ignore feedPost.__source

* Clean up types, leave validation in critical path

* Use dangerousIstype

* Use ANyProfileView

* Use isValidRecord

* Use dangerousIsType

* Fix types in ListCard

* Fix FeedInterstitials types

* Fix types in FeedCard

* Fix types in dms ReportDialog

* Fix types in SearchablePeopleList

* Fix bad type in composer opts

* Starter: ok these need to be loose too

* Clarify docs

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Less code

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use package exports

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>

* Use package exports

* Bump sdk, update $Typed imports

* Format

* Format

* Fix weird TS error

* Remove patch

* Beter name

* It's memo, can validate

* Tighten up parseEmbed, dogfood

* Bump sdk

* Use asPredicate

* Loosen types a bit

* use asPredicate

* Fix types

* Use asPredicate

* Use asPredicate

* Use asPredicate

* Clean up upsertProfile types

* Use asPredicate

* Use Un util

* Fix types

* Use new AnyProfileView

* Use dangerousIsType

* Use asPredicate

* Use asPredicate

* Add fallback content-type to pass typecheck

* Clean up upsertProfile types

* Align types

* Use dangerousIsType

* Use dangerousIsType

* Use asPredicate

* Align types

* Convert findLast

* Align types

* Just ignore type errors and use findLast

* Rename atproto -> bsky

* Add validate util

* Fix type error

* Loosen types

* Export post

* rename atp bsky

* Remove unused code

* minor changes

* Bump deps

* Fix types

* Tighten back up loose check

* Tighten back up loose check

* Fix small bug

* Update comment

* Revert change

---------

Co-authored-by: Matthieu Sieben <matthieusieben@users.noreply.github.com>
Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
This commit is contained in:
Eric Bailey
2025-02-18 08:54:25 -06:00
committed by GitHub
parent e2c0f78719
commit a51fc8e434
93 changed files with 1027 additions and 728 deletions
+7 -6
View File
@@ -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)
+1
View File
@@ -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
+13 -10
View File
@@ -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,
+13 -13
View File
@@ -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
+2 -5
View File
@@ -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
*/
+7 -2
View File
@@ -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',
@@ -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
},
@@ -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
+11 -7
View File
@@ -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
}
}
}
}
+11 -7
View File
@@ -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
+1 -1
View File
@@ -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>
>({
+1 -1
View File
@@ -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>
>({
+7 -4
View File
@@ -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],
})
+5 -1
View File
@@ -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
+8 -3
View File
@@ -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,
+37 -37
View File
@@ -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,
+5 -14
View File
@@ -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,
+1 -1
View File
@@ -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>
>({
+1
View File
@@ -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) {
+7 -4
View File
@@ -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
+5 -1
View File
@@ -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
+1 -1
View File
@@ -4,4 +4,4 @@ export type ThreadgateAllowUISetting =
| {type: 'mention'}
| {type: 'following'}
| {type: 'followers'}
| {type: 'list'; list: unknown}
| {type: 'list'; list: string}
+9 -12
View File
@@ -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) {
@@ -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}
}
+11 -2
View File
@@ -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)
+1 -2
View File
@@ -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
}