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
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecuteQueryReturnValue,
|
|
3
|
+
Interval,
|
|
4
|
+
PostQueryReconcileParams,
|
|
5
|
+
} from './BasePaginator';
|
|
6
|
+
import {
|
|
7
|
+
type MessagePaginatorOptions as BaseMessagePaginatorOptions,
|
|
8
|
+
getMessageCreatedAtTimestamp,
|
|
9
|
+
type JumpToMessageOptions,
|
|
10
|
+
MessageIntervalPaginator,
|
|
11
|
+
type MessageQueryShape,
|
|
12
|
+
} from './MessageIntervalPaginator';
|
|
13
|
+
import type { LocalMessage } from '../../types';
|
|
14
|
+
import { StateStore } from '../../store';
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
JumpToMessageOptions,
|
|
18
|
+
MessageFocusReason,
|
|
19
|
+
MessageFocusSignal,
|
|
20
|
+
MessageFocusSignalState,
|
|
21
|
+
MessagePaginatorFilter,
|
|
22
|
+
MessagePaginatorSort,
|
|
23
|
+
MessagePaginatorState,
|
|
24
|
+
MessageQueryShape,
|
|
25
|
+
} from './MessageIntervalPaginator';
|
|
26
|
+
export { MessageIntervalPaginator } from './MessageIntervalPaginator';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Auxiliary (non-pagination) state for the message paginator: whole-collection aggregates that are
|
|
30
|
+
* independent of the active pagination window (the dual of pagination — values over the entire set,
|
|
31
|
+
* not a page of it). `lastMessageAt` is effectively `MAX(created_at)` over the channel-relevant
|
|
32
|
+
* messages and is the source of truth for channel-list ordering
|
|
33
|
+
* (`channel.messagePaginator.lastMessageAt`): seeded from `ChannelResponse.last_message_at`, then
|
|
34
|
+
* advanced monotonically as newer messages are ingested.
|
|
35
|
+
*/
|
|
36
|
+
export type MessagePaginatorAggregateState = {
|
|
37
|
+
/**
|
|
38
|
+
* The newest channel-relevant message, for display (e.g. a channel/thread list item's last-message
|
|
39
|
+
* preview or latest-reply avatar). A LIVE reference: it advances to a strictly newer message, is
|
|
40
|
+
* refreshed in place when that message is edited/soft-deleted/reacted-to, and is recomputed to the
|
|
41
|
+
* next newest when it is hard-removed. Respects `shouldAdvanceLastMessage` (skips system messages
|
|
42
|
+
* per `skip_last_msg_update_for_system_msgs`, and thread-only replies). `null` until one is ingested.
|
|
43
|
+
*
|
|
44
|
+
* Lives here, NOT derived from pagination `state`, so it stays reactive when a WS message lands in
|
|
45
|
+
* the head interval while an older window is active — the pagination store only emits when the
|
|
46
|
+
* active* interval is impacted (see `BasePaginator.ingestItem`), so a `state`-derived latest would
|
|
47
|
+
* go stale in that case.
|
|
48
|
+
*/
|
|
49
|
+
lastMessage: LocalMessage | null;
|
|
50
|
+
/**
|
|
51
|
+
* Server-provided `ChannelResponse.last_message_at` floor, for channels whose newest message is not
|
|
52
|
+
* loaded (e.g. surfaced by a channel-list query). Kept SEPARATE from {@link lastMessage} so it can
|
|
53
|
+
* outrank a stale/absent loaded message for sorting without overwriting the display message. The
|
|
54
|
+
* sort key {@link MessagePaginator.lastMessageAt} is derived as the max of the two, so the two can
|
|
55
|
+
* never drift out of sync.
|
|
56
|
+
*/
|
|
57
|
+
seededLastMessageAt: Date | null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type MessagePaginatorOptions = BaseMessagePaginatorOptions & {
|
|
61
|
+
/**
|
|
62
|
+
* Controls whether `jumpToTheFirstUnreadMessage()` should prefer the `unreadStateSnapshot`
|
|
63
|
+
* state over `channel.state.read[...]`.
|
|
64
|
+
*
|
|
65
|
+
* - 'snapshot' (default): retrieve the first unread message id from the unreadStateSnapshot state when jumping to the first unread message
|
|
66
|
+
* - 'read-state-only': retrieve the last read message id from the channel read state when jumping to the first unread message
|
|
67
|
+
*/
|
|
68
|
+
unreadReferencePolicy?: 'snapshot' | 'read-state-only';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type UnreadSnapshotState = {
|
|
72
|
+
lastReadAt: Date | null;
|
|
73
|
+
unreadCount: number;
|
|
74
|
+
/**
|
|
75
|
+
* Snapshot of the first unread message id for the user.
|
|
76
|
+
* This is intentionally decoupled from `channel.state.read[...]` because apps
|
|
77
|
+
* may mark the channel read immediately on open, while still wanting to render
|
|
78
|
+
* UI indicators that jump to the previously-unread location.
|
|
79
|
+
*/
|
|
80
|
+
firstUnreadMessageId: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Snapshot of the last read message id for the user (fallback when first unread
|
|
83
|
+
* is not known).
|
|
84
|
+
*/
|
|
85
|
+
lastReadMessageId: string | null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* External, UI-driven signal: `true` while the user is actively viewing the latest messages of
|
|
90
|
+
* this collection (app foregrounded AND the newest message on screen). The owning SDK sets it — the
|
|
91
|
+
* state layer has no viewport. When live, an incoming message is NOT counted as unread (the count /
|
|
92
|
+
* snapshot bump is skipped), so the "N new" separator/banner never flash for a message the user is
|
|
93
|
+
* already looking at. Defaults to `false` (assume not viewing until the SDK proves otherwise).
|
|
94
|
+
*/
|
|
95
|
+
export type LiveViewState = {
|
|
96
|
+
isViewingLive: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* MessagePaginator extends {@link MessageIntervalPaginator} with the unread/live-view concern:
|
|
101
|
+
* an independent unread reference snapshot, the UI-driven "viewing the latest messages" signal, and
|
|
102
|
+
* the "jump to first unread" navigation built on top of them.
|
|
103
|
+
*/
|
|
104
|
+
export class MessagePaginator extends MessageIntervalPaginator {
|
|
105
|
+
private unreadReferencePolicy: 'snapshot' | 'read-state-only';
|
|
106
|
+
/**
|
|
107
|
+
* Independent unread reference state (not tied to `channel.state.read`).
|
|
108
|
+
* Consumers may set this right before calling markRead / when opening a channel.
|
|
109
|
+
*/
|
|
110
|
+
readonly unreadStateSnapshot: StateStore<UnreadSnapshotState>;
|
|
111
|
+
/**
|
|
112
|
+
* UI-driven "viewing the latest messages" signal (see {@link LiveViewState}). Set by the SDK via
|
|
113
|
+
* {@link setViewingLive}; read by the channel to gate the unread bump on `message.new`. Subscribe to
|
|
114
|
+
* this store for reactivity, or read the current boolean directly via the {@link isViewingLive} getter.
|
|
115
|
+
*/
|
|
116
|
+
readonly liveViewState: StateStore<LiveViewState>;
|
|
117
|
+
/**
|
|
118
|
+
* Auxiliary (non-pagination) state — see {@link MessagePaginatorAggregateState}. A store separate
|
|
119
|
+
* from `state` so `lastMessageAt` can be advanced from inside a `state.next` updater
|
|
120
|
+
* (`ingestPage`) without being clobbered, and so consumers subscribe to a quiet signal that only
|
|
121
|
+
* emits when the aggregate actually changes (not on every scroll/pagination emission). This is
|
|
122
|
+
* distinct from the base's `intervalViews` interval-projection store.
|
|
123
|
+
*/
|
|
124
|
+
readonly aggregateState: StateStore<MessagePaginatorAggregateState>;
|
|
125
|
+
|
|
126
|
+
constructor({
|
|
127
|
+
unreadReferencePolicy = 'snapshot',
|
|
128
|
+
...options
|
|
129
|
+
}: MessagePaginatorOptions) {
|
|
130
|
+
super({
|
|
131
|
+
...options,
|
|
132
|
+
paginatorOptions: {
|
|
133
|
+
// Throttle message-list `state` publishes to at most once per 500ms (leading + trailing), so a
|
|
134
|
+
// burst of events coalesces into ~2 renders/sec instead of one per event. Optimistic
|
|
135
|
+
// (local-user) writes bypass the throttle via EntityStore.flushSubscribers → flushState.
|
|
136
|
+
// Overridable per-instance via `paginatorOptions.stateThrottleMs`.
|
|
137
|
+
stateThrottleMs: 500,
|
|
138
|
+
...options.paginatorOptions,
|
|
139
|
+
},
|
|
140
|
+
// NB: the store-backed item index is provided by MessageIntervalPaginator (the common
|
|
141
|
+
// ancestor), so both the main list and the pinned list share the client-global message store.
|
|
142
|
+
});
|
|
143
|
+
this.unreadReferencePolicy = unreadReferencePolicy;
|
|
144
|
+
this.unreadStateSnapshot = new StateStore<UnreadSnapshotState>({
|
|
145
|
+
lastReadAt: null,
|
|
146
|
+
firstUnreadMessageId: null,
|
|
147
|
+
lastReadMessageId: null,
|
|
148
|
+
unreadCount: 0,
|
|
149
|
+
});
|
|
150
|
+
this.liveViewState = new StateStore<LiveViewState>({
|
|
151
|
+
isViewingLive: false,
|
|
152
|
+
});
|
|
153
|
+
this.aggregateState = new StateStore<MessagePaginatorAggregateState>({
|
|
154
|
+
lastMessage: null,
|
|
155
|
+
seededLastMessageAt: null,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Channel-list sort key: the later of the newest loaded message's `created_at` and the server seed.
|
|
161
|
+
* **Derived** (never stored) so it cannot drift from {@link lastMessage}. `null` until seeded or a
|
|
162
|
+
* message is ingested.
|
|
163
|
+
*/
|
|
164
|
+
get lastMessageAt(): Date | null {
|
|
165
|
+
const { lastMessage, seededLastMessageAt } = this.aggregateState.getLatestValue();
|
|
166
|
+
const fromMessage =
|
|
167
|
+
lastMessage?.created_at instanceof Date ? lastMessage.created_at : null;
|
|
168
|
+
if (fromMessage && seededLastMessageAt) {
|
|
169
|
+
return fromMessage >= seededLastMessageAt ? fromMessage : seededLastMessageAt;
|
|
170
|
+
}
|
|
171
|
+
return fromMessage ?? seededLastMessageAt;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The newest channel-relevant message (the monotonic tracked latest), for display. Convenience read
|
|
176
|
+
* of {@link aggregateState}; subscribe to `aggregateState` for reactivity. `null` until a message is
|
|
177
|
+
* ingested (a server-only seed advances {@link lastMessageAt} but leaves this `null`).
|
|
178
|
+
*/
|
|
179
|
+
get lastMessage(): LocalMessage | null {
|
|
180
|
+
return this.aggregateState.getLatestValue().lastMessage;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The main channel list's notion of "latest message" for `channel.state.last_message_at`: on top of
|
|
185
|
+
* the base rule (not shadowed) this excludes thread-only replies (a reply with a `parent_id` that is
|
|
186
|
+
* not shown in the channel is not part of the channel list) and, when the channel is configured with
|
|
187
|
+
* `skip_last_msg_update_for_system_msgs`, system messages. Mirrors the legacy
|
|
188
|
+
* `Channel._trackLastMessage` skip logic.
|
|
189
|
+
*
|
|
190
|
+
* These exclusions are specific to the MAIN channel list. A reply list (thread paginator, which has
|
|
191
|
+
* a `parentMessageId`) is made ENTIRELY of thread replies — there the newest reply is exactly the
|
|
192
|
+
* "latest message", so the exclusions are skipped and only the base rule applies.
|
|
193
|
+
*/
|
|
194
|
+
protected shouldAdvanceLastMessage(message: LocalMessage): boolean {
|
|
195
|
+
if (message.shadowed) return false;
|
|
196
|
+
if (this.parentMessageId) return true;
|
|
197
|
+
const isThreadOnlyReply = !!message.parent_id && !message.show_in_channel;
|
|
198
|
+
if (isThreadOnlyReply) return false;
|
|
199
|
+
const skipSystemMessage =
|
|
200
|
+
!!this.channel.getConfig?.()?.skip_last_msg_update_for_system_msgs &&
|
|
201
|
+
message.type === 'system';
|
|
202
|
+
return !skipSystemMessage;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Monotonically advance {@link lastMessageAt} to `message.created_at` when it is newer than the
|
|
207
|
+
* current value and {@link shouldAdvanceLastMessage} permits it. Writes the dedicated
|
|
208
|
+
* {@link aggregateState} store (not `state`), so it is safe to call from inside a `state.next`
|
|
209
|
+
* updater (`ingestPage`). Returns `true` when the value moved.
|
|
210
|
+
*
|
|
211
|
+
* Called internally on ingest; also public for callers that advance the aggregate without a normal
|
|
212
|
+
* in-window ingest — e.g. offline pending-message replay, where the sent message has not yet been
|
|
213
|
+
* ingested via the `message.new` event.
|
|
214
|
+
*/
|
|
215
|
+
trackLastMessage(message: LocalMessage): boolean {
|
|
216
|
+
if (!this.shouldAdvanceLastMessage(message)) return false;
|
|
217
|
+
const incoming = getMessageCreatedAtTimestamp(message);
|
|
218
|
+
if (incoming === null) return false;
|
|
219
|
+
const current = this.aggregateState.getLatestValue().lastMessage;
|
|
220
|
+
// Refresh in place when this IS the current latest being updated — an edit, soft-delete,
|
|
221
|
+
// reaction, or quoted-message update all re-ingest the same id (same `created_at`). This keeps
|
|
222
|
+
// `lastMessage` a LIVE reference (so a preview shows "deleted"/edited text), and because
|
|
223
|
+
// `aggregateState` emits regardless of the active interval, it stays reactive off-window.
|
|
224
|
+
if (current && current.id === message.id) {
|
|
225
|
+
this.aggregateState.partialNext({ lastMessage: message });
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
// Otherwise only advance to a strictly newer message. Guard against the current message's own
|
|
229
|
+
// timestamp — NOT `lastMessageAt` (which the server seed can inflate); otherwise a seed newer than
|
|
230
|
+
// the loaded window would reject the very message it was derived from.
|
|
231
|
+
const currentTs = current ? getMessageCreatedAtTimestamp(current) : null;
|
|
232
|
+
if (currentTs !== null && incoming <= currentTs) return false;
|
|
233
|
+
this.aggregateState.partialNext({ lastMessage: message });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Seed {@link lastMessageAt} from the server-provided `ChannelResponse.last_message_at`, the
|
|
239
|
+
* authoritative whole-channel aggregate. Monotonic: a no-op when the paginator already advanced
|
|
240
|
+
* past it (e.g. from ingested messages), so seed order does not matter.
|
|
241
|
+
*/
|
|
242
|
+
seedLastMessageAt(value: string | Date | null | undefined) {
|
|
243
|
+
if (!value) return;
|
|
244
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
245
|
+
const timestamp = date.getTime();
|
|
246
|
+
if (!Number.isFinite(timestamp)) return;
|
|
247
|
+
const current = this.aggregateState.getLatestValue().seededLastMessageAt;
|
|
248
|
+
if (current && timestamp <= current.getTime()) return;
|
|
249
|
+
this.aggregateState.partialNext({ seededLastMessageAt: date });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
ingestItem(item: LocalMessage): boolean {
|
|
253
|
+
// Only items that survive the filter advance the aggregate (`ingestItem` also handles removals
|
|
254
|
+
// of items that no longer match). The advance writes the separate `aggregateState` store, so it
|
|
255
|
+
// is independent of `super`'s `state`/index mutations.
|
|
256
|
+
if (this.matchesFilter(item)) {
|
|
257
|
+
this.trackLastMessage(item);
|
|
258
|
+
}
|
|
259
|
+
return super.ingestItem(item);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
ingestPage(
|
|
263
|
+
params: Parameters<MessageIntervalPaginator['ingestPage']>[0],
|
|
264
|
+
): Interval | null {
|
|
265
|
+
const interval = super.ingestPage(params);
|
|
266
|
+
// Advance from each page item; the monotonic max over the page yields the newest. Comparison is
|
|
267
|
+
// by `created_at` against `aggregateState` (no index lookup), so order vs `super` is irrelevant.
|
|
268
|
+
if (params.page?.length) {
|
|
269
|
+
for (const item of params.page) this.trackLastMessage(item);
|
|
270
|
+
}
|
|
271
|
+
return interval;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
removeItem(
|
|
275
|
+
params: Parameters<MessageIntervalPaginator['removeItem']>[0],
|
|
276
|
+
): ReturnType<MessageIntervalPaginator['removeItem']> {
|
|
277
|
+
const removedId =
|
|
278
|
+
params.id ?? (params.item ? this.getItemId(params.item) : undefined);
|
|
279
|
+
const wasLatest =
|
|
280
|
+
!!removedId && this.aggregateState.getLatestValue().lastMessage?.id === removedId;
|
|
281
|
+
const result = super.removeItem(params);
|
|
282
|
+
if (wasLatest) {
|
|
283
|
+
// The tracked latest was hard-removed; fall back to the newest still-loaded message that
|
|
284
|
+
// passes the filter. `trackLastMessage` cannot do this (it only advances), so recompute here.
|
|
285
|
+
this.aggregateState.partialNext({ lastMessage: this.recomputeLastMessage() });
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The still-loaded message with the greatest `created_at` that passes
|
|
292
|
+
* {@link shouldAdvanceLastMessage} (skips system / thread-only per config), from the newest-loaded
|
|
293
|
+
* window. Used to recompute the tracked latest after the current one is removed. `null` when nothing
|
|
294
|
+
* loaded qualifies.
|
|
295
|
+
*
|
|
296
|
+
* "Latest" is defined by `created_at` (matching {@link trackLastMessage}), NOT by the paginator's
|
|
297
|
+
* display order — so this compares timestamps rather than assuming a position, and stays correct
|
|
298
|
+
* whatever `itemOrder` / `requestSort` are configured to.
|
|
299
|
+
*/
|
|
300
|
+
private recomputeLastMessage(): LocalMessage | null {
|
|
301
|
+
let latest: LocalMessage | null = null;
|
|
302
|
+
let latestTimestamp = -Infinity;
|
|
303
|
+
for (const item of this.headItems) {
|
|
304
|
+
if (!this.shouldAdvanceLastMessage(item)) continue;
|
|
305
|
+
const timestamp = getMessageCreatedAtTimestamp(item);
|
|
306
|
+
if (timestamp === null || timestamp <= latestTimestamp) continue;
|
|
307
|
+
latest = item;
|
|
308
|
+
latestTimestamp = timestamp;
|
|
309
|
+
}
|
|
310
|
+
return latest;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* (Re)seed the unread state snapshot from the current own read state.
|
|
315
|
+
*
|
|
316
|
+
* Called after the first-page query, and can be called again by the SDK whenever a channel is
|
|
317
|
+
* (re)opened from a cached paginator — where no fresh first-page query runs — so the snapshot
|
|
318
|
+
* reflects the CURRENT read boundary rather than one frozen at the very first open. Without a
|
|
319
|
+
* re-seed the separator position, unread count and "jump to first unread" target all go stale
|
|
320
|
+
* across reopens.
|
|
321
|
+
*
|
|
322
|
+
* `firstUnreadMessageId` is intentionally reset to `null`: it represents an EXPLICIT mark-unread
|
|
323
|
+
* (set by `notification.mark_unread`), not a live/computed boundary. Persisting a computed value
|
|
324
|
+
* here would make the channel look explicitly marked-unread and suppress auto-mark-read.
|
|
325
|
+
*/
|
|
326
|
+
seedUnreadSnapshot = () => {
|
|
327
|
+
// A paginator query (BasePaginator.executeQuery) awaits the network before running its
|
|
328
|
+
// synchronous postQueryReconcile, which calls this on the first page. If the channel was
|
|
329
|
+
// disconnected while that request was in flight, reading the client below throws ("You can't
|
|
330
|
+
// use a channel after client.disconnect()"), so guard against that.
|
|
331
|
+
if (this.channel.disconnected) return;
|
|
332
|
+
const ownUserId = this.channel.getClient().user?.id;
|
|
333
|
+
const ownReadState = ownUserId ? this.channel.state.read[ownUserId] : undefined;
|
|
334
|
+
if (!ownReadState) return;
|
|
335
|
+
this.setUnreadSnapshot({
|
|
336
|
+
firstUnreadMessageId: null,
|
|
337
|
+
lastReadAt: ownReadState.last_read ?? null,
|
|
338
|
+
lastReadMessageId: ownReadState.last_read_message_id ?? null,
|
|
339
|
+
unreadCount: ownReadState.unread_messages ?? 0,
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Invokes super.postQueryReconcile() and takes an unread state snapshot on the first-page query.
|
|
345
|
+
* The snapshot has to be taken immediately after the query as the viewed channel is marked read
|
|
346
|
+
* immediately after opening it. The snapshot can be used to display unread UI indicators.
|
|
347
|
+
*/
|
|
348
|
+
postQueryReconcile(
|
|
349
|
+
params: PostQueryReconcileParams<LocalMessage, MessageQueryShape>,
|
|
350
|
+
): ExecuteQueryReturnValue<LocalMessage> {
|
|
351
|
+
const result = super.postQueryReconcile(params);
|
|
352
|
+
|
|
353
|
+
if (params.isFirstPage) {
|
|
354
|
+
this.seedUnreadSnapshot();
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Jumps to the unread reference message.
|
|
361
|
+
*
|
|
362
|
+
* IMPORTANT: This intentionally does *not* rely on `channel.state.read[ownUserId]` only,
|
|
363
|
+
* because apps may mark a channel read immediately after opening it, while still
|
|
364
|
+
* wanting to keep "jump to unread" UI indicators alive (based on a snapshot).
|
|
365
|
+
*
|
|
366
|
+
* Resolution order:
|
|
367
|
+
* 1) explicit first-unread id from snapshot/read-state (mark-unread) — jump straight to it
|
|
368
|
+
* 2) last-read timestamp: infer the FIRST UNREAD message from it — using the already-loaded
|
|
369
|
+
* window when it straddles the boundary, otherwise a `created_at_around` query — and jump there
|
|
370
|
+
* 3) last read id from snapshot/read-state (last-resort fallback when no timestamp is available)
|
|
371
|
+
*
|
|
372
|
+
* The timestamp inference (2) is preferred over jumping to the last-read id so that the jump lands
|
|
373
|
+
* ON (and highlights) the first unread message rather than the last read one. The last-read id is
|
|
374
|
+
* only used as a fallback when we cannot infer an unread boundary. The inference is NOT persisted
|
|
375
|
+
* back into the snapshot (that is reserved for explicit mark-unread).
|
|
376
|
+
*/
|
|
377
|
+
jumpToTheFirstUnreadMessage = async (options?: JumpToMessageOptions) => {
|
|
378
|
+
const ownUserId = this.channel.getClient().user?.id;
|
|
379
|
+
if (!ownUserId) return false;
|
|
380
|
+
|
|
381
|
+
const unreadSnapshot =
|
|
382
|
+
this.unreadReferencePolicy === 'snapshot'
|
|
383
|
+
? this.unreadStateSnapshot.getLatestValue()
|
|
384
|
+
: { firstUnreadMessageId: null, lastReadAt: null, lastReadMessageId: null };
|
|
385
|
+
const firstUnreadFromSnapshot = unreadSnapshot.firstUnreadMessageId;
|
|
386
|
+
const lastReadAtFromSnapshot = unreadSnapshot.lastReadAt;
|
|
387
|
+
const lastReadIdFromSnapshot = unreadSnapshot.lastReadMessageId;
|
|
388
|
+
|
|
389
|
+
const ownReadState = this.channel.state.read[ownUserId];
|
|
390
|
+
const firstUnreadFromReadState = ownReadState?.first_unread_message_id ?? null;
|
|
391
|
+
const lastReadAtFromReadState = ownReadState?.last_read ?? null;
|
|
392
|
+
const lastReadIdFromReadState = ownReadState?.last_read_message_id ?? null;
|
|
393
|
+
|
|
394
|
+
// 1) A stable first-unread id is known (mark-unread, or a previously persisted inference):
|
|
395
|
+
// jump straight to it.
|
|
396
|
+
const firstUnreadMessageId = firstUnreadFromSnapshot ?? firstUnreadFromReadState;
|
|
397
|
+
if (firstUnreadMessageId) {
|
|
398
|
+
return await this.jumpToMessage(firstUnreadMessageId, {
|
|
399
|
+
...options,
|
|
400
|
+
focusReason: 'jump-to-first-unread',
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const lastReadMessageId = lastReadIdFromSnapshot ?? lastReadIdFromReadState;
|
|
405
|
+
// Prefer the SNAPSHOT timestamp over the fresh read-state one. The snapshot is the frozen UI
|
|
406
|
+
// boundary the "N new" separator/banner render from, so a banner tap must jump to where that
|
|
407
|
+
// indicator points. On (re)open the SDK marks the channel read (`markReadOnMount`) which advances
|
|
408
|
+
// the read-state `last_read` to ~now and clears the server unread — so preferring read-state
|
|
409
|
+
// would make a later banner tap infer "nothing unread" and jump to the latest message. The
|
|
410
|
+
// snapshot is kept fresh on genuine catch-up (the SDK's mark-read resets it), so it is only
|
|
411
|
+
// "frozen" precisely while there are unreads to jump to.
|
|
412
|
+
const lastReadAt = lastReadAtFromSnapshot ?? lastReadAtFromReadState;
|
|
413
|
+
|
|
414
|
+
// 2) No explicit first-unread id, but we know when the channel was last read. Infer the first
|
|
415
|
+
// unread message from that timestamp so we land ON (and highlight) the first unread message
|
|
416
|
+
// instead of the last read one. Prefer the already-loaded window (the common "a few unreads at
|
|
417
|
+
// the bottom" case — no extra request); only query a page around last-read when the loaded
|
|
418
|
+
// window does not straddle the boundary (i.e. every loaded message is newer than last-read).
|
|
419
|
+
//
|
|
420
|
+
// We deliberately do NOT persist the inferred boundary back into the snapshot: writing
|
|
421
|
+
// `firstUnreadMessageId` would make the channel look explicitly marked-unread and suppress
|
|
422
|
+
// auto-mark-read at the bottom. The separator reads the (re-seeded) snapshot directly.
|
|
423
|
+
if (lastReadAt) {
|
|
424
|
+
let {
|
|
425
|
+
firstUnreadMessageId: inferredFirstUnreadMessageId,
|
|
426
|
+
lastReadMessageId: inferredLastReadMessageId,
|
|
427
|
+
} = this.resolveUnreadBoundaryIdsByTimestamp({
|
|
428
|
+
lastReadAt,
|
|
429
|
+
messages: this.state.getLatestValue().items ?? [],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (!inferredLastReadMessageId) {
|
|
433
|
+
const result = await this.executeQuery({
|
|
434
|
+
queryShape: {
|
|
435
|
+
created_at_around: lastReadAt.toISOString(),
|
|
436
|
+
limit: options?.pageSize,
|
|
437
|
+
},
|
|
438
|
+
updateState: false,
|
|
439
|
+
});
|
|
440
|
+
if (result) {
|
|
441
|
+
({
|
|
442
|
+
firstUnreadMessageId: inferredFirstUnreadMessageId,
|
|
443
|
+
lastReadMessageId: inferredLastReadMessageId,
|
|
444
|
+
} = this.resolveUnreadBoundaryIdsByTimestamp({
|
|
445
|
+
lastReadAt,
|
|
446
|
+
messages: result.stateCandidate.items ?? [],
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const targetMessageId =
|
|
452
|
+
inferredFirstUnreadMessageId ?? inferredLastReadMessageId ?? lastReadMessageId;
|
|
453
|
+
if (targetMessageId) {
|
|
454
|
+
return await this.jumpToMessage(targetMessageId, {
|
|
455
|
+
...options,
|
|
456
|
+
focusReason: 'jump-to-first-unread',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 3) Last resort: jump to the known last-read message when that is all we have.
|
|
462
|
+
if (lastReadMessageId) {
|
|
463
|
+
return await this.jumpToMessage(lastReadMessageId, {
|
|
464
|
+
...options,
|
|
465
|
+
focusReason: 'jump-to-first-unread',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return false;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
setUnreadSnapshot = (next: Partial<UnreadSnapshotState>): UnreadSnapshotState => {
|
|
473
|
+
this.unreadStateSnapshot.partialNext(next);
|
|
474
|
+
return this.unreadStateSnapshot.getLatestValue();
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Set the UI-driven "viewing the latest messages" signal (see {@link LiveViewState}). Called by
|
|
479
|
+
* the SDK from its message-list viewability + app-state tracking. No-ops when unchanged.
|
|
480
|
+
*
|
|
481
|
+
* Intentionally NOT reset by `clearStateAndCache` — it is an external input owned by the SDK, not
|
|
482
|
+
* derived pagination state.
|
|
483
|
+
*/
|
|
484
|
+
/** Current "viewing the latest messages" boolean (convenience read of {@link liveViewState}). */
|
|
485
|
+
get isViewingLive(): boolean {
|
|
486
|
+
return this.liveViewState.getLatestValue().isViewingLive;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
setViewingLive = (isViewingLive: boolean) => {
|
|
490
|
+
if (this.isViewingLive === isViewingLive) return;
|
|
491
|
+
this.liveViewState.next({ isViewingLive });
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
clearUnreadSnapshot = () => {
|
|
495
|
+
this.unreadStateSnapshot.next({
|
|
496
|
+
firstUnreadMessageId: null,
|
|
497
|
+
lastReadMessageId: null,
|
|
498
|
+
lastReadAt: null,
|
|
499
|
+
unreadCount: 0,
|
|
500
|
+
});
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
clearStateAndCache() {
|
|
504
|
+
super.clearStateAndCache();
|
|
505
|
+
this.clearUnreadSnapshot();
|
|
506
|
+
this.aggregateState.next({ lastMessage: null, seededLastMessageAt: null });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { PaginatorCursor, PaginatorOptions } from './BasePaginator';
|
|
2
|
+
import {
|
|
3
|
+
MessageIntervalPaginator,
|
|
4
|
+
type MessageQueryShape,
|
|
5
|
+
} from './MessageIntervalPaginator';
|
|
6
|
+
import type {
|
|
7
|
+
LocalMessage,
|
|
8
|
+
PinnedMessagePaginationOptions,
|
|
9
|
+
SortParamRequest,
|
|
10
|
+
} from '../../types';
|
|
11
|
+
import type { Channel } from '../../channel';
|
|
12
|
+
import { formatMessage, generateUUIDv4 } from '../../utils';
|
|
13
|
+
import { makeComparator } from '../sortCompiler';
|
|
14
|
+
import { resolveDotPathValue } from '../utility.normalization';
|
|
15
|
+
import type { ItemIndexApi } from '../ItemIndex';
|
|
16
|
+
|
|
17
|
+
export type PinnedMessagePaginatorFilter = {
|
|
18
|
+
cid: string;
|
|
19
|
+
pinned: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type PinnedMessagePaginatorOptions = {
|
|
23
|
+
channel: Channel;
|
|
24
|
+
id?: string;
|
|
25
|
+
itemIndex?: ItemIndexApi<LocalMessage>;
|
|
26
|
+
paginatorOptions?: PaginatorOptions<LocalMessage, MessageQueryShape>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pinned-message list paginator.
|
|
31
|
+
*
|
|
32
|
+
* Extends the unread-free {@link MessageIntervalPaginator} base — pinned messages are a subset of
|
|
33
|
+
* the channel and MUST NOT participate in read/unread or delivery-receipt tracking (that belongs to
|
|
34
|
+
* the channel and thread message timelines). By extending the base rather than {@link MessagePaginator}
|
|
35
|
+
* it simply never gets the unread surface.
|
|
36
|
+
*
|
|
37
|
+
* Differences from the main list:
|
|
38
|
+
* - fetches from the `/pinned_messages` endpoint (`channel.getPinnedMessages`) rather than
|
|
39
|
+
* `channel.query({ messages })`;
|
|
40
|
+
* - includes only pinned, non-shadowed messages (`shouldIncludeMessageInInterval`), and filters on
|
|
41
|
+
* `{ cid, pinned: true }` so `ingestItem` auto-adds on pin and auto-removes on unpin;
|
|
42
|
+
* - orders by `pinned_at` ascending (oldest-pinned first), matching the legacy
|
|
43
|
+
* `channel.state.pinnedMessages` order.
|
|
44
|
+
*
|
|
45
|
+
* Navigation (`jumpToMessage` / `jumpToTheLatestMessage`) is inherited and meaningful — the endpoint
|
|
46
|
+
* supports `id_around` — but no unread-coupled navigation exists.
|
|
47
|
+
*/
|
|
48
|
+
export class PinnedMessagePaginator extends MessageIntervalPaginator {
|
|
49
|
+
constructor({
|
|
50
|
+
channel,
|
|
51
|
+
id,
|
|
52
|
+
itemIndex,
|
|
53
|
+
paginatorOptions,
|
|
54
|
+
}: PinnedMessagePaginatorOptions) {
|
|
55
|
+
super({
|
|
56
|
+
channel,
|
|
57
|
+
id: id ?? `pinned-message-paginator-${generateUUIDv4()}`,
|
|
58
|
+
// No explicit index: inherit the store-backed item index from MessageIntervalPaginator so a
|
|
59
|
+
// pinned message shares the single canonical copy with the main list / thread and reflects
|
|
60
|
+
// reactions & edits applied elsewhere. A caller may still inject a custom `itemIndex`.
|
|
61
|
+
itemIndex,
|
|
62
|
+
paginatorOptions,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Order by pinned_at (ascending), overriding the base's created_at comparators. Ascending keeps
|
|
66
|
+
// the head edge (most-recently-pinned) at the end of an interval, matching the base's interval
|
|
67
|
+
// direction getters (which are shared with created_at-asc semantics).
|
|
68
|
+
const tiebreaker = (l: LocalMessage, r: LocalMessage) => {
|
|
69
|
+
const leftId = this.getItemId(l);
|
|
70
|
+
const rightId = this.getItemId(r);
|
|
71
|
+
return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
|
|
72
|
+
};
|
|
73
|
+
const pinnedAtSort: SortParamRequest[] = [{ field: 'pinned_at', direction: 1 }];
|
|
74
|
+
this.sortComparator = makeComparator<LocalMessage>({
|
|
75
|
+
sort: pinnedAtSort,
|
|
76
|
+
resolvePathValue: resolveDotPathValue,
|
|
77
|
+
tiebreaker,
|
|
78
|
+
});
|
|
79
|
+
this.config.itemOrderComparator = makeComparator<LocalMessage>({
|
|
80
|
+
sort: pinnedAtSort,
|
|
81
|
+
resolvePathValue: resolveDotPathValue,
|
|
82
|
+
tiebreaker,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Fetch from the pinned-messages endpoint. The base `query` feeds the resolved query shape
|
|
86
|
+
// (including `id_around` jumps) here as `options`; we return both cursors and let the base gate
|
|
87
|
+
// them by direction.
|
|
88
|
+
this.config.doRequest = async (
|
|
89
|
+
options: MessageQueryShape,
|
|
90
|
+
): Promise<{ cursor?: PaginatorCursor; items: LocalMessage[] }> => {
|
|
91
|
+
const { messages } = await this.channel.getPinnedMessages(
|
|
92
|
+
options as PinnedMessagePaginationOptions,
|
|
93
|
+
[{ direction: 1, field: 'pinned_at' }],
|
|
94
|
+
);
|
|
95
|
+
const items = messages.map(formatMessage);
|
|
96
|
+
return { cursor: this.getCursorFromQueryResults({ items }), items };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
buildMatchFilters = (): PinnedMessagePaginatorFilter => ({
|
|
101
|
+
cid: this.channel.cid,
|
|
102
|
+
pinned: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
shouldIncludeMessageInInterval(message: LocalMessage): boolean {
|
|
106
|
+
return !message.shadowed && !!message.pinned;
|
|
107
|
+
}
|
|
108
|
+
}
|