From 1134c365a5f6c7fe31d7d2825afc65bfec073559 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Nov 2026 09:44:44 +0100 Subject: [PATCH 1/2] fix: handle user.updated in threads --- src/thread.ts | 5 +++++ src/thread_manager.ts | 5 ++++- test/unit/threads.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/thread.ts b/src/thread.ts index 2995a943a..eeb8a6792 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -661,6 +661,11 @@ export class Thread extends WithSubscriptions { this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message)); }; + public handleUserUpdated = (event: PipelineEvent) => { + if (!event.user) return; + this.messagePaginator.reflectUserUpdate(event.user); + }; + // Applies a user ban or deletion to this thread's replies. public handleUserMessagesDeleted = (event: PipelineEvent) => { if (!event.user) return; diff --git a/src/thread_manager.ts b/src/thread_manager.ts index 0e220827a..5299ee88d 100644 --- a/src/thread_manager.ts +++ b/src/thread_manager.ts @@ -338,7 +338,7 @@ export class ThreadManager extends WithSubscriptions { ), ), ), - // A banned or deleted user's replies can be in any thread, so these two are the exception. + // A user's replies can be in any thread, so the user events are the exception. ...(['user.messages.deleted', 'user.deleted'] as const).map((eventType) => this.client.on(eventType, (event) => this.threadStore @@ -346,6 +346,9 @@ export class ThreadManager extends WithSubscriptions { .forEach((thread) => thread.handleUserMessagesDeleted(event)), ), ), + this.client.on('user.updated', (event) => + this.threadStore.values().forEach((thread) => thread.handleUserUpdated(event)), + ), ].map(({ unsubscribe }) => unsubscribe); return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe()); diff --git a/test/unit/threads.test.ts b/test/unit/threads.test.ts index 0b80d9a4b..c7fe5de1c 100644 --- a/test/unit/threads.test.ts +++ b/test/unit/threads.test.ts @@ -2375,6 +2375,43 @@ describe('Threads 2.0', () => { }); }); + it('replaces an updated user in the thread paginator\'s replies on "user.updated"', () => { + const thread = createTestThread(); + subscribeThread(thread); + const authorId = 'renamed-user'; + const replyId = uuidv4(); + thread.messagePaginator.ingestPage({ + page: [ + formatMessage( + generateMsg({ + id: replyId, + parent_id: thread.id, + user: { id: authorId, name: 'Old name' }, + }) as MessageResponse, + ), + ], + isHead: true, + isTail: true, + setActive: true, + }); + + client.dispatchEvent({ + type: 'user.updated', + user: { id: authorId, name: 'New name' }, + created_at: nowNs(), + }); + + expect(thread.messagePaginator.getItem(replyId)?.user?.name).to.equal( + 'New name', + ); + expect( + thread.messagePaginator.items?.find((reply) => reply.id === replyId)?.user + ?.name, + ).to.equal('New name'); + + thread.unregisterSubscriptions(); + }); + it('applies "reaction.new" to a reply the thread paginator already holds', () => { const thread = createTestThread(); subscribeThread(thread); From 899d940d1909c83f4fa84b68293d99ae627a3337 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Mon, 2 Nov 2026 15:27:41 +0100 Subject: [PATCH 2/2] fix: address parent message as well if not yet loaded --- src/thread.ts | 11 +++++++++-- test/unit/threads.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/thread.ts b/src/thread.ts index eeb8a6792..249f1ce34 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -662,8 +662,15 @@ export class Thread extends WithSubscriptions { }; public handleUserUpdated = (event: PipelineEvent) => { - if (!event.user) return; - this.messagePaginator.reflectUserUpdate(event.user); + const { user } = event; + if (!user) return; + this.messagePaginator.reflectUserUpdate(user); + + // the parent message might not yet be loaded in channel.messagePaginator + const parent = this.client.messageStore.get(this.id); + if (parent?.user?.id === user.id && parent.user !== user) { + this.client.messageStore.upsert({ ...parent, user }); + } }; // Applies a user ban or deletion to this thread's replies. diff --git a/test/unit/threads.test.ts b/test/unit/threads.test.ts index c7fe5de1c..435fdef2f 100644 --- a/test/unit/threads.test.ts +++ b/test/unit/threads.test.ts @@ -2412,6 +2412,30 @@ describe('Threads 2.0', () => { thread.unregisterSubscriptions(); }); + it('replaces an updated user in the parent message on "user.updated" when no channel paginator holds it', () => { + const authorId = 'renamed-parent-author'; + const thread = createTestThread({ + parentMessageOverrides: { user: { id: authorId, name: 'Old name' } }, + }); + subscribeThread(thread); + expect(channel.messagePaginator.getItem(thread.id)).to.be.undefined; + expect(thread.state.getLatestValue().parentMessage.user?.name).to.equal( + 'Old name', + ); + + client.dispatchEvent({ + type: 'user.updated', + user: { id: authorId, name: 'New name' }, + created_at: nowNs(), + }); + + expect(thread.state.getLatestValue().parentMessage.user?.name).to.equal( + 'New name', + ); + + thread.unregisterSubscriptions(); + }); + it('applies "reaction.new" to a reply the thread paginator already holds', () => { const thread = createTestThread(); subscribeThread(thread);