tombstone replyTo when the quoted message is deleted

A locally-deleted message was removed from the list, but other messages
replying to it kept a fully-hydrated replyTo (original text and all) until the
server re-sent them on refresh. Render a deleted-message tombstone for any
replyTo pointing at a deleted message, and keep confirmed-delete ids in the
deletedMessages set (rather than dropping them on the firehose delete log) so
the quote stays tombstoned instead of snapping back to the original text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-06-16 20:10:50 +03:00
parent 2cac31536f
commit 19cbd85c4c
+53 -13
View File
@@ -86,6 +86,22 @@ function toSystemMessageView(
return ev.message 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 { export class Convo {
private id: string private id: string
@@ -943,17 +959,17 @@ export class Convo {
ChatBskyConvoDefs.isDeletedMessageView(ev.message) 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.delete(ev.message.id)
this.pastMessages.has(ev.message.id) || this.newMessages.delete(ev.message.id)
this.newMessages.has(ev.message.id) this.deletedMessages.add(ev.message.id)
) { needsCommit = true
this.pastMessages.delete(ev.message.id)
this.newMessages.delete(ev.message.id)
this.deletedMessages.delete(ev.message.id)
needsCommit = true
}
} else if ( } else if (
(ChatBskyConvoDefs.isLogAddReaction(ev) || (ChatBskyConvoDefs.isLogAddReaction(ev) ||
ChatBskyConvoDefs.isLogRemoveReaction(ev)) && ChatBskyConvoDefs.isLogRemoveReaction(ev)) &&
@@ -1296,6 +1312,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 * Items in reverse order, since FlatList inverts
*/ */
@@ -1307,7 +1343,7 @@ export class Convo {
items.unshift({ items.unshift({
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: this.tombstoneDeletedReplyTo(m),
}) })
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
items.unshift({ items.unshift({
@@ -1340,7 +1376,7 @@ export class Convo {
items.push({ items.push({
type: 'message', type: 'message',
key: m.id, key: m.id,
message: m, message: this.tombstoneDeletedReplyTo(m),
}) })
} else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) {
items.push({ items.push({
@@ -1358,13 +1394,17 @@ export class Convo {
}) })
this.pendingMessages.forEach(m => { this.pendingMessages.forEach(m => {
const optimisticReplyTo =
m.optimisticReplyTo && this.deletedMessages.has(m.optimisticReplyTo.id)
? toDeletedMessageView(m.optimisticReplyTo)
: m.optimisticReplyTo
items.push({ items.push({
type: 'pending-message', type: 'pending-message',
key: m.id, key: m.id,
message: { message: {
...m.message, ...m.message,
embed: m.optimisticEmbedView, embed: m.optimisticEmbedView,
replyTo: m.optimisticReplyTo, replyTo: optimisticReplyTo,
$type: 'chat.bsky.convo.defs#messageView', $type: 'chat.bsky.convo.defs#messageView',
id: nanoid(), id: nanoid(),
rev: '__fake__', rev: '__fake__',