Message replies in chat (#10903)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,22 @@ function toSystemMessageView(
|
||||
return ev.message
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a deleted-message tombstone from a (now-deleted) message, preserving
|
||||
* the fields the deleted view carries so a reply can render it as deleted.
|
||||
*/
|
||||
function toDeletedMessageView(
|
||||
m: ChatBskyConvoDefs.MessageView,
|
||||
): $Typed<ChatBskyConvoDefs.DeletedMessageView> {
|
||||
return {
|
||||
$type: 'chat.bsky.convo.defs#deletedMessageView',
|
||||
id: m.id,
|
||||
rev: m.rev,
|
||||
sender: m.sender,
|
||||
sentAt: m.sentAt,
|
||||
}
|
||||
}
|
||||
|
||||
export class Convo {
|
||||
private id: string
|
||||
|
||||
@@ -119,6 +135,7 @@ export class Convo {
|
||||
optimisticEmbedView?:
|
||||
| $Typed<AppBskyEmbedRecord.View>
|
||||
| $Typed<ChatBskyEmbedJoinLink.View>
|
||||
optimisticReplyTo?: $Typed<ChatBskyConvoDefs.MessageView>
|
||||
}
|
||||
> = new Map()
|
||||
private deletedMessages: Set<string> = new Set()
|
||||
@@ -802,6 +819,12 @@ export class Convo {
|
||||
})
|
||||
const {cursor, messages, relatedProfiles} = response.data
|
||||
|
||||
// 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
|
||||
// from the response, so a full page containing a deleted message (e.g.
|
||||
// from a deleted account) comes back short *with* a valid cursor. Using a
|
||||
// count heuristic here would stop history fetching early and hide
|
||||
// messages. The tradeoff is one extra empty fetch at the true top.
|
||||
this.oldestRev = cursor ?? null
|
||||
|
||||
if (relatedProfiles) {
|
||||
@@ -811,14 +834,6 @@ export class Convo {
|
||||
this.applyProfileShadows()
|
||||
}
|
||||
|
||||
/*
|
||||
* If the response contained fewer messages than the limit, we know
|
||||
* there are no more pages, regardless of whether a cursor was returned.
|
||||
*/
|
||||
if (messages.length < (IS_NATIVE ? 30 : 60)) {
|
||||
this.oldestRev = null
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
if (
|
||||
ChatBskyConvoDefs.isMessageView(message) ||
|
||||
@@ -965,17 +980,17 @@ export class Convo {
|
||||
ChatBskyConvoDefs.isDeletedMessageView(ev.message)
|
||||
) {
|
||||
/*
|
||||
* Update if we have this in state. If we don't, don't worry about it.
|
||||
* Remove the message itself, and keep its id in `deletedMessages`
|
||||
* so any message that quotes it keeps rendering a deleted-message
|
||||
* tombstone (see `tombstoneDeletedReplyTo`) rather than reverting
|
||||
* to the original hydrated text. We add here rather than relying on
|
||||
* the optimistic entry so deletes from elsewhere (e.g. another
|
||||
* device) are covered too.
|
||||
*/
|
||||
if (
|
||||
this.pastMessages.has(ev.message.id) ||
|
||||
this.newMessages.has(ev.message.id)
|
||||
) {
|
||||
this.pastMessages.delete(ev.message.id)
|
||||
this.newMessages.delete(ev.message.id)
|
||||
this.deletedMessages.delete(ev.message.id)
|
||||
needsCommit = true
|
||||
}
|
||||
this.pastMessages.delete(ev.message.id)
|
||||
this.newMessages.delete(ev.message.id)
|
||||
this.deletedMessages.add(ev.message.id)
|
||||
needsCommit = true
|
||||
} else if (
|
||||
(ChatBskyConvoDefs.isLogAddReaction(ev) ||
|
||||
ChatBskyConvoDefs.isLogRemoveReaction(ev)) &&
|
||||
@@ -1020,6 +1035,7 @@ export class Convo {
|
||||
optimisticEmbedView?:
|
||||
| $Typed<AppBskyEmbedRecord.View>
|
||||
| $Typed<ChatBskyEmbedJoinLink.View>,
|
||||
optimisticReplyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
||||
) {
|
||||
// Ignore empty messages for now since they have no other purpose atm
|
||||
if (!message.text.trim() && !message.embed) return
|
||||
@@ -1033,6 +1049,7 @@ export class Convo {
|
||||
id: tempId,
|
||||
message,
|
||||
optimisticEmbedView,
|
||||
optimisticReplyTo,
|
||||
})
|
||||
if (this.convo?.view.status === 'request') {
|
||||
this.updateConvo({
|
||||
@@ -1316,6 +1333,26 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a message is deleted locally, it's removed from the list, but other
|
||||
* messages that reply to it still carry a hydrated `replyTo` with the
|
||||
* original text until the server re-sends them. Swap that `replyTo` for a
|
||||
* deleted-message tombstone so the quote reflects the deletion immediately,
|
||||
* matching what the server returns on refresh.
|
||||
*/
|
||||
private tombstoneDeletedReplyTo(
|
||||
m: ChatBskyConvoDefs.MessageView,
|
||||
): ChatBskyConvoDefs.MessageView {
|
||||
const {replyTo} = m
|
||||
if (
|
||||
!ChatBskyConvoDefs.isMessageView(replyTo) ||
|
||||
!this.deletedMessages.has(replyTo.id)
|
||||
) {
|
||||
return m
|
||||
}
|
||||
return {...m, replyTo: toDeletedMessageView(replyTo)}
|
||||
}
|
||||
|
||||
/*
|
||||
* Items in reverse order, since FlatList inverts
|
||||
*/
|
||||
@@ -1327,7 +1364,7 @@ export class Convo {
|
||||
items.unshift({
|
||||
type: 'message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
message: this.tombstoneDeletedReplyTo(m),
|
||||
})
|
||||
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
|
||||
items.unshift({
|
||||
@@ -1360,7 +1397,7 @@ export class Convo {
|
||||
items.push({
|
||||
type: 'message',
|
||||
key: m.id,
|
||||
message: m,
|
||||
message: this.tombstoneDeletedReplyTo(m),
|
||||
})
|
||||
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
|
||||
items.push({
|
||||
@@ -1378,12 +1415,17 @@ export class Convo {
|
||||
})
|
||||
|
||||
this.pendingMessages.forEach(m => {
|
||||
const optimisticReplyTo =
|
||||
m.optimisticReplyTo && this.deletedMessages.has(m.optimisticReplyTo.id)
|
||||
? toDeletedMessageView(m.optimisticReplyTo)
|
||||
: m.optimisticReplyTo
|
||||
items.push({
|
||||
type: 'pending-message',
|
||||
key: m.id,
|
||||
message: {
|
||||
...m.message,
|
||||
embed: m.optimisticEmbedView,
|
||||
replyTo: optimisticReplyTo,
|
||||
$type: 'chat.bsky.convo.defs#messageView',
|
||||
id: nanoid(),
|
||||
rev: '__fake__',
|
||||
|
||||
@@ -113,6 +113,7 @@ type SendMessage = (
|
||||
| $Typed<AppBskyEmbedRecord.View>
|
||||
| $Typed<ChatBskyEmbedJoinLink.View>
|
||||
| undefined,
|
||||
optimisticReplyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
||||
) => void
|
||||
type FetchMessageHistory = () => Promise<void>
|
||||
type MarkConvoAccepted = () => void
|
||||
|
||||
Reference in New Issue
Block a user