migrate runtime call sites to lex clients and sdk actions

Phase 3 tasks 3-7 (parallel wave): composer/post pipeline on
pdsClient/appviewClient with structural+instance blob guards and a golden
CID fixture test; chat Convo/EventBus/queries on the dedicated chat client;
preferences sugar to SDK actions on the PDS client; remaining state/queries
producers (usePostThread unspecced flip, video scoped-token clients,
notifications, starter packs, lists) to client.call; UI runtime sweep
(AtUri from @atproto/syntax, moderation fns from @bsky.app/sdk/moderation,
SDK RichText, ozone reason tokens, guard rewrites via #/types/bsky).

Intermediate checkpoint (hooks skipped): ~114 typecheck errors remain in
cross-boundary consumer files, resolved by the type-only codemod next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-16 21:04:44 +03:00
parent b1a4e1cd16
commit 300a50b69a
296 changed files with 4507 additions and 3903 deletions
+167 -142
View File
@@ -1,23 +1,15 @@
import {
type $Typed,
type AppBskyEmbedRecord,
type ChatBskyActorDefs,
ChatBskyConvoDefs,
type ChatBskyConvoGetLog,
type ChatBskyConvoSendMessage,
type ChatBskyEmbedJoinLink,
type ChatBskyGroupDefs,
} from '@atproto/api'
import {XRPCError} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {type Client} from '@atproto/lex-client'
import {type DatetimeString, type DidString} from '@atproto/syntax'
import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {
isErrorMaybeAppPasswordPermissions,
isNetworkError,
} from '#/lib/strings/errors'
import {getErrorStatus, isXrpcError} from '#/lib/xrpc-error'
import {Logger} from '#/logger'
import {
isProfileShadowApplied,
@@ -45,13 +37,14 @@ import {
} from '#/state/messages/convo/types'
import {type MessagesEventBus} from '#/state/messages/events/agent'
import {type MessagesEventBusError} from '#/state/messages/events/types'
import {type SessionAgent} from '#/state/session'
import {
type ConvoWithDetails,
type GroupConvoMember,
parseConvoView,
} from '#/components/dms/util'
import {IS_NATIVE} from '#/env'
import {app, chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
const logger = Logger.create(Logger.Context.ConversationAgent)
@@ -67,23 +60,32 @@ export function isConvoItemMessage(
}
function toSystemMessageView(
ev: ChatBskyConvoGetLog.OutputSchema['logs'][number],
): ChatBskyConvoDefs.SystemMessageView | null {
const isSystem =
ChatBskyConvoDefs.isLogAddMember(ev) ||
ChatBskyConvoDefs.isLogRemoveMember(ev) ||
ChatBskyConvoDefs.isLogMemberJoin(ev) ||
ChatBskyConvoDefs.isLogMemberLeave(ev) ||
ChatBskyConvoDefs.isLogLockConvo(ev) ||
ChatBskyConvoDefs.isLogUnlockConvo(ev) ||
ChatBskyConvoDefs.isLogLockConvoPermanently(ev) ||
ChatBskyConvoDefs.isLogEditGroup(ev) ||
ChatBskyConvoDefs.isLogCreateJoinLink(ev) ||
ChatBskyConvoDefs.isLogEditJoinLink(ev) ||
ChatBskyConvoDefs.isLogEnableJoinLink(ev) ||
ChatBskyConvoDefs.isLogDisableJoinLink(ev)
if (!isSystem) return null
return ev.message
ev: chat.bsky.convo.getLog.$OutputBody['logs'][number],
): chat.bsky.convo.defs.SystemMessageView | null {
/*
* The guard disjunction is kept inline in the `if` (rather than a separate
* boolean) so TS narrows `ev` to the union of system-message log types, all
* of which carry a `message: SystemMessageView`. The generated lexicon types
* are stricter than the old `@atproto/api` ones (no `[k: string]: unknown`
* index signature), so a separate boolean would not narrow the access.
*/
if (
bsky.isType(chat.bsky.convo.defs.logAddMember, ev) ||
bsky.isType(chat.bsky.convo.defs.logRemoveMember, ev) ||
bsky.isType(chat.bsky.convo.defs.logMemberJoin, ev) ||
bsky.isType(chat.bsky.convo.defs.logMemberLeave, ev) ||
bsky.isType(chat.bsky.convo.defs.logLockConvo, ev) ||
bsky.isType(chat.bsky.convo.defs.logUnlockConvo, ev) ||
bsky.isType(chat.bsky.convo.defs.logLockConvoPermanently, ev) ||
bsky.isType(chat.bsky.convo.defs.logEditGroup, ev) ||
bsky.isType(chat.bsky.convo.defs.logCreateJoinLink, ev) ||
bsky.isType(chat.bsky.convo.defs.logEditJoinLink, ev) ||
bsky.isType(chat.bsky.convo.defs.logEnableJoinLink, ev) ||
bsky.isType(chat.bsky.convo.defs.logDisableJoinLink, ev)
) {
return ev.message
}
return null
}
/**
@@ -91,8 +93,8 @@ function toSystemMessageView(
* the fields the deleted view carries so a reply can render it as deleted.
*/
function toDeletedMessageView(
m: ChatBskyConvoDefs.MessageView,
): $Typed<ChatBskyConvoDefs.DeletedMessageView> {
m: chat.bsky.convo.defs.MessageView,
): $Typed<chat.bsky.convo.defs.DeletedMessageView> {
return {
$type: 'chat.bsky.convo.defs#deletedMessageView',
id: m.id,
@@ -105,9 +107,9 @@ function toDeletedMessageView(
export class Convo {
private id: string
private agent: SessionAgent
private chatClient: Client
private events: MessagesEventBus
private senderUserDid: string
private senderUserDid: DidString
private status: ConvoStatus = ConvoStatus.Uninitialized
private error: ConvoError | undefined
@@ -117,29 +119,29 @@ export class Convo {
private pastMessages: Map<
string,
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| ChatBskyConvoDefs.SystemMessageView
| chat.bsky.convo.defs.MessageView
| chat.bsky.convo.defs.DeletedMessageView
| chat.bsky.convo.defs.SystemMessageView
> = new Map()
private newMessages: Map<
string,
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| ChatBskyConvoDefs.SystemMessageView
| chat.bsky.convo.defs.MessageView
| chat.bsky.convo.defs.DeletedMessageView
| chat.bsky.convo.defs.SystemMessageView
> = new Map()
private pendingMessages: Map<
string,
{
id: string
message: ChatBskyConvoSendMessage.InputSchema['message']
message: chat.bsky.convo.sendMessage.$InputBody['message']
optimisticEmbedView?:
| $Typed<AppBskyEmbedRecord.View>
| $Typed<ChatBskyEmbedJoinLink.View>
optimisticReplyTo?: $Typed<ChatBskyConvoDefs.MessageView>
| $Typed<app.bsky.embed.record.View>
| $Typed<chat.bsky.embed.joinLink.View>
optimisticReplyTo?: $Typed<chat.bsky.convo.defs.MessageView>
}
> = new Map()
private deletedMessages: Set<string> = new Set()
private relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic> =
private relatedProfiles: Map<string, chat.bsky.actor.defs.ProfileViewBasic> =
new Map()
/**
* Accumulated profile shadow state, keyed by did. The profiles this agent
@@ -159,16 +161,16 @@ export class Convo {
convoId: string
convo: ConvoWithDetails | undefined
sender: ChatBskyActorDefs.ProfileViewBasic | undefined
recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined
sender: chat.bsky.actor.defs.ProfileViewBasic | undefined
recipients: chat.bsky.actor.defs.ProfileViewBasic[] | undefined
snapshot: ConvoState | undefined
constructor(params: ConvoParams) {
this.id = nanoid(3)
this.convoId = params.convoId
this.agent = params.agent
this.chatClient = params.chatClient
this.events = params.events
this.senderUserDid = params.agent.assertDid
this.senderUserDid = params.chatClient.assertDid
if (params.placeholderData) {
this.setupPlaceholderData(params.placeholderData)
@@ -589,23 +591,46 @@ export class Convo {
}
}
private setConvo(convo: ChatBskyConvoDefs.ConvoView) {
private setConvo(convo: chat.bsky.convo.defs.ConvoView) {
this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo
if (this.convo) {
for (const member of this.convo.members) {
this.relatedProfiles.set(member.did, member)
// `this.convo` comes from `parseConvoView` in the still-old-typed
// `#/components/dms/util` (migrates in a later task); bridge its member
// shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop
// toLex once dms/util migrates.
this.relatedProfiles.set(
member.did,
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>(member),
)
}
}
this.applyProfileShadows()
}
private updateConvo(convo: Partial<ChatBskyConvoDefs.ConvoView>) {
/*
* The partial merges into `this.convo.view` and is re-parsed by the
* still-old-typed `parseConvoView` (`#/components/dms/util`, migrates in a
* later task), and its callers build it from old-typed `this.convo.details` /
* members. So this boundary stays in the old view world - typing the param
* off `ConvoWithDetails['view']` keeps it internally consistent without a
* per-call `toLex`. TODO(phase4): flip to `chat.bsky.convo.defs.ConvoView`
* once dms/util migrates.
*/
private updateConvo(convo: Partial<ConvoWithDetails['view']>) {
if (this.convo) {
this.convo =
parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ??
this.convo
for (const member of this.convo.members) {
this.relatedProfiles.set(member.did, member)
// `this.convo` comes from `parseConvoView` in the still-old-typed
// `#/components/dms/util` (migrates in a later task); bridge its member
// shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop
// toLex once dms/util migrates.
this.relatedProfiles.set(
member.did,
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>(member),
)
}
this.applyProfileShadows()
}
@@ -709,7 +734,7 @@ export class Convo {
}
private pendingFetchConvo:
| Promise<{convo: ChatBskyConvoDefs.ConvoView}>
| Promise<{convo: chat.bsky.convo.defs.ConvoView}>
| undefined
async fetchConvo() {
if (this.pendingFetchConvo) return this.pendingFetchConvo
@@ -720,13 +745,12 @@ export class Convo {
this.pendingFetchConvo = (async () => {
try {
const response = await networkRetry(2, () => {
return this.agent.chat.bsky.convo.getConvo(
{convoId: this.convoId},
{headers: DM_SERVICE_HEADERS},
)
return this.chatClient.call(chat.bsky.convo.getConvo, {
convoId: this.convoId,
})
})
const convo = response.data.convo
const convo = response.convo
return {
convo,
@@ -763,18 +787,15 @@ export class Convo {
let cursor: string | undefined
do {
const result = await networkRetry(2, () => {
return this.agent.chat.bsky.convo.getConvoMembers(
{
convoId: this.convoId,
limit: 50,
cursor,
},
{headers: DM_SERVICE_HEADERS},
)
return this.chatClient.call(chat.bsky.convo.getConvoMembers, {
convoId: this.convoId,
limit: 50,
cursor,
})
})
cursor = result.data.cursor
cursor = result.cursor
for (const member of result.data.members) {
for (const member of result.members) {
this.relatedProfiles.set(member.did, member)
}
} while (cursor)
@@ -808,16 +829,13 @@ export class Convo {
const nextCursor = this.oldestRev // for TS
const response = await networkRetry(2, () => {
return this.agent.chat.bsky.convo.getMessages(
{
cursor: nextCursor,
convoId: this.convoId,
limit: IS_NATIVE ? 30 : 60,
},
{headers: DM_SERVICE_HEADERS},
)
return this.chatClient.call(chat.bsky.convo.getMessages, {
cursor: nextCursor,
convoId: this.convoId,
limit: IS_NATIVE ? 30 : 60,
})
})
const {cursor, messages, relatedProfiles} = response.data
const {cursor, messages, relatedProfiles} = response
// Trust the cursor for pagination. We can't infer "no more pages" from a
// short page: the server pages by raw rows but strips deleted messages
@@ -836,9 +854,9 @@ export class Convo {
for (const message of messages) {
if (
ChatBskyConvoDefs.isMessageView(message) ||
ChatBskyConvoDefs.isDeletedMessageView(message) ||
ChatBskyConvoDefs.isSystemMessageView(message)
bsky.isType(chat.bsky.convo.defs.messageView, message) ||
bsky.isType(chat.bsky.convo.defs.deletedMessageView, message) ||
bsky.isType(chat.bsky.convo.defs.systemMessageView, message)
) {
/*
* If this message is already in new messages, it was added by the
@@ -913,7 +931,7 @@ export class Convo {
this.commit()
}
ingestFirehose(events: ChatBskyConvoGetLog.OutputSchema['logs']) {
ingestFirehose(events: chat.bsky.convo.getLog.$OutputBody['logs']) {
let needsCommit = false
for (const ev of events) {
@@ -950,8 +968,8 @@ export class Convo {
}
if (
ChatBskyConvoDefs.isLogCreateMessage(ev) &&
ChatBskyConvoDefs.isMessageView(ev.message)
bsky.isType(chat.bsky.convo.defs.logCreateMessage, ev) &&
bsky.isType(chat.bsky.convo.defs.messageView, ev.message)
) {
/*
* If this message is already in past messages, the initial
@@ -976,8 +994,8 @@ export class Convo {
}
needsCommit = true
} else if (
ChatBskyConvoDefs.isLogDeleteMessage(ev) &&
ChatBskyConvoDefs.isDeletedMessageView(ev.message)
bsky.isType(chat.bsky.convo.defs.logDeleteMessage, ev) &&
bsky.isType(chat.bsky.convo.defs.deletedMessageView, ev.message)
) {
/*
* Remove the message itself, and keep its id in `deletedMessages`
@@ -992,9 +1010,9 @@ export class Convo {
this.deletedMessages.add(ev.message.id)
needsCommit = true
} else if (
(ChatBskyConvoDefs.isLogAddReaction(ev) ||
ChatBskyConvoDefs.isLogRemoveReaction(ev)) &&
ChatBskyConvoDefs.isMessageView(ev.message)
(bsky.isType(chat.bsky.convo.defs.logAddReaction, ev) ||
bsky.isType(chat.bsky.convo.defs.logRemoveReaction, ev)) &&
bsky.isType(chat.bsky.convo.defs.messageView, ev.message)
) {
/*
* Update if we have this in state - replace message wholesale. If we don't, don't worry about it.
@@ -1031,11 +1049,11 @@ export class Convo {
private pendingMessageFailure: 'recoverable' | 'unrecoverable' | null = null
sendMessage(
message: ChatBskyConvoSendMessage.InputSchema['message'],
message: chat.bsky.convo.sendMessage.$InputBody['message'],
optimisticEmbedView?:
| $Typed<AppBskyEmbedRecord.View>
| $Typed<ChatBskyEmbedJoinLink.View>,
optimisticReplyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
| $Typed<app.bsky.embed.record.View>
| $Typed<chat.bsky.embed.joinLink.View>,
optimisticReplyTo?: $Typed<chat.bsky.convo.defs.MessageView>,
) {
// Ignore empty messages for now since they have no other purpose atm
if (!message.text.trim() && !message.embed) return
@@ -1110,7 +1128,7 @@ export class Convo {
this.commit()
}
updateJoinLink(joinLink: ChatBskyGroupDefs.JoinLinkView | undefined) {
updateJoinLink(joinLink: chat.bsky.group.defs.JoinLinkView | undefined) {
if (this.convo?.kind !== 'group') {
throw new Error('updateJoinLink can only be called on group convo')
}
@@ -1126,7 +1144,7 @@ export class Convo {
}
updateLockStatus(
lockStatus: ChatBskyConvoDefs.ConvoLockStatus,
lockStatus: chat.bsky.convo.defs.ConvoLockStatus,
lockStatusModerationOverride: boolean,
) {
if (this.convo?.kind !== 'group') {
@@ -1165,14 +1183,10 @@ export class Convo {
const {id, message} = pendingMessage
const response = await this.agent.chat.bsky.convo.sendMessage(
{
convoId: this.convoId,
message,
},
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
)
const res = response.data
const res = await this.chatClient.call(chat.bsky.convo.sendMessage, {
convoId: this.convoId,
message,
})
// remove from queue
this.pendingMessages.delete(id)
@@ -1197,9 +1211,18 @@ export class Convo {
}
}
private handleSendMessageFailure(e: Error | XRPCError) {
if (e instanceof XRPCError) {
if (NETWORK_FAILURE_STATUSES.includes(e.status)) {
private handleSendMessageFailure(e: Error) {
const status = getErrorStatus(e)
if (isXrpcError(e)) {
/*
* A status-less xrpc error is a network/transport failure (lex throws
* `XrpcInternalError`, which carries no HTTP status). The old bridge
* represented the same case with a sentinel `status` of `1`, which is a
* member of `NETWORK_FAILURE_STATUSES` - so a network failure was
* `recoverable`. Preserve that by treating `undefined` status the same
* as a network-failure status here.
*/
if (status === undefined || NETWORK_FAILURE_STATUSES.includes(status)) {
this.pendingMessageFailure = 'recoverable'
} else {
this.pendingMessageFailure = 'unrecoverable'
@@ -1226,7 +1249,7 @@ export class Convo {
default:
if (!isNetworkError(e)) {
logger.warn(`handleSendMessageFailure could not handle error`, {
status: e.status,
status,
message: e.message,
})
}
@@ -1261,16 +1284,15 @@ export class Convo {
)
try {
const {data} = await this.agent.chat.bsky.convo.sendMessageBatch(
const {items} = await this.chatClient.call(
chat.bsky.convo.sendMessageBatch,
{
items: messageArray.map(({message}) => ({
convoId: this.convoId,
message,
})),
},
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
)
const {items} = data
/*
* Insert into `newMessages` as soon as we have a real ID. That way, when
@@ -1304,13 +1326,10 @@ export class Convo {
try {
await networkRetry(2, () => {
return this.agent.chat.bsky.convo.deleteMessageForSelf(
{
convoId: this.convoId,
messageId,
},
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
)
return this.chatClient.call(chat.bsky.convo.deleteMessageForSelf, {
convoId: this.convoId,
messageId,
})
})
} catch (err) {
const e = err as Error
@@ -1341,11 +1360,11 @@ export class Convo {
* matching what the server returns on refresh.
*/
private tombstoneDeletedReplyTo(
m: ChatBskyConvoDefs.MessageView,
): ChatBskyConvoDefs.MessageView {
m: chat.bsky.convo.defs.MessageView,
): chat.bsky.convo.defs.MessageView {
const {replyTo} = m
if (
!ChatBskyConvoDefs.isMessageView(replyTo) ||
!bsky.isType(chat.bsky.convo.defs.messageView, replyTo) ||
!this.deletedMessages.has(replyTo.id)
) {
return m
@@ -1360,19 +1379,19 @@ export class Convo {
const items: ConvoItem[] = []
this.pastMessages.forEach(m => {
if (ChatBskyConvoDefs.isMessageView(m)) {
if (bsky.isType(chat.bsky.convo.defs.messageView, m)) {
items.unshift({
type: 'message',
key: m.id,
message: this.tombstoneDeletedReplyTo(m),
})
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
} else if (bsky.isType(chat.bsky.convo.defs.deletedMessageView, m)) {
items.unshift({
type: 'deleted-message',
key: m.id,
message: m,
})
} else if (ChatBskyConvoDefs.isSystemMessageView(m)) {
} else if (bsky.isType(chat.bsky.convo.defs.systemMessageView, m)) {
items.unshift({
type: 'system-message',
key: m.id,
@@ -1393,19 +1412,19 @@ export class Convo {
}
this.newMessages.forEach(m => {
if (ChatBskyConvoDefs.isMessageView(m)) {
if (bsky.isType(chat.bsky.convo.defs.messageView, m)) {
items.push({
type: 'message',
key: m.id,
message: this.tombstoneDeletedReplyTo(m),
})
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
} else if (bsky.isType(chat.bsky.convo.defs.deletedMessageView, m)) {
items.push({
type: 'deleted-message',
key: m.id,
message: m,
})
} else if (ChatBskyConvoDefs.isSystemMessageView(m)) {
} else if (bsky.isType(chat.bsky.convo.defs.systemMessageView, m)) {
items.push({
type: 'system-message',
key: m.id,
@@ -1429,7 +1448,9 @@ export class Convo {
$type: 'chat.bsky.convo.defs#messageView',
id: nanoid(),
rev: '__fake__',
sentAt: new Date().toISOString(),
// ISO string is a valid datetime; assert the branded type the
// generated MessageView expects for this optimistic-only value.
sentAt: new Date().toISOString() as DatetimeString,
sender: {
$type: 'chat.bsky.convo.defs#messageViewSender',
did: this.senderUserDid,
@@ -1471,16 +1492,18 @@ export class Convo {
* @param emoji - must be one grapheme
*/
async addReaction(messageId: string, emoji: string) {
const optimisticReaction = {
const optimisticReaction: chat.bsky.convo.defs.ReactionView = {
value: emoji,
sender: {did: this.senderUserDid},
createdAt: new Date().toISOString(),
// ISO string is a valid datetime; assert the branded type the generated
// ReactionView expects for this optimistic-only value.
createdAt: new Date().toISOString() as DatetimeString,
}
let restore: null | (() => void) = null
if (this.pastMessages.has(messageId)) {
const prevMessage = this.pastMessages.get(messageId)
if (
ChatBskyConvoDefs.isMessageView(prevMessage) &&
bsky.isType(chat.bsky.convo.defs.messageView, prevMessage) &&
// skip optimistic update if reaction already exists
!prevMessage.reactions?.find(
reaction =>
@@ -1510,7 +1533,7 @@ export class Convo {
} else if (this.newMessages.has(messageId)) {
const prevMessage = this.newMessages.get(messageId)
if (
ChatBskyConvoDefs.isMessageView(prevMessage) &&
bsky.isType(chat.bsky.convo.defs.messageView, prevMessage) &&
!prevMessage.reactions?.find(reaction => reaction.value === emoji)
) {
if (prevMessage.reactions && prevMessage.reactions.length >= 5)
@@ -1529,11 +1552,12 @@ export class Convo {
try {
logger.debug(`Adding reaction ${emoji} to message ${messageId}`)
const {data} = await this.agent.chat.bsky.convo.addReaction(
{messageId, value: emoji, convoId: this.convoId},
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
)
if (ChatBskyConvoDefs.isMessageView(data.message)) {
const data = await this.chatClient.call(chat.bsky.convo.addReaction, {
messageId,
value: emoji,
convoId: this.convoId,
})
if (bsky.isType(chat.bsky.convo.defs.messageView, data.message)) {
if (this.pastMessages.has(messageId)) {
this.pastMessages.set(messageId, data.message)
this.commit()
@@ -1558,7 +1582,7 @@ export class Convo {
let restore: null | (() => void) = null
if (this.pastMessages.has(messageId)) {
const prevMessage = this.pastMessages.get(messageId)
if (ChatBskyConvoDefs.isMessageView(prevMessage)) {
if (bsky.isType(chat.bsky.convo.defs.messageView, prevMessage)) {
this.pastMessages.set(messageId, {
...prevMessage,
reactions: prevMessage.reactions?.filter(
@@ -1575,7 +1599,7 @@ export class Convo {
}
} else if (this.newMessages.has(messageId)) {
const prevMessage = this.newMessages.get(messageId)
if (ChatBskyConvoDefs.isMessageView(prevMessage)) {
if (bsky.isType(chat.bsky.convo.defs.messageView, prevMessage)) {
this.newMessages.set(messageId, {
...prevMessage,
reactions: prevMessage.reactions?.filter(
@@ -1594,10 +1618,11 @@ export class Convo {
try {
logger.debug(`Removing reaction ${emoji} from message ${messageId}`)
await this.agent.chat.bsky.convo.removeReaction(
{messageId, value: emoji, convoId: this.convoId},
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
)
await this.chatClient.call(chat.bsky.convo.removeReaction, {
messageId,
value: emoji,
convoId: this.convoId,
})
} catch (error) {
if (restore) restore()
throw error
+19 -14
View File
@@ -6,7 +6,6 @@ import {
useState,
useSyncExternalStore,
} from 'react'
import {ChatBskyConvoDefs} from '@atproto/api'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
@@ -28,15 +27,20 @@ import {
} from '#/state/queries/messages/conversation'
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {type GroupConvoMember} from '#/components/dms/util'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
export * from '#/state/messages/convo/util'
function membersChanged(
a: ChatBskyConvoDefs.ConvoView['members'],
b: ChatBskyConvoDefs.ConvoView['members'],
) {
/*
* Only the member dids are compared, so accept any member-shaped arrays. This
* also lets the old-typed `convo.convo.members` (from `#/components/dms/util`,
* migrates in a later task) flow in alongside the new lexicon ConvoView
* members.
*/
function membersChanged(a: Array<{did: string}>, b: Array<{did: string}>) {
if (a.length !== b.length) return true
const aDids = new Set(a.map(m => m.did))
return b.some(m => !aDids.has(m.did))
@@ -80,15 +84,16 @@ export function ConvoProvider({
convoId,
}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
const events = useMessagesEventBus()
const [convo] = useState(() => {
const placeholder = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
getConvoKey(convoId),
)
const placeholder =
queryClient.getQueryData<chat.bsky.convo.defs.ConvoView>(
getConvoKey(convoId),
)
return new Convo({
convoId,
agent,
chatClient,
events,
placeholderData: placeholder ? {convo: placeholder} : undefined,
})
@@ -141,14 +146,14 @@ export function ConvoProvider({
const queryKey = event.query.queryKey as string[]
if (queryKey[0] === root && queryKey[1] === id) {
const data = event.query.state.data as
| ChatBskyConvoDefs.ConvoView
| chat.bsky.convo.defs.ConvoView
| undefined
if (data && convo.convo && data.muted !== convo.convo.view.muted) {
convo.updateMuted(data.muted)
}
if (
data &&
ChatBskyConvoDefs.isGroupConvo(data.kind) &&
bsky.isType(chat.bsky.convo.defs.groupConvo, data.kind) &&
convo.convo?.kind === 'group'
) {
if (data.kind.name !== convo.convo.details.name) {
@@ -170,7 +175,7 @@ export function ConvoProvider({
}
if (
data &&
ChatBskyConvoDefs.isGroupConvo(data.kind) &&
bsky.isType(chat.bsky.convo.defs.groupConvo, data.kind) &&
convo.convo?.kind === 'group' &&
(membersChanged(data.members, convo.convo.members) ||
data.kind.memberCount !== convo.convo.details.memberCount)
+17 -23
View File
@@ -1,22 +1,16 @@
import {
type $Typed,
type AppBskyEmbedRecord,
type ChatBskyActorDefs,
type ChatBskyConvoDefs,
type ChatBskyConvoSendMessage,
type ChatBskyEmbedJoinLink,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {type Client} from '@atproto/lex-client'
import {type MessagesEventBus} from '#/state/messages/events/agent'
import {type SessionAgent} from '#/state/session'
import {type ConvoWithDetails} from '#/components/dms/util'
import {app, type chat} from '#/lexicons'
export type ConvoParams = {
convoId: string
agent: SessionAgent
chatClient: Client
events: MessagesEventBus
placeholderData?: {
convo: ChatBskyConvoDefs.ConvoView
convo: chat.bsky.convo.defs.ConvoView
}
}
@@ -74,12 +68,12 @@ export type ConvoItem =
| {
type: 'message'
key: string
message: ChatBskyConvoDefs.MessageView
message: chat.bsky.convo.defs.MessageView
}
| {
type: 'pending-message'
key: string
message: ChatBskyConvoDefs.MessageView
message: chat.bsky.convo.defs.MessageView
failed: boolean
/**
* Retry sending the message. If present, the message is in a failed state.
@@ -89,12 +83,12 @@ export type ConvoItem =
| {
type: 'deleted-message'
key: string
message: ChatBskyConvoDefs.DeletedMessageView
message: chat.bsky.convo.defs.DeletedMessageView
}
| {
type: 'system-message'
key: string
message: ChatBskyConvoDefs.SystemMessageView
message: chat.bsky.convo.defs.SystemMessageView
}
| {
type: 'error'
@@ -108,12 +102,12 @@ export type ConvoItem =
type DeleteMessage = (messageId: string) => Promise<void>
type SendMessage = (
message: ChatBskyConvoSendMessage.InputSchema['message'],
message: chat.bsky.convo.sendMessage.$InputBody['message'],
optimisticEmbedView:
| $Typed<AppBskyEmbedRecord.View>
| $Typed<ChatBskyEmbedJoinLink.View>
| $Typed<app.bsky.embed.record.View>
| $Typed<chat.bsky.embed.joinLink.View>
| undefined,
optimisticReplyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
optimisticReplyTo?: $Typed<chat.bsky.convo.defs.MessageView>,
) => void
type FetchMessageHistory = () => Promise<void>
type MarkConvoAccepted = () => void
@@ -152,7 +146,7 @@ export type ConvoStateReady = {
status: ConvoStatus.Ready
items: ConvoItem[]
convo: ConvoWithDetails
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
relatedProfiles: Map<string, chat.bsky.actor.defs.ProfileViewBasic>
error: undefined
isFetchingHistory: boolean
hasAllHistory: boolean
@@ -167,7 +161,7 @@ export type ConvoStateBackgrounded = {
status: ConvoStatus.Backgrounded
items: ConvoItem[]
convo: ConvoWithDetails
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
relatedProfiles: Map<string, chat.bsky.actor.defs.ProfileViewBasic>
error: undefined
isFetchingHistory: boolean
hasAllHistory: boolean
@@ -182,7 +176,7 @@ export type ConvoStateSuspended = {
status: ConvoStatus.Suspended
items: ConvoItem[]
convo: ConvoWithDetails
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
relatedProfiles: Map<string, chat.bsky.actor.defs.ProfileViewBasic>
error: undefined
isFetchingHistory: boolean
hasAllHistory: boolean
@@ -211,7 +205,7 @@ export type ConvoStateDisabled = {
status: ConvoStatus.Disabled
items: ConvoItem[]
convo: ConvoWithDetails
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
relatedProfiles: Map<string, chat.bsky.actor.defs.ProfileViewBasic>
error: undefined
isFetchingHistory: boolean
hasAllHistory: boolean
+11 -18
View File
@@ -1,9 +1,8 @@
import {type ChatBskyConvoGetLog} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {
isErrorMaybeAppPasswordPermissions,
isNetworkError,
@@ -21,14 +20,14 @@ import {
type MessagesEventBusParams,
MessagesEventBusStatus,
} from '#/state/messages/events/types'
import {type SessionAgent} from '#/state/session'
import {chat} from '#/lexicons'
const logger = Logger.create(Logger.Context.DMsAgent)
export class MessagesEventBus {
private id: string
private agent: SessionAgent
private chatClient: Client
private emitter = new EventEmitter<{event: [MessagesEventBusEvent]}>()
private status: MessagesEventBusStatus = MessagesEventBusStatus.Initializing
@@ -38,7 +37,7 @@ export class MessagesEventBus {
constructor(params: MessagesEventBusParams) {
this.id = nanoid(3)
this.agent = params.agent
this.chatClient = params.chatClient
this.init()
}
@@ -261,14 +260,11 @@ export class MessagesEventBus {
try {
const response = await networkRetry(2, () => {
return this.agent.chat.bsky.convo.getLog(
{},
{headers: DM_SERVICE_HEADERS},
)
return this.chatClient.call(chat.bsky.convo.getLog, {})
})
// throw new Error('UNCOMMENT TO TEST INIT FAILURE')
const {cursor} = response.data
const {cursor} = response
// should always be defined
if (cursor) {
@@ -356,21 +352,18 @@ export class MessagesEventBus {
// )
let needsEmit = false
let batch: ChatBskyConvoGetLog.OutputSchema['logs'] = []
let batch: chat.bsky.convo.getLog.$OutputBody['logs'] = []
try {
const response = await networkRetry(2, () => {
return this.agent.chat.bsky.convo.getLog(
{
cursor: this.latestRev,
},
{headers: DM_SERVICE_HEADERS},
)
return this.chatClient.call(chat.bsky.convo.getLog, {
cursor: this.latestRev,
})
})
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
const {logs: events} = response.data
const {logs: events} = response
for (const ev of events) {
/*
+3 -3
View File
@@ -2,7 +2,7 @@ import {createContext, useContext, useEffect, useState} from 'react'
import {AppState} from 'react-native'
import {MessagesEventBus} from '#/state/messages/events/agent'
import {useAgent, useSession} from '#/state/session'
import {useChatClient, useSession} from '#/state/session'
const MessagesEventBusContext = createContext<MessagesEventBus | null>(null)
MessagesEventBusContext.displayName = 'MessagesEventBusContext'
@@ -42,11 +42,11 @@ export function MessagesEventBusProviderInner({
}: {
children: React.ReactNode
}) {
const agent = useAgent()
const chatClient = useChatClient()
const [bus] = useState(
() =>
new MessagesEventBus({
agent,
chatClient,
}),
)
+4 -4
View File
@@ -1,9 +1,9 @@
import {type ChatBskyConvoGetLog} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {type SessionAgent} from '#/state/session'
import {type chat} from '#/lexicons'
export type MessagesEventBusParams = {
agent: SessionAgent
chatClient: Client
}
export enum MessagesEventBusStatus {
@@ -66,5 +66,5 @@ export type MessagesEventBusEvent =
}
| {
type: 'logs'
logs: ChatBskyConvoGetLog.OutputSchema['logs']
logs: chat.bsky.convo.getLog.$OutputBody['logs']
}
+18 -5
View File
@@ -1,5 +1,6 @@
import {createContext, useContext, useMemo} from 'react'
import {AtpAgent, type ModerationOpts} from '@atproto/api'
import {Client} from '@atproto/lex-client'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const'
@@ -38,18 +39,30 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return undefined
}
return {
userDid,
/*
* `did`/`hiddenPosts` come from persisted storage typed as plain
* `string`, so brand them to the SDK's `DidString`/`AtUriString` slots.
*/
userDid: userDid as ModerationOpts['userDid'],
prefs: {
...moderationPrefs,
labelers: moderationPrefs.labelers.length
? moderationPrefs.labelers
: AtpAgent.appLabelers.map(did => ({
: Client.appLabelers.map(did => ({
did,
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
})),
hiddenPosts: hiddenPosts || [],
hiddenPosts: (hiddenPosts ||
[]) as ModerationOpts['prefs']['hiddenPosts'],
},
labelDefs,
/*
* TODO(phase4): drop this cast once `#/state/preferences/label-defs`
* flips its `InterpretedLabelValueDefinition` source from `@atproto/api`
* to `@bsky.app/sdk/moderation`. The value is already produced by the
* SDK's `interpretLabelValueDefinitions` (see `../queries/preferences`);
* only the intermediate context type is still old-world.
*/
labelDefs: labelDefs,
}
}, [override, userDid, labelDefs, moderationPrefs, hiddenPosts])
+22 -23
View File
@@ -1,8 +1,4 @@
import {
type AppBskyActorDefs,
type AppBskyNotificationDeclaration,
type AppBskyNotificationListActivitySubscriptions,
} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {t} from '@lingui/core/macro'
import {
type InfiniteData,
@@ -13,23 +9,25 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import * as Toast from '#/components/Toast'
import {app} from '#/lexicons'
export const RQKEY_getActivitySubscriptions = ['activity-subscriptions']
export const RQKEY_getNotificationDeclaration = ['notification-declaration']
export function useActivitySubscriptionsQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery({
queryKey: RQKEY_getActivitySubscriptions,
queryFn: async ({pageParam}) => {
const response =
await agent.app.bsky.notification.listActivitySubscriptions({
return await client.call(
app.bsky.notification.listActivitySubscriptions,
{
cursor: pageParam,
})
return response.data
},
)
},
initialPageParam: undefined as string | undefined,
getNextPageParam: prev => prev.cursor,
@@ -37,14 +35,14 @@ export function useActivitySubscriptionsQuery() {
}
export function useNotificationDeclarationQuery() {
const agent = useAgent()
const client = usePdsClient()
const {currentAccount} = useSession()
return useQuery({
queryKey: RQKEY_getNotificationDeclaration,
queryFn: async () => {
try {
const response = await agent.app.bsky.notification.declaration.get({
repo: currentAccount!.did,
const response = await client.get(app.bsky.notification.declaration, {
repo: currentAccount!.did as AtIdentifierString,
rkey: 'self',
})
return response
@@ -57,7 +55,7 @@ export function useNotificationDeclarationQuery() {
value: {
$type: 'app.bsky.notification.declaration',
allowSubscriptions: 'followers',
} satisfies AppBskyNotificationDeclaration.Record,
} satisfies app.bsky.notification.declaration.Main,
}
} else {
throw err
@@ -68,17 +66,18 @@ export function useNotificationDeclarationQuery() {
}
export function useNotificationDeclarationMutation() {
const agent = useAgent()
const client = usePdsClient()
const {currentAccount} = useSession()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (record: AppBskyNotificationDeclaration.Record) => {
const response = await agent.app.bsky.notification.declaration.put(
mutationFn: async (record: app.bsky.notification.declaration.Main) => {
const response = await client.put(
app.bsky.notification.declaration,
record,
{
repo: currentAccount!.did,
repo: currentAccount!.did as AtIdentifierString,
rkey: 'self',
},
record,
)
return response
},
@@ -88,7 +87,7 @@ export function useNotificationDeclarationMutation() {
(old?: {
uri: string
cid: string
value: AppBskyNotificationDeclaration.Record
value: app.bsky.notification.declaration.Main
}) => {
if (!old) return old
return {
@@ -109,9 +108,9 @@ export function useNotificationDeclarationMutation() {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyNotificationListActivitySubscriptions.OutputSchema>
InfiniteData<app.bsky.notification.listActivitySubscriptions.$OutputBody>
>({
queryKey: RQKEY_getActivitySubscriptions,
})
+14 -17
View File
@@ -1,15 +1,12 @@
import {useCallback} from 'react'
import {
type AppBskyActorDefs,
moderateProfile,
type ModerationOpts,
} from '@atproto/api'
import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences'
@@ -27,7 +24,7 @@ export function useActorAutocompleteQuery(
limit?: number,
) {
const moderationOpts = useModerationOpts()
const agent = useAgent()
const client = useAppviewClient()
prefix = prefix.toLowerCase().trim()
if (prefix.endsWith('.')) {
@@ -35,20 +32,20 @@ export function useActorAutocompleteQuery(
prefix = prefix.slice(0, -1)
}
return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({
return useQuery<app.bsky.actor.defs.ProfileViewBasic[]>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(prefix || ''),
async queryFn() {
const res = prefix
? await agent.searchActorsTypeahead({
? await client.call(app.bsky.actor.searchActorsTypeahead, {
q: prefix,
limit: limit || 8,
})
: undefined
return res?.data.actors || []
return res?.actors || []
},
select: useCallback(
(data: AppBskyActorDefs.ProfileViewBasic[]) => {
(data: app.bsky.actor.defs.ProfileViewBasic[]) => {
return computeSuggestions({
q: prefix,
searched: data,
@@ -65,7 +62,7 @@ export type ActorAutocompleteFn = ReturnType<typeof useActorAutocompleteFn>
export function useActorAutocompleteFn() {
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({query, limit = 8}: {query: string; limit?: number}) => {
@@ -77,7 +74,7 @@ export function useActorAutocompleteFn() {
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(query || ''),
queryFn: () =>
agent.searchActorsTypeahead({
client.call(app.bsky.actor.searchActorsTypeahead, {
q: query,
limit,
}),
@@ -91,11 +88,11 @@ export function useActorAutocompleteFn() {
return computeSuggestions({
q: query,
searched: res?.data.actors,
searched: res?.actors,
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
})
},
[queryClient, moderationOpts, agent],
[queryClient, moderationOpts, client],
)
}
@@ -105,10 +102,10 @@ function computeSuggestions({
moderationOpts,
}: {
q?: string
searched?: AppBskyActorDefs.ProfileViewBasic[]
searched?: app.bsky.actor.defs.ProfileViewBasic[]
moderationOpts: ModerationOpts
}) {
let items: AppBskyActorDefs.ProfileViewBasic[] = []
let items: app.bsky.actor.defs.ProfileViewBasic[] = []
for (const item of searched) {
if (!items.find(item2 => item2.handle === item.handle)) {
items.push(item)
+8 -9
View File
@@ -1,4 +1,3 @@
import {type AppBskyActorSearchActors} from '@atproto/api'
import {
type InfiniteData,
keepPreviousData,
@@ -8,7 +7,8 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string, limit?: number) => [
@@ -28,23 +28,22 @@ export function useActorSearch({
maintainData?: boolean
limit?: number
}) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyActorSearchActors.OutputSchema,
app.bsky.actor.searchActors.$OutputBody,
Error,
InfiniteData<AppBskyActorSearchActors.OutputSchema>,
InfiniteData<app.bsky.actor.searchActors.$OutputBody>,
QueryKey,
string | undefined
>({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(query, limit),
queryFn: async ({pageParam}) => {
const res = await agent.searchActors({
return await client.call(app.bsky.actor.searchActors, {
q: query,
limit,
cursor: pageParam,
})
return res.data
},
enabled: enabled && !!query,
initialPageParam: undefined,
@@ -54,7 +53,7 @@ export function useActorSearch({
})
}
function select(data: InfiniteData<AppBskyActorSearchActors.OutputSchema>) {
function select(data: InfiniteData<app.bsky.actor.searchActors.$OutputBody>) {
// enforce uniqueness
const dids = new Set()
@@ -77,7 +76,7 @@ export function* findAllProfilesInQueryData(
did: string,
) {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyActorSearchActors.OutputSchema>
InfiniteData<app.bsky.actor.searchActors.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+13 -15
View File
@@ -1,39 +1,37 @@
import {type ComAtprotoServerCreateAppPassword} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '../session'
import {com} from '#/lexicons'
import {usePdsClient} from '../session'
const RQKEY_ROOT = 'app-passwords'
export const RQKEY = () => [RQKEY_ROOT]
export function useAppPasswordsQuery() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: RQKEY(),
queryFn: async () => {
const res = await agent.com.atproto.server.listAppPasswords({})
return res.data.passwords
const data = await pdsClient.call(com.atproto.server.listAppPasswords, {})
return data.passwords
},
})
}
export function useAppPasswordCreateMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<
ComAtprotoServerCreateAppPassword.OutputSchema,
com.atproto.server.createAppPassword.$OutputBody,
Error,
{name: string; privileged: boolean}
>({
mutationFn: async ({name, privileged}) => {
return (
await agent.com.atproto.server.createAppPassword({
name,
privileged,
})
).data
return await pdsClient.call(com.atproto.server.createAppPassword, {
name,
privileged,
})
},
onSuccess() {
queryClient.invalidateQueries({
@@ -45,10 +43,10 @@ export function useAppPasswordCreateMutation() {
export function useAppPasswordDeleteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<void, Error, {name: string}>({
mutationFn: async ({name}) => {
await agent.com.atproto.server.revokeAppPassword({
await pdsClient.call(com.atproto.server.revokeAppPassword, {
name,
})
},
@@ -1,4 +1,4 @@
import {type AppBskyFeedDefs} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {isNetworkError} from '#/lib/strings/errors'
@@ -8,10 +8,11 @@ import {
optimisticallyDeleteBookmark,
optimisticallySaveBookmark,
} from '#/state/queries/bookmarks/useBookmarksQuery'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
type MutationArgs =
| {action: 'create'; post: AppBskyFeedDefs.PostView}
| {action: 'create'; post: app.bsky.feed.defs.PostView}
| {
action: 'delete'
/**
@@ -23,20 +24,20 @@ type MutationArgs =
export function useBookmarkMutation() {
const qc = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useMutation({
async mutationFn(args: MutationArgs) {
if (args.action === 'create') {
updatePostShadow(qc, args.post.uri, {bookmarked: true})
await agent.app.bsky.bookmark.createBookmark({
await client.call(app.bsky.bookmark.createBookmark, {
uri: args.post.uri,
cid: args.post.cid,
})
} else if (args.action === 'delete') {
updatePostShadow(qc, args.uri, {bookmarked: false})
await agent.app.bsky.bookmark.deleteBookmark({
uri: args.uri,
await client.call(app.bsky.bookmark.deleteBookmark, {
uri: args.uri as AtUriString,
})
}
},
@@ -1,9 +1,5 @@
import {
type $Typed,
type AppBskyBookmarkGetBookmarks,
AppBskyFeedDefs,
AtUri,
} from '@atproto/api'
import {type l} from '@atproto/lex'
import {AtUri} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -16,28 +12,28 @@ import {
embedViewRecordToPostView,
getEmbeddedPost,
} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
export const bookmarksQueryKeyRoot = 'bookmarks'
export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot]
export function useBookmarksQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyBookmarkGetBookmarks.OutputSchema,
app.bsky.bookmark.getBookmarks.$OutputBody,
Error,
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>,
InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>,
QueryKey,
string | undefined
>({
queryKey: createBookmarksQueryKey(),
async queryFn({pageParam}) {
const res = await agent.app.bsky.bookmark.getBookmarks({
return await client.call(app.bsky.bookmark.getBookmarks, {
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -45,7 +41,7 @@ export function useBookmarksQuery() {
}
export async function truncateAndInvalidate(qc: QueryClient) {
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
{queryKey: [bookmarksQueryKeyRoot]},
data => {
if (data) {
@@ -62,9 +58,9 @@ export async function truncateAndInvalidate(qc: QueryClient) {
export async function optimisticallySaveBookmark(
qc: QueryClient,
post: AppBskyFeedDefs.PostView,
post: app.bsky.feed.defs.PostView,
) {
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
{
queryKey: [bookmarksQueryKeyRoot],
},
@@ -75,19 +71,17 @@ export async function optimisticallySaveBookmark(
pages: data.pages.map((page, index) => {
if (index === 0) {
post.$type = 'app.bsky.feed.defs#postView'
const bookmark: app.bsky.bookmark.defs.BookmarkView = {
createdAt: new Date().toISOString() as l.DatetimeString,
subject: {
uri: post.uri,
cid: post.cid,
},
item: post as l.$Typed<app.bsky.feed.defs.PostView>,
}
return {
...page,
bookmarks: [
{
createdAt: new Date().toISOString(),
subject: {
uri: post.uri,
cid: post.cid,
},
item: post as $Typed<AppBskyFeedDefs.PostView>,
},
...page.bookmarks,
],
bookmarks: [bookmark, ...page.bookmarks],
}
}
return page
@@ -101,7 +95,7 @@ export async function optimisticallyDeleteBookmark(
qc: QueryClient,
{uri}: {uri: string},
) {
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
{
queryKey: [bookmarksQueryKeyRoot],
},
@@ -123,9 +117,9 @@ export async function optimisticallyDeleteBookmark(
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
): Generator<app.bsky.feed.defs.PostView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>
InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>
>({
queryKey: [bookmarksQueryKeyRoot],
})
@@ -137,13 +131,7 @@ export function* findAllPostsInQueryData(
}
for (const page of queryData?.pages) {
for (const bookmark of page.bookmarks) {
if (
!bsky.dangerousIsType<AppBskyFeedDefs.PostView>(
bookmark.item,
AppBskyFeedDefs.isPostView,
)
)
continue
if (!bsky.isType(app.bsky.feed.defs.postView, bookmark.item)) continue
if (didOrHandleUriMatches(atUri, bookmark.item)) {
yield bookmark.item
+41 -28
View File
@@ -1,10 +1,6 @@
import {useMemo, useRef} from 'react'
import {
type AppBskyActorDefs,
AppBskyFeedDefs,
AtUri,
moderatePost,
} from '@atproto/api'
import {AtUri} from '@atproto/syntax'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {
@@ -28,7 +24,9 @@ import {
embedViewRecordToPostView,
getEmbeddedPost,
} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
const RQKEY_ROOT = 'feed-previews'
const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
@@ -85,7 +83,7 @@ export type FeedPreviewItem =
| {
type: 'preview:header'
key: string
feed: AppBskyFeedDefs.GeneratorView
feed: app.bsky.feed.defs.GeneratorView
}
| {
type: 'preview:footer'
@@ -97,7 +95,7 @@ export type FeedPreviewItem =
key: string
slice: FeedPostSlice
indexInSlice: number
feed: AppBskyFeedDefs.GeneratorView
feed: app.bsky.feed.defs.GeneratorView
showReplyTo: boolean
hideTopBorder: boolean
}
@@ -108,7 +106,7 @@ export type FeedPreviewItem =
}
export function useFeedPreviews(
feedsMaybeWithDuplicates: AppBskyFeedDefs.GeneratorView[],
feedsMaybeWithDuplicates: app.bsky.feed.defs.GeneratorView[],
isEnabled: boolean = true,
) {
const feeds = useMemo(
@@ -121,7 +119,7 @@ export function useFeedPreviews(
const uris = feeds.map(feed => feed.uri)
const {_} = useLingui()
const agent = useAgent()
const client = useAppviewClient()
const {data: preferences} = usePreferencesQuery()
const userInterests = aggregateUserInterests(preferences)
const moderationOpts = useModerationOpts()
@@ -130,8 +128,8 @@ export function useFeedPreviews(
const processedPageCache = useRef(
new Map<
{
feed: AppBskyFeedDefs.GeneratorView
posts: AppBskyFeedDefs.FeedViewPost[]
feed: app.bsky.feed.defs.GeneratorView
posts: app.bsky.feed.defs.FeedViewPost[]
},
FeedPreviewItem[]
>(),
@@ -143,7 +141,7 @@ export function useFeedPreviews(
queryFn: async ({pageParam}) => {
const feed = feeds[pageParam]
const api = new CustomFeedAPI({
agent,
client,
feedParams: {feed: feed.uri},
userInterests,
})
@@ -207,7 +205,12 @@ export function useFeedPreviews(
if (item.isFallbackMarker) continue
const moderations = item.items.map(item =>
moderatePost(item.post, moderationOpts!),
// TODO(phase4): drop toLex once feed-manip is migrated off
// @atproto/api and yields lex-typed slice items.
moderatePost(
bsky.toLex<app.bsky.feed.defs.PostView>(item.post),
moderationOpts!,
),
)
// apply moderation filters
@@ -233,10 +236,20 @@ export function useFeedPreviews(
const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
uri: subItem.post.uri,
post: subItem.post,
record: subItem.record,
// TODO(phase4): drop toLex once feed-manip is migrated
// off @atproto/api and yields lex-typed slice items.
post: bsky.toLex<app.bsky.feed.defs.PostView>(
subItem.post,
),
record: bsky.toLex<app.bsky.feed.post.Main>(
subItem.record,
),
moderation: moderations[i],
parentAuthor: subItem.parentAuthor,
parentAuthor: subItem.parentAuthor
? bsky.toLex<app.bsky.actor.defs.ProfileViewBasic>(
subItem.parentAuthor,
)
: undefined,
isParentBlocked: subItem.isParentBlocked,
isParentNotFound: subItem.isParentNotFound,
}
@@ -349,13 +362,13 @@ export function useFeedPreviews(
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
): Generator<app.bsky.feed.defs.PostView, undefined> {
const atUri = new AtUri(uri)
const queryDatas = queryClient.getQueriesData<
InfiniteData<{
feed: AppBskyFeedDefs.GeneratorView
posts: AppBskyFeedDefs.FeedViewPost[]
feed: app.bsky.feed.defs.GeneratorView
posts: app.bsky.feed.defs.FeedViewPost[]
}>
>({
queryKey: [RQKEY_ROOT],
@@ -375,7 +388,7 @@ export function* findAllPostsInQueryData(
yield embedViewRecordToPostView(quotedPost)
}
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent)) {
if (didOrHandleUriMatches(atUri, item.reply.parent)) {
yield item.reply.parent
}
@@ -389,7 +402,7 @@ export function* findAllPostsInQueryData(
}
}
if (AppBskyFeedDefs.isPostView(item.reply?.root)) {
if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.root)) {
if (didOrHandleUriMatches(atUri, item.reply.root)) {
yield item.reply.root
}
@@ -407,11 +420,11 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
): Generator<app.bsky.actor.defs.ProfileViewBasic, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<{
feed: AppBskyFeedDefs.GeneratorView
posts: AppBskyFeedDefs.FeedViewPost[]
feed: app.bsky.feed.defs.GeneratorView
posts: app.bsky.feed.defs.FeedViewPost[]
}>
>({
queryKey: [RQKEY_ROOT],
@@ -430,13 +443,13 @@ export function* findAllProfilesInQueryData(
yield quotedPost.author
}
if (
AppBskyFeedDefs.isPostView(item.reply?.parent) &&
bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent) &&
item.reply?.parent?.author.did === did
) {
yield item.reply.parent.author
}
if (
AppBskyFeedDefs.isPostView(item.reply?.root) &&
bsky.isType(app.bsky.feed.defs.postView, item.reply?.root) &&
item.reply?.root?.author.did === did
) {
yield item.reply.root.author
+84 -76
View File
@@ -1,13 +1,7 @@
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
type AppBskyGraphDefs,
type AppBskyUnspeccedGetPopularFeedGenerators,
AtUri,
moderateFeedGenerator,
RichText,
} from '@atproto/api'
import {AtUri, type AtUriString} from '@atproto/syntax'
import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {RichText} from '@bsky.app/sdk/richtext'
import {t} from '@lingui/core/macro'
import {
type InfiniteData,
@@ -26,7 +20,8 @@ import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {app} from '#/lexicons'
import {router} from '#/routes'
import {useModerationOpts} from '../preferences/moderation-opts'
import {type FeedDescriptor} from './post-feed'
@@ -34,7 +29,7 @@ import {precacheResolvedUri} from './resolve-uri'
export type FeedSourceFeedInfo = {
type: 'feed'
view?: AppBskyFeedDefs.GeneratorView
view?: app.bsky.feed.defs.GeneratorView
uri: string
feedDescriptor: FeedDescriptor
route: {
@@ -51,12 +46,12 @@ export type FeedSourceFeedInfo = {
likeCount: number | undefined
acceptsInteractions?: boolean
likeUri: string | undefined
contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
contentMode: app.bsky.feed.defs.GeneratorView['contentMode']
}
export type FeedSourceListInfo = {
type: 'list'
view?: AppBskyGraphDefs.ListView
view?: app.bsky.graph.defs.ListView
uri: string
feedDescriptor: FeedDescriptor
route: {
@@ -93,7 +88,7 @@ const feedSourceNSIDs = {
}
export function hydrateFeedGenerator(
view: AppBskyFeedDefs.GeneratorView,
view: app.bsky.feed.defs.GeneratorView,
): FeedSourceInfo {
const urip = new AtUri(view.uri)
const collection =
@@ -135,7 +130,9 @@ export function hydrateFeedGenerator(
}
}
export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
export function hydrateList(
view: app.bsky.graph.defs.ListView,
): FeedSourceInfo {
const urip = new AtUri(view.uri)
const collection =
urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists'
@@ -184,7 +181,7 @@ export function getAvatarTypeFromUri(uri: string) {
export function useFeedSourceInfoQuery({uri}: {uri: string}) {
const type = getFeedTypeFromUri(uri)
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
staleTime: STALE.INFINITY,
@@ -193,14 +190,16 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
let view: FeedSourceInfo
if (type === 'feed') {
const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri})
view = hydrateFeedGenerator(res.data.view)
const res = await client.call(app.bsky.feed.getFeedGenerator, {
feed: uri as AtUriString,
})
view = hydrateFeedGenerator(res.view)
} else {
const res = await agent.app.bsky.graph.getList({
list: uri,
const res = await client.call(app.bsky.graph.getList, {
list: uri as AtUriString,
limit: 1,
})
view = hydrateList(res.data.list)
view = hydrateList(res.list)
}
return view
@@ -234,7 +233,7 @@ export function createGetPopularFeedsQueryKey(
export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
const {hasSession} = useSession()
const agent = useAgent()
const client = useAppviewClient()
const limit = options?.limit || 10
const {data: preferences} = usePreferencesQuery()
const queryClient = useQueryClient()
@@ -255,24 +254,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
enabled: Boolean(moderationOpts) && options?.enabled !== false,
queryKey: createGetPopularFeedsQueryKey(options),
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
limit,
cursor: pageParam,
})
const res = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit,
cursor: pageParam,
},
)
// precache feeds
for (const feed of res.data.feeds) {
for (const feed of res.feeds) {
const hydratedFeed = hydrateFeedGenerator(feed)
precacheFeed(queryClient, hydratedFeed)
}
return res.data
return res
},
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor,
select: useCallback(
(
data: InfiniteData<AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema>,
data: InfiniteData<app.bsky.unspecced.getPopularFeedGenerators.$OutputBody>,
) => {
const {
savedFeeds,
@@ -336,24 +338,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
}
export function useSearchPopularFeedsMutation() {
const agent = useAgent()
const client = useAppviewClient()
const moderationOpts = useModerationOpts()
return useMutation({
mutationFn: async (query: string) => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
limit: 10,
query: query,
})
const res = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit: 10,
query: query,
},
)
if (moderationOpts) {
return res.data.feeds.filter(feed => {
return res.feeds.filter(feed => {
const decision = moderateFeedGenerator(feed, moderationOpts)
return !decision.ui('contentMedia').blur
})
}
return res.data.feeds
return res.feeds
},
})
}
@@ -371,7 +376,7 @@ export function usePopularFeedsSearch({
query: string
enabled?: boolean
}) {
const agent = useAgent()
const client = useAppviewClient()
const moderationOpts = useModerationOpts()
const enabledInner = enabled ?? Boolean(moderationOpts)
@@ -379,12 +384,15 @@ export function usePopularFeedsSearch({
enabled: enabledInner,
queryKey: createPopularFeedsSearchQueryKey(query),
queryFn: async () => {
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
limit: 15,
query: query,
})
const res = await client.call(
app.bsky.unspecced.getPopularFeedGenerators,
{
limit: 15,
query: query,
},
)
return res.data.feeds
return res.feeds
},
placeholderData: keepPreviousData,
select(data) {
@@ -397,7 +405,7 @@ export function usePopularFeedsSearch({
}
export type SavedFeedSourceInfo = FeedSourceInfo & {
savedFeed: AppBskyActorDefs.SavedFeed
savedFeed: app.bsky.actor.defs.SavedFeed
}
const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
@@ -444,7 +452,7 @@ const createPinnedFeedInfosQueryKey = (
export function usePinnedFeedsInfos() {
const {hasSession} = useSession()
const agent = useAgent()
const client = useAppviewClient()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
@@ -467,13 +475,13 @@ export function usePinnedFeedsInfos() {
const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed')
let feedsPromise = Promise.resolve()
if (pinnedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed
.getFeedGenerators({
feeds: pinnedFeeds.map(f => f.value),
feedsPromise = client
.call(app.bsky.feed.getFeedGenerators, {
feeds: pinnedFeeds.map(f => f.value as AtUriString),
})
.then(res => {
for (let i = 0; i < res.data.feeds.length; i++) {
const feedView = res.data.feeds[i]
for (let i = 0; i < res.feeds.length; i++) {
const feedView = res.feeds[i]
resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
}
})
@@ -482,13 +490,13 @@ export function usePinnedFeedsInfos() {
// Get all lists. This currently has to be done individually.
const pinnedLists = pinnedItems.filter(feed => feed.type === 'list')
const listsPromises = pinnedLists.map(list =>
agent.app.bsky.graph
.getList({
list: list.value,
client
.call(app.bsky.graph.getList, {
list: list.value as AtUriString,
limit: 1,
})
.then(res => {
const listView = res.data.list
const listView = res.list
resolved.set(listView.uri, hydrateList(listView))
}),
)
@@ -536,22 +544,22 @@ export function usePinnedFeedsInfos() {
export type SavedFeedItem =
| {
type: 'feed'
config: AppBskyActorDefs.SavedFeed
view: AppBskyFeedDefs.GeneratorView
config: app.bsky.actor.defs.SavedFeed
view: app.bsky.feed.defs.GeneratorView
}
| {
type: 'list'
config: AppBskyActorDefs.SavedFeed
view: AppBskyGraphDefs.ListView
config: app.bsky.actor.defs.SavedFeed
view: app.bsky.graph.defs.ListView
}
| {
type: 'timeline'
config: AppBskyActorDefs.SavedFeed
config: app.bsky.actor.defs.SavedFeed
view: undefined
}
export function useSavedFeeds() {
const agent = useAgent()
const client = useAppviewClient()
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const savedItems = preferences?.savedFeeds ?? []
const queryClient = useQueryClient()
@@ -574,33 +582,33 @@ export function useSavedFeeds() {
)
},
queryFn: async () => {
const resolvedFeeds = new Map<string, AppBskyFeedDefs.GeneratorView>()
const resolvedLists = new Map<string, AppBskyGraphDefs.ListView>()
const resolvedFeeds = new Map<string, app.bsky.feed.defs.GeneratorView>()
const resolvedLists = new Map<string, app.bsky.graph.defs.ListView>()
const savedFeeds = savedItems.filter(feed => feed.type === 'feed')
const savedLists = savedItems.filter(feed => feed.type === 'list')
let feedsPromise = Promise.resolve()
if (savedFeeds.length > 0) {
feedsPromise = agent.app.bsky.feed
.getFeedGenerators({
feeds: savedFeeds.map(f => f.value),
feedsPromise = client
.call(app.bsky.feed.getFeedGenerators, {
feeds: savedFeeds.map(f => f.value as AtUriString),
})
.then(res => {
res.data.feeds.forEach(f => {
res.feeds.forEach(f => {
resolvedFeeds.set(f.uri, f)
})
})
}
const listsPromises = savedLists.map(list =>
agent.app.bsky.graph
.getList({
list: list.value,
client
.call(app.bsky.graph.getList, {
list: list.value as AtUriString,
limit: 1,
})
.then(res => {
const listView = res.data.list
const listView = res.list
resolvedLists.set(listView.uri, listView)
}),
)
@@ -656,7 +664,7 @@ export function useSavedFeeds() {
const feedInfoQueryKeyRoot = 'feedInfo'
export function useFeedInfo(feedUri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
staleTime: STALE.INFINITY,
@@ -666,11 +674,11 @@ export function useFeedInfo(feedUri: string | undefined) {
return null
}
const res = await agent.app.bsky.feed.getFeedGenerator({
feed: feedUri,
const res = await client.call(app.bsky.feed.getFeedGenerator, {
feed: feedUri as AtUriString,
})
const feedSourceInfo = hydrateFeedGenerator(res.data.view)
const feedSourceInfo = hydrateFeedGenerator(res.view)
return feedSourceInfo
},
})
@@ -690,10 +698,10 @@ function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) {
export function precacheList(
queryClient: QueryClient,
list: AppBskyGraphDefs.ListView,
list: app.bsky.graph.defs.ListView,
) {
precacheResolvedUri(queryClient, list.creator.handle, list.creator.did)
queryClient.setQueryData<AppBskyGraphDefs.ListView>(
queryClient.setQueryData<app.bsky.graph.defs.ListView>(
listQueryKey(list.uri),
list,
)
@@ -701,7 +709,7 @@ export function precacheList(
export function precacheFeedFromGeneratorView(
queryClient: QueryClient,
view: AppBskyFeedDefs.GeneratorView,
view: app.bsky.feed.defs.GeneratorView,
) {
const hydratedFeed = hydrateFeedGenerator(view)
precacheFeed(queryClient, hydratedFeed)
+19 -22
View File
@@ -1,4 +1,3 @@
import {type AppBskyContactGetMatches} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -6,8 +5,9 @@ import {
useQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {type Match} from '#/components/contacts/state'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {STALE} from '.'
@@ -15,13 +15,12 @@ const RQ_KEY_ROOT = 'find-contacts'
export const findContactsStatusQueryKey = [RQ_KEY_ROOT, 'sync-status']
export function useContactsSyncStatusQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
queryKey: findContactsStatusQueryKey,
queryFn: async () => {
const status = await agent.app.bsky.contact.getSyncStatus()
return status.data
return await client.call(app.bsky.contact.getSyncStatus, {})
},
staleTime: STALE.SECONDS.THIRTY,
})
@@ -30,15 +29,14 @@ export function useContactsSyncStatusQuery() {
export const findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches']
export function useContactsMatchesQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery({
queryKey: findContactsGetMatchesQueryKey,
queryFn: async ({pageParam}) => {
const matches = await agent.app.bsky.contact.getMatches({
return await client.call(app.bsky.contact.getMatches, {
cursor: pageParam,
})
return matches.data
},
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -47,20 +45,19 @@ export function useContactsMatchesQuery() {
}
export function optimisticRemoveMatch(queryClient: QueryClient, did: string) {
queryClient.setQueryData<InfiniteData<AppBskyContactGetMatches.OutputSchema>>(
findContactsGetMatchesQueryKey,
old => {
if (!old) return old
queryClient.setQueryData<
InfiniteData<app.bsky.contact.getMatches.$OutputBody>
>(findContactsGetMatchesQueryKey, old => {
if (!old) return old
return {
...old,
pages: old.pages.map(page => ({
...page,
matches: page.matches.filter(match => match.did !== did),
})),
}
},
)
return {
...old,
pages: old.pages.map(page => ({
...page,
matches: page.matches.filter(match => match.did !== did),
})),
}
})
}
export const findContactsMatchesPassthroughQueryKey = (dids: string[]) => [
@@ -95,7 +92,7 @@ export function* findAllProfilesInQueryData(
did: string,
): Generator<bsky.profile.AnyProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyContactGetMatches.OutputSchema>
InfiniteData<app.bsky.contact.getMatches.$OutputBody>
>({
queryKey: findContactsGetMatchesQueryKey,
})
+20 -11
View File
@@ -1,4 +1,3 @@
import {ComAtprotoTempCheckHandleAvailability} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {
@@ -9,9 +8,21 @@ import {
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
import {createFullHandle} from '#/lib/strings/handles'
import {useAnalytics} from '#/analytics'
import * as bsky from '#/types/bsky'
import {Agent} from '../session/agent'
/*
* `com.atproto.temp.checkHandleAvailability` is an entryway-only endpoint that
* isn't generated into `#/lexicons`, so we describe its result union locally.
* The response is discriminated by `$type`; we narrow against these shapes
* rather than a branded lexicon guard.
*/
type CheckHandleAvailabilityResult =
| {$type: 'com.atproto.temp.checkHandleAvailability#resultAvailable'}
| {
$type: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'
suggestions: {handle: string; method: string}[]
}
export const RQKEY_handleAvailability = (
handle: string,
domain: string,
@@ -87,22 +98,20 @@ export async function checkHandleAvailability(
email,
})
const result = data.result as CheckHandleAvailabilityResult
if (
bsky.dangerousIsType<ComAtprotoTempCheckHandleAvailability.ResultAvailable>(
data.result,
ComAtprotoTempCheckHandleAvailability.isResultAvailable,
)
result.$type ===
'com.atproto.temp.checkHandleAvailability#resultAvailable'
) {
return {available: true} as const
} else if (
bsky.dangerousIsType<ComAtprotoTempCheckHandleAvailability.ResultUnavailable>(
data.result,
ComAtprotoTempCheckHandleAvailability.isResultUnavailable,
)
result.$type ===
'com.atproto.temp.checkHandleAvailability#resultUnavailable'
) {
return {
available: false,
suggestions: data.result.suggestions,
suggestions: result.suggestions,
} as const
} else {
throw new Error(
+10 -13
View File
@@ -1,7 +1,4 @@
import {
type AppBskyActorDefs,
type AppBskyGraphGetKnownFollowers,
} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -9,7 +6,8 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const PAGE_SIZE = 50
type RQPageParam = string | undefined
@@ -18,22 +16,21 @@ const RQKEY_ROOT = 'profile-known-followers'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileKnownFollowersQuery(did: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyGraphGetKnownFollowers.OutputSchema,
app.bsky.graph.getKnownFollowers.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetKnownFollowers.OutputSchema>,
InfiniteData<app.bsky.graph.getKnownFollowers.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(did || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getKnownFollowers({
actor: did!,
return await client.call(app.bsky.graph.getKnownFollowers, {
actor: did! as AtIdentifierString,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -44,9 +41,9 @@ export function useProfileKnownFollowersQuery(did: string | undefined) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetKnownFollowers.OutputSchema>
InfiniteData<app.bsky.graph.getKnownFollowers.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+39 -26
View File
@@ -1,4 +1,5 @@
import {type AppBskyLabelerDefs} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {addLabeler, removeLabeler} from '@bsky.app/sdk'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod'
@@ -9,7 +10,8 @@ import {
usePreferencesQuery,
} from '#/state/queries/preferences'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app} from '#/lexicons'
const labelerInfoQueryKeyRoot = 'labeler-info'
export const labelerInfoQueryKey = (did: string) => [
@@ -33,56 +35,60 @@ export function useLabelerInfoQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: !!did && enabled !== false,
queryKey: labelerInfoQueryKey(did as string),
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({
dids: [did!],
const res = await client.call(app.bsky.labeler.getServices, {
dids: [did! as DidString],
detailed: true,
})
return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed
return res.views[0] as app.bsky.labeler.defs.LabelerViewDetailed
},
})
}
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: !!dids.length,
queryKey: labelersInfoQueryKey(dids),
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({dids})
return res.data.views as AppBskyLabelerDefs.LabelerView[]
const res = await client.call(app.bsky.labeler.getServices, {
dids: dids as DidString[],
})
return res.views as app.bsky.labeler.defs.LabelerView[]
},
})
}
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: !!dids.length,
queryKey: createLabelersDetailedInfoQueryKey(dids),
gcTime: GCTIME.INFINITY,
staleTime: STALE.MINUTES.ONE,
queryFn: async () => {
const res = await agent.app.bsky.labeler.getServices({
dids,
const res = await client.call(app.bsky.labeler.getServices, {
dids: dids as DidString[],
detailed: true,
})
return res.data.views as AppBskyLabelerDefs.LabelerViewDetailed[]
return res.views as app.bsky.labeler.defs.LabelerViewDetailed[]
},
})
}
export function useRemoveLabelersMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
async mutationFn({dids}: {dids: string[]}) {
await Promise.all(dids.map(did => agent.removeLabeler(did)))
await Promise.all(
dids.map(did => client.call(removeLabeler, did as DidString)),
)
},
async onSuccess() {
await queryClient.invalidateQueries({
@@ -94,7 +100,8 @@ export function useRemoveLabelersMutation() {
export function useLabelerSubscriptionMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
const preferences = usePreferencesQuery()
return useMutation({
@@ -117,26 +124,32 @@ export function useLabelerSubscriptionMutation() {
const labelerDids = (
preferences.data?.moderationPrefs?.labelers ?? []
).map(l => l.did)
const invalidLabelers: string[] = []
const invalidLabelers: DidString[] = []
if (labelerDids.length) {
const profiles = await agent.getProfiles({actors: labelerDids})
if (profiles.data) {
for (const did of labelerDids) {
const exists = profiles.data.profiles.find(p => p.did === did)
const profiles = await appviewClient.call(app.bsky.actor.getProfiles, {
actors: labelerDids,
})
if (profiles) {
for (const labelerDid of labelerDids) {
const exists = profiles.profiles.find(p => p.did === labelerDid)
if (exists) {
// profile came back but it's not a valid labeler
if (exists.associated && !exists.associated.labeler) {
invalidLabelers.push(did)
invalidLabelers.push(labelerDid)
}
} else {
// no response came back, might be deactivated or takendown
invalidLabelers.push(did)
invalidLabelers.push(labelerDid)
}
}
}
}
if (invalidLabelers.length) {
await Promise.all(invalidLabelers.map(did => agent.removeLabeler(did)))
await Promise.all(
invalidLabelers.map(labelerDid =>
pdsClient.call(removeLabeler, labelerDid),
),
)
}
if (subscribe) {
@@ -144,9 +157,9 @@ export function useLabelerSubscriptionMutation() {
if (labelerCount >= MAX_LABELERS) {
throw new Error('MAX_LABELERS')
}
await agent.addLabeler(did)
await pdsClient.call(addLabeler, did as DidString)
} else {
await agent.removeLabeler(did)
await pdsClient.call(removeLabeler, did as DidString)
}
},
async onSuccess() {
+21 -24
View File
@@ -1,8 +1,5 @@
import {
type AppBskyActorDefs,
type AppBskyGraphDefs,
type AppBskyGraphGetList,
} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {type AtUriString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -12,7 +9,8 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {type SessionAgent, useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -23,23 +21,22 @@ export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export const RQKEY_ALL = (uri: string) => [RQKEY_ROOT_ALL, uri]
export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyGraphGetList.OutputSchema,
app.bsky.graph.getList.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetList.OutputSchema>,
InfiniteData<app.bsky.graph.getList.$OutputBody>,
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri ?? ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getList({
list: uri!, // the enabled flag will prevent this from running until uri is set
return await client.call(app.bsky.graph.getList, {
list: uri! as AtUriString, // the enabled flag will prevent this from running until uri is set
limit,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -48,32 +45,32 @@ export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
}
export function useAllListMembersQuery(uri?: string) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY_ALL(uri ?? ''),
queryFn: async () => {
return getAllListMembers(agent, uri!)
return getAllListMembers(client, uri!)
},
enabled: Boolean(uri),
})
}
export async function getAllListMembers(agent: SessionAgent, uri: string) {
export async function getAllListMembers(client: Client, uri: string) {
let hasMore = true
let cursor: string | undefined
const listItems: AppBskyGraphDefs.ListItemView[] = []
const listItems: app.bsky.graph.defs.ListItemView[] = []
// We want to cap this at 6 pages, just for anything weird happening with the api
let i = 0
while (hasMore && i < 6) {
const res = await agent.app.bsky.graph.getList({
list: uri,
const res = await client.call(app.bsky.graph.getList, {
list: uri as AtUriString,
limit: 50,
cursor,
})
listItems.push(...res.data.items)
hasMore = Boolean(res.data.cursor)
cursor = res.data.cursor
listItems.push(...res.items)
hasMore = Boolean(res.cursor)
cursor = res.cursor
i++
}
return listItems
@@ -92,9 +89,9 @@ export async function invalidateListMembersQuery({
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetList.OutputSchema>
InfiniteData<app.bsky.graph.getList.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
@@ -115,7 +112,7 @@ export function* findAllProfilesInQueryData(
}
const allQueryData = queryClient.getQueriesData<
AppBskyGraphDefs.ListItemView[]
app.bsky.graph.defs.ListItemView[]
>({
queryKey: [RQKEY_ROOT_ALL],
})
+22 -23
View File
@@ -1,8 +1,9 @@
import {
type AppBskyActorDefs,
type AppBskyGraphGetStarterPacksWithMembership,
AtUri,
} from '@atproto/api'
type AtUriString,
type DatetimeString,
type DidString,
} from '@atproto/syntax'
import {AtUri} from '@atproto/syntax'
import {
type InfiniteData,
useMutation,
@@ -13,7 +14,8 @@ import {
RQKEY as LIST_MEMBERS_RQKEY,
RQKEY_ALL as LIST_MEMBERS_ALL_RQKEY,
} from '#/state/queries/list-members'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession} from '#/state/session'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs'
@@ -30,7 +32,7 @@ export function useListMembershipAddMutation({
onError?: (error: Error) => void
} = {}) {
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<
{uri: string; cid: string},
@@ -41,14 +43,11 @@ export function useListMembershipAddMutation({
if (!currentAccount) {
throw new Error('Not signed in')
}
const res = await agent.app.bsky.graph.listitem.create(
{repo: currentAccount.did},
{
subject: actorDid,
list: listUri,
createdAt: new Date().toISOString(),
},
)
const res = await pdsClient.create(app.bsky.graph.listitem, {
subject: actorDid as DidString,
list: listUri as AtUriString,
createdAt: new Date().toISOString() as DatetimeString,
})
return res
},
onSuccess: (data, variables) => {
@@ -66,7 +65,7 @@ export function useListMembershipAddMutation({
// update WITH_MEMBERSHIPS query for starter packs
if (subject) {
queryClient.setQueryData<
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
InfiniteData<app.bsky.graph.getStarterPacksWithMembership.$OutputBody>
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
if (!old) return old
@@ -86,8 +85,8 @@ export function useListMembershipAddMutation({
...spWithMembership.starterPack,
listItemsSample: [
{
uri: data.uri,
subject: subject as AppBskyActorDefs.ProfileView,
uri: data.uri as AtUriString,
subject: subject as app.bsky.actor.defs.ProfileView,
},
...(spWithMembership.starterPack.listItemsSample?.filter(
item => item.subject.did !== variables.actorDid,
@@ -101,8 +100,8 @@ export function useListMembershipAddMutation({
},
},
listItem: {
uri: data.uri,
subject: subject as AppBskyActorDefs.ProfileView,
uri: data.uri as AtUriString,
subject: subject as app.bsky.actor.defs.ProfileView,
},
}
}
@@ -129,7 +128,7 @@ export function useListMembershipRemoveMutation({
onError?: (error: Error) => void
} = {}) {
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<
void,
@@ -141,8 +140,8 @@ export function useListMembershipRemoveMutation({
throw new Error('Not signed in')
}
const membershipUrip = new AtUri(membershipUri)
await agent.app.bsky.graph.listitem.delete({
repo: currentAccount.did,
await pdsClient.delete(app.bsky.graph.listitem, {
repo: currentAccount.did as DidString,
rkey: membershipUrip.rkey,
})
},
@@ -160,7 +159,7 @@ export function useListMembershipRemoveMutation({
// update WITH_MEMBERSHIPS query for starter packs
queryClient.setQueryData<
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
InfiniteData<app.bsky.graph.getStarterPacksWithMembership.$OutputBody>
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
if (!old) return old
+79 -82
View File
@@ -1,13 +1,17 @@
import {type Client} from '@atproto/lex-client'
import {
type $Typed,
type AppBskyGraphDefs,
type AppBskyGraphGetList,
type AppBskyGraphList,
type AtIdentifierString,
AtUri,
type ComAtprotoRepoApplyWrites,
type Facet,
type Un$Typed,
} from '@atproto/api'
type AtUriString,
type DatetimeString,
type NsidString,
} from '@atproto/syntax'
import {
blockActorList,
muteActorList,
unblockActorList,
unmuteActorList,
} from '@bsky.app/sdk'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
@@ -15,7 +19,8 @@ import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {type ImageMeta} from '#/state/gallery'
import {STALE} from '#/state/queries'
import {type SessionAgent, useAgent, useSession} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import {app, com} from '#/lexicons'
import {FEED_INFO_RQKEY_ROOT} from './feed'
import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
@@ -24,19 +29,19 @@ export const RQKEY_ROOT = 'list'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListQuery(uri?: string) {
const agent = useAgent()
return useQuery<AppBskyGraphDefs.ListView, Error>({
const client = useAppviewClient()
return useQuery<app.bsky.graph.defs.ListView, Error>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri || ''),
async queryFn() {
if (!uri) {
throw new Error('URI not provided')
}
const res = await agent.app.bsky.graph.getList({
list: uri,
const res = await client.call(app.bsky.graph.getList, {
list: uri as AtUriString,
limit: 1,
})
return res.data.list
return res.list
},
enabled: !!uri,
})
@@ -46,13 +51,14 @@ export interface ListCreateMutateParams {
purpose: string
name: string
description: string
descriptionFacets: Facet[] | undefined
descriptionFacets: app.bsky.richtext.facet.Main[] | undefined
avatar: ImageMeta | null | undefined
}
export function useListCreateMutation() {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>(
{
async mutationFn({
@@ -71,33 +77,24 @@ export function useListCreateMutation() {
) {
throw new Error('Invalid list purpose: must be curatelist or modlist')
}
const record: Un$Typed<AppBskyGraphList.Record> = {
const record: Omit<app.bsky.graph.list.Main, '$type'> = {
purpose,
name,
description,
descriptionFacets,
avatar: undefined,
createdAt: new Date().toISOString(),
createdAt: new Date().toISOString() as DatetimeString,
}
if (avatar) {
const blobRes = await uploadBlob(agent, avatar.path, avatar.mime)
record.avatar = blobRes.data.blob
const blobRes = await uploadBlob(pdsClient, avatar.path, avatar.mime)
record.avatar = blobRes.blob
}
const res = await agent.app.bsky.graph.list.create(
{
repo: currentAccount.did,
},
record,
)
const res = await pdsClient.create(app.bsky.graph.list, record)
// wait for the appview to update
await whenAppViewReady(
agent,
res.uri,
(v: AppBskyGraphGetList.Response) => {
return typeof v?.data?.list.uri === 'string'
},
)
await whenAppViewReady(appviewClient, res.uri, v => {
return typeof v?.list.uri === 'string'
})
return res
},
onSuccess() {
@@ -114,12 +111,13 @@ export interface ListMetadataMutateParams {
uri: string
name: string
description: string
descriptionFacets: Facet[] | undefined
descriptionFacets: app.bsky.richtext.facet.Main[] | undefined
avatar: ImageMeta | null | undefined
}
export function useListMetadataMutation() {
const {currentAccount} = useSession()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<
{uri: string; cid: string},
@@ -136,7 +134,7 @@ export function useListMetadataMutation() {
}
// get the current record
const {value: record} = await agent.app.bsky.graph.list.get({
const {value: record} = await pdsClient.get(app.bsky.graph.list, {
repo: currentAccount.did,
rkey,
})
@@ -146,31 +144,25 @@ export function useListMetadataMutation() {
record.description = description
record.descriptionFacets = descriptionFacets
if (avatar) {
const blobRes = await uploadBlob(agent, avatar.path, avatar.mime)
record.avatar = blobRes.data.blob
const blobRes = await uploadBlob(pdsClient, avatar.path, avatar.mime)
record.avatar = blobRes.blob
} else if (avatar === null) {
record.avatar = undefined
}
const res = (
await agent.com.atproto.repo.putRecord({
repo: currentAccount.did,
collection: 'app.bsky.graph.list',
rkey,
record,
})
).data
const res = await pdsClient.call(com.atproto.repo.putRecord, {
repo: currentAccount.did,
collection: 'app.bsky.graph.list',
rkey,
record,
})
// wait for the appview to update
await whenAppViewReady(
agent,
res.uri,
(v: AppBskyGraphGetList.Response) => {
const list = v.data.list
return (
list.name === record.name && list.description === record.description
)
},
)
await whenAppViewReady(appviewClient, res.uri, v => {
const list = v.list
return (
list.name === record.name && list.description === record.description
)
})
return res
},
onSuccess(data, variables) {
@@ -190,7 +182,8 @@ export function useListMetadataMutation() {
export function useListDeleteMutation() {
const {currentAccount} = useSession()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<void, Error, {uri: string}>({
mutationFn: async ({uri}) => {
@@ -198,11 +191,11 @@ export function useListDeleteMutation() {
return
}
// fetch all the listitem records that belong to this list
let cursor
let cursor: string | undefined
let listitemRecordUris: string[] = []
for (let i = 0; i < 100; i++) {
const res = await agent.app.bsky.graph.listitem.list({
repo: currentAccount.did,
const res = await pdsClient.list(app.bsky.graph.listitem, {
repo: currentAccount.did as AtIdentifierString,
cursor,
limit: 100,
})
@@ -220,11 +213,11 @@ export function useListDeleteMutation() {
// batch delete the list and listitem records
const createDel = (
uri: string,
): $Typed<ComAtprotoRepoApplyWrites.Delete> => {
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => {
const urip = new AtUri(uri)
return {
$type: 'com.atproto.repo.applyWrites#delete',
collection: urip.collection,
collection: urip.collection as NsidString,
rkey: urip.rkey,
}
}
@@ -234,15 +227,17 @@ export function useListDeleteMutation() {
// apply in chunks
for (const writesChunk of chunk(writes, 10)) {
await agent.com.atproto.repo.applyWrites({
repo: currentAccount.did,
await pdsClient.call(com.atproto.repo.applyWrites, {
repo: currentAccount.did as AtIdentifierString,
writes: writesChunk,
})
}
// wait for the appview to update
await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => {
return !v?.success
// wait for the appview to update. once the list is deleted, getList
// throws (404), `until` catches it and passes `undefined` here, so an
// absent body signals a completed delete.
await whenAppViewReady(appviewClient, uri, v => {
return !v
})
},
onSuccess() {
@@ -257,17 +252,18 @@ export function useListDeleteMutation() {
export function useListMuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
return useMutation<void, Error, {uri: string; mute: boolean}>({
mutationFn: async ({uri, mute}) => {
if (mute) {
await agent.muteModList(uri)
await pdsClient.call(muteActorList, {list: uri as AtUriString})
} else {
await agent.unmuteModList(uri)
await pdsClient.call(unmuteActorList, {list: uri as AtUriString})
}
await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => {
return Boolean(v?.data.list.viewer?.muted) === mute
await whenAppViewReady(appviewClient, uri, v => {
return Boolean(v?.list.viewer?.muted) === mute
})
},
onSuccess(data, variables) {
@@ -280,19 +276,20 @@ export function useListMuteMutation() {
export function useListBlockMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
return useMutation<void, Error, {uri: string; block: boolean}>({
mutationFn: async ({uri, block}) => {
if (block) {
await agent.blockModList(uri)
await pdsClient.call(blockActorList, {list: uri as AtUriString})
} else {
await agent.unblockModList(uri)
await pdsClient.call(unblockActorList, {list: uri as AtUriString})
}
await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => {
await whenAppViewReady(appviewClient, uri, v => {
return block
? typeof v?.data.list.viewer?.blocked === 'string'
: !v?.data.list.viewer?.blocked
? typeof v?.list.viewer?.blocked === 'string'
: !v?.list.viewer?.blocked
})
},
onSuccess(data, variables) {
@@ -304,17 +301,17 @@ export function useListBlockMutation() {
}
async function whenAppViewReady(
agent: SessionAgent,
client: Client,
uri: string,
fn: (res: AppBskyGraphGetList.Response) => boolean,
fn: (res: app.bsky.graph.getList.$OutputBody) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() =>
agent.app.bsky.graph.getList({
list: uri,
client.call(app.bsky.graph.getList, {
list: uri as AtUriString,
limit: 1,
}),
)
+13 -16
View File
@@ -1,7 +1,4 @@
import {
type AppBskyActorDefs,
type AppBskyGraphGetListsWithMembership,
} from '@atproto/api'
import {type AtIdentifierString, type AtUriString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -10,10 +7,11 @@ import {
} from '@tanstack/react-query'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
export type ListWithMembership =
AppBskyGraphGetListsWithMembership.ListWithMembership
app.bsky.graph.getListsWithMembership.ListWithMembership
const listsWithMembershipQueryKeyRoot = 'lists-with-membership'
export const createListsWithMembershipQueryKey = (args: {actor: string}) =>
@@ -26,23 +24,22 @@ export function useListsWithMembershipQuery({
actor: string | undefined
enabled?: boolean
}) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyGraphGetListsWithMembership.OutputSchema,
app.bsky.graph.getListsWithMembership.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>,
InfiniteData<app.bsky.graph.getListsWithMembership.$OutputBody>,
QueryKey,
string | undefined
>({
queryKey: createListsWithMembershipQueryKey({actor: actor ?? ''}),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getListsWithMembership({
actor: actor!, // the enabled flag prevents this from running until actor is set
return await client.call(app.bsky.graph.getListsWithMembership, {
actor: actor! as AtIdentifierString, // the enabled flag prevents this from running until actor is set
limit: 50,
cursor: pageParam,
})
return res.data
},
enabled: Boolean(actor) && enabled,
initialPageParam: undefined,
@@ -61,10 +58,10 @@ export function updateListMembershipOptimistically({
actor: string
listUri: string
membershipUri: string
subject: AppBskyActorDefs.ProfileView
subject: app.bsky.actor.defs.ProfileView
}) {
queryClient.setQueryData<
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>
InfiniteData<app.bsky.graph.getListsWithMembership.$OutputBody>
>(createListsWithMembershipQueryKey({actor}), old => {
if (!old) return old
@@ -77,7 +74,7 @@ export function updateListMembershipOptimistically({
return {
...lwm,
listItem: {
uri: membershipUri,
uri: membershipUri as AtUriString,
subject,
},
}
@@ -99,7 +96,7 @@ export function removeListMembershipOptimistically({
listUri: string
}) {
queryClient.setQueryData<
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>
InfiniteData<app.bsky.graph.getListsWithMembership.$OutputBody>
>(createListsWithMembershipQueryKey({actor}), old => {
if (!old) return old
@@ -1,12 +1,8 @@
import {
type ChatBskyConvoAcceptConvo,
type ChatBskyConvoDefs,
} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {
type ConvoRequestListQueryData,
optimisticDelete as optimisticDeleteRequest,
@@ -29,19 +25,16 @@ export function useAcceptConversation(
onError,
}: {
onMutate?: () => void
onSuccess?: (data: ChatBskyConvoAcceptConvo.OutputSchema) => void
onSuccess?: (data: chat.bsky.convo.acceptConvo.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async () => {
const {data} = await agent.chat.bsky.convo.acceptConvo(
{convoId},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.convo.acceptConvo, {convoId})
return data
},
@@ -52,7 +45,7 @@ export function useAcceptConversation(
queryClient.getQueriesData<ConvoListQueryData>({
queryKey: [CONVO_LIST_ROOT_KEY],
})
let convoBeingAccepted: ChatBskyConvoDefs.ConvoView | null = null
let convoBeingAccepted: chat.bsky.convo.defs.ConvoView | null = null
for (const [_key, data] of queryClient.getQueriesData<ConvoListQueryData>(
{queryKey: CONVO_LIST_PARTIAL_KEY('request')},
)) {
@@ -65,7 +58,7 @@ export function useAcceptConversation(
(old?: ConvoListQueryData) => optimisticDelete(convoId, old),
)
if (convoBeingAccepted) {
const acceptedConvo: ChatBskyConvoDefs.ConvoView = {
const acceptedConvo: chat.bsky.convo.defs.ConvoView = {
...convoBeingAccepted,
status: 'accepted',
}
+29 -22
View File
@@ -1,13 +1,12 @@
import {
type AppBskyActorDefs,
type ChatBskyActorDeclaration,
} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {type SessionAgent} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {usePdsClient, useSession} from '#/state/session'
import {agentToLexClient} from '#/state/session/clients'
import {type SessionAgent} from '#/state/session/session-core'
import {resolveAllowGroupInvites} from '#/components/dms/util'
import {type app, chat, com} from '#/lexicons'
import {RQKEY as PROFILE_RKEY} from '../profile'
export function useUpdateActorDeclaration({
@@ -19,7 +18,7 @@ export function useUpdateActorDeclaration({
}) {
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async (update: {
@@ -28,7 +27,7 @@ export function useUpdateActorDeclaration({
}) => {
if (!currentAccount) throw new Error('Not signed in')
const current =
queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
queryClient.getQueryData<app.bsky.actor.defs.ProfileViewDetailed>(
PROFILE_RKEY(currentAccount.did),
)
const allowIncoming =
@@ -41,8 +40,8 @@ export function useUpdateActorDeclaration({
update.allowGroupInvites ??
current?.associated?.chat?.allowGroupInvites,
})
const result = await agent.com.atproto.repo.putRecord({
repo: currentAccount.did,
const result = await pdsClient.call(com.atproto.repo.putRecord, {
repo: currentAccount.did as DidString,
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
record: {
@@ -57,7 +56,7 @@ export function useUpdateActorDeclaration({
if (!currentAccount) return
queryClient.setQueryData(
PROFILE_RKEY(currentAccount?.did),
(old?: AppBskyActorDefs.ProfileViewDetailed) => {
(old?: app.bsky.actor.defs.ProfileViewDetailed) => {
if (!old) return old
const allowIncoming =
update.allowIncoming ??
@@ -81,7 +80,7 @@ export function useUpdateActorDeclaration({
allowGroupInvites,
},
},
} satisfies AppBskyActorDefs.ProfileViewDetailed
} satisfies app.bsky.actor.defs.ProfileViewDetailed
},
)
},
@@ -101,13 +100,13 @@ export function useUpdateActorDeclaration({
// for use in the settings screen for testing
export function useDeleteActorDeclaration() {
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async () => {
if (!currentAccount) throw new Error('Not signed in')
const result = await agent.api.com.atproto.repo.deleteRecord({
repo: currentAccount.did,
const result = await pdsClient.call(com.atproto.repo.deleteRecord, {
repo: currentAccount.did as DidString,
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
})
@@ -124,12 +123,20 @@ export async function fetchActorDeclarationRecord({
did?: string
}) {
if (!did) return
const res = await agent.com.atproto.repo
.getRecord({
repo: did,
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
})
/*
* This helper is called with a bridge `SessionAgent` threaded from
* `#/ageAssurance/data`. Wrap it as an account lex `Client` so the record
* read goes through the same path as the migrated hooks; the caller keeps
* passing the agent until the bridge is removed (Phase 4). The cast is safe:
* `agentToLexClient` only reads `did` and `fetchHandler`, both of which the
* base `Agent` (and thus `SessionAgent`) provides - its `AtpAgent` parameter
* type is just narrower than it needs. TODO(phase4): drop with the bridge.
*/
const client = agentToLexClient(
agent as unknown as Parameters<typeof agentToLexClient>[0],
)
const res = await client
.get(chat.bsky.actor.declaration, {repo: did as DidString, rkey: 'self'})
.catch(_e => undefined)
return res?.data.value as ChatBskyActorDeclaration.Main
return res?.value
}
+51 -43
View File
@@ -1,20 +1,15 @@
import {
type ChatBskyActorDefs,
ChatBskyConvoDefs,
type ChatBskyConvoListConvos,
type ChatBskyGroupAddMembers,
} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {
type InfiniteData,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import type * as bsky from '#/types/bsky'
import {useChatClient, useSession} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {RQKEY as CONVO_KEY} from './conversation'
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
import {listConvoMembersQueryKey} from './list-convo-members'
@@ -25,12 +20,12 @@ export function useAddGroupMembers(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupAddMembers.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.addMembers.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
const {currentAccount} = useSession()
const {data: myProfile} = useProfileQuery({did: currentAccount?.did})
@@ -42,48 +37,60 @@ export function useAddGroupMembers(
profiles: bsky.profile.AnyProfileView[]
}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.addMembers(
{convoId, members},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.addMembers, {
convoId,
members: members as DidString[],
})
return data
},
onMutate: ({profiles}) => {
if (!convoId) return
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
CONVO_KEY(convoId),
)
const prevConvo =
queryClient.getQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
)
const prevListEntries = queryClient.getQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]})
const prevMemberList = queryClient.getQueryData<
ChatBskyActorDefs.ProfileViewBasic[]
chat.bsky.actor.defs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
const addedBy: ChatBskyActorDefs.ProfileViewBasic | undefined = myProfile
? {
...myProfile,
/*
* The profile views come from producers that still emit the old
* `@atproto/api` shapes (useProfileQuery migrates in a later task), while
* the chat caches are now typed on the lexicon views. Structurally
* identical modulo branded strings. TODO(phase4): drop toLex once those
* producers migrate.
*/
const addedBy: chat.bsky.actor.defs.ProfileViewBasic | undefined =
myProfile
? bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>({
...myProfile,
$type: 'chat.bsky.actor.defs#profileViewBasic',
})
: undefined
const optimisticMembers: chat.bsky.actor.defs.ProfileViewBasic[] =
profiles.map(profile =>
bsky.toLex<chat.bsky.actor.defs.ProfileViewBasic>({
...profile,
$type: 'chat.bsky.actor.defs#profileViewBasic',
}
: undefined
kind: {
$type: 'chat.bsky.actor.defs#groupConvoMember',
role: 'standard',
addedBy,
},
}),
)
const optimisticMembers: ChatBskyActorDefs.ProfileViewBasic[] =
profiles.map(profile => ({
...profile,
$type: 'chat.bsky.actor.defs#profileViewBasic',
kind: {
$type: 'chat.bsky.actor.defs#groupConvoMember',
role: 'standard',
addedBy,
},
}))
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
prev => {
if (!prev) return
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return prev
return {
...prev,
members: [...prev.members, ...optimisticMembers],
@@ -96,7 +103,7 @@ export function useAddGroupMembers(
)
queryClient.setQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]}, prev => {
if (!prev?.pages) return
return {
@@ -105,7 +112,8 @@ export function useAddGroupMembers(
...page,
convos: page.convos.map(convo => {
if (convo.id !== convoId) return convo
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind))
return convo
return {
...convo,
members: [...convo.members, ...optimisticMembers],
@@ -120,7 +128,7 @@ export function useAddGroupMembers(
}
})
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
queryClient.setQueryData<chat.bsky.actor.defs.ProfileViewBasic[]>(
listConvoMembersQueryKey(convoId),
prev => {
if (!prev) return
@@ -132,13 +140,13 @@ export function useAddGroupMembers(
},
onSuccess: data => {
if (convoId) {
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
data.convo,
)
queryClient.setQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]}, prev => {
if (!prev?.pages) return
return {
+19 -32
View File
@@ -1,9 +1,3 @@
import {
type ChatBskyActorDefs,
type ChatBskyConvoDefs,
type ChatBskyConvoGetConvo,
type ChatBskyConvoGetUnreadCounts,
} from '@atproto/api'
import {
type QueryClient,
useMutation,
@@ -11,10 +5,10 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {STALE} from '#/state/queries'
import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {
RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY,
UNREAD_ACCEPTED_CAP,
@@ -30,15 +24,12 @@ export const RQKEY_ROOT = 'convo'
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
export function useConvoQuery({convoId}: {convoId: string}) {
const agent = useAgent()
const chatClient = useChatClient()
return useQuery({
queryKey: RQKEY(convoId),
queryFn: async () => {
const {data} = await agent.chat.bsky.convo.getConvo(
{convoId},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.convo.getConvo, {convoId})
return data.convo
},
staleTime: STALE.INFINITY,
@@ -47,7 +38,7 @@ export function useConvoQuery({convoId}: {convoId: string}) {
export function precacheConvoQuery(
queryClient: QueryClient,
convo: ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
) {
queryClient.setQueryData(RQKEY(convo.id), convo)
}
@@ -55,7 +46,7 @@ export function precacheConvoQuery(
export function useMarkAsReadMutation() {
const optimisticUpdate = useOnMarkAsRead()
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({
@@ -67,16 +58,10 @@ export function useMarkAsReadMutation() {
}) => {
if (!convoId) throw new Error('No convoId provided')
await agent.chat.bsky.convo.updateRead(
{
convoId,
messageId,
},
{
encoding: 'application/json',
headers: DM_SERVICE_HEADERS,
},
)
await chatClient.call(chat.bsky.convo.updateRead, {
convoId,
messageId,
})
},
onMutate({convoId}) {
if (!convoId) throw new Error('No convoId provided')
@@ -90,7 +75,7 @@ export function useMarkAsReadMutation() {
// find the convo so we know which badge counter (if any) to decrement.
// keep scanning past a stale unreadCount === 0 cache so another cache
// holding the true unread state still drives the decrement
let unreadStatus: ChatBskyConvoDefs.ConvoView['status'] | undefined
let unreadStatus: chat.bsky.convo.defs.ConvoView['status'] | undefined
for (const [, data] of prevListQueries) {
if (!data) continue
const convo = getConvoFromQueryData(convoId, data)
@@ -105,11 +90,13 @@ export function useMarkAsReadMutation() {
// the badge count query is a separate server query that the list caches
// don't feed, so decrement it here to keep the badge in sync
const prevUnreadCountsQueries =
queryClient.getQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>({
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
})
queryClient.getQueriesData<chat.bsky.convo.getUnreadCounts.$OutputBody>(
{
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
},
)
if (unreadStatus) {
queryClient.setQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>(
queryClient.setQueriesData<chat.bsky.convo.getUnreadCounts.$OutputBody>(
{queryKey: UNREAD_COUNTS_PARTIAL_KEY},
old => {
if (!old) return old
@@ -193,9 +180,9 @@ export function useMarkAsReadMutation() {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<ChatBskyActorDefs.ProfileViewBasic, void> {
): Generator<chat.bsky.actor.defs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<
ChatBskyConvoGetConvo.OutputSchema['convo']
chat.bsky.convo.getConvo.$OutputBody['convo']
>({
queryKey: [RQKEY_ROOT],
})
@@ -1,27 +1,27 @@
import {type ChatBskyGroupCreateGroup} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {precacheConvoQuery} from './conversation'
export function useCreateGroupChat({
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupCreateGroup.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.createGroup.$OutputBody) => void
onError?: (error: Error) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({name, members}: {name: string; members: string[]}) => {
const {data} = await agent.chat.bsky.group.createGroup(
{name, members},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.group.createGroup, {
name,
members: members as DidString[],
})
return data
},
+19 -17
View File
@@ -1,13 +1,10 @@
import {
ChatBskyConvoDefs,
type ChatBskyGroupCreateJoinLink,
type ChatBskyGroupDefs,
} from '@atproto/api'
import {type DatetimeString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -19,32 +16,34 @@ export function useCreateJoinLink(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupCreateJoinLink.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.createJoinLink.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({
joinRule,
requireApproval,
}: {
joinRule: ChatBskyGroupDefs.JoinRule
joinRule: chat.bsky.group.defs.JoinRule
requireApproval: boolean
}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.createJoinLink(
{convoId, joinRule, requireApproval},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.createJoinLink, {
convoId,
joinRule,
requireApproval,
})
return data
},
onMutate: ({joinRule, requireApproval}) => {
if (!convoId) return
return updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {
@@ -55,7 +54,9 @@ export function useCreateJoinLink(
enabledStatus: 'enabled',
joinRule,
requireApproval,
createdAt: new Date().toISOString(),
// ISO string is a valid datetime; assert the branded type the
// generated JoinLinkView expects for this optimistic-only value.
createdAt: new Date().toISOString() as DatetimeString,
},
},
}
@@ -64,7 +65,8 @@ export function useCreateJoinLink(
onSuccess: data => {
if (convoId) {
updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {...prev.kind, joinLink: data.joinLink},
+14 -14
View File
@@ -1,13 +1,10 @@
import {
ChatBskyConvoDefs,
type ChatBskyGroupDisableJoinLink,
} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -19,26 +16,28 @@ export function useDisableJoinLink(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupDisableJoinLink.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.disableJoinLink.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.disableJoinLink(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.disableJoinLink, {
convoId,
})
return data
},
onMutate: () => {
if (!convoId) return
return updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
if (
!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind) ||
!prev.kind.joinLink
) {
return undefined
}
return {
@@ -53,7 +52,8 @@ export function useDisableJoinLink(
onSuccess: data => {
if (convoId) {
updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {...prev.kind, joinLink: data.joinLink},
@@ -1,9 +1,9 @@
import {ChatBskyConvoDefs, type ChatBskyGroupEditGroup} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -15,26 +15,27 @@ export function useEditGroupChatName(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.editGroup.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({name: groupName}: {name: string}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.editGroup(
{convoId, name: groupName},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.editGroup, {
convoId,
name: groupName,
})
return data
},
onMutate: ({name: groupName}) => {
if (!convoId) return
return updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {...prev.kind, name: groupName},
+17 -16
View File
@@ -1,13 +1,9 @@
import {
ChatBskyConvoDefs,
type ChatBskyGroupDefs,
type ChatBskyGroupEditJoinLink,
} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -19,32 +15,36 @@ export function useEditJoinLink(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupEditJoinLink.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.editJoinLink.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({
joinRule,
requireApproval,
}: {
joinRule: ChatBskyGroupDefs.JoinRule
joinRule: chat.bsky.group.defs.JoinRule
requireApproval: boolean
}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.editJoinLink(
{convoId, joinRule, requireApproval},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.editJoinLink, {
convoId,
joinRule,
requireApproval,
})
return data
},
onMutate: ({joinRule, requireApproval}) => {
if (!convoId) return
return updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
if (
!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind) ||
!prev.kind.joinLink
) {
return undefined
}
return {
@@ -59,7 +59,8 @@ export function useEditJoinLink(
onSuccess: data => {
if (convoId) {
updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {...prev.kind, joinLink: data.joinLink},
+14 -11
View File
@@ -1,10 +1,10 @@
import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -16,26 +16,28 @@ export function useEnableJoinLink(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupEnableJoinLink.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.enableJoinLink.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.enableJoinLink(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.enableJoinLink, {
convoId,
})
return data
},
onMutate: () => {
if (!convoId) return
return updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
if (
!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind) ||
!prev.kind.joinLink
) {
return undefined
}
return {
@@ -50,7 +52,8 @@ export function useEnableJoinLink(
onSuccess: data => {
if (convoId) {
updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {...prev.kind, joinLink: data.joinLink},
@@ -1,7 +1,8 @@
import {type DidString} from '@atproto/syntax'
import {useQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {STALE} from '..'
const RQKEY_ROOT = 'convo-availability'
@@ -11,15 +12,14 @@ export function useGetConvoAvailabilityQuery(
did: string,
{enabled = true}: {enabled?: boolean} = {},
) {
const agent = useAgent()
const chatClient = useChatClient()
return useQuery({
queryKey: RQKEY(did),
queryFn: async () => {
const {data} = await agent.chat.bsky.convo.getConvoAvailability(
{members: [did]},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.convo.getConvoAvailability, {
members: [did as DidString],
})
return data
},
@@ -1,27 +1,26 @@
import {type ChatBskyConvoGetConvoForMembers} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {precacheConvoQuery} from './conversation'
export function useGetConvoForMembers({
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyConvoGetConvoForMembers.OutputSchema) => void
onSuccess?: (data: chat.bsky.convo.getConvoForMembers.$OutputBody) => void
onError?: (error: Error) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async (members: string[]) => {
const {data} = await agent.chat.bsky.convo.getConvoForMembers(
{members: members},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.convo.getConvoForMembers, {
members: members as DidString[],
})
return data
},
+4 -7
View File
@@ -1,7 +1,7 @@
import {useQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {STALE} from '..'
import {createQueryKey} from '../util'
@@ -9,17 +9,14 @@ const chatActorStatusQueryKey = () =>
createQueryKey('chat-actor-status', {}, {persistedVersion: 1})
export function useChatActorStatusQuery() {
const agent = useAgent()
const chatClient = useChatClient()
return useQuery({
gcTime: STALE.INFINITY,
staleTime: STALE.SECONDS.FIFTEEN,
queryKey: chatActorStatusQueryKey(),
queryFn: async () => {
const {data} = await agent.chat.bsky.actor.getStatus(
{},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.actor.getStatus, {})
return data
},
@@ -1,8 +1,8 @@
import {useQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent, useSession} from '#/state/session'
import {useChatClient, useSession} from '#/state/session'
import {useAgeAssurance} from '#/ageAssurance'
import {chat} from '#/lexicons'
import {STALE} from '..'
const RQKEY_ROOT = 'convo-unread-counts'
@@ -18,7 +18,7 @@ export const UNREAD_ACCEPTED_CAP = 100
export const UNREAD_REQUEST_CAP = 100
export function useUnreadCountsQuery() {
const agent = useAgent()
const chatClient = useChatClient()
const {hasSession} = useSession()
const aa = useAgeAssurance()
const includeGroupChats = !aa.flags.groupChatDisabled
@@ -26,10 +26,9 @@ export function useUnreadCountsQuery() {
return useQuery({
queryKey: RQKEY(includeGroupChats),
queryFn: async () => {
const {data} = await agent.chat.bsky.convo.getUnreadCounts(
{includeGroupChats},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.convo.getUnreadCounts, {
includeGroupChats,
})
return data
},
staleTime: STALE.SECONDS.FIFTEEN,
+20 -25
View File
@@ -1,26 +1,21 @@
import {
type ChatBskyActorDefs,
type ChatBskyGroupApproveJoinRequest,
type ChatBskyGroupListJoinRequests,
type ChatBskyGroupRejectJoinRequest,
} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {
type InfiniteData,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {listConvoMembersQueryKey} from './list-convo-members'
import {createListJoinRequestsQueryKey} from './list-join-requests'
type JoinRequestAction = 'approve' | 'reject'
type JoinRequestOutput<A extends JoinRequestAction> = A extends 'approve'
? ChatBskyGroupApproveJoinRequest.OutputSchema
: ChatBskyGroupRejectJoinRequest.OutputSchema
? chat.bsky.group.approveJoinRequest.$OutputBody
: chat.bsky.group.rejectJoinRequest.$OutputBody
export function useJoinRequestMutation<A extends JoinRequestAction>(
action: A,
@@ -34,21 +29,21 @@ export function useJoinRequestMutation<A extends JoinRequestAction>(
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({member}: {member: string}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} =
const data =
action === 'approve'
? await agent.chat.bsky.group.approveJoinRequest(
{convoId, member},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
: await agent.chat.bsky.group.rejectJoinRequest(
{convoId, member},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
? await chatClient.call(chat.bsky.group.approveJoinRequest, {
convoId,
member: member as DidString,
})
: await chatClient.call(chat.bsky.group.rejectJoinRequest, {
convoId,
member: member as DidString,
})
return data as JoinRequestOutput<A>
},
onMutate: ({member}) => {
@@ -57,7 +52,7 @@ export function useJoinRequestMutation<A extends JoinRequestAction>(
const requestsKey = createListJoinRequestsQueryKey({convoId})
const prevRequests =
queryClient.getQueryData<
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
InfiniteData<chat.bsky.group.listJoinRequests.$OutputBody>
>(requestsKey)
const requestedByProfile = prevRequests?.pages
@@ -65,7 +60,7 @@ export function useJoinRequestMutation<A extends JoinRequestAction>(
.find(request => request.requestedBy.did === member)?.requestedBy
queryClient.setQueryData<
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
InfiniteData<chat.bsky.group.listJoinRequests.$OutputBody>
>(requestsKey, prev => {
if (!prev?.pages) return prev
return {
@@ -79,14 +74,14 @@ export function useJoinRequestMutation<A extends JoinRequestAction>(
}
})
let prevMembers: ChatBskyActorDefs.ProfileViewBasic[] | undefined
let prevMembers: chat.bsky.actor.defs.ProfileViewBasic[] | undefined
if (action === 'approve' && requestedByProfile) {
const membersKey = listConvoMembersQueryKey(convoId)
prevMembers =
queryClient.getQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
queryClient.getQueryData<chat.bsky.actor.defs.ProfileViewBasic[]>(
membersKey,
)
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
queryClient.setQueryData<chat.bsky.actor.defs.ProfileViewBasic[]>(
membersKey,
prev => {
if (!prev) return prev
@@ -1,13 +1,9 @@
import {
type ChatBskyConvoLeaveConvo,
type ChatBskyConvoListConvos,
} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {
type ConvoRequestListQueryData,
optimisticDelete as optimisticDeleteRequest,
@@ -22,7 +18,7 @@ export function RQKEY(convoId: string | undefined) {
type ConvoListQueryData = {
pageParams: Array<string | undefined>
pages: Array<ChatBskyConvoListConvos.OutputSchema>
pages: Array<chat.bsky.convo.listConvos.$OutputBody>
}
export function useLeaveConvo(
@@ -33,22 +29,19 @@ export function useLeaveConvo(
onError,
}: {
onMutate?: () => void
onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void
onSuccess?: (data: chat.bsky.convo.leaveConvo.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationKey: RQKEY(convoId),
mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.convo.leaveConvo(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.convo.leaveConvo, {convoId})
return data
},
@@ -1,16 +1,12 @@
import {
ChatBskyConvoDefs,
type ChatBskyConvoListConvoRequests,
ChatBskyGroupDefs,
} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
useInfiniteQuery,
} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
const DEFAULT_LIMIT = 10
@@ -26,17 +22,16 @@ export function useListConvoRequests({
enabled?: boolean
limit?: number
} = {}) {
const agent = useAgent()
const chatClient = useChatClient()
return useInfiniteQuery({
enabled,
queryKey: RQKEY(limit),
queryFn: async ({pageParam}) => {
const {data} = await agent.chat.bsky.convo.listConvoRequests(
{limit, cursor: pageParam},
{headers: DM_SERVICE_HEADERS},
)
return data
return await chatClient.call(chat.bsky.convo.listConvoRequests, {
limit,
cursor: pageParam,
})
},
initialPageParam: undefined as RQPageParam,
getNextPageParam: lastPage => lastPage.cursor,
@@ -45,16 +40,18 @@ export function useListConvoRequests({
export type ConvoRequestListQueryData = {
pageParams: Array<string | undefined>
pages: Array<ChatBskyConvoListConvoRequests.OutputSchema>
pages: Array<chat.bsky.convo.listConvoRequests.$OutputBody>
}
export type ConvoRequestItem =
ChatBskyConvoListConvoRequests.OutputSchema['requests'][number]
chat.bsky.convo.listConvoRequests.$OutputBody['requests'][number]
export function optimisticUpdate(
chatId: string,
old: ConvoRequestListQueryData | undefined,
updateFn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
updateFn: (
convo: chat.bsky.convo.defs.ConvoView,
) => chat.bsky.convo.defs.ConvoView,
): ConvoRequestListQueryData | undefined {
if (!old) return old
@@ -63,7 +60,10 @@ export function optimisticUpdate(
pages: old.pages.map(page => ({
...page,
requests: page.requests.map((item): ConvoRequestItem => {
if (ChatBskyConvoDefs.isConvoView(item) && item.id === chatId) {
if (
bsky.isType(chat.bsky.convo.defs.convoView, item) &&
item.id === chatId
) {
return {
...updateFn(item),
$type: 'chat.bsky.convo.defs#convoView',
@@ -86,7 +86,9 @@ export function optimisticDelete(
pages: old.pages.map(page => ({
...page,
requests: page.requests.filter(
item => !ChatBskyConvoDefs.isConvoView(item) || item.id !== chatId,
item =>
!bsky.isType(chat.bsky.convo.defs.convoView, item) ||
item.id !== chatId,
),
})),
}
@@ -102,7 +104,7 @@ export function markAllRead(
pages: old.pages.map(page => ({
...page,
requests: page.requests.map((item): ConvoRequestItem => {
if (ChatBskyConvoDefs.isConvoView(item)) {
if (bsky.isType(chat.bsky.convo.defs.convoView, item)) {
return {
...item,
$type: 'chat.bsky.convo.defs#convoView',
@@ -127,7 +129,7 @@ export function optimisticDeleteJoinRequest(
...page,
requests: page.requests.filter(
item =>
!ChatBskyGroupDefs.isJoinRequestConvoView(item) ||
!bsky.isType(chat.bsky.group.defs.joinRequestConvoView, item) ||
item.convoId !== convoId,
),
})),
@@ -139,7 +141,7 @@ export function* findAllProfilesInQueryData(
did: string,
) {
const queryDatas = queryClient.getQueriesData<
InfiniteData<ChatBskyConvoListConvoRequests.OutputSchema>
InfiniteData<chat.bsky.convo.listConvoRequests.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
@@ -148,13 +150,15 @@ export function* findAllProfilesInQueryData(
for (const page of queryData.pages) {
for (const item of page.requests) {
if (ChatBskyConvoDefs.isConvoView(item)) {
if (bsky.isType(chat.bsky.convo.defs.convoView, item)) {
for (const member of item.members) {
if (member.did === did) {
yield member
}
}
} else if (ChatBskyGroupDefs.isJoinRequestConvoView(item)) {
} else if (
bsky.isType(chat.bsky.group.defs.joinRequestConvoView, item)
) {
if (item.owner.did === did) {
yield item.owner
}
+151 -111
View File
@@ -1,9 +1,4 @@
import {useCallback, useEffect, useMemo} from 'react'
import {
type ChatBskyActorDefs,
ChatBskyConvoDefs,
type ChatBskyConvoListConvos,
} from '@atproto/api'
import {
type InfiniteData,
type Query,
@@ -14,11 +9,11 @@ import {
} from '@tanstack/react-query'
import throttle from 'lodash.throttle'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {useMessagesEventBus} from '#/state/messages/events'
import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links'
import {useAgent, useSession} from '#/state/session'
import {useChatClient, useSession} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {RQKEY as CONVO_KEY} from './conversation'
import {
@@ -68,7 +63,7 @@ export const RQKEY_PARTIAL = (
* filters client-side or convos leak into lists that should exclude them.
*/
export function convoMatchesQueryKey(
convo: ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
queryKey: QueryKey,
): boolean {
const [, status, readState, kind, lockStatus] = queryKey as ReturnType<
@@ -76,7 +71,7 @@ export function convoMatchesQueryKey(
>
if (status !== 'all' && status !== convo.status) return false
if (readState === 'unread' && convo.unreadCount === 0) return false
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
if (kind === 'direct') return false
if (lockStatus && convo.kind.lockStatus !== lockStatus) return false
} else {
@@ -94,7 +89,7 @@ export function convoMatchesQueryKey(
* longer matches (e.g. unreadCount dropped to 0), mirroring how read/mute
* log events update convos in place everywhere.
*/
export function convoListQueryPredicate(convo: ChatBskyConvoDefs.ConvoView) {
export function convoListQueryPredicate(convo: chat.bsky.convo.defs.ConvoView) {
return (query: Query): boolean => {
const data = query.state.data as ConvoListQueryData | undefined
if (data && getConvoFromQueryData(convo.id, data)) return true
@@ -119,24 +114,20 @@ export function useListConvosQuery({
limit?: number
lockStatus?: 'unlocked' | 'locked' | 'locked-permanently'
} = {}) {
const agent = useAgent()
const chatClient = useChatClient()
return useInfiniteQuery({
enabled,
queryKey: RQKEY(status ?? 'all', readState, kind, lockStatus, limit),
queryFn: async ({pageParam}) => {
const {data} = await agent.chat.bsky.convo.listConvos(
{
limit,
cursor: pageParam,
readState: readState === 'unread' ? 'unread' : undefined,
kind: kind === 'all' ? undefined : kind,
lockStatus,
status,
},
{headers: DM_SERVICE_HEADERS},
)
return data
return await chatClient.call(chat.bsky.convo.listConvos, {
limit,
cursor: pageParam,
readState: readState === 'unread' ? 'unread' : undefined,
kind: kind === 'all' ? undefined : kind,
lockStatus,
status,
})
},
initialPageParam: undefined as RQPageParam,
getNextPageParam: lastPage => lastPage.cursor,
@@ -200,10 +191,10 @@ export function ListConvosProviderInner({
function mutateMembers(
convoId: string,
fn: (
members: ChatBskyActorDefs.ProfileViewBasic[],
) => ChatBskyActorDefs.ProfileViewBasic[],
members: chat.bsky.actor.defs.ProfileViewBasic[],
) => chat.bsky.actor.defs.ProfileViewBasic[],
) {
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
queryClient.setQueryData<chat.bsky.actor.defs.ProfileViewBasic[]>(
listConvoMembersQueryKey(convoId),
old => {
if (!old) return // query doesn't exist yet, skip
@@ -215,8 +206,8 @@ export function ListConvosProviderInner({
function updateConvoInAllLists(
convoId: string,
fn: (
convo: ChatBskyConvoDefs.ConvoView,
) => ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
) => chat.bsky.convo.defs.ConvoView,
) {
queryClient.setQueriesData<ConvoListQueryData>(
{queryKey: [RQKEY_ROOT]},
@@ -231,10 +222,10 @@ export function ListConvosProviderInner({
function mutateConvoView(
convoId: string,
fn: (
convo: ChatBskyConvoDefs.ConvoView,
) => ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
) => chat.bsky.convo.defs.ConvoView,
) {
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
old => (old ? fn(old) : old),
)
@@ -255,7 +246,7 @@ export function ListConvosProviderInner({
function handleMemberAdded(
convoId: string,
did: string,
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
relatedProfiles: chat.bsky.actor.defs.ProfileViewBasic[],
rev: string,
) {
const newMember = relatedProfiles.find(r => r.did === did)
@@ -265,7 +256,7 @@ export function ListConvosProviderInner({
const alreadyKnownMember =
queryClient
.getQueryData<
ChatBskyActorDefs.ProfileViewBasic[]
chat.bsky.actor.defs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
?.some(m => m.did === did) ?? false
mutateMembers(convoId, list =>
@@ -289,7 +280,7 @@ export function ListConvosProviderInner({
const alreadyRemovedMember =
queryClient
.getQueryData<
ChatBskyActorDefs.ProfileViewBasic[]
chat.bsky.actor.defs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
?.some(m => m.did === did) === false
mutateMembers(convoId, list => list.filter(m => m.did !== did))
@@ -302,24 +293,36 @@ export function ListConvosProviderInner({
}
for (const log of events.logs) {
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
if (bsky.isType(chat.bsky.convo.defs.logBeginConvo, log)) {
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logLeaveConvo, log)) {
deleteConvoFromAllLists(log.convoId)
// The viewer is no longer in this convo (they left on another
// device, or were removed - removed members receive a
// logLeaveConvo, not a logRemoveMember). Refetch any cached join
// link preview so its viewer state reflects the lost membership.
void invalidateJoinLinkPreviewsForConvo(queryClient, log.convoId)
} else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logDeleteMessage, log)) {
updateConvoInAllLists(
log.convoId,
withRevGuard(log.rev, convo => {
if (
(ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
ChatBskyConvoDefs.isMessageView(log.message)) &&
(ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) ||
ChatBskyConvoDefs.isMessageView(convo.lastMessage))
(bsky.isType(
chat.bsky.convo.defs.deletedMessageView,
log.message,
) ||
bsky.isType(
chat.bsky.convo.defs.messageView,
log.message,
)) &&
(bsky.isType(
chat.bsky.convo.defs.deletedMessageView,
convo.lastMessage,
) ||
bsky.isType(
chat.bsky.convo.defs.messageView,
convo.lastMessage,
))
) {
return log.message.id === convo.lastMessage.id
? {
@@ -333,9 +336,9 @@ export function ListConvosProviderInner({
}
}),
)
} else if (ChatBskyConvoDefs.isLogCreateMessage(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logCreateMessage, log)) {
// Store in a new var to avoid TS errors due to closures.
const logRef: ChatBskyConvoDefs.LogCreateMessage = log
const logRef: chat.bsky.convo.defs.LogCreateMessage = log
// Get all matching queries
const queries = queryClient.getQueriesData<ConvoListQueryData>({
@@ -343,7 +346,7 @@ export function ListConvosProviderInner({
})
// Check if convo exists in any query
let foundConvo: ChatBskyConvoDefs.ConvoView | null = null
let foundConvo: chat.bsky.convo.defs.ConvoView | null = null
for (const [_key, query] of queries) {
if (!query) continue
const convo = getConvoFromQueryData(logRef.convoId, query)
@@ -386,15 +389,23 @@ export function ListConvosProviderInner({
lastMessage: logRef.message,
unreadCount:
foundConvo.id !== currentConvoId
? (ChatBskyConvoDefs.isMessageView(logRef.message) ||
ChatBskyConvoDefs.isDeletedMessageView(logRef.message)) &&
? (bsky.isType(
chat.bsky.convo.defs.messageView,
logRef.message,
) ||
bsky.isType(
chat.bsky.convo.defs.deletedMessageView,
logRef.message,
)) &&
logRef.message.sender.did !== currentAccount?.did
? foundConvo.unreadCount + 1
: foundConvo.unreadCount
: 0,
}
function filterConvoFromPage(convo: ChatBskyConvoDefs.ConvoView[]) {
function filterConvoFromPage(
convo: chat.bsky.convo.defs.ConvoView[],
) {
return convo.filter(c => c.id !== logRef.convoId)
}
@@ -457,7 +468,7 @@ export function ListConvosProviderInner({
old => moveConvoToTopInRequests(updatedConvo, old),
)
}
} else if (ChatBskyConvoDefs.isLogReadMessage(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logReadMessage, log)) {
updateConvoInAllLists(
log.convoId,
withRevGuard(log.rev, convo => ({
@@ -466,7 +477,7 @@ export function ListConvosProviderInner({
rev: log.rev,
})),
)
} else if (ChatBskyConvoDefs.isLogReadConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logReadConvo, log)) {
updateConvoInAllLists(
log.convoId,
withRevGuard(log.rev, convo => ({
@@ -475,12 +486,12 @@ export function ListConvosProviderInner({
rev: log.rev,
})),
)
} else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logAcceptConvo, log)) {
const requestQueries =
queryClient.getQueriesData<ConvoListQueryData>({
queryKey: RQKEY_PARTIAL('request'),
})
let foundConvo: ChatBskyConvoDefs.ConvoView | null = null
let foundConvo: chat.bsky.convo.defs.ConvoView | null = null
for (const [_key, data] of requestQueries) {
if (!data) continue
foundConvo = getConvoFromQueryData(log.convoId, data)
@@ -496,7 +507,7 @@ export function ListConvosProviderInner({
if (log.rev <= foundConvo.rev) {
continue
}
const acceptedConvo: ChatBskyConvoDefs.ConvoView = {
const acceptedConvo: chat.bsky.convo.defs.ConvoView = {
...foundConvo,
status: 'accepted',
rev: log.rev,
@@ -557,7 +568,7 @@ export function ListConvosProviderInner({
}
},
)
} else if (ChatBskyConvoDefs.isLogMuteConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logMuteConvo, log)) {
mutateConvoView(
log.convoId,
withRevGuard(log.rev, convo => ({
@@ -566,7 +577,7 @@ export function ListConvosProviderInner({
rev: log.rev,
})),
)
} else if (ChatBskyConvoDefs.isLogUnmuteConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logUnmuteConvo, log)) {
mutateConvoView(
log.convoId,
withRevGuard(log.rev, convo => ({
@@ -575,11 +586,11 @@ export function ListConvosProviderInner({
rev: log.rev,
})),
)
} else if (ChatBskyConvoDefs.isLogLockConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logLockConvo, log)) {
mutateConvoView(
log.convoId,
withRevGuard(log.rev, convo => {
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
return {
...convo,
kind: {...convo.kind, lockStatus: 'locked'},
@@ -594,11 +605,11 @@ export function ListConvosProviderInner({
void queryClient.invalidateQueries({
queryKey: CONVO_KEY(log.convoId),
})
} else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logUnlockConvo, log)) {
mutateConvoView(
log.convoId,
withRevGuard(log.rev, convo => {
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
return {
...convo,
kind: {
@@ -613,11 +624,13 @@ export function ListConvosProviderInner({
return {...convo, rev: log.rev}
}),
)
} else if (ChatBskyConvoDefs.isLogLockConvoPermanently(log)) {
} else if (
bsky.isType(chat.bsky.convo.defs.logLockConvoPermanently, log)
) {
mutateConvoView(
log.convoId,
withRevGuard(log.rev, convo => {
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
return {
...convo,
kind: {...convo.kind, lockStatus: 'locked-permanently'},
@@ -628,20 +641,20 @@ export function ListConvosProviderInner({
}),
)
} else if (
ChatBskyConvoDefs.isLogCreateJoinLink(log) ||
ChatBskyConvoDefs.isLogEditJoinLink(log) ||
ChatBskyConvoDefs.isLogEnableJoinLink(log) ||
ChatBskyConvoDefs.isLogDisableJoinLink(log)
bsky.isType(chat.bsky.convo.defs.logCreateJoinLink, log) ||
bsky.isType(chat.bsky.convo.defs.logEditJoinLink, log) ||
bsky.isType(chat.bsky.convo.defs.logEnableJoinLink, log) ||
bsky.isType(chat.bsky.convo.defs.logDisableJoinLink, log)
) {
// Join link data not included in the log event, trigger refetch to get it
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogEditGroup(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logEditGroup, log)) {
// Updated group details (name etc.) aren't included in the log
// event, so refetch to pick them up.
debouncedRefetch()
} else if (
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
bsky.isType(chat.bsky.convo.defs.logApproveJoinRequest, log) ||
bsky.isType(chat.bsky.convo.defs.logRejectJoinRequest, log)
) {
// Route through mutateConvoView (not updateConvoInAllLists) so the
// single-convo cache updates too, keeping the in-convo requests
@@ -652,7 +665,9 @@ export function ListConvosProviderInner({
applyJoinRequestCountDelta(convo, log.rev, -1),
),
)
} else if (ChatBskyConvoDefs.isLogIncomingJoinRequest(log)) {
} else if (
bsky.isType(chat.bsky.convo.defs.logIncomingJoinRequest, log)
) {
// Route through mutateConvoView (not updateConvoInAllLists) so the
// single-convo cache updates too, letting the in-convo requests
// banner appear live.
@@ -662,14 +677,16 @@ export function ListConvosProviderInner({
applyJoinRequestCountDelta(convo, log.rev, 1),
),
)
} else if (ChatBskyConvoDefs.isLogReadJoinRequests(log)) {
} else if (
bsky.isType(chat.bsky.convo.defs.logReadJoinRequests, log)
) {
// The owner marked join requests as read (possibly on another
// device). Zero the unread count but keep the total, mirroring the
// useMarkJoinRequestsRead mutation.
mutateConvoView(
log.convoId,
withRevGuard(log.rev, convo => {
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
return {...convo, rev: log.rev}
}
return {
@@ -679,11 +696,18 @@ export function ListConvosProviderInner({
}
}),
)
} else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) {
} else if (
bsky.isType(chat.bsky.convo.defs.logOutgoingJoinRequest, log)
) {
// Viewer isn't in the chat yet, but the inbox surfaces outgoing
// requests, so refetch to pick up the new entry.
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogWithdrawIncomingJoinRequest(log)) {
} else if (
bsky.isType(
chat.bsky.convo.defs.logWithdrawIncomingJoinRequest,
log,
)
) {
// A requester rescinded their request to a group the viewer owns.
// Mirror of isLogIncomingJoinRequest: decrement the counts.
mutateConvoView(
@@ -692,14 +716,19 @@ export function ListConvosProviderInner({
applyJoinRequestCountDelta(convo, log.rev, -1),
),
)
} else if (ChatBskyConvoDefs.isLogWithdrawOutgoingJoinRequest(log)) {
} else if (
bsky.isType(
chat.bsky.convo.defs.logWithdrawOutgoingJoinRequest,
log,
)
) {
// The viewer rescinded their own outgoing join request (possibly on
// another device). Remove it from the requests inbox cache.
queryClient.setQueriesData<ConvoRequestListQueryData>(
{queryKey: [REQUESTS_RQKEY_ROOT]},
old => optimisticDeleteJoinRequest(log.convoId, old),
)
} else if (ChatBskyConvoDefs.isLogAddReaction(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logAddReaction, log)) {
updateConvoInAllLists(
log.convoId,
withRevGuard(log.rev, convo => {
@@ -713,22 +742,28 @@ export function ListConvosProviderInner({
return {
...convo,
members: [...convo.members, ...relatedProfilesSansMembers],
lastReaction: {
/*
* `log.message` can also be a deleted-message view per the
* log union, which the strict MessageAndReactionView type
* rejects - the old types absorbed this via the open-union
* catch-all. Keep the pre-migration runtime behavior (always
* store the view we got) and assert the cache type.
*/
lastReaction: bsky.toLex<
NonNullable<chat.bsky.convo.defs.ConvoView['lastReaction']>
>({
$type: 'chat.bsky.convo.defs#messageAndReactionView',
reaction: log.reaction,
message: log.message,
},
}),
rev: log.rev,
}
}),
)
} else if (ChatBskyConvoDefs.isLogAddMember(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logAddMember, log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataAddMember>(
data,
ChatBskyConvoDefs.isSystemMessageDataAddMember,
)
bsky.isType(chat.bsky.convo.defs.systemMessageDataAddMember, data)
) {
handleMemberAdded(
log.convoId,
@@ -742,12 +777,12 @@ export function ListConvosProviderInner({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logRemoveMember, log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataRemoveMember>(
bsky.isType(
chat.bsky.convo.defs.systemMessageDataRemoveMember,
data,
ChatBskyConvoDefs.isSystemMessageDataRemoveMember,
)
) {
handleMemberRemoved(log.convoId, data.member.did, log.rev)
@@ -757,12 +792,12 @@ export function ListConvosProviderInner({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogMemberJoin(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logMemberJoin, log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataMemberJoin>(
bsky.isType(
chat.bsky.convo.defs.systemMessageDataMemberJoin,
data,
ChatBskyConvoDefs.isSystemMessageDataMemberJoin,
)
) {
handleMemberAdded(
@@ -776,12 +811,12 @@ export function ListConvosProviderInner({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogMemberLeave(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logMemberLeave, log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataMemberLeave>(
bsky.isType(
chat.bsky.convo.defs.systemMessageDataMemberLeave,
data,
ChatBskyConvoDefs.isSystemMessageDataMemberLeave,
)
) {
handleMemberRemoved(log.convoId, data.member.did, log.rev)
@@ -790,7 +825,7 @@ export function ListConvosProviderInner({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) {
} else if (bsky.isType(chat.bsky.convo.defs.logRemoveReaction, log)) {
queryClient.setQueriesData(
{queryKey: [RQKEY_ROOT]},
(old?: ConvoListQueryData) =>
@@ -801,10 +836,14 @@ export function ListConvosProviderInner({
if (
// if the convo is the same
log.convoId === convo.id &&
ChatBskyConvoDefs.isMessageAndReactionView(
bsky.isType(
chat.bsky.convo.defs.messageAndReactionView,
convo.lastReaction,
) &&
ChatBskyConvoDefs.isMessageView(log.message) &&
bsky.isType(
chat.bsky.convo.defs.messageView,
log.message,
) &&
// ...and the message is the same
convo.lastReaction.message.id === log.message.id &&
// ...and the reaction is the same
@@ -886,7 +925,7 @@ export function useUnreadMessageCount(): {
export type ConvoListQueryData = {
pageParams: Array<string | undefined>
pages: Array<ChatBskyConvoListConvos.OutputSchema>
pages: Array<chat.bsky.convo.listConvos.$OutputBody>
}
export function useOnMarkAsRead() {
@@ -923,8 +962,8 @@ export function useOnMarkAsRead() {
*/
function withRevGuard(
rev: string,
fn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
): (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView {
fn: (convo: chat.bsky.convo.defs.ConvoView) => chat.bsky.convo.defs.ConvoView,
): (convo: chat.bsky.convo.defs.ConvoView) => chat.bsky.convo.defs.ConvoView {
return convo => (rev <= convo.rev ? convo : fn(convo))
}
@@ -932,8 +971,8 @@ function optimisticUpdate(
chatId: string,
old?: ConvoListQueryData,
updateFn?: (
convo: ChatBskyConvoDefs.ConvoView,
) => ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
) => chat.bsky.convo.defs.ConvoView,
) {
if (!old || !updateFn) return old
@@ -949,12 +988,12 @@ function optimisticUpdate(
}
function applyJoinRequestCountDelta(
convo: ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
rev: string,
delta: 1 | -1,
): ChatBskyConvoDefs.ConvoView {
): chat.bsky.convo.defs.ConvoView {
// Join requests are only meaningful for group convos.
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
return {...convo, rev}
}
// Bump the total and unread counts together. Both are clamped at 0 and
@@ -975,7 +1014,7 @@ function applyJoinRequestCountDelta(
}
function moveConvoToTopInRequests(
updatedConvo: ChatBskyConvoDefs.ConvoView,
updatedConvo: chat.bsky.convo.defs.ConvoView,
old: ConvoRequestListQueryData | undefined,
): ConvoRequestListQueryData | undefined {
if (!old) return old
@@ -989,7 +1028,8 @@ function moveConvoToTopInRequests(
pages: old.pages.map((page, i) => {
const filtered = page.requests.filter(
item =>
!ChatBskyConvoDefs.isConvoView(item) || item.id !== updatedConvo.id,
!bsky.isType(chat.bsky.convo.defs.convoView, item) ||
item.id !== updatedConvo.id,
)
if (i === 0) {
return {
@@ -1003,13 +1043,13 @@ function moveConvoToTopInRequests(
}
function removeMemberFromConvoView(
convo: ChatBskyConvoDefs.ConvoView,
convo: chat.bsky.convo.defs.ConvoView,
did: string,
rev: string,
alreadyRemovedMember: boolean,
): ChatBskyConvoDefs.ConvoView {
): chat.bsky.convo.defs.ConvoView {
// Member add/remove/join/leave events are only meaningful for group convos.
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) return convo
const nextMembers = convo.members.filter(m => m.did !== did)
return {
...convo,
@@ -1025,13 +1065,13 @@ function removeMemberFromConvoView(
}
function addMemberToConvoView(
convo: ChatBskyConvoDefs.ConvoView,
member: ChatBskyActorDefs.ProfileViewBasic,
convo: chat.bsky.convo.defs.ConvoView,
member: chat.bsky.actor.defs.ProfileViewBasic,
rev: string,
alreadyKnownMember: boolean,
): ChatBskyConvoDefs.ConvoView {
): chat.bsky.convo.defs.ConvoView {
// Member add/remove/join/leave events are only meaningful for group convos.
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) return convo
const alreadyInCuratedList = convo.members.some(m => m.did === member.did)
const nextMembers = alreadyInCuratedList
? convo.members
@@ -1077,7 +1117,7 @@ export function* findAllProfilesInQueryData(
did: string,
) {
const queryDatas = queryClient.getQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
@@ -1,10 +1,9 @@
import {type ChatBskyActorDefs} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {STALE} from '#/state/queries'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
const RQKEY_ROOT = 'listConvoMembers'
export const listConvoMembersQueryKey = (convoId: string) =>
@@ -18,21 +17,22 @@ export function useListConvoMembersQuery({
placeholderData,
}: {
convoId: string
placeholderData?: ChatBskyActorDefs.ProfileViewBasic[]
placeholderData?: chat.bsky.actor.defs.ProfileViewBasic[]
}) {
const agent = useAgent()
const chatClient = useChatClient()
return useQuery({
queryKey: listConvoMembersQueryKey(convoId),
queryFn: async () => {
const members = []
let cursor
let cursor: string | undefined
do {
const {data} = await agent.chat.bsky.convo.getConvoMembers(
{convoId, cursor, limit: LIMIT},
{headers: DM_SERVICE_HEADERS},
)
const data = await chatClient.call(chat.bsky.convo.getConvoMembers, {
convoId,
cursor,
limit: LIMIT,
})
members.push(...data.members)
cursor = data.cursor
} while (cursor)
@@ -47,9 +47,9 @@ export function useListConvoMembersQuery({
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<ChatBskyActorDefs.ProfileViewBasic, void> {
): Generator<chat.bsky.actor.defs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<
ChatBskyActorDefs.ProfileViewBasic[]
chat.bsky.actor.defs.ProfileViewBasic[]
>({
queryKey: [RQKEY_ROOT],
})
@@ -1,11 +1,11 @@
import {useEffect} from 'react'
import {ChatBskyConvoDefs} from '@atproto/api'
import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useMessagesEventBus} from '#/state/messages/events'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {STALE} from '..'
export const JOIN_REQUESTS_THRESHOLD = 20
@@ -22,7 +22,7 @@ export function useListJoinRequestsQuery({
convoId: string | undefined
enabled?: boolean
}) {
const agent = useAgent()
const chatClient = useChatClient()
const queryClient = useQueryClient()
const messagesBus = useMessagesEventBus()
const isEnabled = enabled !== false && !!convoId
@@ -35,9 +35,9 @@ export function useListJoinRequestsQuery({
if (event.type !== 'logs') return
for (const log of event.logs) {
if (
ChatBskyConvoDefs.isLogIncomingJoinRequest(log) ||
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
bsky.isType(chat.bsky.convo.defs.logIncomingJoinRequest, log) ||
bsky.isType(chat.bsky.convo.defs.logApproveJoinRequest, log) ||
bsky.isType(chat.bsky.convo.defs.logRejectJoinRequest, log)
) {
void queryClient.invalidateQueries({
queryKey: createListJoinRequestsQueryKey({convoId}),
@@ -54,11 +54,11 @@ export function useListJoinRequestsQuery({
enabled: isEnabled,
queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}),
queryFn: async ({pageParam}) => {
const {data} = await agent.chat.bsky.group.listJoinRequests(
{convoId: convoId!, cursor: pageParam, limit: JOIN_REQUESTS_THRESHOLD},
{headers: DM_SERVICE_HEADERS},
)
return data
return await chatClient.call(chat.bsky.group.listJoinRequests, {
convoId: convoId!,
cursor: pageParam,
limit: JOIN_REQUESTS_THRESHOLD,
})
},
initialPageParam: undefined as string | undefined,
getNextPageParam: page => page.cursor,
@@ -1,8 +1,9 @@
import {type DidString} from '@atproto/syntax'
import {useInfiniteQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
const listMutualGroupsQueryKeyRoot = 'list-mutual-groups'
@@ -18,7 +19,7 @@ export function useListMutualGroupsQuery({
enabled?: boolean
limit?: number
}) {
const agent = useAgent()
const chatClient = useChatClient()
const isEnabled = enabled !== false && !!subject
return useInfiniteQuery({
@@ -27,11 +28,11 @@ export function useListMutualGroupsQuery({
enabled: isEnabled,
queryKey: createListMutualGroupsQueryKey({subject: subject ?? ''}),
queryFn: async ({pageParam}) => {
const {data} = await agent.chat.bsky.group.listMutualGroups(
{subject: subject!, cursor: pageParam, limit},
{headers: DM_SERVICE_HEADERS},
)
return data
return await chatClient.call(chat.bsky.group.listMutualGroups, {
subject: subject! as DidString,
cursor: pageParam,
limit,
})
},
initialPageParam: undefined as string | undefined,
getNextPageParam: page => page.cursor,
+11 -14
View File
@@ -1,8 +1,8 @@
import {ChatBskyConvoDefs, type ChatBskyConvoLockConvo} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -15,7 +15,7 @@ export function useLockConvo(
onError,
}: {
onSuccess?: (
data: ChatBskyConvoLockConvo.OutputSchema,
data: chat.bsky.convo.lockConvo.$OutputBody,
variables: {lock: boolean; silent?: boolean},
) => void
onError?: (
@@ -25,29 +25,26 @@ export function useLockConvo(
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({lock}: {lock: boolean; silent?: boolean}) => {
if (!convoId) throw new Error('No convoId provided')
if (lock) {
const {data} = await agent.chat.bsky.convo.lockConvo(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.convo.lockConvo, {convoId})
return data
} else {
const {data} = await agent.chat.bsky.convo.unlockConvo(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.convo.unlockConvo, {
convoId,
})
return data
}
},
onMutate: ({lock}) => {
if (!convoId) return
return updateConvoOptimistic(queryClient, convoId, prev => {
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind))
return undefined
return {
...prev,
kind: {
@@ -1,9 +1,9 @@
import {ChatBskyConvoDefs} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {RQKEY as CONVO_KEY} from './conversation'
import {
type ConvoListQueryData,
@@ -12,26 +12,25 @@ import {
export function useMarkJoinRequestsRead(convoId: string | undefined) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided')
await agent.chat.bsky.group.updateJoinRequestsRead(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
await chatClient.call(chat.bsky.group.updateJoinRequestsRead, {convoId})
},
onMutate: () => {
if (!convoId) return
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
CONVO_KEY(convoId),
)
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView | undefined>(
const prevConvo =
queryClient.getQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
)
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView | undefined>(
CONVO_KEY(convoId),
old => {
if (!old || !ChatBskyConvoDefs.isGroupConvo(old.kind)) return old
if (!old || !bsky.isType(chat.bsky.convo.defs.groupConvo, old.kind))
return old
return {
...old,
kind: {...old.kind, unreadJoinRequestCount: 0},
@@ -53,7 +52,7 @@ export function useMarkJoinRequestsRead(convoId: string | undefined) {
convos: page.convos.map(convo => {
if (
convo.id !== convoId ||
!ChatBskyConvoDefs.isGroupConvo(convo.kind)
!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)
) {
return convo
}
@@ -1,8 +1,7 @@
import {type ChatBskyConvoMuteConvo} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {
rollbackConvoOptimistic,
updateConvoOptimistic,
@@ -14,27 +13,23 @@ export function useMuteConvo(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyConvoMuteConvo.OutputSchema) => void
onSuccess?: (data: chat.bsky.convo.muteConvo.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({mute}: {mute: boolean}) => {
if (!convoId) throw new Error('No convoId provided')
if (mute) {
const {data} = await agent.chat.bsky.convo.muteConvo(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.convo.muteConvo, {convoId})
return data
} else {
const {data} = await agent.chat.bsky.convo.unmuteConvo(
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.convo.unmuteConvo, {
convoId,
})
return data
}
},
+21 -24
View File
@@ -1,18 +1,14 @@
import {
type ChatBskyActorDefs,
ChatBskyConvoDefs,
type ChatBskyConvoListConvos,
type ChatBskyGroupRemoveMembers,
} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {
type InfiniteData,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {RQKEY as CONVO_KEY} from './conversation'
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
import {listConvoMembersQueryKey} from './list-convo-members'
@@ -23,42 +19,43 @@ export function useRemoveFromGroupChat(
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupRemoveMembers.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.removeMembers.$OutputBody) => void
onError?: (error: Error) => void
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({members}: {members: string[]}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.removeMembers(
{convoId, members},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.group.removeMembers, {
convoId,
members: members as DidString[],
})
return data
},
onMutate: ({members}) => {
if (!convoId) return
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
CONVO_KEY(convoId),
)
const prevConvo =
queryClient.getQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
)
const prevListEntries = queryClient.getQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]})
const prevMemberList = queryClient.getQueryData<
ChatBskyActorDefs.ProfileViewBasic[]
chat.bsky.actor.defs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
prev => {
if (!prev) return
const nextMembers = prev.members.filter(m => !members.includes(m.did))
const removed = prev.members.length - nextMembers.length
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) {
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) {
return {...prev, members: nextMembers}
}
return {
@@ -73,7 +70,7 @@ export function useRemoveFromGroupChat(
)
queryClient.setQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]}, prev => {
if (!prev?.pages) return
return {
@@ -86,7 +83,7 @@ export function useRemoveFromGroupChat(
m => !members.includes(m.did),
)
const removed = convo.members.length - nextMembers.length
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) {
return {...convo, members: nextMembers}
}
return {
@@ -102,7 +99,7 @@ export function useRemoveFromGroupChat(
}
})
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
queryClient.setQueryData<chat.bsky.actor.defs.ProfileViewBasic[]>(
listConvoMembersQueryKey(convoId),
prev => {
if (!prev) return
@@ -1,19 +1,18 @@
import {type ChatBskyGroupRequestJoin} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session'
import {useChatClient, useSession} from '#/state/session'
import {chat} from '#/lexicons'
import {RQKEY_ROOT as REQUESTS_RQKEY_ROOT} from './list-conversation-requests'
export function useRequestJoinGroupChat({
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.requestJoin.$OutputBody) => void
onError?: (error: Error) => void
} = {}) {
const agent = useAgent()
const chatClient = useChatClient()
const queryClient = useQueryClient()
const {hasSession} = useSession()
@@ -22,11 +21,8 @@ export function useRequestJoinGroupChat({
if (!hasSession) throw new Error('Must be logged in to join')
if (!code) throw new Error('No invite code')
const res = await agent.chat.bsky.group.requestJoin(
{code},
{headers: DM_SERVICE_HEADERS},
)
return res.data
const res = await chatClient.call(chat.bsky.group.requestJoin, {code})
return res
},
onSuccess: data => {
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
@@ -1,13 +1,15 @@
import {type ChatBskyActorDeclaration} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {networkRetry} from '#/lib/async/retry'
import {logger} from '#/logger'
import {type SessionAgent} from '#/state/session'
import {agentToLexClient} from '#/state/session/clients'
import {
getDidFromAgentSession,
getOtherRequiredDataFromCache,
setOtherRequiredDataActorDeclarationCache,
} from '#/ageAssurance/data'
import {chat} from '#/lexicons'
/**
* Updates the chat actor declaration record to restrict who can contact the
@@ -49,7 +51,7 @@ export async function restrictChatSettings({
)
}
const record: ChatBskyActorDeclaration.Main = {
const record: chat.bsky.actor.declaration.Main = {
$type: 'chat.bsky.actor.declaration',
allowIncoming: restrictIncoming
? 'none'
@@ -67,13 +69,23 @@ export async function restrictChatSettings({
return
}
/*
* Callers thread a bridge `SessionAgent` (session-core / birthdate); wrap it
* as an account lex `Client` so the record write goes through the lex path.
* The cast is safe: `agentToLexClient` only reads `did` and `fetchHandler`,
* both of which the base `Agent` provides - its `AtpAgent` parameter type is
* just narrower than it needs. TODO(phase4): take a Client directly once the
* bridge is removed.
*/
const client = agentToLexClient(
agent as unknown as Parameters<typeof agentToLexClient>[0],
)
try {
await networkRetry(3, () =>
agent.com.atproto.repo.putRecord({
repo: did,
collection: 'chat.bsky.actor.declaration',
client.put(chat.bsky.actor.declaration, record, {
repo: did as DidString,
rkey: 'self',
record,
}),
)
// important, update local cache to avoid running this again
+12 -12
View File
@@ -1,9 +1,8 @@
import {type ChatBskyConvoGetUnreadCounts} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY} from './get-unread-counts'
import {
type ConvoRequestListQueryData,
@@ -29,14 +28,13 @@ export function useUpdateAllRead(
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const chatClient = useChatClient()
return useMutation({
mutationFn: async () => {
const {data} = await agent.chat.bsky.convo.updateAllRead(
{status},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
const data = await chatClient.call(chat.bsky.convo.updateAllRead, {
status,
})
return data
},
@@ -99,10 +97,12 @@ export function useUpdateAllRead(
// zero out the badge count query that actually drives the unread badge,
// since it's a separate server query that the list caches don't feed
const prevUnreadCountsQueries =
queryClient.getQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>({
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
})
queryClient.setQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>(
queryClient.getQueriesData<chat.bsky.convo.getUnreadCounts.$OutputBody>(
{
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
},
)
queryClient.setQueriesData<chat.bsky.convo.getUnreadCounts.$OutputBody>(
{queryKey: UNREAD_COUNTS_PARTIAL_KEY},
old => {
if (!old) return old
@@ -1,24 +1,21 @@
import {
type ChatBskyConvoDefs,
type ChatBskyConvoListConvos,
} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
type QueryKey,
} from '@tanstack/react-query'
import {type chat} from '#/lexicons'
import {RQKEY as CONVO_KEY} from '../conversation'
import {RQKEY_ROOT as CONVO_LIST_KEY} from '../list-conversations'
type ConvoUpdater = (
prev: ChatBskyConvoDefs.ConvoView,
) => ChatBskyConvoDefs.ConvoView | undefined
prev: chat.bsky.convo.defs.ConvoView,
) => chat.bsky.convo.defs.ConvoView | undefined
export type ConvoCacheSnapshot = {
prevConvo: ChatBskyConvoDefs.ConvoView | undefined
prevConvo: chat.bsky.convo.defs.ConvoView | undefined
prevListEntries: Array<
[QueryKey, InfiniteData<ChatBskyConvoListConvos.OutputSchema> | undefined]
[QueryKey, InfiniteData<chat.bsky.convo.listConvos.$OutputBody> | undefined]
>
}
@@ -34,14 +31,14 @@ export function updateConvoOptimistic(
convoId: string,
updater: ConvoUpdater,
): ConvoCacheSnapshot {
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
const prevConvo = queryClient.getQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
)
const prevListEntries = queryClient.getQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]})
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
queryClient.setQueryData<chat.bsky.convo.defs.ConvoView>(
CONVO_KEY(convoId),
prev => {
if (!prev) return
@@ -51,7 +48,7 @@ export function updateConvoOptimistic(
)
queryClient.setQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
InfiniteData<chat.bsky.convo.listConvos.$OutputBody>
>({queryKey: [CONVO_LIST_KEY]}, prev => {
if (!prev?.pages) return
return {
@@ -1,9 +1,8 @@
import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session'
import {useChatClient, useSession} from '#/state/session'
import {chat} from '#/lexicons'
import {
type ConvoRequestListQueryData,
optimisticDeleteJoinRequest,
@@ -14,10 +13,10 @@ export function useWithdrawJoinGroupChatRequest({
onSuccess,
onError,
}: {
onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void
onSuccess?: (data: chat.bsky.group.withdrawJoinRequest.$OutputBody) => void
onError?: (error: Error) => void
} = {}) {
const agent = useAgent()
const chatClient = useChatClient()
const queryClient = useQueryClient()
const {hasSession} = useSession()
@@ -27,11 +26,10 @@ export function useWithdrawJoinGroupChatRequest({
throw new Error('Must be logged in to withdraw a join request')
if (!convoId) throw new Error('No convoId provided')
const res = await agent.chat.bsky.group.withdrawJoinRequest(
{convoId},
{headers: DM_SERVICE_HEADERS},
)
return res.data
const res = await chatClient.call(chat.bsky.group.withdrawJoinRequest, {
convoId,
})
return res
},
onSuccess: (data, {convoId}) => {
queryClient.setQueriesData<ConvoRequestListQueryData>(
+8 -9
View File
@@ -1,4 +1,3 @@
import {type AppBskyActorDefs, type AppBskyGraphGetBlocks} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -6,28 +5,28 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const RQKEY_ROOT = 'my-blocked-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyBlockedAccountsQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyGraphGetBlocks.OutputSchema,
app.bsky.graph.getBlocks.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetBlocks.OutputSchema>,
InfiniteData<app.bsky.graph.getBlocks.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getBlocks({
return await client.call(app.bsky.graph.getBlocks, {
limit: 30,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -37,9 +36,9 @@ export function useMyBlockedAccountsQuery() {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetBlocks.OutputSchema>
InfiniteData<app.bsky.graph.getBlocks.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+19 -18
View File
@@ -1,9 +1,10 @@
import {type AppBskyGraphDefs} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {accumulate} from '#/lib/async/accumulate'
import {STALE} from '#/state/queries'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {app} from '#/lexicons'
export type MyListsFilter =
| 'all'
@@ -16,50 +17,50 @@ export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter]
export function useMyListsQuery(filter: MyListsFilter) {
const {currentAccount} = useSession()
const agent = useAgent()
return useQuery<AppBskyGraphDefs.ListView[]>({
const client = useAppviewClient()
return useQuery<app.bsky.graph.defs.ListView[]>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(filter),
async queryFn() {
let lists: AppBskyGraphDefs.ListView[] = []
let lists: app.bsky.graph.defs.ListView[] = []
const promises = [
accumulate(cursor =>
agent.app.bsky.graph
.getLists({
actor: currentAccount!.did,
client
.call(app.bsky.graph.getLists, {
actor: currentAccount!.did as AtIdentifierString,
cursor,
limit: 50,
})
.then(res => ({
cursor: res.data.cursor,
items: res.data.lists,
cursor: res.cursor,
items: res.lists,
})),
),
]
if (filter === 'all-including-subscribed' || filter === 'mod') {
promises.push(
accumulate(cursor =>
agent.app.bsky.graph
.getListMutes({
client
.call(app.bsky.graph.getListMutes, {
cursor,
limit: 50,
})
.then(res => ({
cursor: res.data.cursor,
items: res.data.lists,
cursor: res.cursor,
items: res.lists,
})),
),
)
promises.push(
accumulate(cursor =>
agent.app.bsky.graph
.getListBlocks({
client
.call(app.bsky.graph.getListBlocks, {
cursor,
limit: 50,
})
.then(res => ({
cursor: res.data.cursor,
items: res.data.lists,
cursor: res.cursor,
items: res.lists,
})),
),
)
+8 -9
View File
@@ -1,4 +1,3 @@
import {type AppBskyActorDefs, type AppBskyGraphGetMutes} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -6,28 +5,28 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const RQKEY_ROOT = 'my-muted-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyMutedAccountsQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyGraphGetMutes.OutputSchema,
app.bsky.graph.getMutes.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetMutes.OutputSchema>,
InfiniteData<app.bsky.graph.getMutes.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getMutes({
return await client.call(app.bsky.graph.getMutes, {
limit: 30,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -37,9 +36,9 @@ export function useMyMutedAccountsQuery() {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetMutes.OutputSchema>
InfiniteData<app.bsky.graph.getMutes.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+18 -15
View File
@@ -17,12 +17,8 @@
*/
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
moderatePost,
} from '@atproto/api'
import {AtUri} from '@atproto/syntax'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {
type InfiniteData,
type QueryClient,
@@ -33,9 +29,10 @@ import {
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
import type * as bsky from '#/types/bsky'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
@@ -60,7 +57,7 @@ export function useNotificationFeedQuery(opts: {
enabled?: boolean
filter: 'all' | 'mentions'
}) {
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const unreads = useUnreadNotificationsApi()
@@ -106,7 +103,7 @@ export function useNotificationFeedQuery(opts: {
]
}
const {page: fetchedPage} = await fetchPage({
agent,
client,
limit: PAGE_SIZE,
cursor: pageParam,
queryClient,
@@ -199,7 +196,9 @@ export function useNotificationFeedQuery(opts: {
* a `$type` field on the `subject`. But if the nested
* `record` is a post, we know it's a post view.
*/
if (AppBskyFeedPost.isRecord(item.subject?.record)) {
if (
bsky.isType(app.bsky.feed.post, item.subject?.record)
) {
const mod = moderatePost(item.subject, moderationOpts!)
if (mod.ui('contentList').filter) {
return false
@@ -276,7 +275,7 @@ export function useNotificationFeedQuery(opts: {
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, void> {
): Generator<app.bsky.feed.defs.PostView, void> {
const atUri = new AtUri(uri)
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
@@ -295,10 +294,14 @@ export function* findAllPostsInQueryData(
}
}
if (AppBskyFeedDefs.isPostView(item.subject)) {
if (bsky.isType(app.bsky.feed.defs.postView, item.subject)) {
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
yield embedViewRecordToPostView(quotedPost)
// TODO(phase4): drop toLex once ../util is migrated and
// embedViewRecordToPostView returns the lexicon PostView.
yield bsky.toLex<app.bsky.feed.defs.PostView>(
embedViewRecordToPostView(quotedPost),
)
}
}
}
@@ -333,7 +336,7 @@ export function* findAllProfilesInQueryData(
) {
yield item.subject.author
}
if (AppBskyFeedDefs.isPostView(item.subject)) {
if (bsky.isType(app.bsky.feed.defs.postView, item.subject)) {
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
+27 -32
View File
@@ -1,7 +1,3 @@
import {
type AppBskyNotificationDefs,
type ChatBskyNotificationDefs,
} from '@atproto/api'
import {t} from '@lingui/core/macro'
import {
type QueryClient,
@@ -10,10 +6,10 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useAppviewClient, useChatClient} from '#/state/session'
import * as Toast from '#/components/Toast'
import {app, chat} from '#/lexicons'
const RQKEY_ROOT = 'notification-settings'
const RQKEY_APP = [RQKEY_ROOT, 'app']
@@ -24,18 +20,18 @@ const RQKEY_CHAT = [RQKEY_ROOT, 'chat']
// fetched and cached separately. This combined type names every preference for
// the generic settings dialog, but it is never the shape of a query response.
export type NotificationSettingsPreferences = Omit<
AppBskyNotificationDefs.Preferences,
app.bsky.notification.defs.Preferences,
'chat'
> &
Partial<Pick<ChatBskyNotificationDefs.Preferences, 'chat' | 'chatRequest'>>
Partial<Pick<chat.bsky.notification.defs.Preferences, 'chat' | 'chatRequest'>>
export type AppNotificationSettingsPreferences = Omit<
AppBskyNotificationDefs.Preferences,
app.bsky.notification.defs.Preferences,
'chat'
>
export type ChatNotificationSettingsPreferences = Pick<
ChatBskyNotificationDefs.Preferences,
chat.bsky.notification.defs.Preferences,
'chat' | 'chatRequest'
>
@@ -45,9 +41,9 @@ export type NotificationSettingsPreferenceName = Exclude<
>
export type NotificationSettingsPreference =
| AppBskyNotificationDefs.Preference
| AppBskyNotificationDefs.FilterablePreference
| ChatBskyNotificationDefs.ChatPreference
| app.bsky.notification.defs.Preference
| app.bsky.notification.defs.FilterablePreference
| chat.bsky.notification.defs.ChatPreference
export function isChatPreferenceName(
name: NotificationSettingsPreferenceName,
@@ -58,7 +54,7 @@ export function isChatPreferenceName(
type NotificationSettingsUpdate = Partial<NotificationSettingsPreferences>
type AppNotificationSettingsUpdate = Partial<
Omit<AppBskyNotificationDefs.Preferences, '$type' | 'chat'>
Omit<app.bsky.notification.defs.Preferences, '$type' | 'chat'>
>
type ChatNotificationSettingsUpdate =
@@ -67,13 +63,13 @@ type ChatNotificationSettingsUpdate =
export function useNotificationSettingsQuery({
enabled,
}: {enabled?: boolean} = {}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
queryKey: RQKEY_APP,
queryFn: async (): Promise<AppNotificationSettingsPreferences> => {
const res = await agent.app.bsky.notification.getPreferences()
return appPreferencesWithoutChat(res.data.preferences)
const res = await client.call(app.bsky.notification.getPreferences)
return appPreferencesWithoutChat(res.preferences)
},
enabled,
})
@@ -82,21 +78,20 @@ export function useNotificationSettingsQuery({
export function useChatNotificationSettingsQuery({
enabled,
}: {enabled?: boolean} = {}) {
const agent = useAgent()
const client = useChatClient()
return useQuery({
queryKey: RQKEY_CHAT,
queryFn: async (): Promise<ChatNotificationSettingsPreferences> => {
const res = await agent.chat.bsky.notification.getPreferences(undefined, {
headers: DM_SERVICE_HEADERS,
})
return chatPreferencesForSettings(res.data.preferences)
const res = await client.call(chat.bsky.notification.getPreferences)
return chatPreferencesForSettings(res.preferences)
},
enabled,
})
}
export function useNotificationSettingsUpdateMutation() {
const agent = useAgent()
const appviewClient = useAppviewClient()
const chatClient = useChatClient()
const queryClient = useQueryClient()
return useMutation({
@@ -104,13 +99,13 @@ export function useNotificationSettingsUpdateMutation() {
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
await Promise.all([
hasUpdates(appUpdate)
? agent.app.bsky.notification.putPreferencesV2(appUpdate)
? appviewClient.call(
app.bsky.notification.putPreferencesV2,
appUpdate,
)
: undefined,
hasUpdates(chatUpdate)
? agent.chat.bsky.notification.putPreferences(chatUpdate, {
headers: DM_SERVICE_HEADERS,
encoding: 'application/json',
})
? chatClient.call(chat.bsky.notification.putPreferences, chatUpdate)
: undefined,
])
},
@@ -156,15 +151,15 @@ function optimisticUpdateNotificationSettings(
}
function appPreferencesWithoutChat(
preferences: AppBskyNotificationDefs.Preferences,
): Omit<AppBskyNotificationDefs.Preferences, 'chat'> {
preferences: app.bsky.notification.defs.Preferences,
): Omit<app.bsky.notification.defs.Preferences, 'chat'> {
const {chat: _ignoredChat, ...appPreferences} = preferences
return appPreferences
}
function chatPreferencesForSettings(
preferences: ChatBskyNotificationDefs.Preferences,
): Pick<ChatBskyNotificationDefs.Preferences, 'chat' | 'chatRequest'> {
preferences: chat.bsky.notification.defs.Preferences,
): Pick<chat.bsky.notification.defs.Preferences, 'chat' | 'chatRequest'> {
return {
chat: preferences.chat,
chatRequest: preferences.chatRequest,
+8 -10
View File
@@ -1,8 +1,4 @@
import {
type AppBskyFeedDefs,
type AppBskyGraphDefs,
type AppBskyNotificationListNotifications,
} from '@atproto/api'
import {type app} from '#/lexicons'
export type NotificationType =
| StarterPackNotificationType
@@ -11,11 +7,11 @@ export type NotificationType =
export type FeedNotification =
| (FeedNotificationBase & {
type: StarterPackNotificationType
subject?: AppBskyGraphDefs.StarterPackViewBasic
subject?: app.bsky.graph.defs.StarterPackViewBasic
})
| (FeedNotificationBase & {
type: OtherNotificationType
subject?: AppBskyFeedDefs.PostView
subject?: app.bsky.feed.defs.PostView
})
export interface FeedPage {
@@ -54,8 +50,10 @@ type OtherNotificationType =
type FeedNotificationBase = {
_reactKey: string
notification: AppBskyNotificationListNotifications.Notification
additional?: AppBskyNotificationListNotifications.Notification[]
notification: app.bsky.notification.listNotifications.Notification
additional?: app.bsky.notification.listNotifications.Notification[]
subjectUri?: string
subject?: AppBskyFeedDefs.PostView | AppBskyGraphDefs.StarterPackViewBasic
subject?:
| app.bsky.feed.defs.PostView
| app.bsky.graph.defs.StarterPackViewBasic
}
+19 -7
View File
@@ -11,6 +11,8 @@ import {
useState,
} from 'react'
import {AppState} from 'react-native'
import {type DatetimeString} from '@atproto/syntax'
import {updateSeenNotifications} from '@bsky.app/sdk'
import {useQueryClient} from '@tanstack/react-query'
import {EventEmitter} from 'eventemitter3'
@@ -18,7 +20,7 @@ import BroadcastChannel from '#/lib/broadcast'
import {resetBadgeCount} from '#/lib/notifications/notifications'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import {RQKEY as RQKEY_NOTIFS} from './feed'
import {type CachedFeedPage, type FeedPage} from './types'
import {fetchPage} from './util'
@@ -52,7 +54,8 @@ apiContext.displayName = 'NotificationsUnreadApiContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const {hasSession} = useSession()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
@@ -120,8 +123,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return {
async markAllRead() {
// update server
await agent.updateSeenNotifications(
cacheRef.current.syncedAt.toISOString(),
await pdsClient.call(
updateSeenNotifications,
// toISOString() always yields a valid datetime string
cacheRef.current.syncedAt.toISOString() as DatetimeString,
)
// update & broadcast
@@ -135,7 +140,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
isPoll,
}: {invalidate?: boolean; isPoll?: boolean} = {}) {
try {
if (!agent.session) return
if (!hasSession) return
if (AppState.currentState !== 'active') {
return
}
@@ -156,7 +161,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// count
const {page, indexedAt: lastIndexed} = await fetchPage({
agent,
client: appviewClient,
cursor: undefined,
limit: 40,
queryClient,
@@ -207,7 +212,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
},
}
}, [setNumUnread, queryClient, moderationOpts, agent])
}, [
setNumUnread,
queryClient,
moderationOpts,
appviewClient,
pdsClient,
hasSession,
])
checkUnreadRef.current = api.checkUnread
return (
+38 -48
View File
@@ -1,20 +1,15 @@
import {type Client} from '@atproto/lex-client'
import {type AtUriString} from '@atproto/syntax'
import {
type AppBskyFeedDefs,
AppBskyFeedLike,
AppBskyFeedPost,
AppBskyFeedRepost,
type AppBskyGraphDefs,
AppBskyGraphStarterpack,
type AppBskyNotificationListNotifications,
hasMutedWord,
moderateNotification,
type ModerationOpts,
} from '@atproto/api'
} from '@bsky.app/sdk/moderation'
import {type QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation'
import {type SessionAgent} from '#/state/session'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {precacheProfile} from '../profile'
import {
@@ -38,7 +33,7 @@ const MS_2DAY = MS_1HR * 48
// =
export async function fetchPage({
agent,
client,
cursor,
limit,
queryClient,
@@ -46,7 +41,7 @@ export async function fetchPage({
fetchAdditionalData,
reasons,
}: {
agent: SessionAgent
client: Client
cursor: string | undefined
limit: number
queryClient: QueryClient
@@ -57,16 +52,16 @@ export async function fetchPage({
page: FeedPage
indexedAt: string | undefined
}> {
const res = await agent.listNotifications({
const res = await client.call(app.bsky.notification.listNotifications, {
limit,
cursor,
reasons,
})
const indexedAt = res.data.notifications[0]?.indexedAt
const indexedAt = res.notifications[0]?.indexedAt
// filter out notifs by mod rules
const notifs = res.data.notifications.filter(
const notifs = res.notifications.filter(
notif => !shouldFilterNotif(notif, moderationOpts),
)
@@ -76,7 +71,7 @@ export async function fetchPage({
// we fetch subjects of notifications (usually posts) now instead of lazily
// in the UI to avoid relayouts
if (fetchAdditionalData) {
const subjects = await fetchSubjects(agent, notifsGrouped)
const subjects = await fetchSubjects(client, notifsGrouped)
for (const notif of notifsGrouped) {
if (notif.subjectUri) {
if (
@@ -96,17 +91,17 @@ export async function fetchPage({
}
}
let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date()
let seenAt = res.seenAt ? new Date(res.seenAt) : new Date()
if (Number.isNaN(seenAt.getTime())) {
seenAt = new Date()
}
return {
page: {
cursor: res.data.cursor,
cursor: res.cursor,
seenAt,
items: notifsGrouped,
priority: res.data.priority ?? false,
priority: res.priority ?? false,
},
indexedAt,
}
@@ -116,7 +111,7 @@ export async function fetchPage({
// =
export function shouldFilterNotif(
notif: AppBskyNotificationListNotifications.Notification,
notif: app.bsky.notification.listNotifications.Notification,
moderationOpts: ModerationOpts | undefined,
): boolean {
const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
@@ -128,10 +123,7 @@ export function shouldFilterNotif(
}
if (
notif.reason === 'subscribed-post' &&
bsky.dangerousIsType<AppBskyFeedPost.Record>(
notif.record,
AppBskyFeedPost.isRecord,
) &&
bsky.isType(app.bsky.feed.post, notif.record) &&
hasMutedWord({
mutedWords: moderationOpts.prefs.mutedWords,
text: notif.record.text,
@@ -150,7 +142,7 @@ export function shouldFilterNotif(
}
export function groupNotifications(
notifs: AppBskyNotificationListNotifications.Notification[],
notifs: app.bsky.notification.listNotifications.Notification[],
): FeedNotification[] {
const groupedNotifs: FeedNotification[] = []
for (const notif of notifs) {
@@ -207,17 +199,21 @@ export function groupNotifications(
}
async function fetchSubjects(
agent: SessionAgent,
client: Client,
groupedNotifs: FeedNotification[],
): Promise<{
posts: Map<string, AppBskyFeedDefs.PostView>
starterPacks: Map<string, AppBskyGraphDefs.StarterPackViewBasic>
posts: Map<string, app.bsky.feed.defs.PostView>
starterPacks: Map<string, app.bsky.graph.defs.StarterPackViewBasic>
}> {
const postUris = new Set<string>()
const packUris = new Set<string>()
/*
* Subject/reason-subject URIs arrive as plain `string` on the notification,
* so brand them to the at-uri slot the getPosts/getStarterPacks params expect.
*/
const postUris = new Set<AtUriString>()
const packUris = new Set<AtUriString>()
for (const notif of groupedNotifs) {
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
postUris.add(notif.subjectUri)
postUris.add(notif.subjectUri as AtUriString)
} else if (
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
) {
@@ -228,25 +224,25 @@ async function fetchSubjects(
const packUriChunks = chunk(Array.from(packUris), 25)
const postsChunks = await Promise.all(
postUriChunks.map(uris =>
agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts),
client.call(app.bsky.feed.getPosts, {uris}).then(res => res.posts),
),
)
const packsChunks = await Promise.all(
packUriChunks.map(uris =>
agent.app.bsky.graph
.getStarterPacks({uris})
.then(res => res.data.starterPacks),
client
.call(app.bsky.graph.getStarterPacks, {uris})
.then(res => res.starterPacks),
),
)
const postsMap = new Map<string, AppBskyFeedDefs.PostView>()
const packsMap = new Map<string, AppBskyGraphDefs.StarterPackViewBasic>()
const postsMap = new Map<string, app.bsky.feed.defs.PostView>()
const packsMap = new Map<string, app.bsky.graph.defs.StarterPackViewBasic>()
for (const post of postsChunks.flat()) {
if (AppBskyFeedPost.isRecord(post.record)) {
if (bsky.isType(app.bsky.feed.post, post.record)) {
postsMap.set(post.uri, post)
}
}
for (const pack of packsChunks.flat()) {
if (AppBskyGraphStarterpack.isRecord(pack.record)) {
if (bsky.isType(app.bsky.graph.starterpack, pack.record)) {
packsMap.set(pack.uri, pack)
}
}
@@ -257,7 +253,7 @@ async function fetchSubjects(
}
function toKnownType(
notif: AppBskyNotificationListNotifications.Notification,
notif: app.bsky.notification.listNotifications.Notification,
): NotificationType {
if (notif.reason === 'like') {
if (notif.reasonSubject?.includes('feed.generator')) {
@@ -286,7 +282,7 @@ function toKnownType(
function getSubjectUri(
type: NotificationType,
notif: AppBskyNotificationListNotifications.Notification,
notif: app.bsky.notification.listNotifications.Notification,
): string | undefined {
if (
type === 'reply' ||
@@ -302,14 +298,8 @@ function getSubjectUri(
type === 'repost-via-repost'
) {
if (
bsky.dangerousIsType<AppBskyFeedRepost.Record>(
notif.record,
AppBskyFeedRepost.isRecord,
) ||
bsky.dangerousIsType<AppBskyFeedLike.Record>(
notif.record,
AppBskyFeedLike.isRecord,
)
bsky.isType(app.bsky.feed.repost, notif.record) ||
bsky.isType(app.bsky.feed.like, notif.record)
) {
return typeof notif.record.subject?.uri === 'string'
? notif.record.subject?.uri
+3 -3
View File
@@ -1,7 +1,7 @@
import {type AppBskyActorDefs} from '@atproto/api'
import {type app} from '#/lexicons'
export type Data = Record<string, unknown> | undefined
export type BaseNux<
T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data},
> = Pick<AppBskyActorDefs.Nux, 'id' | 'completed' | 'expiresAt'> & T
T extends Pick<app.bsky.actor.defs.Nux, 'id' | 'expiresAt'> & {data: Data},
> = Pick<app.bsky.actor.defs.Nux, 'id' | 'completed' | 'expiresAt'> & T
+5 -4
View File
@@ -1,4 +1,4 @@
import {type AppBskyActorDefs, nuxSchema} from '@atproto/api'
import {nuxSchema} from '@bsky.app/sdk/utils'
import {
type AppNux,
@@ -6,8 +6,9 @@ import {
nuxNames,
NuxSchemas,
} from '#/state/queries/nuxs/definitions'
import {type app} from '#/lexicons'
export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined {
export function parseAppNux(nux: app.bsky.actor.defs.Nux): AppNux | undefined {
if (!nuxNames.has(nux.id as Nux)) return
if (!nuxSchema.safeParse(nux).success) return
@@ -32,11 +33,11 @@ export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined {
} as AppNux
}
export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux {
export function serializeAppNux(nux: AppNux): app.bsky.actor.defs.Nux {
const {data, ...rest} = nux
const schema = NuxSchemas[nux.id]
const result: AppBskyActorDefs.Nux = {
const result: app.bsky.actor.defs.Nux = {
...rest,
data: undefined,
}
+78 -39
View File
@@ -1,14 +1,12 @@
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {AppState} from 'react-native'
import {type AtIdentifierString, type Client} from '@atproto/lex-client'
import {AtUri, type AtUriString} from '@atproto/syntax'
import {
type AppBskyActorDefs,
AppBskyFeedDefs,
type AppBskyFeedPost,
AtUri,
moderatePost,
type ModerationDecision,
type ModerationPrefs,
} from '@atproto/api'
} from '@bsky.app/sdk/moderation'
import {
type InfiniteData,
type QueryClient,
@@ -32,9 +30,11 @@ import {DISCOVER_FEED_URI} from '#/lib/constants'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {type SessionAgent, useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {KnownError} from '#/view/com/posts/PostFeedErrorMessage'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts} from '../preferences/moderation-opts'
import {usePreferencesQuery} from './preferences'
@@ -79,10 +79,10 @@ export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
export interface FeedPostSliceItem {
_reactKey: string
uri: string
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
post: app.bsky.feed.defs.PostView
record: app.bsky.feed.post.Main
moderation: ModerationDecision
parentAuthor?: AppBskyActorDefs.ProfileViewBasic
parentAuthor?: app.bsky.actor.defs.ProfileViewBasic
isParentBlocked?: boolean
isParentNotFound?: boolean
}
@@ -97,8 +97,8 @@ export interface FeedPostSlice {
reqId: string | undefined
feedPostUri: string
reason?:
| AppBskyFeedDefs.ReasonRepost
| AppBskyFeedDefs.ReasonPin
| app.bsky.feed.defs.ReasonRepost
| app.bsky.feed.defs.ReasonPin
| ReasonFeedSource
| {[k: string]: unknown; $type: string}
}
@@ -106,7 +106,7 @@ export interface FeedPostSlice {
export interface FeedPageUnselected {
api: FeedAPI
cursor: string | undefined
feed: AppBskyFeedDefs.FeedViewPost[]
feed: app.bsky.feed.defs.FeedViewPost[]
fetchedAt: number
}
@@ -147,7 +147,7 @@ export function usePostFeedQuery(
f => f.pinned && f.value === 'following',
) ?? -1
const enableFollowingToDiscoverFallback = followingPinnedIndex === 0
const agent = useAgent()
const client = useAppviewClient()
const lastRun = useRef<{
data: InfiniteData<FeedPageUnselected>
args: typeof selectArgs
@@ -192,7 +192,7 @@ export function usePostFeedQuery(
feedDesc,
feedParams: params || {},
feedTuners,
agent,
client,
// Not in the query key because they don't change:
userInterests,
// Not in the query key. Reacting to it switching isn't important:
@@ -209,7 +209,7 @@ export function usePostFeedQuery(
* moderations happen later, which results in some posts being shown and
* some not.
*/
if (!agent.session) {
if (!client.did) {
assertSomePostsPassModeration(
res.feed,
preferences?.moderationPrefs ||
@@ -287,7 +287,12 @@ export function usePostFeedQuery(
.tune(page.feed)
.map(slice => {
const moderations = slice.items.map(item =>
moderatePost(item.post, moderationOpts!),
moderatePost(
// TODO(phase4): drop toLex once feed-manip is migrated
// off @atproto/api and yields lex-typed slice items.
bsky.toLex<app.bsky.feed.defs.PostView>(item.post),
moderationOpts!,
),
)
// apply moderation filter
@@ -337,10 +342,20 @@ export function usePostFeedQuery(
const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri,
post: item.post,
record: item.record,
// TODO(phase4): drop toLex once feed-manip is migrated
// off @atproto/api and yields lex-typed slice items.
post: bsky.toLex<app.bsky.feed.defs.PostView>(
item.post,
),
record: bsky.toLex<app.bsky.feed.post.Main>(
item.record,
),
moderation: moderations[i],
parentAuthor: item.parentAuthor,
parentAuthor: item.parentAuthor
? bsky.toLex<app.bsky.actor.defs.ProfileViewBasic>(
item.parentAuthor,
)
: undefined,
isParentBlocked: item.isParentBlocked,
isParentNotFound: item.isParentNotFound,
}
@@ -442,62 +457,86 @@ function createApi({
feedParams,
feedTuners,
userInterests,
agent,
client,
enableFollowingToDiscoverFallback,
}: {
feedDesc: FeedDescriptor
feedParams: FeedParams
feedTuners: FeedTunerFn[]
userInterests?: string
agent: SessionAgent
client: Client
enableFollowingToDiscoverFallback: boolean
}) {
if (feedDesc === 'following') {
if (feedParams.mergeFeedEnabled) {
return new MergeFeedAPI({
agent,
client,
feedParams,
feedTuners,
userInterests,
})
} else {
if (enableFollowingToDiscoverFallback) {
return new HomeFeedAPI({agent, userInterests})
return new HomeFeedAPI({client, userInterests})
} else {
return new FollowingFeedAPI({agent})
return new FollowingFeedAPI({client})
}
}
} else if (feedDesc.startsWith('author')) {
const [__, actor, filter] = feedDesc.split('|')
return new AuthorFeedAPI({agent, feedParams: {actor, filter}})
/*
* The FeedAPI $Params types treat limit/includePins as required (post-parse
* shape), but they are supplied at fetch() time, so the constructor receives
* a partial. Cast to satisfy the constructor param.
*/
return new AuthorFeedAPI({
client,
feedParams: {
actor: actor as AtIdentifierString,
filter,
} as app.bsky.feed.getAuthorFeed.$Params,
})
} else if (feedDesc.startsWith('likes')) {
const [__, actor] = feedDesc.split('|')
return new LikesFeedAPI({agent, feedParams: {actor}})
return new LikesFeedAPI({
client,
feedParams: {
actor: actor as AtIdentifierString,
} as app.bsky.feed.getActorLikes.$Params,
})
} else if (feedDesc.startsWith('feedgen')) {
const [__, feed] = feedDesc.split('|')
return new CustomFeedAPI({
agent,
feedParams: {feed},
client,
feedParams: {feed: feed as AtUriString},
userInterests,
})
} else if (feedDesc.startsWith('list')) {
const [__, list] = feedDesc.split('|')
return new ListFeedAPI({agent, feedParams: {list}})
return new ListFeedAPI({
client,
feedParams: {
list: list as AtUriString,
} as app.bsky.feed.getListFeed.$Params,
})
} else if (feedDesc.startsWith('posts')) {
const [__, uriList] = feedDesc.split('|')
return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}})
return new PostListFeedAPI({
client,
feedParams: {uris: uriList.split(',') as AtUriString[]},
})
} else if (feedDesc === 'demo') {
return new DemoFeedAPI({agent})
return new DemoFeedAPI({client})
} else {
// shouldnt happen
return new FollowingFeedAPI({agent})
return new FollowingFeedAPI({client})
}
}
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
): Generator<app.bsky.feed.defs.PostView, undefined> {
const atUri = new AtUri(uri)
const queryDatas = queryClient.getQueriesData<
@@ -520,7 +559,7 @@ export function* findAllPostsInQueryData(
yield embedViewRecordToPostView(quotedPost)
}
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent)) {
if (didOrHandleUriMatches(atUri, item.reply.parent)) {
yield item.reply.parent
}
@@ -534,7 +573,7 @@ export function* findAllPostsInQueryData(
}
}
if (AppBskyFeedDefs.isPostView(item.reply?.root)) {
if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.root)) {
if (didOrHandleUriMatches(atUri, item.reply.root)) {
yield item.reply.root
}
@@ -552,7 +591,7 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
): Generator<app.bsky.actor.defs.ProfileViewBasic, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
@@ -572,13 +611,13 @@ export function* findAllProfilesInQueryData(
yield quotedPost.author
}
if (
AppBskyFeedDefs.isPostView(item.reply?.parent) &&
bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent) &&
item.reply?.parent?.author.did === did
) {
yield item.reply.parent.author
}
if (
AppBskyFeedDefs.isPostView(item.reply?.root) &&
bsky.isType(app.bsky.feed.defs.postView, item.reply?.root) &&
item.reply?.root?.author.did === did
) {
yield item.reply.root.author
@@ -589,7 +628,7 @@ export function* findAllProfilesInQueryData(
}
function assertSomePostsPassModeration(
feed: AppBskyFeedDefs.FeedViewPost[],
feed: app.bsky.feed.defs.FeedViewPost[],
moderationPrefs: ModerationPrefs,
) {
// no posts in this feed
@@ -1,8 +1,9 @@
import {type AppBskyActorDefs} from '@atproto/api'
import {setPostInteractionSettings} from '@bsky.app/sdk'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {app} from '#/lexicons'
export function usePostInteractionSettingsMutation({
onError,
@@ -12,10 +13,10 @@ export function usePostInteractionSettingsMutation({
onSettled?: () => void
} = {}) {
const qc = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
async mutationFn(props: AppBskyActorDefs.PostInteractionSettingsPref) {
await agent.setPostInteractionSettings(props)
async mutationFn(props: app.bsky.actor.defs.PostInteractionSettingsPref) {
await client.call(setPostInteractionSettings, props)
},
async onSuccess() {
await qc.invalidateQueries({
+16 -14
View File
@@ -1,4 +1,4 @@
import {type AppBskyActorDefs, type AppBskyFeedGetLikes} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -9,7 +9,8 @@ import {
import {STALE} from '#/state/queries'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -19,22 +20,21 @@ const RQKEY_ROOT = 'liked-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function useLikedByQuery(resolvedUri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyFeedGetLikes.OutputSchema,
app.bsky.feed.getLikes.$OutputBody,
Error,
InfiniteData<AppBskyFeedGetLikes.OutputSchema>,
InfiniteData<app.bsky.feed.getLikes.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.getLikes({
uri: resolvedUri || '',
return await client.call(app.bsky.feed.getLikes, {
uri: (resolvedUri || '') as AtUriString,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -59,12 +59,14 @@ export const createLikedBySampleQueryKey = (args: {uri: string}) =>
* perturb the liked-by screen's pagination.
*/
export function useLikedBySampleQuery({uri}: {uri: string | undefined}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}),
queryFn: async () => {
const res = await agent.getLikes({uri: uri ?? '', limit: SAMPLE_SIZE})
return res.data
return await client.call(app.bsky.feed.getLikes, {
uri: (uri ?? '') as AtUriString,
limit: SAMPLE_SIZE,
})
},
staleTime: STALE.MINUTES.FIVE,
enabled: !!uri,
@@ -79,9 +81,9 @@ export function useLikedBySampleQuery({uri}: {uri: string | undefined}) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetLikes.OutputSchema>
InfiniteData<app.bsky.feed.getLikes.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
@@ -98,7 +100,7 @@ export function* findAllProfilesInQueryData(
}
}
const sampleQueryDatas =
queryClient.getQueriesData<AppBskyFeedGetLikes.OutputSchema>({
queryClient.getQueriesData<app.bsky.feed.getLikes.$OutputBody>({
queryKey: [likedBySampleQueryKeyRoot],
})
for (const [_queryKey, queryData] of sampleQueryDatas) {
+23 -20
View File
@@ -1,10 +1,4 @@
import {
type AppBskyActorDefs,
AppBskyEmbedRecord,
type AppBskyFeedDefs,
type AppBskyFeedGetQuotes,
AtUri,
} from '@atproto/api'
import {AtUri, type AtUriString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -12,7 +6,9 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
@@ -26,22 +22,21 @@ const RQKEY_ROOT = 'post-quotes'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostQuotesQuery(resolvedUri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyFeedGetQuotes.OutputSchema,
app.bsky.feed.getQuotes.$OutputBody,
Error,
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>,
InfiniteData<app.bsky.feed.getQuotes.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.api.app.bsky.feed.getQuotes({
uri: resolvedUri || '',
return await client.call(app.bsky.feed.getQuotes, {
uri: (resolvedUri || '') as AtUriString,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -53,8 +48,16 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) {
return {
...page,
posts: page.posts.filter(post => {
if (post.embed && AppBskyEmbedRecord.isView(post.embed)) {
if (AppBskyEmbedRecord.isViewDetached(post.embed.record)) {
if (
post.embed &&
bsky.isType(app.bsky.embed.record.view, post.embed)
) {
if (
bsky.isType(
app.bsky.embed.record.viewDetached,
post.embed.record,
)
) {
return false
}
}
@@ -70,9 +73,9 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
): Generator<app.bsky.actor.defs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>
InfiniteData<app.bsky.feed.getQuotes.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
@@ -97,9 +100,9 @@ export function* findAllProfilesInQueryData(
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
): Generator<app.bsky.feed.defs.PostView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>
InfiniteData<app.bsky.feed.getQuotes.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+10 -13
View File
@@ -1,7 +1,4 @@
import {
type AppBskyActorDefs,
type AppBskyFeedGetRepostedBy,
} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -9,7 +6,8 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -19,22 +17,21 @@ const RQKEY_ROOT = 'post-reposted-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyFeedGetRepostedBy.OutputSchema,
app.bsky.feed.getRepostedBy.$OutputBody,
Error,
InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema>,
InfiniteData<app.bsky.feed.getRepostedBy.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.getRepostedBy({
uri: resolvedUri || '',
return await client.call(app.bsky.feed.getRepostedBy, {
uri: (resolvedUri || '') as AtUriString,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -45,9 +42,9 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema>
InfiniteData<app.bsky.feed.getRepostedBy.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+70 -57
View File
@@ -1,5 +1,6 @@
import {useCallback} from 'react'
import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {AtUri, type AtUriString, type HandleString} from '@atproto/syntax'
import {deleteLike, deletePost, deleteRepost, like, repost} from '@bsky.app/sdk'
import {
type QueryClient,
useMutation,
@@ -10,10 +11,11 @@ import {
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {type Shadow} from '#/state/cache/types'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useAnalytics} from '#/analytics'
import {type Metrics, toClout} from '#/analytics/metrics'
import {app, com} from '#/lexicons'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
import {findProfileQueryData} from './profile'
@@ -21,8 +23,8 @@ const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
export function usePostQuery(uri: string | undefined) {
const agent = useAgent()
return useQuery<AppBskyFeedDefs.PostView>({
const client = useAppviewClient()
return useQuery<app.bsky.feed.defs.PostView>({
queryKey: RQKEY(uri || ''),
queryFn: async () => {
if (!uri) throw new Error('[unreachable] No URI provided')
@@ -30,16 +32,17 @@ export function usePostQuery(uri: string | undefined) {
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
const {did} = await client.call(com.atproto.identity.resolveHandle, {
handle: urip.host as HandleString,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
urip.host = did
}
const res = await agent.getPosts({uris: [urip.toString()]})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
const data = await client.call(app.bsky.feed.getPosts, {
uris: [urip.toString()],
})
if (data.posts[0]) {
return data.posts[0]
}
throw new Error('No data')
@@ -51,14 +54,14 @@ export function usePostQuery(uri: string | undefined) {
export function precachePost(
queryClient: QueryClient,
uri: string,
post: AppBskyFeedDefs.PostView,
post: app.bsky.feed.defs.PostView,
) {
queryClient.setQueryData(RQKEY(uri), post)
}
export function useGetPost() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({uri}: {uri: string}) => {
return queryClient.fetchQuery({
@@ -67,55 +70,51 @@ export function useGetPost() {
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
const {did} = await client.call(
com.atproto.identity.resolveHandle,
{handle: urip.host as HandleString},
)
urip.host = did
}
const res = await agent.getPosts({
const data = await client.call(app.bsky.feed.getPosts, {
uris: [urip.toString()],
})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
if (data.posts[0]) {
return data.posts[0]
}
throw new Error('useGetPost: post not found')
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
export function useGetPosts() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({uris}: {uris: string[]}) => {
return queryClient.fetchQuery({
queryKey: RQKEY(uris.join(',') || ''),
async queryFn() {
const res = await agent.getPosts({
uris,
const data = await client.call(app.bsky.feed.getPosts, {
uris: uris as AtUriString[],
})
if (res.success) {
return res.data.posts
} else {
throw new Error('useGetPosts failed')
}
return data.posts
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
export function usePostLikeMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
viaRepost: {uri: string; cid: string} | undefined,
feedDescriptor: string | undefined,
logContext: Metrics['post:like']['logContext'],
@@ -179,20 +178,20 @@ export function usePostLikeMutationQueue(
function usePostLikeMutation(
feedDescriptor: string | undefined,
logContext: Metrics['post:like']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
) {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const postAuthor = post.author
const agent = useAgent()
const pdsClient = usePdsClient()
const ax = useAnalytics()
return useMutation<
{uri: string}, // responds with the uri of the like
{uri: AtUriString}, // responds with the uri of the like
Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({
mutationFn: ({uri, cid, via}) => {
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
@@ -215,7 +214,11 @@ function usePostLikeMutation(
: undefined,
feedDescriptor: feedDescriptor,
})
return agent.like(uri, cid, via)
return pdsClient.call(like, {
uri: uri as AtUriString,
cid: cid,
via: via ? {uri: via.uri as AtUriString, cid: via.cid} : undefined,
})
},
})
}
@@ -223,9 +226,9 @@ function usePostLikeMutation(
function usePostUnlikeMutation(
feedDescriptor: string | undefined,
logContext: Metrics['post:unlike']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
) {
const agent = useAgent()
const pdsClient = usePdsClient()
const ax = useAnalytics()
return useMutation<void, Error, {postUri: string; likeUri: string}>({
mutationFn: ({postUri, likeUri}) => {
@@ -235,13 +238,13 @@ function usePostUnlikeMutation(
logContext,
feedDescriptor,
})
return agent.deleteLike(likeUri)
return pdsClient.call(deleteLike, likeUri as AtUriString)
},
})
}
export function usePostRepostMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
viaRepost: {uri: string; cid: string} | undefined,
feedDescriptor: string | undefined,
logContext: Metrics['post:repost']['logContext'],
@@ -307,12 +310,12 @@ export function usePostRepostMutationQueue(
function usePostRepostMutation(
feedDescriptor: string | undefined,
logContext: Metrics['post:repost']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
) {
const agent = useAgent()
const pdsClient = usePdsClient()
const ax = useAnalytics()
return useMutation<
{uri: string}, // responds with the uri of the repost
{uri: AtUriString}, // responds with the uri of the repost
Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({
@@ -323,7 +326,11 @@ function usePostRepostMutation(
logContext,
feedDescriptor,
})
return agent.repost(uri, cid, via)
return pdsClient.call(repost, {
uri: uri as AtUriString,
cid: cid,
via: via ? {uri: via.uri as AtUriString, cid: via.cid} : undefined,
})
},
})
}
@@ -331,9 +338,9 @@ function usePostRepostMutation(
function usePostUnrepostMutation(
feedDescriptor: string | undefined,
logContext: Metrics['post:unrepost']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
) {
const agent = useAgent()
const pdsClient = usePdsClient()
const ax = useAnalytics()
return useMutation<void, Error, {postUri: string; repostUri: string}>({
mutationFn: ({postUri, repostUri}) => {
@@ -343,17 +350,17 @@ function usePostUnrepostMutation(
logContext,
feedDescriptor,
})
return agent.deleteRepost(repostUri)
return pdsClient.call(deleteRepost, repostUri as AtUriString)
},
})
}
export function usePostDeleteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<void, Error, {uri: string}>({
mutationFn: async ({uri}) => {
await agent.deletePost(uri)
await pdsClient.call(deletePost, uri as AtUriString)
},
onSuccess(_, variables) {
updatePostShadow(queryClient, variables.uri, {isDeleted: true})
@@ -362,7 +369,7 @@ export function usePostDeleteMutation() {
}
export function useThreadMuteMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>,
post: Shadow<app.bsky.feed.defs.PostView>,
rootUri: string,
) {
const threadMuteMutation = useThreadMuteMutation()
@@ -407,23 +414,29 @@ export function useThreadMuteMutationQueue(
}
function useThreadMuteMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<
{},
Error,
{uri: string} // the root post's uri
>({
mutationFn: ({uri}) => {
return agent.api.app.bsky.graph.muteThread({root: uri})
mutationFn: async ({uri}) => {
await pdsClient.call(app.bsky.graph.muteThread, {
root: uri as AtUriString,
})
return {}
},
})
}
function useThreadUnmuteMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<{}, Error, {uri: string}>({
mutationFn: ({uri}) => {
return agent.api.app.bsky.graph.unmuteThread({root: uri})
mutationFn: async ({uri}) => {
await pdsClient.call(app.bsky.graph.unmuteThread, {
root: uri as AtUriString,
})
return {}
},
})
}
+39 -45
View File
@@ -1,11 +1,6 @@
import {useRef} from 'react'
import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
type AppBskyFeedDefs,
AppBskyFeedPostgate,
AtUri,
} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {AtUri} from '@atproto/syntax'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {networkRetry, retry} from '#/lib/async/retry'
@@ -19,28 +14,28 @@ import {
mergePostgateRecords,
POSTGATE_COLLECTION,
} from '#/state/queries/postgate/util'
import {type SessionAgent, useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {app, com} from '#/lexicons'
import * as bsky from '#/types/bsky'
export async function getPostgateRecord({
agent,
pdsClient,
postUri,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
}): Promise<AppBskyFeedPostgate.Record | undefined> {
}): Promise<app.bsky.feed.postgate.Main | undefined> {
const urip = new AtUri(postUri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, {
handle: urip.host as `${string}.${string}`,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
urip.host = did
}
try {
const {data} = await retry(
const data = await retry(
2,
e => {
/*
@@ -54,17 +49,14 @@ export async function getPostgateRecord({
return true
},
() =>
agent.api.com.atproto.repo.getRecord({
pdsClient.call(com.atproto.repo.getRecord, {
repo: urip.host,
collection: POSTGATE_COLLECTION,
rkey: urip.rkey,
}),
)
if (
data.value &&
bsky.validate(data.value, AppBskyFeedPostgate.validateRecord)
) {
if (data.value && bsky.matches(app.bsky.feed.postgate, data.value)) {
return data.value
} else {
return undefined
@@ -84,19 +76,19 @@ export async function getPostgateRecord({
}
export async function writePostgateRecord({
agent,
pdsClient,
postUri,
postgate,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
postgate: AppBskyFeedPostgate.Record
postgate: app.bsky.feed.postgate.Main
}) {
const postUrip = new AtUri(postUri)
await networkRetry(2, () =>
agent.api.com.atproto.repo.putRecord({
repo: agent.session!.did,
pdsClient.call(com.atproto.repo.putRecord, {
repo: pdsClient.assertDid,
collection: POSTGATE_COLLECTION,
rkey: postUrip.rkey,
record: postgate,
@@ -106,24 +98,24 @@ export async function writePostgateRecord({
export async function upsertPostgate(
{
agent,
pdsClient,
postUri,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
},
callback: (
postgate: AppBskyFeedPostgate.Record | undefined,
) => Promise<AppBskyFeedPostgate.Record | undefined>,
postgate: app.bsky.feed.postgate.Main | undefined,
) => Promise<app.bsky.feed.postgate.Main | undefined>,
) {
const prev = await getPostgateRecord({
agent,
pdsClient,
postUri,
})
const next = await callback(prev)
if (!next) return
await writePostgateRecord({
agent,
pdsClient,
postUri,
postgate: next,
})
@@ -134,18 +126,20 @@ export const createPostgateQueryKey = (postUri: string) => [
postUri,
]
export function usePostgateQuery({postUri}: {postUri: string}) {
const agent = useAgent()
const pdsClient = usePdsClient()
return useQuery({
staleTime: STALE.SECONDS.THIRTY,
queryKey: createPostgateQueryKey(postUri),
async queryFn() {
return await getPostgateRecord({agent, postUri}).then(res => res ?? null)
return await getPostgateRecord({pdsClient, postUri}).then(
res => res ?? null,
)
},
})
}
export function useWritePostgateMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
@@ -153,10 +147,10 @@ export function useWritePostgateMutation() {
postgate,
}: {
postUri: string
postgate: AppBskyFeedPostgate.Record
postgate: app.bsky.feed.postgate.Main
}) => {
return writePostgateRecord({
agent,
pdsClient,
postUri,
postgate,
})
@@ -170,10 +164,10 @@ export function useWritePostgateMutation() {
}
export function useToggleQuoteDetachmentMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
const getPosts = useGetPosts()
const prevEmbed = useRef<AppBskyFeedDefs.PostView['embed']>(undefined)
const prevEmbed = useRef<app.bsky.feed.defs.PostView['embed']>(undefined)
return useMutation({
mutationFn: async ({
@@ -181,7 +175,7 @@ export function useToggleQuoteDetachmentMutation() {
quoteUri,
action,
}: {
post: AppBskyFeedDefs.PostView
post: app.bsky.feed.defs.PostView
quoteUri: string
action: 'detach' | 'reattach'
}) => {
@@ -199,7 +193,7 @@ export function useToggleQuoteDetachmentMutation() {
})
}
await upsertPostgate({agent, postUri: quoteUri}, async prev => {
await upsertPostgate({pdsClient, postUri: quoteUri}, async prev => {
if (prev) {
if (action === 'detach') {
return mergePostgateRecords(prev, {
@@ -247,8 +241,8 @@ export function useToggleQuoteDetachmentMutation() {
if (action === 'detach' && prevEmbed.current) {
// detach failed, add the embed back
if (
AppBskyEmbedRecord.isView(prevEmbed.current) ||
AppBskyEmbedRecordWithMedia.isView(prevEmbed.current)
bsky.isType(app.bsky.embed.record.view, prevEmbed.current) ||
bsky.isType(app.bsky.embed.recordWithMedia.view, prevEmbed.current)
) {
updatePostShadow(queryClient, post.uri, {
embed: prevEmbed.current,
@@ -263,7 +257,7 @@ export function useToggleQuoteDetachmentMutation() {
}
export function useToggleQuotepostEnabledMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation({
mutationFn: async ({
@@ -273,7 +267,7 @@ export function useToggleQuotepostEnabledMutation() {
postUri: string
action: 'enable' | 'disable'
}) => {
await upsertPostgate({agent, postUri: postUri}, async prev => {
await upsertPostgate({pdsClient, postUri: postUri}, async prev => {
if (prev) {
if (action === 'disable') {
return mergePostgateRecords(prev, {
+43 -39
View File
@@ -1,31 +1,28 @@
import {
type $Typed,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
type AppBskyFeedDefs,
type AppBskyFeedPostgate,
AtUri,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {AtUri, type AtUriString, toDatetimeString} from '@atproto/syntax'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate'
export function createPostgateRecord(
postgate: Partial<AppBskyFeedPostgate.Record> & {
post: AppBskyFeedPostgate.Record['post']
postgate: Omit<Partial<app.bsky.feed.postgate.Main>, 'post'> & {
post: string
},
): AppBskyFeedPostgate.Record {
): app.bsky.feed.postgate.Main {
return {
$type: POSTGATE_COLLECTION,
createdAt: new Date().toISOString(),
post: postgate.post,
createdAt: toDatetimeString(new Date()),
post: postgate.post as AtUriString,
detachedEmbeddingUris: postgate.detachedEmbeddingUris || [],
embeddingRules: postgate.embeddingRules || [],
}
}
export function mergePostgateRecords(
prev: AppBskyFeedPostgate.Record,
next: Partial<AppBskyFeedPostgate.Record>,
prev: app.bsky.feed.postgate.Main,
next: Partial<app.bsky.feed.postgate.Main>,
) {
const detachedEmbeddingUris = Array.from(
new Set([
@@ -50,10 +47,10 @@ export function createEmbedViewDetachedRecord({
uri,
}: {
uri: string
}): $Typed<AppBskyEmbedRecord.View> {
const record: $Typed<AppBskyEmbedRecord.ViewDetached> = {
}): $Typed<app.bsky.embed.record.View> {
const record: $Typed<app.bsky.embed.record.ViewDetached> = {
$type: 'app.bsky.embed.record#viewDetached',
uri,
uri: uri as AtUriString,
detached: true,
}
return {
@@ -69,24 +66,27 @@ export function createMaybeDetachedQuoteEmbed({
detached,
}:
| {
post: AppBskyFeedDefs.PostView
quote: AppBskyFeedDefs.PostView
post: app.bsky.feed.defs.PostView
quote: app.bsky.feed.defs.PostView
quoteUri: undefined
detached: false
}
| {
post: AppBskyFeedDefs.PostView
post: app.bsky.feed.defs.PostView
quote: undefined
quoteUri: string
detached: true
}): AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined {
if (AppBskyEmbedRecord.isView(post.embed)) {
}):
| app.bsky.embed.record.View
| app.bsky.embed.recordWithMedia.View
| undefined {
if (bsky.isType(app.bsky.embed.record.view, post.embed)) {
if (detached) {
return createEmbedViewDetachedRecord({uri: quoteUri})
} else {
return createEmbedRecordView({post: quote})
}
} else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
} else if (bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed)) {
if (detached) {
return {
...post.embed,
@@ -99,8 +99,8 @@ export function createMaybeDetachedQuoteEmbed({
}
export function createEmbedViewRecordFromPost(
post: AppBskyFeedDefs.PostView,
): $Typed<AppBskyEmbedRecord.ViewRecord> {
post: app.bsky.feed.defs.PostView,
): $Typed<app.bsky.embed.record.ViewRecord> {
return {
$type: 'app.bsky.embed.record#viewRecord',
uri: post.uri,
@@ -120,8 +120,8 @@ export function createEmbedViewRecordFromPost(
export function createEmbedRecordView({
post,
}: {
post: AppBskyFeedDefs.PostView
}): AppBskyEmbedRecord.View {
post: app.bsky.feed.defs.PostView
}): app.bsky.embed.record.View {
return {
$type: 'app.bsky.embed.record#view',
record: createEmbedViewRecordFromPost(post),
@@ -132,10 +132,10 @@ export function createEmbedRecordWithMediaView({
post,
quote,
}: {
post: AppBskyFeedDefs.PostView
quote: AppBskyFeedDefs.PostView
}): AppBskyEmbedRecordWithMedia.View | undefined {
if (!AppBskyEmbedRecordWithMedia.isView(post.embed)) return
post: app.bsky.feed.defs.PostView
quote: app.bsky.feed.defs.PostView
}): app.bsky.embed.recordWithMedia.View | undefined {
if (!bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed)) return
return {
...(post.embed || {}),
record: {
@@ -149,11 +149,11 @@ export function getMaybeDetachedQuoteEmbed({
post,
}: {
viewerDid: string
post: AppBskyFeedDefs.PostView
post: app.bsky.feed.defs.PostView
}) {
if (AppBskyEmbedRecord.isView(post.embed)) {
if (bsky.isType(app.bsky.embed.record.view, post.embed)) {
// detached
if (AppBskyEmbedRecord.isViewDetached(post.embed.record)) {
if (bsky.isType(app.bsky.embed.record.viewDetached, post.embed.record)) {
const urip = new AtUri(post.embed.record.uri)
return {
embed: post.embed,
@@ -164,7 +164,7 @@ export function getMaybeDetachedQuoteEmbed({
}
// post
if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
if (bsky.isType(app.bsky.embed.record.viewRecord, post.embed.record)) {
const urip = new AtUri(post.embed.record.uri)
return {
embed: post.embed,
@@ -173,9 +173,11 @@ export function getMaybeDetachedQuoteEmbed({
isDetached: false,
}
}
} else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
} else if (bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed)) {
// detached
if (AppBskyEmbedRecord.isViewDetached(post.embed.record.record)) {
if (
bsky.isType(app.bsky.embed.record.viewDetached, post.embed.record.record)
) {
const urip = new AtUri(post.embed.record.record.uri)
return {
embed: post.embed,
@@ -186,7 +188,9 @@ export function getMaybeDetachedQuoteEmbed({
}
// post
if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) {
if (
bsky.isType(app.bsky.embed.record.viewRecord, post.embed.record.record)
) {
const urip = new AtUri(post.embed.record.record.uri)
return {
embed: post.embed,
+1 -1
View File
@@ -1,4 +1,4 @@
import {DEFAULT_LABEL_SETTINGS} from '@atproto/api'
import {DEFAULT_LABEL_SETTINGS} from '@bsky.app/sdk'
import {
type ThreadViewPreferences,
+103 -64
View File
@@ -1,9 +1,27 @@
import {useCallback} from 'react'
import {type DidString} from '@atproto/syntax'
import {
type AppBskyActorDefs,
addSavedFeeds,
type BskyFeedViewPreference,
type LabelPreference,
} from '@atproto/api'
dismissNudges,
getPreferences,
overwriteSavedFeeds,
queueNudges,
removeMutedWord,
removeMutedWords,
removeSavedFeeds,
setActiveProgressGuide,
setAdultContentEnabled,
setContentLabelPref,
setFeedViewPrefs,
setIsBetaUser,
setThreadViewPrefs,
setVerificationPrefs,
updateMutedWord,
updateSavedFeeds,
upsertMutedWords,
} from '@bsky.app/sdk'
import {type LabelPreference} from '@bsky.app/sdk/moderation'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
@@ -20,11 +38,13 @@ import {
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
@@ -37,7 +57,7 @@ export const preferencesQueryKey = createQueryKey(
)
export function usePreferencesQuery() {
const agent = useAgent()
const client = usePdsClient()
const aa = useAgeAssurance()
const query = useQuery({
@@ -47,14 +67,14 @@ export function usePreferencesQuery() {
queryKey: preferencesQueryKey,
gcTime: GCTIME.INFINITY,
queryFn: async () => {
if (!agent.did) {
if (!client.did) {
return DEFAULT_LOGGED_OUT_PREFERENCES
} else {
const res = await agent.getPreferences()
const res = await client.call(getPreferences)
// save to local storage to ensure there are labels on initial requests
void saveLabelers(
agent.did,
client.did,
res.moderationPrefs.labelers.map(l => l.did),
)
@@ -90,8 +110,16 @@ export function usePreferencesQuery() {
) {
data = {
...data,
moderationPrefs: makeAgeRestrictedModerationPrefs(
data.moderationPrefs,
/*
* TODO(phase4): drop the toLex bridges once
* `#/ageAssurance/util` (makeAgeRestrictedModerationPrefs) sources
* `ModerationPrefs` from `@bsky.app/sdk/moderation` instead of
* `@atproto/api`. The two shapes differ only in scalar branding
* (e.g. MutedWord.actorTarget's UnknownString), so the values are
* structurally interchangeable at this boundary.
*/
moderationPrefs: toLex(
makeAgeRestrictedModerationPrefs(toLex(data.moderationPrefs)),
),
}
}
@@ -113,11 +141,11 @@ export function usePreferencesQuery() {
export function useClearPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async () => {
await agent.app.bsky.actor.putPreferences({preferences: []})
await client.call(app.bsky.actor.putPreferences, {preferences: []})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -128,7 +156,7 @@ export function useClearPreferencesMutation() {
export function usePreferencesSetContentLabelMutation() {
const ax = useAnalytics()
const agent = useAgent()
const client = usePdsClient()
const queryClient = useQueryClient()
return useMutation<
@@ -137,7 +165,11 @@ export function usePreferencesSetContentLabelMutation() {
{label: string; visibility: LabelPreference; labelerDid: string | undefined}
>({
mutationFn: async ({label, visibility, labelerDid}) => {
await agent.setContentLabelPref(label, visibility, labelerDid)
await client.call(setContentLabelPref, {
key: label,
value: visibility,
labelerDid: labelerDid as DidString | undefined,
})
ax.metric('moderation:changeLabelPreference', {preference: visibility})
// triggers a refetch
await queryClient.invalidateQueries({
@@ -149,7 +181,7 @@ export function usePreferencesSetContentLabelMutation() {
export function useSetContentLabelMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async ({
@@ -161,7 +193,11 @@ export function useSetContentLabelMutation() {
visibility: LabelPreference
labelerDid?: string
}) => {
await agent.setContentLabelPref(label, visibility, labelerDid)
await client.call(setContentLabelPref, {
key: label,
value: visibility,
labelerDid: labelerDid as DidString | undefined,
})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -172,11 +208,11 @@ export function useSetContentLabelMutation() {
export function usePreferencesSetAdultContentMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, {enabled: boolean}>({
mutationFn: async ({enabled}) => {
await agent.setAdultContentEnabled(enabled)
await client.call(setAdultContentEnabled, enabled)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -187,7 +223,7 @@ export function usePreferencesSetAdultContentMutation() {
export function useSetFeedViewPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
mutationFn: async prefs => {
@@ -195,7 +231,7 @@ export function useSetFeedViewPreferencesMutation() {
* special handling here, merged into `feedViewPrefs` above, since
* following was previously called `home`
*/
await agent.setFeedViewPrefs('home', prefs)
await client.call(setFeedViewPrefs, {feed: 'home', ...prefs})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -212,11 +248,11 @@ export function useSetThreadViewPreferencesMutation({
onError?: (error: unknown) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
mutationFn: async prefs => {
await agent.setThreadViewPrefs(prefs)
await client.call(setThreadViewPrefs, prefs)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -229,11 +265,11 @@ export function useSetThreadViewPreferencesMutation({
export function useOverwriteSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
return useMutation<void, unknown, app.bsky.actor.defs.SavedFeed[]>({
mutationFn: async savedFeeds => {
await agent.overwriteSavedFeeds(savedFeeds)
await client.call(overwriteSavedFeeds, savedFeeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -244,15 +280,15 @@ export function useOverwriteSavedFeedsMutation() {
export function useAddSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<
void,
unknown,
Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[]
Pick<app.bsky.actor.defs.SavedFeed, 'type' | 'value' | 'pinned'>[]
>({
mutationFn: async savedFeeds => {
await agent.addSavedFeeds(savedFeeds)
await client.call(addSavedFeeds, savedFeeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -263,11 +299,11 @@ export function useAddSavedFeedsMutation() {
export function useRemoveFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, Pick<AppBskyActorDefs.SavedFeed, 'id'>>({
return useMutation<void, unknown, Pick<app.bsky.actor.defs.SavedFeed, 'id'>>({
mutationFn: async savedFeed => {
await agent.removeSavedFeeds([savedFeed.id])
await client.call(removeSavedFeeds, [savedFeed.id])
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -278,21 +314,21 @@ export function useRemoveFeedMutation() {
export function useReplaceForYouWithDiscoverFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async ({
forYouFeedConfig,
discoverFeedConfig,
}: {
forYouFeedConfig: AppBskyActorDefs.SavedFeed | undefined
discoverFeedConfig: AppBskyActorDefs.SavedFeed | undefined
forYouFeedConfig: app.bsky.actor.defs.SavedFeed | undefined
discoverFeedConfig: app.bsky.actor.defs.SavedFeed | undefined
}) => {
if (forYouFeedConfig) {
await agent.removeSavedFeeds([forYouFeedConfig.id])
await client.call(removeSavedFeeds, [forYouFeedConfig.id])
}
if (!discoverFeedConfig) {
await agent.addSavedFeeds([
await client.call(addSavedFeeds, [
{
type: 'feed',
value: PROD_DEFAULT_FEED('whats-hot'),
@@ -300,7 +336,7 @@ export function useReplaceForYouWithDiscoverFeedMutation() {
},
])
} else {
await agent.updateSavedFeeds([
await client.call(updateSavedFeeds, [
{
...discoverFeedConfig,
pinned: true,
@@ -317,11 +353,11 @@ export function useReplaceForYouWithDiscoverFeedMutation() {
export function useUpdateSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
return useMutation<void, unknown, app.bsky.actor.defs.SavedFeed[]>({
mutationFn: async feeds => {
await agent.updateSavedFeeds(feeds)
await client.call(updateSavedFeeds, feeds)
// triggers a refetch
await queryClient.invalidateQueries({
@@ -333,11 +369,11 @@ export function useUpdateSavedFeedsMutation() {
export function useUpsertMutedWordsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await agent.upsertMutedWords(mutedWords)
mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => {
await client.call(upsertMutedWords, mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -348,11 +384,11 @@ export function useUpsertMutedWordsMutation() {
export function useUpdateMutedWordMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await agent.updateMutedWord(mutedWord)
mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => {
await client.call(updateMutedWord, mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -363,11 +399,11 @@ export function useUpdateMutedWordMutation() {
export function useRemoveMutedWordMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await agent.removeMutedWord(mutedWord)
mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => {
await client.call(removeMutedWord, mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -378,11 +414,11 @@ export function useRemoveMutedWordMutation() {
export function useRemoveMutedWordsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await agent.removeMutedWords(mutedWords)
mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => {
await client.call(removeMutedWords, mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -393,11 +429,11 @@ export function useRemoveMutedWordsMutation() {
export function useQueueNudgesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (nudges: string | string[]) => {
await agent.bskyAppQueueNudges(nudges)
await client.call(queueNudges, Array.isArray(nudges) ? nudges : [nudges])
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -408,11 +444,14 @@ export function useQueueNudgesMutation() {
export function useDismissNudgesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (nudges: string | string[]) => {
await agent.bskyAppDismissNudges(nudges)
await client.call(
dismissNudges,
Array.isArray(nudges) ? nudges : [nudges],
)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -423,13 +462,13 @@ export function useDismissNudgesMutation() {
export function useSetActiveProgressGuideMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (
guide: AppBskyActorDefs.BskyAppProgressGuide | undefined,
guide: app.bsky.actor.defs.BskyAppProgressGuide | undefined,
) => {
await agent.bskyAppSetActiveProgressGuide(guide)
await client.call(setActiveProgressGuide, guide)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -440,11 +479,11 @@ export function useSetActiveProgressGuideMutation() {
export function useSetIsBetaUserMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation({
mutationFn: async (isBetaUser: boolean) => {
await agent.setIsBetaUser(isBetaUser)
await client.call(setIsBetaUser, isBetaUser)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -456,11 +495,11 @@ export function useSetIsBetaUserMutation() {
export function useSetVerificationPrefsMutation() {
const ax = useAnalytics()
const queryClient = useQueryClient()
const agent = useAgent()
const client = usePdsClient()
return useMutation<void, unknown, AppBskyActorDefs.VerificationPrefs>({
return useMutation<void, unknown, app.bsky.actor.defs.VerificationPrefs>({
mutationFn: async prefs => {
await agent.setVerificationPrefs(prefs)
await client.call(setVerificationPrefs, prefs)
if (prefs.hideBadges) {
ax.metric('verification:settings:hideBadges', {})
} else {
+3 -2
View File
@@ -1,5 +1,6 @@
import {useMemo} from 'react'
import {AtpAgent, interpretLabelValueDefinitions} from '@atproto/api'
import {Client} from '@atproto/lex-client'
import {interpretLabelValueDefinitions} from '@bsky.app/sdk/moderation'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useLabelersDetailedInfoQuery} from '../labeler'
@@ -13,7 +14,7 @@ export function useMyLabelersQuery({
const prefs = usePreferencesQuery()
let dids = Array.from(
new Set(
AtpAgent.appLabelers.concat(
(Client.appLabelers as readonly string[]).concat(
prefs.data?.moderationPrefs.labelers.map(l => l.did) || [],
),
),
+1 -1
View File
@@ -1,4 +1,4 @@
import {type BskyFeedViewPreference, type BskyPreferences} from '@atproto/api'
import {type BskyFeedViewPreference, type BskyPreferences} from '@bsky.app/sdk'
export type UsePreferencesQueryResponse = Omit<
BskyPreferences,
+11 -12
View File
@@ -1,14 +1,13 @@
import {
type AppBskyFeedGetActorFeeds,
moderateFeedGenerator,
} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {
type InfiniteData,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
const PAGE_SIZE = 50
@@ -24,25 +23,25 @@ export function useProfileFeedgensQuery(
) {
const moderationOpts = useModerationOpts()
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyFeedGetActorFeeds.OutputSchema,
app.bsky.feed.getActorFeeds.$OutputBody,
Error,
InfiniteData<AppBskyFeedGetActorFeeds.OutputSchema>,
InfiniteData<app.bsky.feed.getActorFeeds.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(did),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.feed.getActorFeeds({
actor: did,
const data = await client.call(app.bsky.feed.getActorFeeds, {
actor: did as AtIdentifierString,
limit: PAGE_SIZE,
cursor: pageParam,
})
res.data.feeds.sort((a, b) => {
data.feeds.sort((a, b) => {
return (b.likeCount || 0) - (a.likeCount || 0)
})
return res.data
return data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
+10 -13
View File
@@ -1,7 +1,4 @@
import {
type AppBskyActorDefs,
type AppBskyGraphGetFollowers,
} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -9,8 +6,9 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAnalytics} from '#/analytics'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const DEFAULT_SORT = 'latest'
const PAGE_SIZE = 30
@@ -33,26 +31,25 @@ export function useProfileFollowersQuery(
) {
const ax = useAnalytics()
const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable)
const agent = useAgent()
const client = useAppviewClient()
const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined
return useInfiniteQuery<
AppBskyGraphGetFollowers.OutputSchema,
app.bsky.graph.getFollowers.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetFollowers.OutputSchema>,
InfiniteData<app.bsky.graph.getFollowers.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(did || '', sortParam),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getFollowers({
actor: did || '',
return await client.call(app.bsky.graph.getFollowers, {
actor: (did || '') as AtIdentifierString,
limit: PAGE_SIZE,
cursor: pageParam,
sort: sortParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -63,9 +60,9 @@ export function useProfileFollowersQuery(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetFollowers.OutputSchema>
InfiniteData<app.bsky.graph.getFollowers.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+10 -10
View File
@@ -1,4 +1,4 @@
import {type AppBskyActorDefs, type AppBskyGraphGetFollows} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -7,8 +7,9 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import {useAnalytics} from '#/analytics'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const DEFAULT_SORT = 'latest'
const PAGE_SIZE = 30
@@ -34,27 +35,26 @@ export function useProfileFollowsQuery(
) {
const ax = useAnalytics()
const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable)
const agent = useAgent()
const client = useAppviewClient()
const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined
return useInfiniteQuery<
AppBskyGraphGetFollows.OutputSchema,
app.bsky.graph.getFollows.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetFollows.OutputSchema>,
InfiniteData<app.bsky.graph.getFollows.$OutputBody>,
QueryKey,
RQPageParam
>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(did || '', sortParam),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getFollows({
actor: did || '',
return await client.call(app.bsky.graph.getFollows, {
actor: (did || '') as AtIdentifierString,
limit: limit || PAGE_SIZE,
cursor: pageParam,
sort: sortParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -65,9 +65,9 @@ export function useProfileFollowsQuery(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetFollows.OutputSchema>
InfiniteData<app.bsky.graph.getFollows.$OutputBody>
>({
queryKey: [RQKEY_ROOT],
})
+9 -9
View File
@@ -1,11 +1,13 @@
import {type AppBskyGraphGetLists, moderateUserList} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {moderateUserList} from '@bsky.app/sdk/moderation'
import {
type InfiniteData,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
const PAGE_SIZE = 30
@@ -17,23 +19,21 @@ export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
const moderationOpts = useModerationOpts()
const enabled = opts?.enabled !== false && Boolean(moderationOpts)
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery<
AppBskyGraphGetLists.OutputSchema,
app.bsky.graph.getLists.$OutputBody,
Error,
InfiniteData<AppBskyGraphGetLists.OutputSchema>,
InfiniteData<app.bsky.graph.getLists.$OutputBody>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(did),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getLists({
actor: did,
return await client.call(app.bsky.graph.getLists, {
actor: did as AtIdentifierString,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
+87 -81
View File
@@ -1,14 +1,19 @@
import {useCallback} from 'react'
import {type Un$Typed} from '@atproto/lex'
import {
type AppBskyActorDefs,
type AppBskyActorGetProfile,
type AppBskyActorGetProfiles,
type AppBskyActorProfile,
type AppBskyGraphGetFollows,
AtUri,
type ComAtprotoRepoUploadBlob,
type Un$Typed,
} from '@atproto/api'
type AtIdentifierString,
type Client,
type DidString,
} from '@atproto/lex-client'
import {type DatetimeString} from '@atproto/lex-schema'
import {AtUri, type AtUriString} from '@atproto/syntax'
import {
deleteFollow,
follow,
muteActor,
unmuteActor,
upsertProfile,
} from '@bsky.app/sdk'
import {
type InfiniteData,
keepPreviousData,
@@ -32,10 +37,11 @@ import {
useUnstableProfileViewCache,
} from '#/state/queries/unstable-profile-cache'
import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
import {type SessionAgent, useAgent, useSession} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useAnalytics} from '#/analytics'
import {type Metrics, toClout} from '#/analytics/metrics'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {
ProgressGuideAction,
@@ -67,9 +73,9 @@ export function useProfileQuery({
did: string | undefined
staleTime?: number
}) {
const agent = useAgent()
const client = useAppviewClient()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
return useQuery<app.bsky.actor.defs.ProfileViewDetailed>({
// WARNING
// this staleTime is load-bearing
// if you remove it, the UI infinite-loops
@@ -78,12 +84,13 @@ export function useProfileQuery({
refetchOnWindowFocus: true,
queryKey: RQKEY(did ?? ''),
queryFn: async () => {
const res = await agent.getProfile({actor: did ?? ''})
return res.data
return await client.call(app.bsky.actor.getProfile, {
actor: (did ?? '') as AtIdentifierString,
})
},
placeholderData: () => {
if (!did) return
return getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed
return getUnstableProfile(did) as app.bsky.actor.defs.ProfileViewDetailed
},
enabled: !!did,
})
@@ -96,21 +103,22 @@ export function useProfilesQuery({
handles: string[]
maintainData?: boolean
}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
enabled: handles.length > 0,
staleTime: STALE.MINUTES.FIVE,
queryKey: profilesQueryKey(handles),
queryFn: async () => {
const res = await agent.getProfiles({actors: handles})
return res.data
return await client.call(app.bsky.actor.getProfiles, {
actors: handles as AtIdentifierString[],
})
},
placeholderData: maintainData ? keepPreviousData : undefined,
})
}
export function usePrefetchProfileQuery() {
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const prefetchProfileQuery = useCallback(
async (did: string) => {
@@ -118,30 +126,32 @@ export function usePrefetchProfileQuery() {
staleTime: STALE.SECONDS.THIRTY,
queryKey: RQKEY(did),
queryFn: async () => {
const res = await agent.getProfile({actor: did || ''})
return res.data
return await client.call(app.bsky.actor.getProfile, {
actor: (did || '') as AtIdentifierString,
})
},
})
},
[queryClient, agent],
[queryClient, client],
)
return prefetchProfileQuery
}
interface ProfileUpdateParams {
profile: AppBskyActorDefs.ProfileViewDetailed
profile: app.bsky.actor.defs.ProfileViewDetailed
updates:
| Un$Typed<AppBskyActorProfile.Record>
| Un$Typed<app.bsky.actor.profile.Main>
| ((
existing: Un$Typed<AppBskyActorProfile.Record>,
) => Un$Typed<AppBskyActorProfile.Record>)
existing: Un$Typed<app.bsky.actor.profile.Main>,
) => Un$Typed<app.bsky.actor.profile.Main>)
newUserAvatar?: ImageMeta | undefined | null
newUserBanner?: ImageMeta | undefined | null
checkCommitted?: (res: AppBskyActorGetProfile.Response) => boolean
checkCommitted?: (res: app.bsky.actor.getProfile.$OutputBody) => boolean
}
export function useProfileUpdateMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
const appviewClient = useAppviewClient()
const updateProfileVerificationCache = useUpdateProfileVerificationCache()
return useMutation<void, Error, ProfileUpdateParams>({
mutationFn: async ({
@@ -151,28 +161,24 @@ export function useProfileUpdateMutation() {
newUserBanner,
checkCommitted,
}) => {
let newUserAvatarPromise:
| Promise<ComAtprotoRepoUploadBlob.Response>
| undefined
let newUserAvatarPromise: ReturnType<typeof uploadBlob> | undefined
if (newUserAvatar) {
newUserAvatarPromise = uploadBlob(
agent,
pdsClient,
newUserAvatar.path,
newUserAvatar.mime,
)
}
let newUserBannerPromise:
| Promise<ComAtprotoRepoUploadBlob.Response>
| undefined
let newUserBannerPromise: ReturnType<typeof uploadBlob> | undefined
if (newUserBanner) {
newUserBannerPromise = uploadBlob(
agent,
pdsClient,
newUserBanner.path,
newUserBanner.mime,
)
}
await agent.upsertProfile(async existing => {
let next: Un$Typed<AppBskyActorProfile.Record> = existing || {}
await pdsClient.call(upsertProfile, async existing => {
let next: Un$Typed<app.bsky.actor.profile.Main> = existing || {}
if (typeof updates === 'function') {
next = updates(next)
} else {
@@ -184,37 +190,37 @@ export function useProfileUpdateMutation() {
}
if (newUserAvatarPromise) {
const res = await newUserAvatarPromise
next.avatar = res.data.blob
next.avatar = res.blob
} else if (newUserAvatar === null) {
next.avatar = undefined
}
if (newUserBannerPromise) {
const res = await newUserBannerPromise
next.banner = res.data.blob
next.banner = res.blob
} else if (newUserBanner === null) {
next.banner = undefined
}
return next
})
await whenAppViewReady(
agent,
appviewClient,
profile.did,
checkCommitted ||
(res => {
if (typeof newUserAvatar !== 'undefined') {
if (newUserAvatar === null && res.data.avatar) {
if (newUserAvatar === null && res.avatar) {
// url hasn't cleared yet
return false
} else if (res.data.avatar === profile.avatar) {
} else if (res.avatar === profile.avatar) {
// url hasn't changed yet
return false
}
}
if (typeof newUserBanner !== 'undefined') {
if (newUserBanner === null && res.data.banner) {
if (newUserBanner === null && res.banner) {
// url hasn't cleared yet
return false
} else if (res.data.banner === profile.banner) {
} else if (res.banner === profile.banner) {
// url hasn't changed yet
return false
}
@@ -223,8 +229,8 @@ export function useProfileUpdateMutation() {
return true
}
return (
res.data.displayName === updates.displayName &&
res.data.description === updates.description
res.displayName === updates.displayName &&
res.description === updates.description
)
}),
)
@@ -248,7 +254,7 @@ export function useProfileFollowMutationQueue(
position?: number,
contextProfileDid?: string,
) {
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const did = profile.did
@@ -290,7 +296,7 @@ export function useProfileFollowMutationQueue(
// Optimistically update profile follows cache for avatar displays
if (currentAccount?.did) {
type FollowsQueryData =
InfiniteData<AppBskyGraphGetFollows.OutputSchema>
InfiniteData<app.bsky.graph.getFollows.$OutputBody>
queryClient.setQueryData<FollowsQueryData>(
PROFILE_FOLLOWS_RQKEY(currentAccount.did),
old => {
@@ -307,7 +313,7 @@ export function useProfileFollowMutationQueue(
{
...old.pages[0],
follows: [
profile as AppBskyActorDefs.ProfileView,
profile as app.bsky.actor.defs.ProfileView,
...old.pages[0].follows,
],
},
@@ -329,12 +335,12 @@ export function useProfileFollowMutationQueue(
}
if (finalFollowingUri) {
void agent.app.bsky.graph
.getSuggestedFollowsByActor({
actor: did,
void client
.call(app.bsky.graph.getSuggestedFollowsByActor, {
actor: did as AtIdentifierString,
})
.then(res => {
const dids = res.data.suggestions
const dids = res.suggestions
.filter(a => !a.viewer?.following)
.map(a => a.did)
.slice(0, 8)
@@ -371,13 +377,13 @@ function useProfileFollowMutation(
) {
const ax = useAnalytics()
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
const {captureAction} = useProgressGuideControls()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
@@ -396,7 +402,7 @@ function useProfileFollowMutation(
position,
contextProfileDid,
})
return await agent.follow(did)
return await pdsClient.call(follow, {did: did as DidString})
},
})
}
@@ -405,11 +411,11 @@ function useProfileUnfollowMutation(
logContext: Metrics['profile:unfollow']['logContext'],
) {
const ax = useAnalytics()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => {
ax.metric('profile:unfollow', {logContext})
return await agent.deleteFollow(followUri)
return await pdsClient.call(deleteFollow, followUri as AtUriString)
},
})
}
@@ -468,10 +474,10 @@ export function useProfileMuteMutationQueue(
function useProfileMuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await agent.mute(did)
await pdsClient.call(muteActor, {actor: did as AtIdentifierString})
},
onSuccess() {
void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
@@ -481,10 +487,10 @@ function useProfileMuteMutation() {
function useProfileUnmuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
const pdsClient = usePdsClient()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await agent.unmute(did)
await pdsClient.call(unmuteActor, {actor: did as AtIdentifierString})
},
onSuccess() {
void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
@@ -556,17 +562,17 @@ export function useProfileBlockMutationQueue(
function useProfileBlockMutation() {
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
if (!currentAccount) {
throw new Error('Not signed in')
}
return await agent.app.bsky.graph.block.create(
{repo: currentAccount.did},
{subject: did, createdAt: new Date().toISOString()},
)
return await pdsClient.create(app.bsky.graph.block, {
subject: did as DidString,
createdAt: new Date().toISOString() as DatetimeString,
})
},
onSuccess(_, {did}) {
void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
@@ -577,7 +583,7 @@ function useProfileBlockMutation() {
function useProfileUnblockMutation() {
const {currentAccount} = useSession()
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; blockUri: string}>({
mutationFn: async ({blockUri}) => {
@@ -585,10 +591,7 @@ function useProfileUnblockMutation() {
throw new Error('Not signed in')
}
const {rkey} = new AtUri(blockUri)
await agent.app.bsky.graph.block.delete({
repo: currentAccount.did,
rkey,
})
await pdsClient.delete(app.bsky.graph.block, {rkey})
},
onSuccess(_, {did}) {
resetProfilePostsQueries(queryClient, did, 1000)
@@ -597,24 +600,27 @@ function useProfileUnblockMutation() {
}
async function whenAppViewReady(
agent: SessionAgent,
client: Client,
actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean,
fn: (res: app.bsky.actor.getProfile.$OutputBody) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() => agent.app.bsky.actor.getProfile({actor}),
() =>
client.call(app.bsky.actor.getProfile, {
actor: actor as AtIdentifierString,
}),
)
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewDetailed, void> {
): Generator<app.bsky.actor.defs.ProfileViewDetailed, void> {
const profileQueryDatas =
queryClient.getQueriesData<AppBskyActorDefs.ProfileViewDetailed>({
queryClient.getQueriesData<app.bsky.actor.defs.ProfileViewDetailed>({
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of profileQueryDatas) {
@@ -626,7 +632,7 @@ export function* findAllProfilesInQueryData(
}
}
const profilesQueryDatas =
queryClient.getQueriesData<AppBskyActorGetProfiles.OutputSchema>({
queryClient.getQueriesData<app.bsky.actor.getProfiles.$OutputBody>({
queryKey: [profilesQueryKeyRoot],
})
for (const [_queryKey, queryData] of profilesQueryDatas) {
@@ -644,8 +650,8 @@ export function* findAllProfilesInQueryData(
export function findProfileQueryData(
queryClient: QueryClient,
did: string,
): AppBskyActorDefs.ProfileViewDetailed | undefined {
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
): app.bsky.actor.defs.ProfileViewDetailed | undefined {
return queryClient.getQueryData<app.bsky.actor.defs.ProfileViewDetailed>(
RQKEY(did),
)
}
+29 -16
View File
@@ -1,8 +1,13 @@
import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {type ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve'
import {
type ResolveClients,
type ResolvedLink,
resolveGif,
resolveLink,
} from '#/lib/api/resolve'
import {STALE} from '#/state/queries/index'
import {type SessionAgent, useAgent} from '#/state/session'
import {useAgent, useChatClient, useLexClient} from '#/state/session'
import {type Gif} from '#/features/gifPicker/types'
export const RQKEY_LINK_ROOT = 'resolve-link'
@@ -11,24 +16,37 @@ export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url]
export const RQKEY_GIF_ROOT = 'resolve-gif'
export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url]
export function resolveLinkQueryOptions(agent: SessionAgent, url: string) {
export function resolveLinkQueryOptions(clients: ResolveClients, url: string) {
return queryOptions({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_LINK(url),
queryFn: () => resolveLink(agent, url),
queryFn: () => resolveLink(clients, url),
})
}
export function useResolveLinkQuery(url: string) {
/**
* Bundle the clients the link resolver needs from the session hooks. The
* appview client serves the `app.bsky.*` reads and handle resolution, the chat
* client serves group join-link previews, and the bridge agent is still passed
* through to the not-yet-migrated `getLinkMeta`.
*/
export function useResolveClients(): ResolveClients {
const appview = useLexClient()
const chat = useChatClient()
const agent = useAgent()
return useQuery(resolveLinkQueryOptions(agent, url))
return {appview, chat, agent}
}
export function useResolveLinkQuery(url: string) {
const clients = useResolveClients()
return useQuery(resolveLinkQueryOptions(clients, url))
}
export function fetchResolveLinkQuery(
queryClient: QueryClient,
agent: SessionAgent,
clients: ResolveClients,
url: string,
) {
return queryClient.fetchQuery(resolveLinkQueryOptions(agent, url))
return queryClient.fetchQuery(resolveLinkQueryOptions(clients, url))
}
export function precacheResolveLinkQuery(
queryClient: QueryClient,
@@ -39,25 +57,20 @@ export function precacheResolveLinkQuery(
}
export function useResolveGifQuery(gif: Gif) {
const agent = useAgent()
return useQuery({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_GIF(gif.url),
queryFn: async () => {
return await resolveGif(agent, gif)
return await resolveGif(gif)
},
})
}
export function fetchResolveGifQuery(
queryClient: QueryClient,
agent: SessionAgent,
gif: Gif,
) {
export function fetchResolveGifQuery(queryClient: QueryClient, gif: Gif) {
return queryClient.fetchQuery({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_GIF(gif.url),
queryFn: async () => {
return await resolveGif(agent, gif)
return await resolveGif(gif)
},
})
}
+13 -9
View File
@@ -1,15 +1,17 @@
import {AtUri} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {AtUri, type HandleString} from '@atproto/syntax'
import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {type SessionAgent, useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {com} from '#/lexicons'
import {useUnstableProfileViewCache} from './profile'
const RQKEY_ROOT = 'resolved-did'
export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle]
const resolvedDidQueryOptions = (
agent: SessionAgent,
client: Client,
getUnstableProfile: (did: string) => {did: string} | undefined,
didOrHandle: string | undefined,
) =>
@@ -21,8 +23,10 @@ const resolvedDidQueryOptions = (
// Just return the did if it's already one
if (didOrHandle.startsWith('did:')) return didOrHandle
const res = await agent.resolveHandle({handle: didOrHandle})
return res.data.did
const {did} = await client.call(com.atproto.identity.resolveHandle, {
handle: didOrHandle as HandleString,
})
return did
},
initialData: () => {
// Return undefined if no did or handle
@@ -37,11 +41,11 @@ export function useResolveUriQuery(uri: string | undefined) {
const urip = new AtUri(uri || '')
const host = urip.host
const agent = useAgent()
const client = useAppviewClient()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery({
...resolvedDidQueryOptions(agent, getUnstableProfile, host),
...resolvedDidQueryOptions(client, getUnstableProfile, host),
select: did => ({
did,
uri: AtUri.make(did, urip.collection, urip.rkey).toString(),
@@ -50,11 +54,11 @@ export function useResolveUriQuery(uri: string | undefined) {
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery(
resolvedDidQueryOptions(agent, getUnstableProfile, didOrHandle),
resolvedDidQueryOptions(client, getUnstableProfile, didOrHandle),
)
}
+29 -9
View File
@@ -4,12 +4,22 @@
* tested in isolation (the search-posts query hook re-exports these).
*/
import {type AppBskyFeedSearchPostsV2} from '@atproto/api'
import {type l} from '@atproto/lex'
import {
filtersToApiParams,
type SearchFilters,
} from '#/screens/Search/searchParams'
import {type app} from '#/lexicons'
/**
* Input params for `app.bsky.feed.searchPostsV2`. Uses the schema's input type
* (not `$Params`, which is the output type with defaults applied and `limit`
* required) since these are the params we build up and pass to `client.call`.
*/
type SearchPostsV2Params = l.InferInput<
typeof app.bsky.feed.searchPostsV2.$params
>
const DATE_RE = /^\d{4}-\d{2}-\d{2}/
@@ -204,21 +214,27 @@ function mergeList(a?: string[], b?: string[]): string[] | undefined {
export function buildSearchPostsV2Filters(
embedded: Omit<ExtractedSearchParams, 'q'>,
filters?: SearchFilters,
): AppBskyFeedSearchPostsV2.QueryParams {
): SearchPostsV2Params {
const apiFilters = filters ? filtersToApiParams(filters) : {}
const params: AppBskyFeedSearchPostsV2.QueryParams = {}
const params: SearchPostsV2Params = {}
/*
* The values below originate from free-text operators and the advanced-search
* dialog as plain strings; the lexicon input params brand them by format
* (at-identifier, uri, etc.). The backend validates these, so we assert the
* branded types at assignment rather than validating client-side.
*/
const authors = mergeList(
embedded.author ? [embedded.author] : undefined,
apiFilters.authors,
)
if (authors) params.authors = authors
if (authors) params.authors = authors as SearchPostsV2Params['authors']
const mentions = mergeList(
embedded.mentions ? [embedded.mentions] : undefined,
apiFilters.mentions,
)
if (mentions) params.mentions = mentions
if (mentions) params.mentions = mentions as SearchPostsV2Params['mentions']
const domains = mergeList(
embedded.domain ? [embedded.domain] : undefined,
@@ -230,7 +246,7 @@ export function buildSearchPostsV2Filters(
embedded.url ? [embedded.url] : undefined,
apiFilters.urls,
)
if (urls) params.urls = urls
if (urls) params.urls = urls as SearchPostsV2Params['urls']
const hashtags = mergeList(embedded.tag, apiFilters.hashtags)
if (hashtags) params.hashtags = hashtags
@@ -250,12 +266,16 @@ export function buildSearchPostsV2Filters(
* are always include), so they pass straight through from the dialog filters.
*/
if (apiFilters.excludeAuthors)
params.excludeAuthors = apiFilters.excludeAuthors
params.excludeAuthors =
apiFilters.excludeAuthors as SearchPostsV2Params['excludeAuthors']
if (apiFilters.excludeMentions)
params.excludeMentions = apiFilters.excludeMentions
params.excludeMentions =
apiFilters.excludeMentions as SearchPostsV2Params['excludeMentions']
if (apiFilters.excludeDomains)
params.excludeDomains = apiFilters.excludeDomains
if (apiFilters.excludeUrls) params.excludeUrls = apiFilters.excludeUrls
if (apiFilters.excludeUrls)
params.excludeUrls =
apiFilters.excludeUrls as SearchPostsV2Params['excludeUrls']
if (apiFilters.excludeHashtags)
params.excludeHashtags = apiFilters.excludeHashtags
+13 -17
View File
@@ -1,10 +1,6 @@
import {useCallback, useMemo, useRef} from 'react'
import {
type AppBskyFeedDefs,
type AppBskyFeedSearchPostsV2,
AtUri,
moderatePost,
} from '@atproto/api'
import {AtUri} from '@atproto/syntax'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {
type InfiniteData,
type QueryClient,
@@ -13,8 +9,9 @@ import {
} from '@tanstack/react-query'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {type SearchFilters} from '#/screens/Search/searchParams'
import {app} from '#/lexicons'
import {
appendFromMe,
buildSearchPostsV2Filters,
@@ -48,7 +45,7 @@ export function useSearchPostsV2Query({
enabled?: boolean
filters?: SearchFilters
}) {
const agent = useAgent()
const client = useAppviewClient()
const moderationOpts = useModerationOpts()
const selectArgs = useMemo(
() => ({
@@ -59,15 +56,15 @@ export function useSearchPostsV2Query({
[query, filters?.author, filters?.from, moderationOpts],
)
const lastRun = useRef<{
data: InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>
data: InfiniteData<app.bsky.feed.searchPostsV2.$OutputBody>
args: typeof selectArgs
result: InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>
result: InfiniteData<app.bsky.feed.searchPostsV2.$OutputBody>
} | null>(null)
return useInfiniteQuery<
AppBskyFeedSearchPostsV2.OutputSchema,
app.bsky.feed.searchPostsV2.$OutputBody,
Error,
InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>,
InfiniteData<app.bsky.feed.searchPostsV2.$OutputBody>,
QueryKey,
string | undefined
>({
@@ -81,7 +78,7 @@ export function useSearchPostsV2Query({
const {q, ...embedded} = extractSearchPostsParams(query)
const builtFilters = buildSearchPostsV2Filters(embedded, filters)
const finalQuery = appendFromMe(q, filters?.from === 'me')
const res = await agent.app.bsky.feed.searchPostsV2({
return await client.call(app.bsky.feed.searchPostsV2, {
...builtFilters,
query: finalQuery,
limit: 25,
@@ -93,13 +90,12 @@ export function useSearchPostsV2Query({
sort: sort === 'latest' ? 'recent' : sort,
allTime: true,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled: enabled ?? !!moderationOpts,
select: useCallback(
(data: InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>) => {
(data: InfiniteData<app.bsky.feed.searchPostsV2.$OutputBody>) => {
const {moderationOpts, isSearchingSpecificUser} = selectArgs
/*
@@ -175,9 +171,9 @@ export function useSearchPostsV2Query({
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
): Generator<app.bsky.feed.defs.PostView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedSearchPostsV2.OutputSchema>
InfiniteData<app.bsky.feed.searchPostsV2.$OutputBody>
>({
queryKey: [searchPostsQueryKeyRoot],
})
+97 -94
View File
@@ -1,12 +1,6 @@
import {
AppBskyFeedDefs,
AppBskyGraphDefs,
type AppBskyGraphGetStarterPack,
AppBskyGraphStarterpack,
type AppBskyRichtextFacet,
AtUri,
RichText,
} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {AtUri, type AtUriString, type DatetimeString} from '@atproto/syntax'
import {RichText} from '@bsky.app/sdk/richtext'
import {
type QueryClient,
useMutation,
@@ -25,7 +19,8 @@ import {
import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
import {STALE} from '#/state/queries/index'
import {invalidateListMembersQuery} from '#/state/queries/list-members'
import {type SessionAgent, useAgent} from '#/state/session'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app, com} from '#/lexicons'
import * as bsky from '#/types/bsky'
const RQKEY_ROOT = 'starter-pack'
@@ -55,9 +50,9 @@ export function useStarterPackQuery({
did?: string
rkey?: string
}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery<AppBskyGraphDefs.StarterPackView>({
return useQuery<app.bsky.graph.defs.StarterPackView>({
queryKey: RQKEY(uri ? {uri} : {did, rkey}),
queryFn: async () => {
if (!uri) {
@@ -66,10 +61,10 @@ export function useStarterPackQuery({
uri = httpStarterPackUriToAtUri(uri) as string
}
const res = await agent.app.bsky.graph.getStarterPack({
starterPack: uri,
const res = await client.call(app.bsky.graph.getStarterPack, {
starterPack: uri as AtUriString,
})
return res.data.starterPack
return res.starterPack
},
enabled: Boolean(uri) || Boolean(did && rkey),
staleTime: STALE.MINUTES.FIVE,
@@ -92,7 +87,7 @@ interface UseCreateStarterPackMutationParams {
name: string
description?: string
profiles: bsky.profile.AnyProfileView[]
feeds?: AppBskyFeedDefs.GeneratorView[]
feeds?: app.bsky.feed.defs.GeneratorView[]
}
export function useCreateStarterPackMutation({
@@ -103,7 +98,8 @@ export function useCreateStarterPackMutation({
onError: (e: Error) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
return useMutation<
{uri: string; cid: string},
@@ -111,10 +107,10 @@ export function useCreateStarterPackMutation({
UseCreateStarterPackMutationParams
>({
mutationFn: async ({name, description, feeds, profiles}) => {
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined
if (description) {
const rt = new RichText({text: description})
await rt.detectFacets(agent)
await rt.detectFacets(pdsClient)
descriptionFacets = rt.facets
}
@@ -124,30 +120,25 @@ export function useCreateStarterPackMutation({
description,
profiles,
descriptionFacets,
agent,
client: pdsClient,
})
return await agent.app.bsky.graph.starterpack.create(
{
repo: agent.assertDid,
},
{
name,
description,
descriptionFacets,
list: listRes?.uri,
feeds: feeds?.map(f => ({uri: f.uri})),
createdAt: new Date().toISOString(),
},
)
return await pdsClient.create(app.bsky.graph.starterpack, {
name,
description,
descriptionFacets,
list: listRes?.uri as AtUriString,
feeds: feeds?.map(f => ({uri: f.uri})),
createdAt: new Date().toISOString() as DatetimeString,
})
},
onSuccess: async data => {
await whenAppViewReady(agent, data.uri, v => {
return typeof v?.data.starterPack.uri === 'string'
await whenAppViewReady(appviewClient, data.uri, v => {
return typeof v?.starterPack.uri === 'string'
})
await invalidateActorStarterPacksQuery({
queryClient,
did: agent.session!.did,
did: pdsClient.assertDid,
})
onSuccess(data)
},
@@ -165,14 +156,15 @@ export function useEditStarterPackMutation({
onError: (error: Error) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
return useMutation<
void,
Error,
UseCreateStarterPackMutationParams & {
currentStarterPack: AppBskyGraphDefs.StarterPackView
currentListItems: AppBskyGraphDefs.ListItemView[]
currentStarterPack: app.bsky.graph.defs.StarterPackView
currentListItems: app.bsky.graph.defs.ListItemView[]
}
>({
mutationFn: async ({
@@ -183,32 +175,36 @@ export function useEditStarterPackMutation({
currentStarterPack,
currentListItems,
}) => {
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined
if (description) {
const rt = new RichText({text: description})
await rt.detectFacets(agent)
await rt.detectFacets(pdsClient)
descriptionFacets = rt.facets
}
if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) {
if (!bsky.isType(app.bsky.graph.starterpack, currentStarterPack.record)) {
throw new Error('Invalid starter pack')
}
const removedItems = currentListItems.filter(
i =>
i.subject.did !== agent.session?.did &&
i.subject.did !== pdsClient.did &&
!profiles.find(p => p.did === i.subject.did && p.did),
)
if (removedItems.length !== 0) {
const chunks = chunk(removedItems, 50)
for (const chunk of chunks) {
await agent.com.atproto.repo.applyWrites({
repo: agent.session!.did,
writes: chunk.map(i => ({
$type: 'com.atproto.repo.applyWrites#delete',
collection: 'app.bsky.graph.listitem',
rkey: new AtUri(i.uri).rkey,
})),
await pdsClient.call(com.atproto.repo.applyWrites, {
repo: pdsClient.assertDid,
writes: chunk.map(
(
i,
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({
$type: 'com.atproto.repo.applyWrites#delete',
collection: 'app.bsky.graph.listitem',
rkey: new AtUri(i.uri).rkey,
}),
),
})
}
}
@@ -219,28 +215,33 @@ export function useEditStarterPackMutation({
if (addedProfiles.length > 0) {
const chunks = chunk(addedProfiles, 50)
for (const chunk of chunks) {
await agent.com.atproto.repo.applyWrites({
repo: agent.session!.did,
writes: chunk.map(p => ({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.graph.listitem',
value: {
$type: 'app.bsky.graph.listitem',
subject: p.did,
list: currentStarterPack.list?.uri,
createdAt: new Date().toISOString(),
},
})),
await pdsClient.call(com.atproto.repo.applyWrites, {
repo: pdsClient.assertDid,
writes: chunk.map(
(
p,
): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.graph.listitem',
value: {
$type: 'app.bsky.graph.listitem',
subject: p.did,
list: currentStarterPack.list?.uri,
createdAt: new Date().toISOString(),
},
}),
),
})
}
}
const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey
await agent.com.atproto.repo.putRecord({
repo: agent.session!.did,
await pdsClient.call(com.atproto.repo.putRecord, {
repo: pdsClient.assertDid,
collection: 'app.bsky.graph.starterpack',
rkey,
record: {
$type: 'app.bsky.graph.starterpack',
name,
description,
descriptionFacets,
@@ -253,12 +254,12 @@ export function useEditStarterPackMutation({
},
onSuccess: async (_, {currentStarterPack}) => {
const parsed = parseStarterPackUri(currentStarterPack.uri)
await whenAppViewReady(agent, currentStarterPack.uri, v => {
return currentStarterPack.cid !== v?.data.starterPack.cid
await whenAppViewReady(appviewClient, currentStarterPack.uri, v => {
return currentStarterPack.cid !== v?.starterPack.cid
})
await invalidateActorStarterPacksQuery({
queryClient,
did: agent.session!.did,
did: pdsClient.assertDid,
})
if (currentStarterPack.list) {
await invalidateListMembersQuery({
@@ -268,7 +269,7 @@ export function useEditStarterPackMutation({
}
await invalidateStarterPack({
queryClient,
did: agent.session!.did,
did: pdsClient.assertDid,
rkey: parsed!.rkey,
})
onSuccess()
@@ -286,35 +287,34 @@ export function useDeleteStarterPackMutation({
onSuccess: () => void
onError: (error: Error) => void
}) {
const agent = useAgent()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => {
if (!agent.session) {
throw new Error(`Requires signed in user`)
}
const did = pdsClient.assertDid
if (listUri) {
await agent.app.bsky.graph.list.delete({
repo: agent.session.did,
await pdsClient.delete(app.bsky.graph.list, {
repo: did,
rkey: new AtUri(listUri).rkey,
})
}
await agent.app.bsky.graph.starterpack.delete({
repo: agent.session.did,
await pdsClient.delete(app.bsky.graph.starterpack, {
repo: did,
rkey,
})
},
onSuccess: async (_, {listUri, rkey}) => {
const uri = createStarterPackUri({
did: agent.session!.did,
did: pdsClient.assertDid,
rkey,
})
if (uri) {
await whenAppViewReady(agent, uri, v => {
return Boolean(v?.data?.starterPack) === false
await whenAppViewReady(appviewClient, uri, v => {
return Boolean(v?.starterPack) === false
})
}
@@ -323,11 +323,11 @@ export function useDeleteStarterPackMutation({
}
await invalidateActorStarterPacksQuery({
queryClient,
did: agent.session!.did,
did: pdsClient.assertDid,
})
await invalidateStarterPack({
queryClient,
did: agent.session!.did,
did: pdsClient.assertDid,
rkey,
})
onSuccess()
@@ -339,48 +339,51 @@ export function useDeleteStarterPackMutation({
}
async function whenAppViewReady(
agent: SessionAgent,
client: Client,
uri: string,
fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean,
fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() => agent.app.bsky.graph.getStarterPack({starterPack: uri}),
() =>
client.call(app.bsky.graph.getStarterPack, {
starterPack: uri as AtUriString,
}),
)
}
export function precacheStarterPack(
queryClient: QueryClient,
starterPack:
| AppBskyGraphDefs.StarterPackViewBasic
| AppBskyGraphDefs.StarterPackView,
| app.bsky.graph.defs.StarterPackViewBasic
| app.bsky.graph.defs.StarterPackView,
) {
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
if (!bsky.isType(app.bsky.graph.starterpack, starterPack.record)) {
return
}
let starterPackView: AppBskyGraphDefs.StarterPackView | undefined
if (AppBskyGraphDefs.isStarterPackView(starterPack)) {
let starterPackView: app.bsky.graph.defs.StarterPackView | undefined
if (bsky.isType(app.bsky.graph.defs.starterPackView, starterPack)) {
starterPackView = starterPack
} else if (
AppBskyGraphDefs.isStarterPackViewBasic(starterPack) &&
bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
bsky.isType(app.bsky.graph.defs.starterPackViewBasic, starterPack) &&
bsky.matches(app.bsky.graph.starterpack, starterPack.record)
) {
let feeds: AppBskyFeedDefs.GeneratorView[] | undefined
let feeds: app.bsky.feed.defs.GeneratorView[] | undefined
if (starterPack.record.feeds) {
feeds = []
for (const feed of starterPack.record.feeds) {
// note: types are wrong? claims to be `FeedItem`, but we actually
// get un$typed `GeneratorView` objects here -sfn
if (bsky.validate(feed, AppBskyFeedDefs.validateGeneratorView)) {
if (bsky.matches(app.bsky.feed.defs.generatorView, feed)) {
feeds.push(feed)
}
}
}
const listView: AppBskyGraphDefs.ListViewBasic = {
const listView: app.bsky.graph.defs.ListViewBasic = {
uri: starterPack.record.list,
// This will be populated once the data from server is fetched
cid: '',
+15 -16
View File
@@ -1,9 +1,5 @@
import {useCallback, useMemo} from 'react'
import {
type AppBskyActorDefs,
type AppBskyActorGetSuggestions,
type AppBskyGraphGetSuggestedFollowsByActor,
} from '@atproto/api'
import {type AtIdentifierString} from '@atproto/syntax'
import {
type InfiniteData,
type QueryClient,
@@ -12,8 +8,8 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
import type * as bsky from '#/types/bsky'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
@@ -32,18 +28,21 @@ export function useSuggestedFollowsByActorQuery({
enabled?: boolean
staleTime?: number
}) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
staleTime,
queryKey: suggestedFollowsByActorQueryKey(did),
queryFn: async () => {
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
actor: did,
})
const suggestions = res.data.suggestions.filter(
const data = await client.call(
app.bsky.graph.getSuggestedFollowsByActor,
{
actor: did as AtIdentifierString,
},
)
const suggestions = data.suggestions.filter(
profile => !profile.viewer?.following,
)
return {suggestions, recId: res.data.recIdStr}
return {suggestions, recId: data.recIdStr}
},
enabled,
})
@@ -102,7 +101,7 @@ export function useSuggestedFollowsByActorWithDismiss({
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
): Generator<app.bsky.actor.defs.ProfileView, void> {
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
yield* findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did)
}
@@ -112,7 +111,7 @@ function* findAllProfilesInSuggestedFollowsQueryData(
did: string,
) {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyActorGetSuggestions.OutputSchema>
InfiniteData<app.bsky.actor.getSuggestions.$OutputBody>
>({
queryKey: [suggestedFollowsQueryKeyRoot],
})
@@ -135,7 +134,7 @@ function* findAllProfilesInSuggestedFollowsByActorQueryData(
did: string,
) {
const queryDatas =
queryClient.getQueriesData<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema>(
queryClient.getQueriesData<app.bsky.graph.getSuggestedFollowsByActor.$OutputBody>(
{
queryKey: [suggestedFollowsByActorQueryKeyRoot],
},
+38 -40
View File
@@ -1,4 +1,5 @@
import {type AppBskyFeedDefs, AppBskyFeedThreadgate, AtUri} from '@atproto/api'
import {type Client} from '@atproto/lex-client'
import {AtUri} from '@atproto/syntax'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {networkRetry, retry} from '#/lib/async/retry'
@@ -12,8 +13,9 @@ import {
threadgateViewToAllowUISetting,
} from '#/state/queries/threadgate/util'
import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread'
import {type SessionAgent, useAgent} from '#/state/session'
import {usePdsClient} from '#/state/session'
import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies'
import {app, com} from '#/lexicons'
import * as bsky from '#/types/bsky'
export * from '#/state/queries/threadgate/types'
@@ -35,9 +37,9 @@ export function useThreadgateRecordQuery({
initialData,
}: {
postUri?: string
initialData?: AppBskyFeedThreadgate.Record
initialData?: app.bsky.feed.threadgate.Main
} = {}) {
const agent = useAgent()
const pdsClient = usePdsClient()
return useQuery({
enabled: !!postUri,
@@ -46,7 +48,7 @@ export function useThreadgateRecordQuery({
staleTime: STALE.MINUTES.ONE,
async queryFn() {
return getThreadgateRecord({
agent,
pdsClient,
postUri: postUri!,
})
},
@@ -63,7 +65,7 @@ export function useThreadgateViewQuery({
initialData,
}: {
postUri?: string
initialData?: AppBskyFeedDefs.ThreadgateView
initialData?: app.bsky.feed.defs.ThreadgateView
} = {}) {
const getPost = useGetPost()
@@ -80,24 +82,23 @@ export function useThreadgateViewQuery({
}
export async function getThreadgateRecord({
agent,
pdsClient,
postUri,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
}): Promise<AppBskyFeedThreadgate.Record | null> {
}): Promise<app.bsky.feed.threadgate.Main | null> {
const urip = new AtUri(postUri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, {
handle: urip.host as `${string}.${string}`,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
urip.host = did
}
try {
const {data} = await retry(
const data = await retry(
2,
e => {
/*
@@ -111,17 +112,14 @@ export async function getThreadgateRecord({
return true
},
() =>
agent.api.com.atproto.repo.getRecord({
pdsClient.call(com.atproto.repo.getRecord, {
repo: urip.host,
collection: 'app.bsky.feed.threadgate',
rkey: urip.rkey,
}),
)
if (
data.value &&
bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord)
) {
if (data.value && bsky.matches(app.bsky.feed.threadgate, data.value)) {
return data.value
} else {
return null
@@ -141,13 +139,13 @@ export async function getThreadgateRecord({
}
export async function writeThreadgateRecord({
agent,
pdsClient,
postUri,
threadgate,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
threadgate: AppBskyFeedThreadgate.Record
threadgate: app.bsky.feed.threadgate.Main
}) {
const postUrip = new AtUri(postUri)
const record = createThreadgateRecord({
@@ -157,8 +155,8 @@ export async function writeThreadgateRecord({
})
await networkRetry(2, () =>
agent.api.com.atproto.repo.putRecord({
repo: agent.session!.did,
pdsClient.call(com.atproto.repo.putRecord, {
repo: pdsClient.assertDid,
collection: 'app.bsky.feed.threadgate',
rkey: postUrip.rkey,
record,
@@ -168,25 +166,25 @@ export async function writeThreadgateRecord({
export async function upsertThreadgate(
{
agent,
pdsClient,
postUri,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
},
callback: (
threadgate: AppBskyFeedThreadgate.Record | null,
) => Promise<AppBskyFeedThreadgate.Record | undefined>,
threadgate: app.bsky.feed.threadgate.Main | null,
) => Promise<app.bsky.feed.threadgate.Main | undefined>,
) {
const prev = await getThreadgateRecord({
agent,
pdsClient,
postUri,
})
const next = await callback(prev)
if (!next) return
validateThreadgateRecordOrThrow(next)
await writeThreadgateRecord({
agent,
pdsClient,
postUri,
threadgate: next,
})
@@ -196,15 +194,15 @@ export async function upsertThreadgate(
* Update the allow list for a threadgate record.
*/
export async function updateThreadgateAllow({
agent,
pdsClient,
postUri,
allow,
}: {
agent: SessionAgent
pdsClient: Client
postUri: string
allow: ThreadgateAllowUISetting[]
}) {
return upsertThreadgate({agent, postUri}, async prev => {
return upsertThreadgate({pdsClient, postUri}, async prev => {
if (prev) {
return {
...prev,
@@ -220,7 +218,7 @@ export async function updateThreadgateAllow({
}
export function useSetThreadgateAllowMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
const getPost = useGetPost()
const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache()
@@ -233,7 +231,7 @@ export function useSetThreadgateAllowMutation() {
postUri: string
allow: ThreadgateAllowUISetting[]
}) => {
return upsertThreadgate({agent, postUri}, async prev => {
return upsertThreadgate({pdsClient, postUri}, async prev => {
if (prev) {
return {
...prev,
@@ -248,7 +246,7 @@ export function useSetThreadgateAllowMutation() {
})
},
async onSuccess(_, {postUri, allow}) {
const data = await retry<AppBskyFeedDefs.ThreadgateView | undefined>(
const data = await retry<app.bsky.feed.defs.ThreadgateView | undefined>(
5, // 5 tries
_e => true,
async () => {
@@ -285,7 +283,7 @@ export function useSetThreadgateAllowMutation() {
}
export function useToggleReplyVisibilityMutation() {
const agent = useAgent()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
const hiddenReplies = useThreadgateHiddenReplyUrisAPI()
@@ -305,7 +303,7 @@ export function useToggleReplyVisibilityMutation() {
hiddenReplies.removeHiddenReplyUri(replyUri)
}
await upsertThreadgate({agent, postUri}, async prev => {
await upsertThreadgate({pdsClient, postUri}, async prev => {
if (prev) {
if (action === 'hide') {
return mergeThreadgateRecords(prev, {
@@ -358,9 +356,9 @@ export class InvalidInteractionSettingsError extends Error {
}
export function validateThreadgateRecordOrThrow(
record: AppBskyFeedThreadgate.Record,
record: app.bsky.feed.threadgate.Main,
) {
const result = AppBskyFeedThreadgate.validateRecord(record)
const result = bsky.safeParse(app.bsky.feed.threadgate, record)
if (result.success) {
if ((result.value.hiddenReplies?.length ?? 0) > MAX_HIDDEN_REPLIES) {
+36 -26
View File
@@ -1,26 +1,27 @@
import {type AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
import {type AtUriString, toDatetimeString} from '@atproto/syntax'
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
export function threadgateViewToAllowUISetting(
threadgateView: AppBskyFeedDefs.ThreadgateView | undefined,
threadgateView: app.bsky.feed.defs.ThreadgateView | undefined,
): ThreadgateAllowUISetting[] {
// Validate the record for clarity, since backwards compat code is a little confusing
const threadgate =
threadgateView &&
bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord)
bsky.matches(app.bsky.feed.threadgate, threadgateView.record)
? threadgateView.record
: undefined
return threadgateRecordToAllowUISetting(threadgate)
}
/**
* Converts a full {@link AppBskyFeedThreadgate.Record} to a list of
* Converts a full {@link app.bsky.feed.threadgate.Main} to a list of
* {@link ThreadgateAllowUISetting}, for use by app UI.
*/
export function threadgateRecordToAllowUISetting(
threadgate: AppBskyFeedThreadgate.Record | undefined,
threadgate: app.bsky.feed.threadgate.Main | undefined,
): ThreadgateAllowUISetting[] {
/*
* If `threadgate` doesn't exist (default), or if `threadgate.allow === undefined`, it means
@@ -40,13 +41,13 @@ export function threadgateRecordToAllowUISetting(
const settings: ThreadgateAllowUISetting[] = threadgate.allow
.map(allow => {
let setting: ThreadgateAllowUISetting | undefined
if (AppBskyFeedThreadgate.isMentionRule(allow)) {
if (bsky.isType(app.bsky.feed.threadgate.mentionRule, allow)) {
setting = {type: 'mention'}
} else if (AppBskyFeedThreadgate.isFollowingRule(allow)) {
} else if (bsky.isType(app.bsky.feed.threadgate.followingRule, allow)) {
setting = {type: 'following'}
} else if (AppBskyFeedThreadgate.isListRule(allow)) {
} else if (bsky.isType(app.bsky.feed.threadgate.listRule, allow)) {
setting = {type: 'list', list: allow.list}
} else if (AppBskyFeedThreadgate.isFollowerRule(allow)) {
} else if (bsky.isType(app.bsky.feed.threadgate.followerRule, allow)) {
setting = {type: 'followers'}
}
return setting
@@ -57,7 +58,7 @@ export function threadgateRecordToAllowUISetting(
/**
* Converts an array of {@link ThreadgateAllowUISetting} to the `allow` prop on
* {@link AppBskyFeedThreadgate.Record}.
* {@link app.bsky.feed.threadgate.Main}.
*
* If the `allow` property on the record is undefined, we infer that to mean
* that everyone can reply. If it's an empty array, we infer that to mean that
@@ -65,12 +66,12 @@ export function threadgateRecordToAllowUISetting(
*/
export function threadgateAllowUISettingToAllowRecordValue(
threadgate: ThreadgateAllowUISetting[],
): AppBskyFeedThreadgate.Record['allow'] {
): app.bsky.feed.threadgate.Main['allow'] {
if (threadgate.find(v => v.type === 'everybody')) {
return undefined
}
let allow: Exclude<AppBskyFeedThreadgate.Record['allow'], undefined> = []
let allow: Exclude<app.bsky.feed.threadgate.Main['allow'], undefined> = []
if (!threadgate.find(v => v.type === 'nobody')) {
for (const rule of threadgate) {
@@ -83,7 +84,7 @@ export function threadgateAllowUISettingToAllowRecordValue(
} else if (rule.type === 'list') {
allow.push({
$type: 'app.bsky.feed.threadgate#listRule',
list: rule.list,
list: rule.list as AtUriString,
})
}
}
@@ -93,18 +94,20 @@ export function threadgateAllowUISettingToAllowRecordValue(
}
/**
* Merges two {@link AppBskyFeedThreadgate.Record} objects, combining their
* Merges two {@link app.bsky.feed.threadgate.Main} objects, combining their
* `allow` and `hiddenReplies` arrays and de-deduplicating them.
*
* Note: `allow` can be undefined here, be sure you don't accidentally set it
* to an empty array. See other comments in this file.
*/
export function mergeThreadgateRecords(
prev: AppBskyFeedThreadgate.Record,
next: Partial<AppBskyFeedThreadgate.Record>,
): AppBskyFeedThreadgate.Record {
prev: app.bsky.feed.threadgate.Main,
next: Omit<Partial<app.bsky.feed.threadgate.Main>, 'hiddenReplies'> & {
hiddenReplies?: string[]
},
): app.bsky.feed.threadgate.Main {
// can be undefined if everyone can reply!
const allow: AppBskyFeedThreadgate.Record['allow'] | undefined =
const allow: app.bsky.feed.threadgate.Main['allow'] | undefined =
prev.allow || next.allow
? [...(prev.allow || []), ...(next.allow || [])].filter(
(v, i, a) => a.findIndex(t => t.$type === v.$type) === i,
@@ -112,7 +115,7 @@ export function mergeThreadgateRecords(
: undefined
const hiddenReplies = Array.from(
new Set([...(prev.hiddenReplies || []), ...(next.hiddenReplies || [])]),
)
) as AtUriString[]
return createThreadgateRecord({
post: prev.post,
@@ -122,21 +125,28 @@ export function mergeThreadgateRecords(
}
/**
* Create a new {@link AppBskyFeedThreadgate.Record} object with the given
* properties.
* Create a new {@link app.bsky.feed.threadgate.Main} object with the given
* properties. `post` is accepted as a plain string (callers hold raw AT-URIs)
* and asserted to the branded `AtUriString` here.
*/
export function createThreadgateRecord(
threadgate: Partial<AppBskyFeedThreadgate.Record>,
): AppBskyFeedThreadgate.Record {
threadgate: Omit<
Partial<app.bsky.feed.threadgate.Main>,
'post' | 'hiddenReplies'
> & {
post?: string
hiddenReplies?: string[]
},
): app.bsky.feed.threadgate.Main {
if (!threadgate.post) {
throw new Error('Cannot create a threadgate record without a post URI')
}
return {
$type: 'app.bsky.feed.threadgate',
post: threadgate.post,
createdAt: new Date().toISOString(),
post: threadgate.post as AtUriString,
createdAt: toDatetimeString(new Date()),
allow: threadgate.allow, // can be undefined!
hiddenReplies: threadgate.hiddenReplies || [],
hiddenReplies: (threadgate.hiddenReplies || []) as AtUriString[],
}
}
@@ -1,7 +1,3 @@
import {
type AppBskyActorDefs,
type AppBskyUnspeccedGetSuggestedOnboardingUsers,
} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {createBskyTopicsHeader} from '#/lib/api/feed/utils'
@@ -10,6 +6,7 @@ import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -66,13 +63,12 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedOnboardingUsers.OutputSchema>(
{
queryKey: [getSuggestedOnboardingUsersQueryKeyRoot],
},
)
): Generator<app.bsky.actor.defs.ProfileView, void> {
const responses = queryClient.getQueriesData<{
actors: app.bsky.actor.defs.ProfileView[]
}>({
queryKey: [getSuggestedOnboardingUsersQueryKeyRoot],
})
for (const [_key, response] of responses) {
if (!response) {
continue
@@ -1,7 +1,3 @@
import {
type AppBskyActorDefs,
type AppBskyUnspeccedGetSuggestedUsersForDiscover,
} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {
@@ -13,6 +9,7 @@ import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
export type QueryProps = {
limit?: number
@@ -58,13 +55,12 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsersForDiscover.OutputSchema>(
{
queryKey: [getSuggestedUsersForDiscoverQueryKeyRoot],
},
)
): Generator<app.bsky.actor.defs.ProfileView, void> {
const responses = queryClient.getQueriesData<{
actors: app.bsky.actor.defs.ProfileView[]
}>({
queryKey: [getSuggestedUsersForDiscoverQueryKeyRoot],
})
for (const [_key, response] of responses) {
if (!response) {
continue
@@ -1,7 +1,3 @@
import {
type AppBskyActorDefs,
type AppBskyUnspeccedGetSuggestedUsersForExplore,
} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {
@@ -13,6 +9,7 @@ import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -60,13 +57,12 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsersForExplore.OutputSchema>(
{
queryKey: [getSuggestedUsersForExploreQueryKeyRoot],
},
)
): Generator<app.bsky.actor.defs.ProfileView, void> {
const responses = queryClient.getQueriesData<{
actors: app.bsky.actor.defs.ProfileView[]
}>({
queryKey: [getSuggestedUsersForExploreQueryKeyRoot],
})
for (const [_key, response] of responses) {
if (!response) {
continue
@@ -1,7 +1,3 @@
import {
type AppBskyActorDefs,
type AppBskyUnspeccedGetSuggestedUsersForSeeMore,
} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {
@@ -13,6 +9,7 @@ import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
import {type app} from '#/lexicons'
export type QueryProps = {
category?: string | null
@@ -66,13 +63,12 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) {
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsersForSeeMore.OutputSchema>(
{
queryKey: [getSuggestedUsersForSeeMoreQueryKeyRoot],
},
)
): Generator<app.bsky.actor.defs.ProfileView, void> {
const responses = queryClient.getQueriesData<{
actors: app.bsky.actor.defs.ProfileView[]
}>({
queryKey: [getSuggestedUsersForSeeMoreQueryKeyRoot],
})
for (const [_key, response] of responses) {
if (!response) {
continue
@@ -1,5 +1,5 @@
import {useCallback, useMemo} from 'react'
import {hasMutedWord} from '@atproto/api'
import {hasMutedWord} from '@bsky.app/sdk/moderation'
import {useQuery} from '@tanstack/react-query'
import {
+5 -8
View File
@@ -1,27 +1,24 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api'
/**
* See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams}
* See the `below` param on `app.bsky.unspecced.getPostThreadV2.$Params`
*/
export const LINEAR_VIEW_BELOW = 10
/**
* See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams}
* See the `branchingFactor` param on `app.bsky.unspecced.getPostThreadV2.$Params`
*/
export const LINEAR_VIEW_BF = 1
/**
* See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams}
* See the `below` param on `app.bsky.unspecced.getPostThreadV2.$Params`
*/
export const TREE_VIEW_BELOW = 4
/**
* See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams}
* See the `branchingFactor` param on `app.bsky.unspecced.getPostThreadV2.$Params`
*/
export const TREE_VIEW_BF = undefined
/**
* See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams}
* See the `below` param on `app.bsky.unspecced.getPostThreadV2.$Params`
*/
export const TREE_VIEW_BELOW_DESKTOP = 6
+15 -11
View File
@@ -1,4 +1,5 @@
import {useCallback, useMemo, useState} from 'react'
import {type AtUriString} from '@atproto/syntax'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -27,10 +28,11 @@ import {
} from '#/state/queries/usePostThread/types'
import {getThreadgateRecord} from '#/state/queries/usePostThread/utils'
import * as views from '#/state/queries/usePostThread/views'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import {useBreakpoints} from '#/alf'
import {IS_WEB} from '#/env'
import {app} from '#/lexicons'
export * from '#/state/queries/usePostThread/context'
export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
@@ -38,7 +40,7 @@ export * from '#/state/queries/usePostThread/types'
export function usePostThread({anchor}: {anchor?: string}) {
const qc = useQueryClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const {hasSession} = useSession()
const {gtPhone} = useBreakpoints()
const moderationOpts = useModerationOpts()
@@ -71,12 +73,15 @@ export function usePostThread({anchor}: {anchor?: string}) {
enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
queryKey: postThreadQueryKey,
async queryFn(ctx) {
const {data} = await agent.app.bsky.unspecced.getPostThreadV2({
anchor: anchor!,
branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF,
below,
sort: sort,
})
const data = await appviewClient.call(
app.bsky.unspecced.getPostThreadV2,
{
anchor: anchor! as AtUriString,
branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF,
below,
sort: sort,
},
)
/*
* Initialize `ctx.meta` to track if we know we have additional replies
@@ -161,10 +166,9 @@ export function usePostThread({anchor}: {anchor?: string}) {
enabled: additionalQueryEnabled,
queryKey: postThreadOtherQueryKey,
async queryFn() {
const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({
anchor: anchor!,
return await appviewClient.call(app.bsky.unspecced.getPostThreadOtherV2, {
anchor: anchor! as AtUriString,
})
return data
},
})
const serverOtherThreadItems: ThreadItem[] = useMemo(() => {
+56 -41
View File
@@ -1,13 +1,6 @@
import {useCallback} from 'react'
import {
type $Typed,
type AppBskyActorDefs,
type AppBskyFeedDefs,
AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadOtherV2,
type AppBskyUnspeccedGetPostThreadV2,
AtUri,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {AtUri} from '@atproto/syntax'
import {type QueryClient, useQueryClient} from '@tanstack/react-query'
import {
@@ -36,6 +29,8 @@ import {
embedViewRecordToPostView,
getEmbeddedPost,
} from '#/state/queries/util'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
export function createCacheMutator({
queryClient,
@@ -51,18 +46,18 @@ export function createCacheMutator({
return {
insertReplies(
parentUri: string,
replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[],
replies: app.bsky.unspecced.getPostThreadV2.ThreadItem[],
) {
/*
* Main thread query mutator.
*/
queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
queryClient.setQueryData<app.bsky.unspecced.getPostThreadV2.$OutputBody>(
postThreadQueryKey,
data => {
if (!data) return
return {
...data,
thread: mutator<AppBskyUnspeccedGetPostThreadV2.ThreadItem>([
thread: mutator<app.bsky.unspecced.getPostThreadV2.ThreadItem>([
...data.thread,
]),
}
@@ -72,15 +67,15 @@ export function createCacheMutator({
/*
* Additional replies query mutator.
*/
queryClient.setQueryData<AppBskyUnspeccedGetPostThreadOtherV2.OutputSchema>(
queryClient.setQueryData<app.bsky.unspecced.getPostThreadOtherV2.$OutputBody>(
postThreadOtherQueryKey,
data => {
if (!data) return
return {
...data,
thread: mutator<AppBskyUnspeccedGetPostThreadOtherV2.ThreadItem>([
...data.thread,
]),
thread: mutator<app.bsky.unspecced.getPostThreadOtherV2.ThreadItem>(
[...data.thread],
),
}
},
)
@@ -89,7 +84,10 @@ export function createCacheMutator({
for (let i = 0; i < thread.length; i++) {
const parent = thread[i]
if (!AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) continue
if (
!bsky.isType(app.bsky.unspecced.defs.threadItemPost, parent.value)
)
continue
if (parent.uri !== parentUri) continue
/*
@@ -124,7 +122,8 @@ export function createCacheMutator({
const isParentRoot = parent.depth === 0
const isParentBelowRoot = parent.depth > 0
const optimisticReply = replies.at(0)
const opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost(
const opIsReplier = bsky.isType(
app.bsky.unspecced.defs.threadItemPost,
optimisticReply?.value,
)
? opDid === optimisticReply.value.post.author.did
@@ -172,8 +171,8 @@ export function createCacheMutator({
* Unused atm, post shadow does the trick, but it would be nice to clean up
* the whole sub-tree on deletes.
*/
deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) {
queryClient.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
deletePost(post: app.bsky.unspecced.getPostThreadV2.ThreadItem) {
queryClient.setQueryData<app.bsky.unspecced.getPostThreadV2.$OutputBody>(
postThreadQueryKey,
queryData => {
if (!queryData) return
@@ -182,7 +181,10 @@ export function createCacheMutator({
for (let i = 0; i < thread.length; i++) {
const existingPost = thread[i]
if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue
if (
!bsky.isType(app.bsky.unspecced.defs.threadItemPost, post.value)
)
continue
if (existingPost.uri === post.uri) {
const branch = getBranch(thread, i, existingPost.depth)
@@ -204,7 +206,7 @@ export function createCacheMutator({
export function getThreadPlaceholder(
queryClient: QueryClient,
uri: string,
): $Typed<AppBskyUnspeccedGetPostThreadV2.ThreadItem> | void {
): $Typed<app.bsky.unspecced.getPostThreadV2.ThreadItem> | void {
let partial
for (let item of getThreadPlaceholderCandidates(queryClient, uri)) {
/*
@@ -231,8 +233,8 @@ export function* getThreadPlaceholderCandidates(
uri: string,
): Generator<
$Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
Omit<app.bsky.unspecced.getPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<app.bsky.unspecced.defs.ThreadItemPost>
}
>,
void
@@ -253,33 +255,37 @@ export function* getThreadPlaceholderCandidates(
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(post)
}
/*
* TODO(phase4): drop toLex once the feed/quote/search/bookmarks/explore
* `findAllPostsInQueryData` generators are migrated to `#/lexicons` types.
*/
for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(post)
yield postViewToThreadPlaceholder(bsky.toLex(post))
}
for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(post)
yield postViewToThreadPlaceholder(bsky.toLex(post))
}
for (let post of findAllPostsInSearchQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(post)
yield postViewToThreadPlaceholder(bsky.toLex(post))
}
for (let post of findAllPostsInBookmarksQueryData(queryClient, uri)) {
yield postViewToThreadPlaceholder(post)
yield postViewToThreadPlaceholder(bsky.toLex(post))
}
for (let post of findAllPostsInExploreFeedPreviewsQueryData(
queryClient,
uri,
)) {
yield postViewToThreadPlaceholder(post)
yield postViewToThreadPlaceholder(bsky.toLex(post))
}
}
export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, void> {
): Generator<app.bsky.feed.defs.PostView, void> {
const atUri = new AtUri(uri)
const queryDatas =
queryClient.getQueriesData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>({
queryClient.getQueriesData<app.bsky.unspecced.getPostThreadV2.$OutputBody>({
queryKey: [postThreadQueryKeyRoot],
})
@@ -289,14 +295,18 @@ export function* findAllPostsInQueryData(
const {thread} = queryData
for (const item of thread) {
if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
if (bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) {
if (didOrHandleUriMatches(atUri, item.value.post)) {
yield item.value.post
}
const qp = getEmbeddedPost(item.value.post.embed)
if (qp && didOrHandleUriMatches(atUri, qp)) {
yield embedViewRecordToPostView(qp)
/*
* TODO(phase4): drop toLex once `embedViewRecordToPostView` in
* `#/state/queries/util` is migrated to `#/lexicons` types.
*/
yield bsky.toLex(embedViewRecordToPostView(qp))
}
}
}
@@ -306,9 +316,9 @@ export function* findAllPostsInQueryData(
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
): Generator<app.bsky.actor.defs.ProfileViewBasic, void> {
const queryDatas =
queryClient.getQueriesData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>({
queryClient.getQueriesData<app.bsky.unspecced.getPostThreadV2.$OutputBody>({
queryKey: [postThreadQueryKeyRoot],
})
@@ -318,14 +328,18 @@ export function* findAllProfilesInQueryData(
const {thread} = queryData
for (const item of thread) {
if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
if (bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) {
if (item.value.post.author.did === did) {
yield item.value.post.author
}
const qp = getEmbeddedPost(item.value.post.embed)
if (qp && qp.author.did === did) {
yield qp.author
/*
* TODO(phase4): drop toLex once `getEmbeddedPost` in
* `#/state/queries/util` is migrated to `#/lexicons` types.
*/
yield bsky.toLex(qp.author)
}
}
}
@@ -337,14 +351,15 @@ export function useUpdatePostThreadThreadgateQueryCache() {
const context = usePostThreadContext()
return useCallback(
(threadgate: AppBskyFeedDefs.ThreadgateView) => {
(threadgate: app.bsky.feed.defs.ThreadgateView) => {
if (!context) return
function mutator<T>(thread: ApiThreadItem[]): T[] {
for (let i = 0; i < thread.length; i++) {
const item = thread[i]
if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) continue
if (!bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value))
continue
if (item.depth === 0) {
thread.splice(i, 1, {
@@ -363,13 +378,13 @@ export function useUpdatePostThreadThreadgateQueryCache() {
return thread as T[]
}
qc.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>(
qc.setQueryData<app.bsky.unspecced.getPostThreadV2.$OutputBody>(
context.postThreadQueryKey,
data => {
if (!data) return
return {
...data,
thread: mutator<AppBskyUnspeccedGetPostThreadV2.ThreadItem>([
thread: mutator<app.bsky.unspecced.getPostThreadV2.ThreadItem>([
...data.thread,
]),
}
+47 -15
View File
@@ -1,4 +1,4 @@
import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api'
import {type ModerationOpts} from '@bsky.app/sdk/moderation'
import {
type ApiThreadItem,
@@ -14,6 +14,8 @@ import {
storeTraversalMetadata,
} from '#/state/queries/usePostThread/utils'
import * as views from '#/state/queries/usePostThread/views'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
export function sortAndAnnotateThreadItems(
thread: ApiThreadItem[],
@@ -45,7 +47,7 @@ export function sortAndAnnotateThreadItems(
let parentMetadata: TraversalMetadata | undefined
let metadata: TraversalMetadata | undefined
if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
if (bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) {
parentMetadata = metadatas.get(
getPostRecord(item.value.post).reply?.parent?.uri || '',
)
@@ -64,13 +66,24 @@ export function sortAndAnnotateThreadItems(
* _up_ from there.
*/
} else if (item.depth === 0) {
if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) {
if (
bsky.isType(
app.bsky.unspecced.defs.threadItemNoUnauthenticated,
item.value,
)
) {
threadItems.push(views.threadPostNoUnauthenticated(item))
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) {
} else if (
bsky.isType(app.bsky.unspecced.defs.threadItemNotFound, item.value)
) {
threadItems.push(views.threadPostNotFound(item))
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) {
} else if (
bsky.isType(app.bsky.unspecced.defs.threadItemBlocked, item.value)
) {
threadItems.push(views.threadPostBlocked(item))
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
} else if (
bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)
) {
const post = views.threadPost({
uri: item.uri,
depth: item.depth,
@@ -84,7 +97,10 @@ export function sortAndAnnotateThreadItems(
const parent = thread[pi]
if (
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value)
bsky.isType(
app.bsky.unspecced.defs.threadItemNoUnauthenticated,
parent.value,
)
) {
const post = views.threadPostNoUnauthenticated(parent)
post.ui = getThreadPostNoUnauthenticatedUI({
@@ -96,13 +112,22 @@ export function sortAndAnnotateThreadItems(
threadItems.unshift(post)
// for now, break parent traversal at first no-unauthed
break parentTraversal
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) {
} else if (
bsky.isType(
app.bsky.unspecced.defs.threadItemNotFound,
parent.value,
)
) {
threadItems.unshift(views.threadPostNotFound(parent))
break parentTraversal
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) {
} else if (
bsky.isType(app.bsky.unspecced.defs.threadItemBlocked, parent.value)
) {
threadItems.unshift(views.threadPostBlocked(parent))
break parentTraversal
} else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) {
} else if (
bsky.isType(app.bsky.unspecced.defs.threadItemPost, parent.value)
) {
threadItems.unshift(
views.threadPost({
uri: parent.uri,
@@ -122,16 +147,21 @@ export function sortAndAnnotateThreadItems(
* we could.
*/
const shouldBreak =
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) ||
AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) ||
AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)
bsky.isType(
app.bsky.unspecced.defs.threadItemNoUnauthenticated,
item.value,
) ||
bsky.isType(app.bsky.unspecced.defs.threadItemNotFound, item.value) ||
bsky.isType(app.bsky.unspecced.defs.threadItemBlocked, item.value)
if (shouldBreak) {
const branch = getBranch(thread, i, item.depth)
// could insert tombstone
i = branch.end
continue traversal
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
} else if (
bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)
) {
if (parentMetadata) {
/*
* Set this value before incrementing the `repliesSeenCounter` later
@@ -179,7 +209,9 @@ export function sortAndAnnotateThreadItems(
for (let ci = startIndex; ci <= branch.end; ci++) {
const child = thread[ci]
if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) {
if (
bsky.isType(app.bsky.unspecced.defs.threadItemPost, child.value)
) {
const childParentMetadata = metadatas.get(
getPostRecord(child.value.post).reply?.parent?.uri || '',
)
+16 -22
View File
@@ -1,16 +1,10 @@
import {
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyFeedThreadgate,
type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadOtherV2,
type AppBskyUnspeccedGetPostThreadV2,
type ModerationDecision,
} from '@atproto/api'
import {type ModerationDecision} from '@bsky.app/sdk/moderation'
import {type app} from '#/lexicons'
export type ApiThreadItem =
| AppBskyUnspeccedGetPostThreadV2.ThreadItem
| AppBskyUnspeccedGetPostThreadOtherV2.ThreadItem
| app.bsky.unspecced.getPostThreadV2.ThreadItem
| app.bsky.unspecced.getPostThreadOtherV2.ThreadItem
export const postThreadQueryKeyRoot = 'post-thread-v2' as const
@@ -18,13 +12,13 @@ export const createPostThreadQueryKey = (props: PostThreadParams) =>
[postThreadQueryKeyRoot, props] as const
export const createPostThreadOtherQueryKey = (
props: Omit<AppBskyUnspeccedGetPostThreadOtherV2.QueryParams, 'anchor'> & {
props: Omit<app.bsky.unspecced.getPostThreadOtherV2.$Params, 'anchor'> & {
anchor?: string
},
) => [postThreadQueryKeyRoot, 'other', props] as const
export type PostThreadParams = Pick<
AppBskyUnspeccedGetPostThreadV2.QueryParams,
app.bsky.unspecced.getPostThreadV2.$Params,
'sort'
> & {
anchor?: string
@@ -33,9 +27,9 @@ export type PostThreadParams = Pick<
export type UsePostThreadQueryResult = {
hasOtherReplies: boolean
thread: AppBskyUnspeccedGetPostThreadV2.ThreadItem[]
threadgate?: Omit<AppBskyFeedDefs.ThreadgateView, 'record'> & {
record: AppBskyFeedThreadgate.Record
thread: app.bsky.unspecced.getPostThreadV2.ThreadItem[]
threadgate?: Omit<app.bsky.feed.defs.ThreadgateView, 'record'> & {
record: app.bsky.feed.threadgate.Main
}
}
@@ -45,9 +39,9 @@ export type ThreadItem =
key: string
uri: string
depth: number
value: Omit<AppBskyUnspeccedDefs.ThreadItemPost, 'post'> & {
post: Omit<AppBskyFeedDefs.PostView, 'record'> & {
record: AppBskyFeedPost.Record
value: Omit<app.bsky.unspecced.defs.ThreadItemPost, 'post'> & {
post: Omit<app.bsky.feed.defs.PostView, 'record'> & {
record: app.bsky.feed.post.Main
}
}
isBlurred: boolean
@@ -67,7 +61,7 @@ export type ThreadItem =
key: string
uri: string
depth: number
value: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated
value: app.bsky.unspecced.defs.ThreadItemNoUnauthenticated
ui: {
showParentReplyLine: boolean
showChildReplyLine: boolean
@@ -78,14 +72,14 @@ export type ThreadItem =
key: string
uri: string
depth: number
value: AppBskyUnspeccedDefs.ThreadItemNotFound
value: app.bsky.unspecced.defs.ThreadItemNotFound
}
| {
type: 'threadPostBlocked'
key: string
uri: string
depth: number
value: AppBskyUnspeccedDefs.ThreadItemBlocked
value: app.bsky.unspecced.defs.ThreadItemBlocked
}
| {
type: 'replyComposer'
+9 -23
View File
@@ -1,38 +1,24 @@
import {
type AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedThreadgate,
AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri,
} from '@atproto/api'
import {AtUri} from '@atproto/syntax'
import {
type ApiThreadItem,
type ThreadItem,
type TraversalMetadata,
} from '#/state/queries/usePostThread/types'
import {app} from '#/lexicons'
import {isDevMode} from '#/storage/hooks/dev-mode'
import * as bsky from '#/types/bsky'
export function getThreadgateRecord(
view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'],
view: app.bsky.unspecced.getPostThreadV2.$OutputBody['threadgate'],
) {
return bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
view?.record,
AppBskyFeedThreadgate.isRecord,
)
return bsky.isType(app.bsky.feed.threadgate, view?.record)
? view?.record
: undefined
}
export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) {
if (
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
) {
export function getRootPostAtUri(post: app.bsky.feed.defs.PostView) {
if (bsky.isType(app.bsky.feed.post, post.record)) {
/**
* If the record has no `reply` field, it is a root post.
*/
@@ -45,8 +31,8 @@ export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) {
}
}
export function getPostRecord(post: AppBskyFeedDefs.PostView) {
return post.record as AppBskyFeedPost.Record
export function getPostRecord(post: app.bsky.feed.defs.PostView) {
return post.record as app.bsky.feed.post.Main
}
export function getTraversalMetadata({
@@ -60,7 +46,7 @@ export function getTraversalMetadata({
nextItem?: ApiThreadItem
parentMetadata?: TraversalMetadata
}): TraversalMetadata {
if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
if (!bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) {
throw new Error(`Expected thread item to be a post`)
}
const repliesCount = item.value.post.replyCount || 0
+13 -19
View File
@@ -1,13 +1,6 @@
import {
type $Typed,
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyUnspeccedDefs,
type AppBskyUnspeccedGetPostThreadV2,
AtUri,
moderatePost,
type ModerationOpts,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {AtUri} from '@atproto/syntax'
import {moderatePost, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {makeProfileLink} from '#/lib/routes/links'
import {
@@ -15,6 +8,7 @@ import {
type ThreadItem,
type TraversalMetadata,
} from '#/state/queries/usePostThread/types'
import {type app} from '#/lexicons'
export function threadPostNoUnauthenticated({
uri,
@@ -26,7 +20,7 @@ export function threadPostNoUnauthenticated({
key: uri,
uri,
depth,
value: value as AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated,
value: value as app.bsky.unspecced.defs.ThreadItemNoUnauthenticated,
// @ts-ignore populated by the traversal
ui: {},
}
@@ -42,7 +36,7 @@ export function threadPostNotFound({
key: uri,
uri,
depth,
value: value as AppBskyUnspeccedDefs.ThreadItemNotFound,
value: value as app.bsky.unspecced.defs.ThreadItemNotFound,
}
}
@@ -56,7 +50,7 @@ export function threadPostBlocked({
key: uri,
uri,
depth,
value: value as AppBskyUnspeccedDefs.ThreadItemBlocked,
value: value as app.bsky.unspecced.defs.ThreadItemBlocked,
}
}
@@ -69,7 +63,7 @@ export function threadPost({
}: {
uri: string
depth: number
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
value: $Typed<app.bsky.unspecced.defs.ThreadItemPost>
moderationOpts: ModerationOpts
threadgateHiddenReplies: Set<string>
}): Extract<ThreadItem, {type: 'threadPost'}> {
@@ -91,8 +85,8 @@ export function threadPost({
* Do not spread anything here, load bearing for post shadow strict
* equality reference checks.
*/
post: value.post as Omit<AppBskyFeedDefs.PostView, 'record'> & {
record: AppBskyFeedPost.Record
post: value.post as Omit<app.bsky.feed.defs.PostView, 'record'> & {
record: app.bsky.feed.post.Main
},
},
isBlurred,
@@ -161,10 +155,10 @@ export function skeleton({
}
export function postViewToThreadPlaceholder(
post: AppBskyFeedDefs.PostView,
post: app.bsky.feed.defs.PostView,
): $Typed<
Omit<AppBskyUnspeccedGetPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
Omit<app.bsky.unspecced.getPostThreadV2.ThreadItem, 'value'> & {
value: $Typed<app.bsky.unspecced.defs.ThreadItemPost>
}
> {
return {

Some files were not shown because too many files have changed in this diff Show More