stream-chat 9.50.3 → 10.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.browser.js +19798 -14472
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +19817 -14482
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +19795 -14469
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/ChannelManager.d.ts +221 -0
- package/dist/types/EventHandlerPipeline.d.ts +104 -0
- package/dist/types/api-client.d.ts +26 -0
- package/dist/types/campaign.d.ts +0 -44
- package/dist/types/channel.d.ts +454 -479
- package/dist/types/channel_batch_updater.d.ts +0 -93
- package/dist/types/channel_state.d.ts +53 -201
- package/dist/types/client.d.ts +451 -1920
- package/dist/types/client_state.d.ts +4 -4
- package/dist/types/configuration/InstanceConfigurationService.d.ts +26 -0
- package/dist/types/configuration/index.d.ts +1 -0
- package/dist/types/configuration/types.d.ts +53 -0
- package/dist/types/connection.d.ts +49 -41
- package/dist/types/connection_fallback.d.ts +6 -4
- package/dist/types/constants.d.ts +0 -4
- package/dist/types/custom_types.d.ts +2 -0
- package/dist/types/entityStore/EntityStore.d.ts +111 -0
- package/dist/types/entityStore/StoreBackedItemIndex.d.ts +60 -0
- package/dist/types/entityStore/applyReactionLocally.d.ts +26 -0
- package/dist/types/entityStore/index.d.ts +2 -0
- package/dist/types/errors.d.ts +10 -2
- package/dist/types/gen/chat/ChannelApi.d.ts +45 -0
- package/dist/types/gen/chat/ChatApi.d.ts +334 -0
- package/dist/types/gen/model-decoders/decoders.d.ts +3 -0
- package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
- package/dist/types/gen/models/index.d.ts +8836 -0
- package/dist/types/gen/moderation/ModerationApi.d.ts +50 -0
- package/dist/types/gen-imports.d.ts +3 -0
- package/dist/types/index.d.ts +8 -7
- package/dist/types/insights.d.ts +9 -8
- package/dist/types/logger.d.ts +9 -0
- package/dist/types/messageComposer/LocationComposer.d.ts +7 -4
- package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -2
- package/dist/types/messageComposer/attachmentManager.d.ts +8 -4
- package/dist/types/messageComposer/configuration/types.d.ts +11 -11
- package/dist/types/messageComposer/linkPreviewsManager.d.ts +25 -13
- package/dist/types/messageComposer/messageComposer.d.ts +10 -10
- package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +3 -3
- package/dist/types/messageComposer/middleware/pollComposer/types.d.ts +4 -7
- package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +3 -3
- package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +7 -7
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +218 -4
- package/dist/types/messageComposer/middleware/textComposer/types.d.ts +9 -4
- package/dist/types/messageComposer/pollComposer.d.ts +5 -4
- package/dist/types/messageComposer/textComposer.d.ts +8 -3
- package/dist/types/messageComposer/types.d.ts +6 -28
- package/dist/types/messageDelivery/MessageDeliveryReporter.d.ts +40 -25
- package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +53 -12
- package/dist/types/messageOperations/MessageOperationStatePolicy.d.ts +19 -0
- package/dist/types/messageOperations/MessageOperations.d.ts +17 -0
- package/dist/types/messageOperations/index.d.ts +3 -0
- package/dist/types/messageOperations/types.d.ts +49 -0
- package/dist/types/moderation.d.ts +21 -203
- package/dist/types/notifications/types.d.ts +8 -7
- package/dist/types/offline-support/offline_support_api.d.ts +111 -87
- package/dist/types/offline-support/offline_sync_manager.d.ts +0 -7
- package/dist/types/offline-support/types.d.ts +19 -23
- package/dist/types/offline-support/util.d.ts +4 -2
- package/dist/types/pagination/FilterBuilder.d.ts +1 -1
- package/dist/types/pagination/ItemIndex.d.ts +25 -0
- package/dist/types/pagination/cursorDerivation/createdAtAroundPaginationFlags.d.ts +9 -0
- package/dist/types/pagination/cursorDerivation/idAroundPaginationFlags.d.ts +6 -0
- package/dist/types/pagination/cursorDerivation/index.d.ts +1 -0
- package/dist/types/pagination/cursorDerivation/linearPaginationFlags.d.ts +6 -0
- package/dist/types/pagination/filterCompiler.d.ts +7 -0
- package/dist/types/pagination/index.d.ts +1 -3
- package/dist/types/pagination/paginators/BasePaginator.d.ts +848 -0
- package/dist/types/pagination/paginators/ChannelPaginator.d.ts +212 -0
- package/dist/types/pagination/paginators/MessageIntervalPaginator.d.ts +282 -0
- package/dist/types/pagination/paginators/MessagePaginator.d.ts +208 -0
- package/dist/types/pagination/paginators/PinnedMessagePaginator.d.ts +39 -0
- package/dist/types/pagination/paginators/ReminderPaginator.d.ts +19 -0
- package/dist/types/pagination/paginators/UserGroupPaginator.d.ts +24 -0
- package/dist/types/pagination/paginators/index.d.ts +7 -0
- package/dist/types/pagination/paginators/stateThrottling.d.ts +2 -0
- package/dist/types/pagination/sortCompiler.d.ts +49 -0
- package/dist/types/pagination/types.normalization.d.ts +11 -0
- package/dist/types/pagination/utility.normalization.d.ts +18 -0
- package/dist/types/pagination/utility.queryChannel.d.ts +26 -0
- package/dist/types/pagination/utility.search.d.ts +13 -0
- package/dist/types/permissions.d.ts +1 -1
- package/dist/types/poll.d.ts +29 -32
- package/dist/types/poll_manager.d.ts +2 -2
- package/dist/types/reminders/Reminder.d.ts +2 -5
- package/dist/types/reminders/ReminderManager.d.ts +2 -9
- package/dist/types/search/MessageSearchSource.d.ts +4 -4
- package/dist/types/search/UserSearchSource.d.ts +1 -1
- package/dist/types/search/types.d.ts +3 -3
- package/dist/types/segment.d.ts +0 -34
- package/dist/types/signing.d.ts +105 -65
- package/dist/types/store.d.ts +1 -0
- package/dist/types/thread.d.ts +70 -34
- package/dist/types/thread_manager.d.ts +3 -3
- package/dist/types/token_manager.d.ts +15 -10
- package/dist/types/types.d.ts +197 -3404
- package/dist/types/utils/FixedSizeQueueCache.d.ts +12 -6
- package/dist/types/utils/WithSubscriptions.d.ts +2 -1
- package/dist/types/utils/concurrency.d.ts +4 -4
- package/dist/types/utils/mergeWith/mergeWith.d.ts +3 -3
- package/dist/types/utils/mergeWith/mergeWithCore.d.ts +8 -3
- package/dist/types/utils/retryable.d.ts +34 -0
- package/dist/types/utils/throttling/throttle.d.ts +36 -0
- package/dist/types/utils.d.ts +100 -194
- package/package.json +7 -3
- package/src/ChannelManager.ts +724 -0
- package/src/CooldownTimer.ts +2 -2
- package/src/EventHandlerPipeline.ts +228 -0
- package/src/LiveLocationManager.ts +12 -11
- package/src/api-client.ts +275 -0
- package/src/campaign.ts +1 -77
- package/src/channel.ts +1344 -1282
- package/src/channel_batch_updater.ts +1 -211
- package/src/channel_state.ts +144 -1013
- package/src/client.ts +936 -3995
- package/src/client_state.ts +4 -4
- package/src/configuration/InstanceConfigurationService.ts +73 -0
- package/src/configuration/index.ts +1 -0
- package/src/configuration/types.ts +81 -0
- package/src/connection.ts +145 -94
- package/src/connection_fallback.ts +29 -27
- package/src/constants.ts +1 -5
- package/src/custom_types.ts +4 -1
- package/src/entityStore/EntityStore.ts +208 -0
- package/src/entityStore/StoreBackedItemIndex.ts +115 -0
- package/src/entityStore/applyReactionLocally.ts +113 -0
- package/src/entityStore/index.ts +2 -0
- package/src/errors.ts +14 -2
- package/src/gen/chat/ChannelApi.ts +275 -0
- package/src/gen/chat/ChatApi.ts +2554 -0
- package/src/gen/model-decoders/decoders.ts +2680 -0
- package/src/gen/model-decoders/event-decoder-mapping.ts +198 -0
- package/src/gen/models/index.ts +12673 -0
- package/src/gen/moderation/ModerationApi.ts +607 -0
- package/src/gen-imports.ts +3 -0
- package/src/index.ts +11 -12
- package/src/insights.ts +6 -5
- package/src/logger.ts +28 -0
- package/src/messageComposer/LocationComposer.ts +9 -5
- package/src/messageComposer/MessageComposerEffectHandlers.ts +1 -0
- package/src/messageComposer/attachmentIdentity.ts +16 -9
- package/src/messageComposer/attachmentManager.ts +13 -8
- package/src/messageComposer/configuration/types.ts +11 -11
- package/src/messageComposer/linkPreviewsManager.ts +9 -10
- package/src/messageComposer/messageComposer.ts +57 -53
- package/src/messageComposer/middleware/messageComposer/attachments.ts +1 -2
- package/src/messageComposer/middleware/messageComposer/cleanData.ts +1 -1
- package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +2 -2
- package/src/messageComposer/middleware/messageComposer/messageComposerState.ts +2 -2
- package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +4 -3
- package/src/messageComposer/middleware/messageComposer/textComposer.ts +2 -2
- package/src/messageComposer/middleware/messageComposer/types.ts +3 -4
- package/src/messageComposer/middleware/messageComposer/userDataInjection.ts +9 -5
- package/src/messageComposer/middleware/pollComposer/state.ts +0 -2
- package/src/messageComposer/middleware/pollComposer/types.ts +4 -7
- package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +10 -1
- package/src/messageComposer/middleware/textComposer/commandEffects.ts +4 -4
- package/src/messageComposer/middleware/textComposer/commandUtils.ts +3 -6
- package/src/messageComposer/middleware/textComposer/commands.ts +3 -3
- package/src/messageComposer/middleware/textComposer/mentionUtils.ts +1 -1
- package/src/messageComposer/middleware/textComposer/mentions.ts +31 -15
- package/src/messageComposer/middleware/textComposer/types.ts +9 -4
- package/src/messageComposer/pollComposer.ts +6 -8
- package/src/messageComposer/textComposer.ts +27 -2
- package/src/messageComposer/types.ts +6 -28
- package/src/messageDelivery/MessageDeliveryReporter.ts +101 -45
- package/src/messageDelivery/MessageReceiptsTracker.ts +294 -21
- package/src/messageOperations/MessageOperationStatePolicy.ts +77 -0
- package/src/messageOperations/MessageOperations.ts +212 -0
- package/src/messageOperations/index.ts +10 -0
- package/src/messageOperations/types.ts +64 -0
- package/src/moderation.ts +38 -431
- package/src/notifications/types.ts +8 -7
- package/src/offline-support/offline_support_api.ts +200 -133
- package/src/offline-support/offline_sync_manager.ts +23 -44
- package/src/offline-support/types.ts +23 -26
- package/src/offline-support/util.ts +5 -4
- package/src/pagination/FilterBuilder.ts +4 -1
- package/src/pagination/ItemIndex.ts +25 -0
- package/src/pagination/cursorDerivation/createdAtAroundPaginationFlags.ts +73 -0
- package/src/pagination/cursorDerivation/idAroundPaginationFlags.ts +53 -0
- package/src/pagination/cursorDerivation/index.ts +1 -0
- package/src/pagination/cursorDerivation/linearPaginationFlags.ts +86 -0
- package/src/pagination/filterCompiler.ts +196 -0
- package/src/pagination/index.ts +1 -3
- package/src/pagination/paginators/BasePaginator.ts +2859 -0
- package/src/pagination/paginators/ChannelPaginator.ts +729 -0
- package/src/pagination/paginators/MessageIntervalPaginator.ts +1116 -0
- package/src/pagination/paginators/MessagePaginator.ts +508 -0
- package/src/pagination/paginators/PinnedMessagePaginator.ts +108 -0
- package/src/pagination/paginators/ReminderPaginator.ts +107 -0
- package/src/pagination/paginators/UserGroupPaginator.ts +127 -0
- package/src/pagination/paginators/index.ts +7 -0
- package/src/pagination/paginators/stateThrottling.ts +31 -0
- package/src/pagination/sortCompiler.ts +193 -0
- package/src/pagination/types.normalization.ts +14 -0
- package/src/pagination/utility.normalization.ts +106 -0
- package/src/pagination/utility.queryChannel.ts +84 -0
- package/src/pagination/utility.search.ts +80 -0
- package/src/permissions.ts +7 -8
- package/src/poll.ts +101 -110
- package/src/poll_manager.ts +14 -8
- package/src/reminders/Reminder.ts +2 -5
- package/src/reminders/ReminderManager.ts +19 -21
- package/src/search/BaseSearchSource.ts +0 -3
- package/src/search/ChannelMemberSearchSource.ts +7 -1
- package/src/search/ChannelSearchSource.ts +10 -3
- package/src/search/MessageSearchSource.ts +29 -30
- package/src/search/UserSearchSource.ts +12 -8
- package/src/search/types.ts +3 -4
- package/src/segment.ts +1 -95
- package/src/signing.ts +107 -67
- package/src/store.ts +2 -1
- package/src/thread.ts +562 -238
- package/src/thread_manager.ts +37 -16
- package/src/token_manager.ts +21 -10
- package/src/types.ts +364 -4533
- package/src/uploadManager.ts +8 -0
- package/src/utils/FixedSizeQueueCache.ts +12 -6
- package/src/utils/WithSubscriptions.ts +2 -1
- package/src/utils/concurrency.ts +4 -4
- package/src/utils/mergeWith/mergeWith.ts +3 -3
- package/src/utils/mergeWith/mergeWithCore.ts +140 -164
- package/src/utils/mergeWith/mergeWithDiff.ts +3 -3
- package/src/utils/retryable.ts +117 -0
- package/src/utils/throttling/throttle.ts +130 -0
- package/src/utils.ts +208 -754
- package/dist/types/channel_manager.d.ts +0 -122
- package/dist/types/events.d.ts +0 -72
- package/dist/types/pagination/BasePaginator.d.ts +0 -69
- package/dist/types/pagination/ReminderPaginator.d.ts +0 -16
- package/dist/types/pagination/UserGroupPaginator.d.ts +0 -21
- package/src/channel_manager.ts +0 -830
- package/src/events.ts +0 -75
- package/src/pagination/BasePaginator.ts +0 -184
- package/src/pagination/ReminderPaginator.ts +0 -56
- package/src/pagination/UserGroupPaginator.ts +0 -93
package/src/thread.ts
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
import { StateStore } from './store';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
findIndexInSortedArray,
|
|
3
|
+
computeOwnReactions,
|
|
5
4
|
formatMessage,
|
|
6
|
-
|
|
5
|
+
localMessageToNewMessagePayload,
|
|
7
6
|
} from './utils';
|
|
7
|
+
import { applyReactionLocally } from './entityStore';
|
|
8
8
|
import type {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
DraftResponse,
|
|
10
|
+
EventAPIResponse,
|
|
11
|
+
EventType,
|
|
11
12
|
LocalMessage,
|
|
12
|
-
|
|
13
|
+
MarkReadRequest,
|
|
13
14
|
MessageResponse,
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
ReactionRequest,
|
|
16
|
+
ReadStateResponse,
|
|
17
|
+
SendReactionRequest,
|
|
18
|
+
SortParamRequest,
|
|
19
|
+
ThreadStateResponse,
|
|
16
20
|
UserResponse,
|
|
17
21
|
} from './types';
|
|
18
|
-
import
|
|
22
|
+
import { isEphemeral } from './errors';
|
|
23
|
+
import type {
|
|
24
|
+
Channel,
|
|
25
|
+
DeleteMessageWithStateUpdateParams,
|
|
26
|
+
SendMessageWithStateUpdateParams,
|
|
27
|
+
UpdateMessageWithStateUpdateParams,
|
|
28
|
+
} from './channel';
|
|
19
29
|
import type { StreamChat } from './client';
|
|
20
30
|
import type { CustomThreadData } from './custom_types';
|
|
21
31
|
import { MessageComposer } from './messageComposer';
|
|
32
|
+
import { MessageOperations } from './messageOperations';
|
|
22
33
|
import { WithSubscriptions } from './utils/WithSubscriptions';
|
|
23
|
-
|
|
24
|
-
type
|
|
25
|
-
sort?: { created_at: AscDesc }[];
|
|
26
|
-
} & MessagePaginationOptions & { user?: UserResponse; user_id?: string };
|
|
34
|
+
import { MessagePaginator } from './pagination';
|
|
35
|
+
import type { PipelineEvent } from './EventHandlerPipeline';
|
|
27
36
|
|
|
28
37
|
export type ThreadState = {
|
|
29
38
|
/**
|
|
@@ -37,27 +46,18 @@ export type ThreadState = {
|
|
|
37
46
|
deletedAt: Date | null;
|
|
38
47
|
isLoading: boolean;
|
|
39
48
|
isStateStale: boolean;
|
|
40
|
-
pagination: ThreadRepliesPagination;
|
|
41
49
|
/**
|
|
42
50
|
* Thread is identified by and has a one-to-one relation with its parent message.
|
|
43
51
|
* We use parent message id as a thread id.
|
|
44
52
|
*/
|
|
45
53
|
parentMessage: LocalMessage;
|
|
46
|
-
participants:
|
|
54
|
+
participants: ThreadStateResponse['thread_participants'];
|
|
47
55
|
read: ThreadReadState;
|
|
48
|
-
replies: Array<LocalMessage>;
|
|
49
56
|
replyCount: number;
|
|
50
57
|
title: string;
|
|
51
58
|
updatedAt: Date | null;
|
|
52
59
|
};
|
|
53
60
|
|
|
54
|
-
export type ThreadRepliesPagination = {
|
|
55
|
-
isLoadingNext: boolean;
|
|
56
|
-
isLoadingPrev: boolean;
|
|
57
|
-
nextCursor: string | null;
|
|
58
|
-
prevCursor: string | null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
61
|
export type ThreadUserReadState = {
|
|
62
62
|
lastReadAt: Date;
|
|
63
63
|
unreadMessageCount: number;
|
|
@@ -69,51 +69,27 @@ export type ThreadUserReadState = {
|
|
|
69
69
|
export type ThreadReadState = Record<string, ThreadUserReadState | undefined>;
|
|
70
70
|
|
|
71
71
|
const DEFAULT_PAGE_LIMIT = 50;
|
|
72
|
-
const DEFAULT_SORT:
|
|
73
|
-
const
|
|
74
|
-
// TODO: remove this once we move to API v2
|
|
75
|
-
export const THREAD_RESPONSE_RESERVED_KEYS: Record<keyof ThreadResponse, true> = {
|
|
76
|
-
active_participant_count: true,
|
|
77
|
-
channel: true,
|
|
78
|
-
channel_cid: true,
|
|
79
|
-
created_at: true,
|
|
80
|
-
created_by: true,
|
|
81
|
-
created_by_user_id: true,
|
|
82
|
-
deleted_at: true,
|
|
83
|
-
draft: true,
|
|
84
|
-
last_message_at: true,
|
|
85
|
-
latest_replies: true,
|
|
86
|
-
parent_message: true,
|
|
87
|
-
parent_message_id: true,
|
|
88
|
-
participant_count: true,
|
|
89
|
-
read: true,
|
|
90
|
-
reply_count: true,
|
|
91
|
-
thread_participants: true,
|
|
92
|
-
title: true,
|
|
93
|
-
updated_at: true,
|
|
94
|
-
};
|
|
72
|
+
const DEFAULT_SORT: SortParamRequest[] = [{ field: 'created_at', direction: -1 }];
|
|
73
|
+
const DEFAULT_ITEM_ORDER: SortParamRequest[] = [{ field: 'created_at', direction: 1 }];
|
|
95
74
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
export type CustomThreadMarkReadRequestFn = (params: {
|
|
76
|
+
thread: Thread;
|
|
77
|
+
options?: MarkReadRequest;
|
|
78
|
+
}) => Promise<EventAPIResponse | null> | void;
|
|
99
79
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const customKey = key as keyof CustomThreadData;
|
|
106
|
-
|
|
107
|
-
custom[customKey] = threadData[customKey];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return custom;
|
|
80
|
+
export type ThreadInstanceConfig = {
|
|
81
|
+
requestHandlers?: {
|
|
82
|
+
markReadRequest?: CustomThreadMarkReadRequestFn;
|
|
83
|
+
};
|
|
111
84
|
};
|
|
112
85
|
|
|
113
86
|
export class Thread extends WithSubscriptions {
|
|
87
|
+
public readonly configState = new StateStore<ThreadInstanceConfig>({});
|
|
114
88
|
public readonly state: StateStore<ThreadState>;
|
|
115
89
|
public readonly id: string;
|
|
116
90
|
public readonly messageComposer: MessageComposer;
|
|
91
|
+
public readonly messagePaginator: MessagePaginator;
|
|
92
|
+
public readonly messageOperations: MessageOperations;
|
|
117
93
|
|
|
118
94
|
private client: StreamChat;
|
|
119
95
|
private failedRepliesMap: Map<string, LocalMessage> = new Map();
|
|
@@ -121,66 +97,203 @@ export class Thread extends WithSubscriptions {
|
|
|
121
97
|
constructor({
|
|
122
98
|
client,
|
|
123
99
|
threadData,
|
|
100
|
+
channel,
|
|
101
|
+
parentMessage,
|
|
102
|
+
draft,
|
|
124
103
|
}: {
|
|
125
104
|
client: StreamChat;
|
|
126
|
-
threadData
|
|
105
|
+
threadData?: ThreadStateResponse;
|
|
106
|
+
channel?: Channel;
|
|
107
|
+
parentMessage?: MessageResponse | LocalMessage;
|
|
108
|
+
draft?: DraftResponse;
|
|
127
109
|
}) {
|
|
128
110
|
super();
|
|
111
|
+
if (threadData) {
|
|
112
|
+
if (!threadData.channel) {
|
|
113
|
+
throw new Error('Thread channel is required when threadData is provided');
|
|
114
|
+
}
|
|
115
|
+
if (!threadData.parent_message) {
|
|
116
|
+
throw new Error('Thread parent_message is required when threadData is provided');
|
|
117
|
+
}
|
|
118
|
+
const threadChannel = client.channel(
|
|
119
|
+
threadData.channel.type,
|
|
120
|
+
threadData.channel.id,
|
|
121
|
+
{ custom: threadData.channel.custom },
|
|
122
|
+
);
|
|
123
|
+
threadChannel._hydrateMembers({
|
|
124
|
+
members: threadData.channel.members ?? [],
|
|
125
|
+
overrideCurrentState: false,
|
|
126
|
+
});
|
|
129
127
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
this.state = new StateStore<ThreadState>({
|
|
129
|
+
// local only
|
|
130
|
+
active: false,
|
|
131
|
+
isLoading: false,
|
|
132
|
+
isStateStale: false,
|
|
133
|
+
// 99.9% should never change
|
|
134
|
+
channel: threadChannel,
|
|
135
|
+
createdAt: new Date(threadData.created_at),
|
|
136
|
+
// rest
|
|
137
|
+
deletedAt: threadData.deleted_at ? new Date(threadData.deleted_at) : null,
|
|
138
|
+
parentMessage: formatMessage(threadData.parent_message),
|
|
139
|
+
participants: threadData.thread_participants,
|
|
140
|
+
read: formatReadState(
|
|
141
|
+
!threadData.read || threadData.read.length === 0
|
|
142
|
+
? getPlaceholderReadResponse(client.userID)
|
|
143
|
+
: threadData.read,
|
|
144
|
+
),
|
|
145
|
+
// Use the parent message's reply_count, not the top-level threadData.reply_count. The
|
|
146
|
+
// thread endpoints (getThread/queryThreads) return a top level reply_count that EXCLUDES
|
|
147
|
+
// soft-deleted replies, while parent_message.reply_count (and the channel's own copy)
|
|
148
|
+
// INCLUDE them so the top level value renders fewer replies than the channel badge shows.
|
|
149
|
+
// parent_message.reply_count is the authoritative, channel consistent count.
|
|
150
|
+
replyCount: threadData.parent_message.reply_count ?? 0,
|
|
151
|
+
updatedAt: threadData.updated_at ? new Date(threadData.updated_at) : null,
|
|
152
|
+
title: threadData.title,
|
|
153
|
+
custom: threadData.custom ?? {},
|
|
154
|
+
});
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
user: { id: client.userID },
|
|
145
|
-
unread_messages: 0,
|
|
146
|
-
last_read: new Date().toISOString(),
|
|
147
|
-
},
|
|
148
|
-
]
|
|
149
|
-
: [];
|
|
156
|
+
this.id = threadData.parent_message_id;
|
|
157
|
+
} else {
|
|
158
|
+
if (!channel) {
|
|
159
|
+
throw new Error('Channel is required when threadData is not provided');
|
|
160
|
+
}
|
|
150
161
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
createdAt
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
162
|
+
if (!parentMessage || !parentMessage.id) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
'Parent message with a valid id is required when threadData is not provided',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const formattedParentMessage = formatMessage(parentMessage);
|
|
169
|
+
const createdAt = parentMessage.created_at
|
|
170
|
+
? new Date(parentMessage.created_at)
|
|
171
|
+
: new Date();
|
|
172
|
+
|
|
173
|
+
this.state = new StateStore<ThreadState>({
|
|
174
|
+
active: false,
|
|
175
|
+
channel,
|
|
176
|
+
createdAt,
|
|
177
|
+
custom: {},
|
|
178
|
+
deletedAt: formattedParentMessage.deleted_at ?? null,
|
|
179
|
+
isLoading: false,
|
|
180
|
+
isStateStale: false,
|
|
181
|
+
parentMessage: formattedParentMessage,
|
|
182
|
+
participants: [],
|
|
183
|
+
read: formatReadState(getPlaceholderReadResponse(client.userID)),
|
|
184
|
+
replyCount: parentMessage.reply_count ?? 0,
|
|
185
|
+
title: '',
|
|
186
|
+
updatedAt: parentMessage.updated_at ? new Date(parentMessage.updated_at) : null,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.id = parentMessage.id;
|
|
190
|
+
}
|
|
175
191
|
|
|
176
|
-
this.id = threadData.parent_message_id;
|
|
177
192
|
this.client = client;
|
|
178
193
|
|
|
194
|
+
this.messagePaginator = new MessagePaginator({
|
|
195
|
+
channel: this.channel,
|
|
196
|
+
parentMessageId: this.id,
|
|
197
|
+
requestSort: DEFAULT_SORT,
|
|
198
|
+
itemOrder: DEFAULT_ITEM_ORDER,
|
|
199
|
+
paginatorOptions: {
|
|
200
|
+
pageSize: DEFAULT_PAGE_LIMIT,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Seed the reply paginator from the thread's `latest_replies` so a thread we already hold
|
|
205
|
+
// data for (queried via the ThreadManager or hydrated from a ThreadResponse) renders its
|
|
206
|
+
// first reply page instantly, without a network fetch on open. `latest_replies` is the most
|
|
207
|
+
// recent window of replies, so it reaches the head (nothing newer to load); it reaches the
|
|
208
|
+
// tail only when it already contains every reply. Threads built from a bare parent message
|
|
209
|
+
// carry no replies to seed and fall back to a paginator fetch on first open.
|
|
210
|
+
if (threadData?.latest_replies?.length) {
|
|
211
|
+
this.messagePaginator.setItems({
|
|
212
|
+
valueOrFactory: threadData.latest_replies.map(formatMessage),
|
|
213
|
+
isFirstPage: true,
|
|
214
|
+
isLastPage: threadData.latest_replies.length === (threadData.reply_count ?? 0),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Seed the reply paginator's lastMessageAt floor from the thread's server-provided
|
|
219
|
+
// `last_message_at` (analogous to the channel seed in Channel._initializeState), so a thread whose
|
|
220
|
+
// newest reply is not among `latest_replies` still reports the correct latest-activity timestamp.
|
|
221
|
+
this.messagePaginator.seedLastMessageAt(threadData?.last_message_at);
|
|
222
|
+
|
|
179
223
|
this.messageComposer = new MessageComposer({
|
|
180
224
|
client,
|
|
181
|
-
composition: threadData
|
|
225
|
+
composition: threadData?.draft ?? draft,
|
|
182
226
|
compositionContext: this,
|
|
183
227
|
});
|
|
228
|
+
|
|
229
|
+
this.messageOperations = new MessageOperations({
|
|
230
|
+
ingest: (m) => {
|
|
231
|
+
this.messagePaginator.ingestItem(m);
|
|
232
|
+
this.channel.getClient().messageStore.flushSubscribers(m.id);
|
|
233
|
+
},
|
|
234
|
+
get: (id) => this.messagePaginator.getItem(id),
|
|
235
|
+
normalizeOutgoingMessage: (m) => ({
|
|
236
|
+
...m,
|
|
237
|
+
parent_id: this.id,
|
|
238
|
+
}),
|
|
239
|
+
handlers: () => {
|
|
240
|
+
const { requestHandlers } = this.channel.configState.getLatestValue();
|
|
241
|
+
const deleteMessageRequest = requestHandlers?.deleteMessageRequest;
|
|
242
|
+
const sendMessageRequest = requestHandlers?.sendMessageRequest;
|
|
243
|
+
const retrySendMessageRequest = requestHandlers?.retrySendMessageRequest;
|
|
244
|
+
const updateMessageRequest = requestHandlers?.updateMessageRequest;
|
|
245
|
+
return {
|
|
246
|
+
delete: deleteMessageRequest
|
|
247
|
+
? (p) =>
|
|
248
|
+
deleteMessageRequest({
|
|
249
|
+
localMessage: p.localMessage,
|
|
250
|
+
options: p.options,
|
|
251
|
+
})
|
|
252
|
+
: undefined,
|
|
253
|
+
send: sendMessageRequest
|
|
254
|
+
? (p) =>
|
|
255
|
+
sendMessageRequest({
|
|
256
|
+
localMessage: p.localMessage,
|
|
257
|
+
message: p.message,
|
|
258
|
+
options: p.options,
|
|
259
|
+
})
|
|
260
|
+
: undefined,
|
|
261
|
+
retry: retrySendMessageRequest
|
|
262
|
+
? (p) =>
|
|
263
|
+
retrySendMessageRequest({
|
|
264
|
+
localMessage: p.localMessage,
|
|
265
|
+
message: p.message,
|
|
266
|
+
options: p.options,
|
|
267
|
+
})
|
|
268
|
+
: undefined,
|
|
269
|
+
update: updateMessageRequest
|
|
270
|
+
? (p) =>
|
|
271
|
+
updateMessageRequest({
|
|
272
|
+
localMessage: p.localMessage,
|
|
273
|
+
options: p.options,
|
|
274
|
+
})
|
|
275
|
+
: undefined,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
defaults: {
|
|
279
|
+
delete: async (id, o) => {
|
|
280
|
+
const result = await this.channel.getClient().deleteMessage({ id, ...o });
|
|
281
|
+
return { message: result.message };
|
|
282
|
+
},
|
|
283
|
+
send: async (m, o) => {
|
|
284
|
+
const result = await this.channel.sendMessage({ message: m, ...o });
|
|
285
|
+
return { message: result.message };
|
|
286
|
+
},
|
|
287
|
+
update: async (m, o) => {
|
|
288
|
+
const result = await this.channel.getClient().updateMessage({
|
|
289
|
+
id: m.id,
|
|
290
|
+
message: localMessageToNewMessagePayload(m),
|
|
291
|
+
...o,
|
|
292
|
+
});
|
|
293
|
+
return { message: result.message };
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
184
297
|
}
|
|
185
298
|
|
|
186
299
|
get channel() {
|
|
@@ -211,7 +324,11 @@ export class Thread extends WithSubscriptions {
|
|
|
211
324
|
this.state.partialNext({ isLoading: true });
|
|
212
325
|
|
|
213
326
|
try {
|
|
214
|
-
const
|
|
327
|
+
const loadedReplyCount = this.messagePaginator.items?.length ?? 0;
|
|
328
|
+
const thread = await this.client.getThreadAndHydrate(this.id, {
|
|
329
|
+
watch: true,
|
|
330
|
+
reply_limit: loadedReplyCount || this.messagePaginator.pageSize,
|
|
331
|
+
});
|
|
215
332
|
this.hydrateState(thread);
|
|
216
333
|
} finally {
|
|
217
334
|
this.state.partialNext({ isLoading: false });
|
|
@@ -239,11 +356,12 @@ export class Thread extends WithSubscriptions {
|
|
|
239
356
|
participants,
|
|
240
357
|
read,
|
|
241
358
|
replyCount,
|
|
242
|
-
replies,
|
|
243
359
|
updatedAt,
|
|
244
360
|
} = thread.state.getLatestValue();
|
|
245
361
|
|
|
246
|
-
// Preserve pending replies
|
|
362
|
+
// Preserve pending (failed) replies so they survive the hydrate. The messagePaginator is now
|
|
363
|
+
// the sole reply source, so we merge the incoming newest page into it and re-ingest the
|
|
364
|
+
// pending replies (mirrors the previous state.replies concat behavior).
|
|
247
365
|
const pendingReplies = Array.from(this.failedRepliesMap.values());
|
|
248
366
|
|
|
249
367
|
this.state.partialNext({
|
|
@@ -255,10 +373,21 @@ export class Thread extends WithSubscriptions {
|
|
|
255
373
|
participants,
|
|
256
374
|
read,
|
|
257
375
|
replyCount,
|
|
258
|
-
replies: pendingReplies.length ? replies.concat(pendingReplies) : replies,
|
|
259
376
|
updatedAt,
|
|
260
377
|
isStateStale: false,
|
|
261
378
|
});
|
|
379
|
+
|
|
380
|
+
if (parentMessage && this.hasSubscriptions) {
|
|
381
|
+
this.client.messageStore.upsert(parentMessage);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.messagePaginator.mergeNewestPage(
|
|
385
|
+
thread.messagePaginator.state.getLatestValue().items ?? [],
|
|
386
|
+
);
|
|
387
|
+
pendingReplies.forEach((reply) => this.messagePaginator.ingestItem(reply));
|
|
388
|
+
// Carry the re-queried thread's last-activity floor so lastMessageAt stays fresh even when the
|
|
389
|
+
// merged page does not include the newest reply. Monotonic, so an older value is a no-op.
|
|
390
|
+
this.messagePaginator.seedLastMessageAt(thread.messagePaginator.lastMessageAt);
|
|
262
391
|
};
|
|
263
392
|
|
|
264
393
|
public registerSubscriptions = () => {
|
|
@@ -267,6 +396,7 @@ export class Thread extends WithSubscriptions {
|
|
|
267
396
|
return;
|
|
268
397
|
}
|
|
269
398
|
|
|
399
|
+
this.addUnsubscribeFunction(this.subscribeParentMessageFromStore());
|
|
270
400
|
this.addUnsubscribeFunction(this.subscribeThreadUpdated());
|
|
271
401
|
this.addUnsubscribeFunction(this.subscribeMarkActiveThreadRead());
|
|
272
402
|
this.addUnsubscribeFunction(this.subscribeReloadActiveStaleThread());
|
|
@@ -276,6 +406,7 @@ export class Thread extends WithSubscriptions {
|
|
|
276
406
|
this.addUnsubscribeFunction(this.subscribeRepliesUnread());
|
|
277
407
|
this.addUnsubscribeFunction(this.subscribeMessageDeleted());
|
|
278
408
|
this.addUnsubscribeFunction(this.subscribeMessageUpdated());
|
|
409
|
+
this.addUnsubscribeFunction(this.subscribeUserMessagesDeleted());
|
|
279
410
|
};
|
|
280
411
|
|
|
281
412
|
private subscribeThreadUpdated = () =>
|
|
@@ -290,8 +421,7 @@ export class Thread extends WithSubscriptions {
|
|
|
290
421
|
title: threadData.title,
|
|
291
422
|
updatedAt: new Date(threadData.updated_at),
|
|
292
423
|
deletedAt: threadData.deleted_at ? new Date(threadData.deleted_at) : null,
|
|
293
|
-
|
|
294
|
-
custom: constructCustomDataObject(threadData),
|
|
424
|
+
custom: threadData.custom ?? {},
|
|
295
425
|
});
|
|
296
426
|
}).unsubscribe;
|
|
297
427
|
|
|
@@ -303,7 +433,7 @@ export class Thread extends WithSubscriptions {
|
|
|
303
433
|
}),
|
|
304
434
|
({ active, unreadMessageCount }) => {
|
|
305
435
|
if (!active || !unreadMessageCount) return;
|
|
306
|
-
this.
|
|
436
|
+
this.throttledMarkRead();
|
|
307
437
|
},
|
|
308
438
|
);
|
|
309
439
|
|
|
@@ -318,7 +448,7 @@ export class Thread extends WithSubscriptions {
|
|
|
318
448
|
);
|
|
319
449
|
|
|
320
450
|
private subscribeMarkThreadStale = () =>
|
|
321
|
-
this.client.on('user.watching.stop', (event) => {
|
|
451
|
+
this.client.on('user.watching.stop', (event: PipelineEvent) => {
|
|
322
452
|
const { channel } = this.state.getLatestValue();
|
|
323
453
|
|
|
324
454
|
if (
|
|
@@ -370,13 +500,13 @@ export class Thread extends WithSubscriptions {
|
|
|
370
500
|
|
|
371
501
|
this.upsertReplyLocally({
|
|
372
502
|
message: event.message,
|
|
373
|
-
//
|
|
503
|
+
// MessageRequest from current user could have been added optimistically,
|
|
374
504
|
// so the actual timestamp might differ in the event
|
|
375
505
|
timestampChanged: isOwnMessage,
|
|
376
506
|
});
|
|
377
507
|
|
|
378
508
|
if (active) {
|
|
379
|
-
this.
|
|
509
|
+
this.throttledMarkRead();
|
|
380
510
|
}
|
|
381
511
|
|
|
382
512
|
const nextRead: ThreadReadState = {};
|
|
@@ -439,6 +569,7 @@ export class Thread extends WithSubscriptions {
|
|
|
439
569
|
private subscribeMessageDeleted = () =>
|
|
440
570
|
this.client.on('message.deleted', (event) => {
|
|
441
571
|
if (!event.message) return;
|
|
572
|
+
const formattedMessage = formatMessage(event.message);
|
|
442
573
|
|
|
443
574
|
// Deleted message is a reply of this thread
|
|
444
575
|
if (event.message.parent_id === this.id) {
|
|
@@ -454,62 +585,161 @@ export class Thread extends WithSubscriptions {
|
|
|
454
585
|
if (event.message.id === this.id) {
|
|
455
586
|
this.updateParentMessageLocally({ message: event.message });
|
|
456
587
|
}
|
|
588
|
+
|
|
589
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formattedMessage);
|
|
457
590
|
}).unsubscribe;
|
|
458
591
|
|
|
459
592
|
private subscribeMessageUpdated = () => {
|
|
460
|
-
const
|
|
461
|
-
|
|
593
|
+
const messageUpdateTypes: EventType[] = ['message.updated', 'message.undeleted'];
|
|
594
|
+
const reactionTypes: EventType[] = [
|
|
462
595
|
'reaction.new',
|
|
463
596
|
'reaction.deleted',
|
|
464
597
|
'reaction.updated',
|
|
465
598
|
];
|
|
466
599
|
|
|
467
|
-
const
|
|
600
|
+
const unsubscribeMessageUpdated = messageUpdateTypes.map(
|
|
601
|
+
(eventType) =>
|
|
602
|
+
this.client.on(eventType, (event: PipelineEvent) => {
|
|
603
|
+
if (!event.message) return;
|
|
604
|
+
// A `message.updated` WS event carries `own_reactions: []`; upserting it verbatim would
|
|
605
|
+
// wipe the current user's reactions on an edit. Preserve them off the copy we already hold
|
|
606
|
+
// — the reply paginator for a reply, `state.parentMessage` for the parent (the parent is
|
|
607
|
+
// not held in any paginator, so it needs the same treatment directly).
|
|
608
|
+
const message =
|
|
609
|
+
event.message.parent_id === this.id
|
|
610
|
+
? {
|
|
611
|
+
...event.message,
|
|
612
|
+
own_reactions:
|
|
613
|
+
this.messagePaginator.getItem(event.message.id)?.own_reactions ??
|
|
614
|
+
event.message.own_reactions,
|
|
615
|
+
}
|
|
616
|
+
: !event.message.parent_id && event.message.id === this.id
|
|
617
|
+
? {
|
|
618
|
+
...event.message,
|
|
619
|
+
own_reactions:
|
|
620
|
+
this.state.getLatestValue().parentMessage?.own_reactions ??
|
|
621
|
+
event.message.own_reactions,
|
|
622
|
+
}
|
|
623
|
+
: event.message;
|
|
624
|
+
this.updateParentMessageOrReplyLocally(message);
|
|
625
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(event.message));
|
|
626
|
+
}).unsubscribe,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const unsubscribeReactions = reactionTypes.map(
|
|
468
630
|
(eventType) =>
|
|
469
|
-
this.client.on(eventType, (event) => {
|
|
470
|
-
if (event.message)
|
|
471
|
-
|
|
631
|
+
this.client.on(eventType, (event: PipelineEvent) => {
|
|
632
|
+
if (!event.message || !event.reaction) return;
|
|
633
|
+
const { message, reaction } = event;
|
|
634
|
+
if (message.parent_id === this.id) {
|
|
635
|
+
// Preserve/apply the current user's `own_reactions` off the reply paginator itself,
|
|
636
|
+
// independently of the channel (mirrors the channel's main-list reflectReaction).
|
|
637
|
+
this.messagePaginator.reflectReaction({
|
|
638
|
+
enforceUnique: eventType === 'reaction.updated',
|
|
639
|
+
message,
|
|
640
|
+
reaction,
|
|
641
|
+
removed: eventType === 'reaction.deleted',
|
|
642
|
+
});
|
|
643
|
+
} else if (!message.parent_id && message.id === this.id) {
|
|
644
|
+
// Reaction on the PARENT. The parent isn't in a paginator, so apply the current user's
|
|
645
|
+
// own_reactions delta here (mirroring the reply path's reflectReaction) rather than
|
|
646
|
+
// copying the WS event verbatim — which would drop own_reactions the event omits.
|
|
647
|
+
const own_reactions = computeOwnReactions({
|
|
648
|
+
current:
|
|
649
|
+
this.state.getLatestValue().parentMessage?.own_reactions ??
|
|
650
|
+
message.own_reactions ??
|
|
651
|
+
[],
|
|
652
|
+
enforceUnique: eventType === 'reaction.updated',
|
|
653
|
+
reaction,
|
|
654
|
+
removed: eventType === 'reaction.deleted',
|
|
655
|
+
userId: this.client.userID,
|
|
656
|
+
});
|
|
657
|
+
this.updateParentMessageLocally({ message: { ...message, own_reactions } });
|
|
472
658
|
}
|
|
659
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message));
|
|
473
660
|
}).unsubscribe,
|
|
474
661
|
);
|
|
475
662
|
|
|
663
|
+
const unsubscribeFunctions = [...unsubscribeMessageUpdated, ...unsubscribeReactions];
|
|
664
|
+
|
|
476
665
|
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
477
666
|
};
|
|
478
667
|
|
|
668
|
+
private subscribeUserMessagesDeleted = () => {
|
|
669
|
+
// Apply a user ban / deletion to this thread's own reply list. Previously
|
|
670
|
+
// channel.state.deleteUserMessages marked banned-user replies deleted in the (now removed)
|
|
671
|
+
// channel.state.threads shadow; the reply paginator is the thread's source of truth now.
|
|
672
|
+
const eventTypes: EventType[] = ['user.messages.deleted', 'user.deleted'];
|
|
673
|
+
|
|
674
|
+
const unsubscribeFunctions = eventTypes.map(
|
|
675
|
+
(eventType) =>
|
|
676
|
+
this.client.on(eventType, (event: PipelineEvent) => {
|
|
677
|
+
if (!event.user) return;
|
|
678
|
+
// user.deleted carries the deletion time on the user; user.messages.deleted on the event.
|
|
679
|
+
const deletedAtSource =
|
|
680
|
+
eventType === 'user.deleted' ? event.user.deleted_at : event.created_at;
|
|
681
|
+
this.messagePaginator.applyMessageDeletionForUser({
|
|
682
|
+
userId: event.user.id,
|
|
683
|
+
hardDelete: !!event.hard_delete,
|
|
684
|
+
deletedAt: deletedAtSource ? new Date(deletedAtSource) : new Date(),
|
|
685
|
+
});
|
|
686
|
+
}).unsubscribe,
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// The parent message lives in the client-global message store (one canonical POJO per id);
|
|
693
|
+
// `state.parentMessage` and the fields derived from it are a projection of that copy. Every update
|
|
694
|
+
// to the parent — optimistic reaction, WS reaction/edit/delete, reply-count bump — writes the
|
|
695
|
+
// store, which reflects it here through this single subscription (one per thread, not one per
|
|
696
|
+
// message). Seeds the store on first subscribe when no other collection holds the parent yet
|
|
697
|
+
// (e.g. a thread opened from a notification, its parent not in the channel window).
|
|
698
|
+
private subscribeParentMessageFromStore = () => {
|
|
699
|
+
const store = this.client.messageStore;
|
|
700
|
+
const parent = this.state.getLatestValue().parentMessage;
|
|
701
|
+
if (parent && !store.has(parent.id)) store.upsert(parent);
|
|
702
|
+
|
|
703
|
+
return store.subscribe(this.id, (message) => {
|
|
704
|
+
if (!message) return;
|
|
705
|
+
this.state.next((current) => ({
|
|
706
|
+
...current,
|
|
707
|
+
deletedAt: message.deleted_at ?? null,
|
|
708
|
+
parentMessage: message,
|
|
709
|
+
participants:
|
|
710
|
+
normalizeThreadParticipants(message.thread_participants, current.channel.cid) ??
|
|
711
|
+
current.participants,
|
|
712
|
+
replyCount: message.reply_count ?? current.replyCount,
|
|
713
|
+
}));
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
|
|
479
717
|
public unregisterSubscriptions = () => {
|
|
480
718
|
const symbol = super.unregisterSubscriptions();
|
|
719
|
+
// Release the reply paginator's hold on the shared message store. The parent subscription is
|
|
720
|
+
// torn down by `super.unregisterSubscriptions()` (it was added as an unsubscribe function), but
|
|
721
|
+
// the reply paginator's per-id links live in its item index, not in the subscription list — so a
|
|
722
|
+
// removed thread would otherwise stay pinned by `messageStore.subscribers` and keep its replies
|
|
723
|
+
// alive. `getThread` builds a fresh instance if this thread is re-opened, so this is a discard.
|
|
724
|
+
this.messagePaginator.dispose();
|
|
481
725
|
this.state.partialNext({ isStateStale: true });
|
|
482
726
|
return symbol;
|
|
483
727
|
};
|
|
484
728
|
|
|
729
|
+
// todo: can be removed with the next breaking change and use MessagePaginator only
|
|
485
730
|
public deleteReplyLocally = ({ message }: { message: MessageResponse }) => {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
needle: formatMessage(message),
|
|
490
|
-
sortedArray: replies,
|
|
491
|
-
sortDirection: 'ascending',
|
|
492
|
-
selectValueToCompare: (reply) => reply.created_at.getTime(),
|
|
493
|
-
selectKey: (reply) => reply.id,
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
if (replies[index]?.id !== message.id) {
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const updatedReplies = [...replies];
|
|
501
|
-
updatedReplies.splice(index, 1);
|
|
502
|
-
|
|
503
|
-
this.state.partialNext({
|
|
504
|
-
replies: updatedReplies,
|
|
505
|
-
});
|
|
731
|
+
// The reply messagePaginator is the reply list source. removeItem is a no-op when the reply
|
|
732
|
+
// isn't loaded, so it's safe to run unconditionally.
|
|
733
|
+
this.messagePaginator.removeItem({ id: message.id });
|
|
506
734
|
};
|
|
507
735
|
|
|
736
|
+
// todo: can be removed with the next breaking change and use MessagePaginator only
|
|
508
737
|
public upsertReplyLocally = ({
|
|
509
738
|
message,
|
|
510
|
-
timestampChanged = false,
|
|
511
739
|
}: {
|
|
512
740
|
message: MessageResponse | LocalMessage;
|
|
741
|
+
// Accepted for backward compatibility but no longer used — the messagePaginator repositions
|
|
742
|
+
// by created_at on ingest, so a changed timestamp is handled without an explicit flag.
|
|
513
743
|
timestampChanged?: boolean;
|
|
514
744
|
}) => {
|
|
515
745
|
if (message.parent_id !== this.id) {
|
|
@@ -518,36 +748,33 @@ export class Thread extends WithSubscriptions {
|
|
|
518
748
|
|
|
519
749
|
const formattedMessage = formatMessage(message);
|
|
520
750
|
|
|
521
|
-
|
|
751
|
+
// todo: do we really need to keep the failedRepliesMap?
|
|
752
|
+
if (formattedMessage.status === 'failed') {
|
|
522
753
|
// store failed reply so that it's not lost when reloading or hydrating
|
|
523
754
|
this.failedRepliesMap.set(formattedMessage.id, formattedMessage);
|
|
524
755
|
} else if (this.failedRepliesMap.has(message.id)) {
|
|
525
756
|
this.failedRepliesMap.delete(message.id);
|
|
526
757
|
}
|
|
527
758
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
replies: addToMessageList(current.replies, formattedMessage, timestampChanged),
|
|
531
|
-
}));
|
|
759
|
+
// The reply messagePaginator is the reply list source.
|
|
760
|
+
this.messagePaginator.ingestItem(formattedMessage);
|
|
532
761
|
};
|
|
533
762
|
|
|
763
|
+
// todo: can be removed with the next breaking change and use MessagePaginator only
|
|
534
764
|
public updateParentMessageLocally = ({ message }: { message: MessageResponse }) => {
|
|
535
765
|
if (message.id !== this.id) {
|
|
536
766
|
throw new Error('Message does not belong to this thread');
|
|
537
767
|
}
|
|
538
768
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
parentMessage: formattedMessage,
|
|
546
|
-
replyCount: message.reply_count ?? current.replyCount,
|
|
547
|
-
};
|
|
548
|
-
});
|
|
769
|
+
// The parent's content lives in the client-global message store; `state.parentMessage` (and the
|
|
770
|
+
// fields derived from it) is a projection kept in sync by `subscribeParentMessageFromStore`.
|
|
771
|
+
// Writing the store fans the change out to every collection holding this id and reflects it here.
|
|
772
|
+
if (this.client.messageStore.has(message.id)) {
|
|
773
|
+
this.client.messageStore.upsert(formatMessage(message));
|
|
774
|
+
}
|
|
549
775
|
};
|
|
550
776
|
|
|
777
|
+
// todo: can be removed with the next breaking change and use MessagePaginator only
|
|
551
778
|
public updateParentMessageOrReplyLocally = (message: MessageResponse) => {
|
|
552
779
|
if (message.parent_id === this.id) {
|
|
553
780
|
this.upsertReplyLocally({ message });
|
|
@@ -558,85 +785,187 @@ export class Thread extends WithSubscriptions {
|
|
|
558
785
|
}
|
|
559
786
|
};
|
|
560
787
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}: QueryRepliesOptions = {}) =>
|
|
580
|
-
this.channel.getReplies(this.id, { limit, ...otherOptions }, sort);
|
|
788
|
+
/**
|
|
789
|
+
* Sends a message with optimistic local state update.
|
|
790
|
+
*/
|
|
791
|
+
async sendMessageWithLocalUpdate({
|
|
792
|
+
localMessage,
|
|
793
|
+
message,
|
|
794
|
+
options,
|
|
795
|
+
sendMessageRequestFn,
|
|
796
|
+
}: SendMessageWithStateUpdateParams): Promise<void> {
|
|
797
|
+
await this.messageOperations.send(
|
|
798
|
+
{
|
|
799
|
+
localMessage,
|
|
800
|
+
message,
|
|
801
|
+
options,
|
|
802
|
+
},
|
|
803
|
+
sendMessageRequestFn,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
581
806
|
|
|
582
|
-
|
|
583
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Retry sending a failed message.
|
|
809
|
+
*/
|
|
810
|
+
async retrySendMessageWithLocalUpdate(
|
|
811
|
+
params: Omit<SendMessageWithStateUpdateParams, 'message'>,
|
|
812
|
+
) {
|
|
813
|
+
await this.messageOperations.retry(
|
|
814
|
+
{
|
|
815
|
+
localMessage: { ...params.localMessage, type: 'regular' },
|
|
816
|
+
options: params.options,
|
|
817
|
+
},
|
|
818
|
+
params.sendMessageRequestFn,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
584
821
|
|
|
585
|
-
|
|
586
|
-
|
|
822
|
+
/**
|
|
823
|
+
* Updates a message with optimistic local state update.
|
|
824
|
+
*
|
|
825
|
+
* The update flows through `messagePaginator`, which is the sole reply source.
|
|
826
|
+
*/
|
|
827
|
+
async updateMessageWithLocalUpdate(params: UpdateMessageWithStateUpdateParams) {
|
|
828
|
+
await this.messageOperations.update(
|
|
829
|
+
{
|
|
830
|
+
localMessage: params.localMessage,
|
|
831
|
+
options: params.options,
|
|
832
|
+
},
|
|
833
|
+
params.updateMessageRequestFn,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
587
836
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
837
|
+
/**
|
|
838
|
+
* Deletes a message with local state update.
|
|
839
|
+
*/
|
|
840
|
+
async deleteMessageWithLocalUpdate(params: DeleteMessageWithStateUpdateParams) {
|
|
841
|
+
await this.messageOperations.delete(
|
|
842
|
+
{
|
|
843
|
+
localMessage: params.localMessage,
|
|
844
|
+
options: params.options,
|
|
845
|
+
},
|
|
846
|
+
params.deleteMessageRequestFn,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
594
849
|
|
|
595
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Adds a reaction to a reply with an optimistic local state update, mirroring
|
|
852
|
+
* {@link Channel.addReactionWithLocalUpdate}. The optimistic message is applied to THIS thread's
|
|
853
|
+
* paginator (so pure replies get optimism the channel paginator can't give); the request routes
|
|
854
|
+
* through the parent channel since reactions are channel-level.
|
|
855
|
+
*/
|
|
856
|
+
async addReactionWithLocalUpdate({
|
|
857
|
+
messageId,
|
|
858
|
+
reaction,
|
|
859
|
+
options,
|
|
860
|
+
}: {
|
|
861
|
+
messageId: string;
|
|
862
|
+
reaction: ReactionRequest;
|
|
863
|
+
options?: Pick<SendReactionRequest, 'enforce_unique' | 'skip_push'>;
|
|
864
|
+
}) {
|
|
865
|
+
const client = this.channel.getClient();
|
|
866
|
+
const undo = applyReactionLocally(client, {
|
|
867
|
+
enforceUnique: options?.enforce_unique ?? false,
|
|
868
|
+
messageId,
|
|
869
|
+
reaction,
|
|
870
|
+
});
|
|
596
871
|
|
|
597
|
-
|
|
598
|
-
|
|
872
|
+
try {
|
|
873
|
+
const response = await this.channel.sendReaction({
|
|
874
|
+
id: messageId,
|
|
875
|
+
reaction,
|
|
876
|
+
...options,
|
|
877
|
+
});
|
|
878
|
+
// reconcile the server copy only if we still hold it — a bare upsert of an unheld id would
|
|
879
|
+
// orphan it (the store's refcount GC only reclaims held ids).
|
|
880
|
+
if (response?.message && client.messageStore.has(response.message.id)) {
|
|
881
|
+
client.messageStore.upsert(formatMessage(response.message));
|
|
882
|
+
}
|
|
883
|
+
} catch (error) {
|
|
884
|
+
if (undo && (!client.offlineDb || !isEphemeral(error as Error))) {
|
|
885
|
+
undo();
|
|
886
|
+
}
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
599
890
|
|
|
600
|
-
|
|
891
|
+
/**
|
|
892
|
+
* Removes the current user's reaction from a reply with an optimistic local state update,
|
|
893
|
+
* mirroring {@link Thread.addReactionWithLocalUpdate}.
|
|
894
|
+
*/
|
|
895
|
+
async deleteReactionWithLocalUpdate({
|
|
896
|
+
messageId,
|
|
897
|
+
type,
|
|
898
|
+
}: {
|
|
899
|
+
messageId: string;
|
|
900
|
+
type: string;
|
|
901
|
+
}) {
|
|
902
|
+
const client = this.channel.getClient();
|
|
903
|
+
const undo = applyReactionLocally(client, {
|
|
904
|
+
messageId,
|
|
905
|
+
reaction: { type },
|
|
906
|
+
removed: true,
|
|
907
|
+
});
|
|
601
908
|
|
|
602
909
|
try {
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
910
|
+
const response = await this.channel.deleteReaction({ id: messageId, type });
|
|
911
|
+
// reconcile the server copy only if we still hold it — a bare upsert of an unheld id would
|
|
912
|
+
// orphan it (the store's refcount GC only reclaims held ids).
|
|
913
|
+
if (response?.message && client.messageStore.has(response.message.id)) {
|
|
914
|
+
client.messageStore.upsert(formatMessage(response.message));
|
|
915
|
+
}
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (undo && (!client.offlineDb || !isEphemeral(error as Error))) {
|
|
918
|
+
undo();
|
|
919
|
+
}
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
606
923
|
|
|
607
|
-
|
|
608
|
-
|
|
924
|
+
public markRead = async ({ force = false }: { force?: boolean } = {}) => {
|
|
925
|
+
if (this.ownUnreadCount === 0 && !force) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
609
928
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
nextReplies = [...current.replies];
|
|
613
|
-
nextReplies[insertionMethodKey](...replies);
|
|
614
|
-
}
|
|
929
|
+
return await this.client.messageDeliveryReporter.markRead(this);
|
|
930
|
+
};
|
|
615
931
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
replies: nextReplies,
|
|
619
|
-
pagination: {
|
|
620
|
-
...current.pagination,
|
|
621
|
-
[cursorKey]: data.messages.length < limit ? null : maybeNextCursor,
|
|
622
|
-
[loadingKey]: false,
|
|
623
|
-
},
|
|
624
|
-
};
|
|
625
|
-
});
|
|
626
|
-
} catch (error) {
|
|
627
|
-
this.client.logger('error', (error as Error).message);
|
|
628
|
-
this.state.next((current) => ({
|
|
629
|
-
...current,
|
|
630
|
-
pagination: {
|
|
631
|
-
...current.pagination,
|
|
632
|
-
[loadingKey]: false,
|
|
633
|
-
},
|
|
634
|
-
}));
|
|
635
|
-
}
|
|
932
|
+
private throttledMarkRead = () => {
|
|
933
|
+
this.client.messageDeliveryReporter.throttledMarkRead(this);
|
|
636
934
|
};
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* @deprecated Use `thread.markRead` instead.
|
|
938
|
+
*/
|
|
939
|
+
public markAsRead = ({ force = false }: { force?: boolean } = {}) =>
|
|
940
|
+
this.markRead({ force });
|
|
637
941
|
}
|
|
638
942
|
|
|
639
|
-
|
|
943
|
+
type MessageThreadParticipant = NonNullable<
|
|
944
|
+
MessageResponse['thread_participants']
|
|
945
|
+
>[number];
|
|
946
|
+
type ThreadParticipant = NonNullable<ThreadStateResponse['thread_participants']>[number];
|
|
947
|
+
|
|
948
|
+
const normalizeThreadParticipants = (
|
|
949
|
+
participants: MessageResponse['thread_participants'] | undefined,
|
|
950
|
+
channelCid: string,
|
|
951
|
+
): ThreadStateResponse['thread_participants'] | undefined => {
|
|
952
|
+
if (!participants) return undefined;
|
|
953
|
+
|
|
954
|
+
const now = new Date();
|
|
955
|
+
|
|
956
|
+
return participants.map(
|
|
957
|
+
(participant: MessageThreadParticipant) =>
|
|
958
|
+
({
|
|
959
|
+
channel_cid: channelCid,
|
|
960
|
+
created_at: now,
|
|
961
|
+
last_read_at: now,
|
|
962
|
+
user: participant as UserResponse,
|
|
963
|
+
user_id: participant.id,
|
|
964
|
+
}) as ThreadParticipant,
|
|
965
|
+
);
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const formatReadState = (read: ReadStateResponse[]): ThreadReadState =>
|
|
640
969
|
read.reduce<ThreadReadState>((state, userRead) => {
|
|
641
970
|
state[userRead.user.id] = {
|
|
642
971
|
user: userRead.user,
|
|
@@ -647,21 +976,16 @@ const formatReadState = (read: ReadResponse[]): ThreadReadState =>
|
|
|
647
976
|
return state;
|
|
648
977
|
}, {});
|
|
649
978
|
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
: (thread.latest_replies.at(0)?.id ?? null),
|
|
661
|
-
isLoadingNext: false,
|
|
662
|
-
isLoadingPrev: false,
|
|
663
|
-
};
|
|
664
|
-
};
|
|
979
|
+
const getPlaceholderReadResponse = (currentUserId?: string): ReadStateResponse[] =>
|
|
980
|
+
currentUserId
|
|
981
|
+
? [
|
|
982
|
+
{
|
|
983
|
+
user: { id: currentUserId } as UserResponse,
|
|
984
|
+
unread_messages: 0,
|
|
985
|
+
last_read: new Date(),
|
|
986
|
+
},
|
|
987
|
+
]
|
|
988
|
+
: [];
|
|
665
989
|
|
|
666
990
|
const ownUnreadCountSelector =
|
|
667
991
|
(currentUserId: string | undefined) => (state: ThreadState) =>
|