Skip to content

Commit 79b47bf

Browse files
Fix parentMessage deleted, initial read object & reaction.updated
1 parent 1e6bd63 commit 79b47bf

File tree

3 files changed

+74
-29
lines changed

3 files changed

+74
-29
lines changed

src/thread.ts

+32-20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { StateStore } from './store';
44
import type {
55
AscDesc,
66
DefaultGenerics,
7+
EventTypes,
78
ExtendableGenerics,
89
FormatMessageResponse,
910
MessagePaginationOptions,
@@ -79,6 +80,12 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
7980
});
8081
channel._hydrateMembers(threadData.channel.members ?? []);
8182

83+
// For when read object is undefined and due to that unreadMessageCount for
84+
// the current user isn't being incremented on message.new
85+
const placeholderReadResponse: ReadResponse[] = client.userID
86+
? [{ user: { id: client.userID }, unread_messages: 0, last_read: new Date().toISOString() }]
87+
: [];
88+
8289
this.state = new StateStore<ThreadState<SCG>>({
8390
active: false,
8491
channel,
@@ -89,7 +96,9 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
8996
pagination: repliesPaginationFromInitialThread(threadData),
9097
parentMessage: formatMessage(threadData.parent_message),
9198
participants: threadData.thread_participants,
92-
read: formatReadState(threadData.read ?? []),
99+
read: formatReadState(
100+
!threadData.read || threadData.read.length === 0 ? placeholderReadResponse : threadData.read,
101+
),
93102
replies: threadData.latest_replies.map(formatMessage),
94103
replyCount: threadData.reply_count ?? 0,
95104
updatedAt: threadData.updated_at ? new Date(threadData.updated_at) : null,
@@ -182,7 +191,7 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
182191
this.unsubscribeFunctions.add(this.subscribeMarkThreadStale());
183192
this.unsubscribeFunctions.add(this.subscribeNewReplies());
184193
this.unsubscribeFunctions.add(this.subscribeRepliesRead());
185-
this.unsubscribeFunctions.add(this.subscribeReplyDeleted());
194+
this.unsubscribeFunctions.add(this.subscribeMessageDeleted());
186195
this.unsubscribeFunctions.add(this.subscribeMessageUpdated());
187196
};
188197

@@ -294,20 +303,30 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
294303
}));
295304
}).unsubscribe;
296305

297-
private subscribeReplyDeleted = () =>
306+
private subscribeMessageDeleted = () =>
298307
this.client.on('message.deleted', (event) => {
299-
if (event.message?.parent_id !== this.id) return;
308+
if (!event.message) return;
309+
310+
// Deleted message is a reply of this thread
311+
if (event.message.parent_id === this.id) {
312+
if (event.hard_delete) {
313+
this.deleteReplyLocally({ message: event.message });
314+
} else {
315+
// Handle soft delete (updates deleted_at timestamp)
316+
this.upsertReplyLocally({ message: event.message });
317+
}
318+
}
300319

301-
if (event.hard_delete) {
302-
this.deleteReplyLocally({ message: event.message });
303-
} else {
304-
// Handle soft delete (updates deleted_at timestamp)
305-
this.upsertReplyLocally({ message: event.message });
320+
// Deleted message is parent message of this thread
321+
if (event.message.id === this.id) {
322+
this.updateParentMessageLocally({ message: event.message });
306323
}
307324
}).unsubscribe;
308325

309326
private subscribeMessageUpdated = () => {
310-
const unsubscribeFunctions = ['message.updated', 'reaction.new', 'reaction.deleted'].map(
327+
const eventTypes: EventTypes[] = ['message.updated', 'reaction.new', 'reaction.deleted', 'reaction.updated'];
328+
329+
const unsubscribeFunctions = eventTypes.map(
311330
(eventType) =>
312331
this.client.on(eventType, (event) => {
313332
if (event.message) {
@@ -375,27 +394,20 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
375394
}));
376395
};
377396

378-
public updateParentMessageLocally = (message: MessageResponse<SCG>) => {
397+
public updateParentMessageLocally = ({ message }: { message: MessageResponse<SCG> }) => {
379398
if (message.id !== this.id) {
380399
throw new Error('Message does not belong to this thread');
381400
}
382401

383402
this.state.next((current) => {
384403
const formattedMessage = formatMessage(message);
385404

386-
const newData: typeof current = {
405+
return {
387406
...current,
388407
deletedAt: formattedMessage.deleted_at,
389408
parentMessage: formattedMessage,
390409
replyCount: message.reply_count ?? current.replyCount,
391410
};
392-
393-
// update channel on channelData change (unlikely but handled anyway)
394-
if (message.channel) {
395-
newData['channel'] = this.client.channel(message.channel.type, message.channel.id, message.channel);
396-
}
397-
398-
return newData;
399411
});
400412
};
401413

@@ -405,7 +417,7 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
405417
}
406418

407419
if (!message.parent_id && message.id === this.id) {
408-
this.updateParentMessageLocally(message);
420+
this.updateParentMessageLocally({ message });
409421
}
410422
};
411423

test/unit/test-utils/generateThread.js renamed to test/unit/test-utils/generateThreadResponse.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import { generateUser } from './generateUser';
33

4-
export const generateThread = (channel, parent, opts = {}) => {
4+
export const generateThreadResponse = (channel, parent, opts = {}) => {
55
return {
66
parent_message_id: parent.id,
77
parent_message: parent,

test/unit/threads.test.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
33

44
import { generateChannel } from './test-utils/generateChannel';
55
import { generateMsg } from './test-utils/generateMessage';
6-
import { generateThread } from './test-utils/generateThread';
6+
import { generateThreadResponse } from './test-utils/generateThreadResponse';
77

88
import sinon from 'sinon';
99
import {
@@ -35,7 +35,7 @@ describe('Threads 2.0', () => {
3535
} = {}) {
3636
return new Thread({
3737
client,
38-
threadData: generateThread(
38+
threadData: generateThreadResponse(
3939
{ ...channelResponse, ...channelOverrides },
4040
{ ...parentMessageResponse, ...parentMessageOverrides },
4141
overrides,
@@ -54,7 +54,12 @@ describe('Threads 2.0', () => {
5454

5555
describe('Thread', () => {
5656
it('initializes properly', () => {
57-
const thread = new Thread({ client, threadData: generateThread(channelResponse, parentMessageResponse) });
57+
const threadResponse = generateThreadResponse(channelResponse, parentMessageResponse);
58+
const thread = new Thread({ client, threadData: threadResponse });
59+
const state = thread.state.getLatestValue();
60+
61+
expect(threadResponse.read).to.have.lengthOf(0);
62+
expect(state.read).to.have.keys([TEST_USER_ID]);
5863

5964
expect(thread.id).to.equal(parentMessageResponse.id);
6065
expect(thread.channel.data?.name).to.equal(channelResponse.name);
@@ -134,7 +139,7 @@ describe('Threads 2.0', () => {
134139
it('prevents updating a parent message if the ids do not match', () => {
135140
const thread = createTestThread();
136141
const message = generateMsg() as MessageResponse;
137-
expect(() => thread.updateParentMessageLocally(message)).to.throw();
142+
expect(() => thread.updateParentMessageLocally({ message })).to.throw();
138143
});
139144

140145
it('updates parent message and related top-level properties', () => {
@@ -152,7 +157,7 @@ describe('Threads 2.0', () => {
152157
deleted_at: new Date().toISOString(),
153158
}) as MessageResponse;
154159

155-
thread.updateParentMessageLocally(updatedMessage);
160+
thread.updateParentMessageLocally({ message: updatedMessage });
156161

157162
const stateAfter = thread.state.getLatestValue();
158163
expect(stateAfter.deletedAt).to.be.not.null;
@@ -603,7 +608,7 @@ describe('Threads 2.0', () => {
603608
client.dispatchEvent({
604609
type: 'message.read',
605610
user: { id: 'bob' },
606-
thread: generateThread(channelResponse, generateMsg()) as ThreadResponse,
611+
thread: generateThreadResponse(channelResponse, generateMsg()) as ThreadResponse,
607612
});
608613

609614
const stateAfter = thread.state.getLatestValue();
@@ -631,7 +636,10 @@ describe('Threads 2.0', () => {
631636
client.dispatchEvent({
632637
type: 'message.read',
633638
user: { id: 'bob' },
634-
thread: generateThread(channelResponse, generateMsg({ id: parentMessageResponse.id })) as ThreadResponse,
639+
thread: generateThreadResponse(
640+
channelResponse,
641+
generateMsg({ id: parentMessageResponse.id }),
642+
) as ThreadResponse,
635643
created_at: createdAt.toISOString(),
636644
});
637645

@@ -858,10 +866,35 @@ describe('Threads 2.0', () => {
858866

859867
thread.unregisterSubscriptions();
860868
});
869+
870+
it('handles deletion of the thread (updates deleted_at and parentMessage properties)', () => {
871+
const thread = createTestThread();
872+
thread.registerSubscriptions();
873+
874+
const stateBefore = thread.state.getLatestValue();
875+
876+
const parentMessage = generateMsg({
877+
id: thread.id,
878+
deleted_at: new Date().toISOString(),
879+
type: 'deleted',
880+
}) as MessageResponse;
881+
882+
expect(thread.id).to.equal(parentMessage.id);
883+
expect(stateBefore.deletedAt).to.be.null;
884+
885+
client.dispatchEvent({ type: 'message.deleted', message: parentMessage });
886+
887+
const stateAfter = thread.state.getLatestValue();
888+
889+
expect(stateAfter.deletedAt).to.be.a('date');
890+
expect(stateAfter.deletedAt!.toISOString()).to.equal(parentMessage.deleted_at);
891+
expect(stateAfter.parentMessage.deleted_at).to.be.a('date');
892+
expect(stateAfter.parentMessage.deleted_at!.toISOString()).to.equal(parentMessage.deleted_at);
893+
});
861894
});
862895

863896
describe('Events: message.updated, reaction.new, reaction.deleted', () => {
864-
(['message.updated', 'reaction.new', 'reaction.deleted'] as const).forEach((eventType) => {
897+
(['message.updated', 'reaction.new', 'reaction.deleted', 'reaction.updated'] as const).forEach((eventType) => {
865898
it(`updates reply or parent message on "${eventType}"`, () => {
866899
const thread = createTestThread();
867900
const updateParentMessageOrReplyLocallySpy = sinon.spy(thread, 'updateParentMessageOrReplyLocally');

0 commit comments

Comments
 (0)