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,724 @@
|
|
|
1
|
+
import { EventHandlerPipeline } from './EventHandlerPipeline';
|
|
2
|
+
import { WithSubscriptions } from './utils/WithSubscriptions';
|
|
3
|
+
import type { EventType } from './types';
|
|
4
|
+
import type { ChannelPaginator } from './pagination';
|
|
5
|
+
import type { StreamChat } from './client';
|
|
6
|
+
import type { Unsubscribe } from './store';
|
|
7
|
+
import { StateStore } from './store';
|
|
8
|
+
import type {
|
|
9
|
+
EventHandlerPipelineHandler,
|
|
10
|
+
FindEventHandlerParams,
|
|
11
|
+
InsertEventHandlerPayload,
|
|
12
|
+
LabeledEventHandler,
|
|
13
|
+
PipelineEvent,
|
|
14
|
+
} from './EventHandlerPipeline';
|
|
15
|
+
import { getChannel } from './pagination/utility.queryChannel';
|
|
16
|
+
import type { Channel } from './channel';
|
|
17
|
+
|
|
18
|
+
export type ChannelManagerEventHandlerContext = {
|
|
19
|
+
channelManager: ChannelManager;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type EventHandlerContext = ChannelManagerEventHandlerContext;
|
|
23
|
+
|
|
24
|
+
type SupportedEventType = EventType | (string & {});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves which paginators should be the "owners" of a channel
|
|
28
|
+
* when the channel matches multiple paginator filters.
|
|
29
|
+
*
|
|
30
|
+
* Return a set of paginator ids that should keep/own the item.
|
|
31
|
+
* Returning an empty set means the channel will be removed everywhere.
|
|
32
|
+
*/
|
|
33
|
+
export type PaginatorOwnershipResolver = (args: {
|
|
34
|
+
channel: Channel;
|
|
35
|
+
matchingPaginators: ChannelPaginator[];
|
|
36
|
+
}) => string[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convenience factory for a priority-based ownership resolver.
|
|
40
|
+
* - Provide an ordered list of paginator ids from highest to lowest priority.
|
|
41
|
+
* - If two or more paginators match a channel, the one with the highest priority wins.
|
|
42
|
+
* - If none of the matching paginator ids are in the priority list, all matches are kept (back-compat).
|
|
43
|
+
*/
|
|
44
|
+
export const createPriorityOwnershipResolver = (
|
|
45
|
+
priority?: string[],
|
|
46
|
+
): PaginatorOwnershipResolver => {
|
|
47
|
+
if (!priority) {
|
|
48
|
+
return ({ matchingPaginators }) => matchingPaginators.map((p) => p.id);
|
|
49
|
+
}
|
|
50
|
+
const rank = new Map<string, number>(priority.map((id, index) => [id, index]));
|
|
51
|
+
return ({ matchingPaginators }) => {
|
|
52
|
+
if (matchingPaginators.length <= 1) {
|
|
53
|
+
return matchingPaginators.map((p) => p.id);
|
|
54
|
+
}
|
|
55
|
+
// The winner is the first item in the sorted array of matching paginators
|
|
56
|
+
const winner = [...matchingPaginators].sort((a, b) => {
|
|
57
|
+
const rankA = rank.get(a.id);
|
|
58
|
+
const rankB = rank.get(b.id);
|
|
59
|
+
const valueA = rankA === undefined ? Number.POSITIVE_INFINITY : rankA;
|
|
60
|
+
const valueB = rankB === undefined ? Number.POSITIVE_INFINITY : rankB;
|
|
61
|
+
return valueA - valueB;
|
|
62
|
+
})[0];
|
|
63
|
+
const winnerValue = rank.get(winner.id);
|
|
64
|
+
// If no explicit priority is set for any, keep all (preserve current behavior)
|
|
65
|
+
if (winnerValue === undefined) {
|
|
66
|
+
return matchingPaginators.map((p) => p.id);
|
|
67
|
+
}
|
|
68
|
+
return [winner.id];
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The cid the event refers to. Events are inconsistent about how they identify their channel: some carry
|
|
74
|
+
* a top-level `cid`, some only `channel_type` + `channel_id`, and some (e.g.
|
|
75
|
+
* `notification.added_to_channel`) have all three optional and identify the channel solely through the
|
|
76
|
+
* required `event.channel`.
|
|
77
|
+
*/
|
|
78
|
+
const getCidFromEvent = (event: PipelineEvent): string | undefined => {
|
|
79
|
+
if (event.cid) return event.cid;
|
|
80
|
+
// todo: is there a central method to construct the cid from type and channel id?
|
|
81
|
+
if (event.channel_id && event.channel_type) {
|
|
82
|
+
return `${event.channel_type}:${event.channel_id}`;
|
|
83
|
+
}
|
|
84
|
+
return event.channel?.cid;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getCachedChannelFromEvent = (
|
|
88
|
+
event: PipelineEvent,
|
|
89
|
+
cache: Record<string, Channel>,
|
|
90
|
+
): Channel | undefined => {
|
|
91
|
+
const cid = getCidFromEvent(event);
|
|
92
|
+
return cid ? cache[cid] : undefined;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const reEmit: EventHandlerPipelineHandler<EventHandlerContext> = ({
|
|
96
|
+
event,
|
|
97
|
+
ctx: { channelManager },
|
|
98
|
+
}) => {
|
|
99
|
+
if (!event.cid) return;
|
|
100
|
+
const channel = channelManager.client.activeChannels[event.cid];
|
|
101
|
+
if (!channel) return;
|
|
102
|
+
channelManager.paginators.forEach((paginator) => {
|
|
103
|
+
const items = paginator.items;
|
|
104
|
+
const { state } = paginator.locateByItem(channel);
|
|
105
|
+
if ((state?.currentIndex ?? -1) > -1 && items) {
|
|
106
|
+
paginator.state.partialNext({ items: [...items] });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const removeItem: EventHandlerPipelineHandler<EventHandlerContext> = ({
|
|
112
|
+
event,
|
|
113
|
+
ctx: { channelManager },
|
|
114
|
+
}) => {
|
|
115
|
+
// `getCidFromEvent`, not `event.cid`: on `channel.deleted` the cid can arrive nested in
|
|
116
|
+
// `event.channel` only, and the legacy ChannelManager removed by `event.cid || event.channel?.cid`
|
|
117
|
+
const cid = getCidFromEvent(event);
|
|
118
|
+
if (!cid) return;
|
|
119
|
+
const channel = channelManager.client.activeChannels[cid];
|
|
120
|
+
channelManager.paginators.forEach((paginator) => {
|
|
121
|
+
paginator.removeItem({ id: cid, item: channel });
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// todo: documentation: show how to implement allowNewMessagesFromUnfilteredChannels just by inserting event handler
|
|
126
|
+
// at the start of the handler pipeline and filter out events for unknown channels
|
|
127
|
+
export const ignoreEventsForUnknownChannels: EventHandlerPipelineHandler<
|
|
128
|
+
EventHandlerContext
|
|
129
|
+
> = ({ event, ctx: { channelManager } }) => {
|
|
130
|
+
const channel: Channel | undefined = getCachedChannelFromEvent(
|
|
131
|
+
event,
|
|
132
|
+
channelManager.client.activeChannels,
|
|
133
|
+
);
|
|
134
|
+
if (!channel) return { action: 'stop' };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const updateLists: EventHandlerPipelineHandler<EventHandlerContext> = async ({
|
|
138
|
+
event,
|
|
139
|
+
ctx: { channelManager },
|
|
140
|
+
}) => {
|
|
141
|
+
let channel: Channel | undefined = getCachedChannelFromEvent(
|
|
142
|
+
event,
|
|
143
|
+
channelManager.client.activeChannels,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (!channel) {
|
|
147
|
+
const [type, id] = getCidFromEvent(event)?.split(':') ?? [];
|
|
148
|
+
if (!type) return;
|
|
149
|
+
|
|
150
|
+
channel = await getChannel({
|
|
151
|
+
client: channelManager.client,
|
|
152
|
+
id,
|
|
153
|
+
type,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!channel) return;
|
|
158
|
+
|
|
159
|
+
const matchingPaginators = channelManager.paginators.filter((p) =>
|
|
160
|
+
p.matchesFilter(channel),
|
|
161
|
+
);
|
|
162
|
+
const matchingIds = new Set(matchingPaginators.map((p) => p.id));
|
|
163
|
+
|
|
164
|
+
const ownerIds = channelManager.resolveOwnership(channel, matchingPaginators);
|
|
165
|
+
|
|
166
|
+
channelManager.paginators.forEach((paginator) => {
|
|
167
|
+
if (!matchingIds.has(paginator.id)) {
|
|
168
|
+
// remove if it does not match the filter anymore
|
|
169
|
+
paginator.removeItem({ item: channel });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Only if owners are specified, the items is removed from the non-owner matching paginators
|
|
174
|
+
if (ownerIds.size > 0 && !ownerIds.has(paginator.id)) {
|
|
175
|
+
// matched, but not selected to own - remove to enforce exclusivity
|
|
176
|
+
paginator.removeItem({ item: channel });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Selected owner: ingest. The manager never boosts by default on any event — the sort is the
|
|
181
|
+
// single source of truth for order, so a channel that just became relevant (new message, added,
|
|
182
|
+
// unhidden) relocates via its updated sort key. Boosting remains a public per-paginator primitive
|
|
183
|
+
// (`paginator.boost`) for integrators to opt into for specific channels (VIP/mention/deep-link).
|
|
184
|
+
paginator.ingestItem(channel);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// we have to make sure that client.activeChannels is always up-to-date
|
|
189
|
+
const channelDeletedHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
190
|
+
handle: removeItem,
|
|
191
|
+
id: 'ChannelManager:default-handler:channel.deleted',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// fixme: this handler should not be handled by the channel manager but as Channel does not have reactive state,
|
|
195
|
+
// we need to re-emit the whole list to reflect the changes
|
|
196
|
+
const channelUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
197
|
+
handle: reEmit,
|
|
198
|
+
id: 'ChannelManager:default-handler:channel.updated',
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// fixme: this handler should not be handled by the channel manager but as Channel does not have reactive state,
|
|
202
|
+
// we need to re-emit the whole list to reflect the changes
|
|
203
|
+
const channelTruncatedHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
204
|
+
handle: reEmit,
|
|
205
|
+
id: 'ChannelManager:default-handler:channel.truncated',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const channelVisibleHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
209
|
+
handle: updateLists,
|
|
210
|
+
id: 'ChannelManager:default-handler:channel.visible',
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const channelHiddenHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
214
|
+
handle: updateLists,
|
|
215
|
+
id: 'ChannelManager:default-handler:channel.hidden',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// members filter - should not be impacted as id is stable - cannot be updated
|
|
219
|
+
// member.user.name - can be impacted
|
|
220
|
+
const memberUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
221
|
+
handle: updateLists,
|
|
222
|
+
id: 'ChannelManager:default-handler:member.updated',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const messageNewHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
226
|
+
handle: updateLists,
|
|
227
|
+
id: 'ChannelManager:default-handler:message.new',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const notificationAddedToChannelHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
231
|
+
handle: updateLists,
|
|
232
|
+
id: 'ChannelManager:default-handler:notification.added_to_channel',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const notificationMessageNewHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
236
|
+
handle: updateLists,
|
|
237
|
+
id: 'ChannelManager:default-handler:notification.message_new',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const notificationRemovedFromChannelHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
241
|
+
handle: removeItem,
|
|
242
|
+
id: 'ChannelManager:default-handler:notification.removed_from_channel',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* `muted` is filterable (resolved from `client.mutedChannels`), but a mute change emits only this
|
|
247
|
+
* user-level event — nothing about the channel — so without re-routing here a channel would keep its
|
|
248
|
+
* old list until some unrelated event touched it. The client applies `event.me.channel_mutes` before
|
|
249
|
+
* its listeners run, so the filters already see the new state.
|
|
250
|
+
*/
|
|
251
|
+
const notificationChannelMutesUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
252
|
+
handle: ({ ctx: { channelManager } }) => {
|
|
253
|
+
const seen = new Set<string>();
|
|
254
|
+
channelManager.paginators.forEach((paginator) => {
|
|
255
|
+
(paginator.items ?? []).forEach((channel) => {
|
|
256
|
+
if (seen.has(channel.cid)) return; // prevent ingestig -> emitting more than once
|
|
257
|
+
seen.add(channel.cid);
|
|
258
|
+
channelManager.ingestChannel(channel);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
id: 'ChannelManager:default-handler:notification.channel_mutes_updated',
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// fixme: updates users for member object in all the channels which are loaded with that member - normalization would be beneficial
|
|
266
|
+
const userPresenceChangedHandler: LabeledEventHandler<EventHandlerContext> = {
|
|
267
|
+
handle: ({ event, ctx: { channelManager } }) => {
|
|
268
|
+
const eventUser = event.user;
|
|
269
|
+
if (!eventUser?.id) return;
|
|
270
|
+
channelManager.paginators.forEach((paginator) => {
|
|
271
|
+
const paginatorItems = paginator.items;
|
|
272
|
+
if (!paginatorItems) return;
|
|
273
|
+
let updated = false;
|
|
274
|
+
paginatorItems.forEach((channel) => {
|
|
275
|
+
if (channel.state.members[eventUser.id]) {
|
|
276
|
+
channel.state.members[eventUser.id].user = event.user;
|
|
277
|
+
updated = true;
|
|
278
|
+
}
|
|
279
|
+
if (channel.state.membership.user?.id === eventUser.id) {
|
|
280
|
+
channel.state.membership.user = eventUser;
|
|
281
|
+
updated = true;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (updated) {
|
|
285
|
+
// fixme: user is not reactive and so the whole list has to be re-rendered
|
|
286
|
+
paginator.state.partialNext({ items: [...paginatorItems] });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
id: 'ChannelManager:default-handler:user.presence.changed',
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export type ChannelManagerState = {
|
|
294
|
+
paginators: ChannelPaginator[];
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export type ChannelManagerEventHandlers = Partial<
|
|
298
|
+
Record<SupportedEventType, LabeledEventHandler<EventHandlerContext>[]>
|
|
299
|
+
>;
|
|
300
|
+
|
|
301
|
+
export type ChannelManagerOptions = {
|
|
302
|
+
client: StreamChat;
|
|
303
|
+
paginators?: ChannelPaginator[];
|
|
304
|
+
/**
|
|
305
|
+
* The complete set of event handlers to run, replacing the defaults rather than extending them.
|
|
306
|
+
* Start from `ChannelManager.getDefaultHandlers()` (a fresh copy) and enrich it, unless the
|
|
307
|
+
* intention really is to route events differently from scratch. Note that the manager the client
|
|
308
|
+
* instantiates (`client.channelManager`) is constructed without this option — customize that one
|
|
309
|
+
* through `addEventHandler` / `setEventHandlers` / `removeEventHandlers`.
|
|
310
|
+
*/
|
|
311
|
+
eventHandlers?: ChannelManagerEventHandlers;
|
|
312
|
+
/**
|
|
313
|
+
* Decide which paginator(s) should own a channel when multiple match.
|
|
314
|
+
* Defaults to keeping the channel in all matching paginators.
|
|
315
|
+
* Channels are kept only in the paginators that are listed in the ownershipResolver array.
|
|
316
|
+
* Empty ownershipResolver array means that the channel is kept in all matching paginators.
|
|
317
|
+
*/
|
|
318
|
+
ownershipResolver?: PaginatorOwnershipResolver | string[];
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export class ChannelManager extends WithSubscriptions {
|
|
322
|
+
client: StreamChat;
|
|
323
|
+
state: StateStore<ChannelManagerState>;
|
|
324
|
+
protected _pipelines = new Map<
|
|
325
|
+
SupportedEventType,
|
|
326
|
+
EventHandlerPipeline<EventHandlerContext>
|
|
327
|
+
>();
|
|
328
|
+
protected ownershipResolver?: PaginatorOwnershipResolver;
|
|
329
|
+
/**
|
|
330
|
+
* The `filterQueryResults` each registered paginator had before this manager wrapped it.
|
|
331
|
+
*
|
|
332
|
+
* A server query bypasses the manager — the paginator fetches and ingests a page on its own, and
|
|
333
|
+
* the backend knows nothing about client-side ownership — so a "Work" (`{ team: 'work' }`) channel
|
|
334
|
+
* comes back in a catch-all list's page too and would render in both. `wrapPaginatorFiltering`
|
|
335
|
+
* therefore composes the ownership check onto that hook, which every page passes through.
|
|
336
|
+
*
|
|
337
|
+
* Keys = already wrapped (so re-inserting does not wrap twice); values = what to restore on
|
|
338
|
+
* removal, since a detached paginator must stop applying rules resolved against lists it left.
|
|
339
|
+
* Weakly keyed so a dropped paginator is not kept alive by this manager.
|
|
340
|
+
*/
|
|
341
|
+
protected filterQueryResultsBeforeWrapping = new WeakMap<
|
|
342
|
+
ChannelPaginator,
|
|
343
|
+
ChannelPaginator['filterQueryResults']
|
|
344
|
+
>();
|
|
345
|
+
|
|
346
|
+
protected static readonly defaultEventHandlers: ChannelManagerEventHandlers = {
|
|
347
|
+
'channel.deleted': [channelDeletedHandler],
|
|
348
|
+
'channel.updated': [channelUpdatedHandler],
|
|
349
|
+
'channel.truncated': [channelTruncatedHandler],
|
|
350
|
+
'channel.hidden': [channelHiddenHandler],
|
|
351
|
+
'channel.visible': [channelVisibleHandler],
|
|
352
|
+
'member.updated': [memberUpdatedHandler],
|
|
353
|
+
'message.new': [messageNewHandler],
|
|
354
|
+
'notification.added_to_channel': [notificationAddedToChannelHandler],
|
|
355
|
+
'notification.channel_mutes_updated': [notificationChannelMutesUpdatedHandler],
|
|
356
|
+
'notification.message_new': [notificationMessageNewHandler],
|
|
357
|
+
'notification.removed_from_channel': [notificationRemovedFromChannelHandler],
|
|
358
|
+
'user.presence.changed': [userPresenceChangedHandler],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
constructor({
|
|
362
|
+
client,
|
|
363
|
+
eventHandlers,
|
|
364
|
+
paginators,
|
|
365
|
+
ownershipResolver,
|
|
366
|
+
}: ChannelManagerOptions) {
|
|
367
|
+
super();
|
|
368
|
+
this.client = client;
|
|
369
|
+
this.state = new StateStore({ paginators: paginators ?? [] });
|
|
370
|
+
this.setOwnershipResolver(ownershipResolver);
|
|
371
|
+
|
|
372
|
+
// A supplied map replaces the defaults wholesale — an event type missing from it is simply not
|
|
373
|
+
// handled. `getDefaultHandlers()` returns a copy to enrich when that is what you want.
|
|
374
|
+
const finalEventHandlers = eventHandlers ?? ChannelManager.getDefaultHandlers();
|
|
375
|
+
for (const [type, handlers] of Object.entries(finalEventHandlers)) {
|
|
376
|
+
if (handlers) this.ensurePipeline(type).replaceAll(handlers);
|
|
377
|
+
}
|
|
378
|
+
// Ensure ownership rules are applied to initial paginators' query results
|
|
379
|
+
this.paginators.forEach((p) => this.wrapPaginatorFiltering(p));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
get paginators(): ChannelPaginator[] {
|
|
383
|
+
return this.state.getLatestValue().paginators;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
get pipelines(): Map<SupportedEventType, EventHandlerPipeline<EventHandlerContext>> {
|
|
387
|
+
return this._pipelines;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private get ctx(): EventHandlerContext {
|
|
391
|
+
return { channelManager: this };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Returns deep copy of default handlers mapping.
|
|
396
|
+
* The defaults can be enriched with custom handlers or the custom handlers can be replaced.
|
|
397
|
+
*/
|
|
398
|
+
static getDefaultHandlers(): ChannelManagerEventHandlers {
|
|
399
|
+
const src = ChannelManager.defaultEventHandlers;
|
|
400
|
+
const out: ChannelManagerEventHandlers = {};
|
|
401
|
+
for (const [type, handlers] of Object.entries(src)) {
|
|
402
|
+
if (!handlers) continue;
|
|
403
|
+
out[type as SupportedEventType] = [...handlers];
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Replace the rule deciding which paginator(s) own a channel matched by several of them. Pass an
|
|
410
|
+
* ordered array of paginator ids (highest priority first) for the built-in priority resolver, a
|
|
411
|
+
* custom resolver function, or `undefined` to go back to the default (a channel is kept in every
|
|
412
|
+
* paginator whose filter it matches).
|
|
413
|
+
*
|
|
414
|
+
* Available as a setter because the manager is constructed by the client, before the app has had
|
|
415
|
+
* a chance to create its paginators — their ids are therefore only known later.
|
|
416
|
+
*/
|
|
417
|
+
setOwnershipResolver(ownershipResolver?: PaginatorOwnershipResolver | string[]) {
|
|
418
|
+
if (!ownershipResolver) {
|
|
419
|
+
this.ownershipResolver = undefined;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.ownershipResolver = Array.isArray(ownershipResolver)
|
|
423
|
+
? createPriorityOwnershipResolver(ownershipResolver)
|
|
424
|
+
: ownershipResolver;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Which paginators should own the channel among the ones that matched.
|
|
429
|
+
* Default behavior keeps the channel in all matching paginators.
|
|
430
|
+
*/
|
|
431
|
+
resolveOwnership(
|
|
432
|
+
channel: Channel,
|
|
433
|
+
matchingPaginators: ChannelPaginator[],
|
|
434
|
+
): Set<string> {
|
|
435
|
+
return new Set(this.ownershipResolver?.({ channel, matchingPaginators }) ?? []);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Route a channel into the paginator(s) that should own it, and remove it from any list it
|
|
440
|
+
* no longer belongs to. Ownership is resolved exactly as for live WS updates — the channel is
|
|
441
|
+
* ingested into every paginator whose filter it matches (or, when an ownership resolver picks
|
|
442
|
+
* winners among several matches, only into the owner(s)).
|
|
443
|
+
*
|
|
444
|
+
* Use this to surface a channel the app just opened — a search result, a freshly created DM —
|
|
445
|
+
* in the list(s) without a full re-query. `ingestItem` dedupes by cid and inserts in sort
|
|
446
|
+
* order, so calling this repeatedly is safe.
|
|
447
|
+
*
|
|
448
|
+
* A channel that matches no paginator is not added anywhere. To have such channels still
|
|
449
|
+
* appear, register a catch-all paginator (empty filter) with the lowest ownership priority as
|
|
450
|
+
* a local fallback list.
|
|
451
|
+
*/
|
|
452
|
+
ingestChannel(channel: Channel) {
|
|
453
|
+
const matchingPaginators = this.paginators.filter((p) => p.matchesFilter(channel));
|
|
454
|
+
const matchingIds = new Set(matchingPaginators.map((p) => p.id));
|
|
455
|
+
const ownerIds = this.resolveOwnership(channel, matchingPaginators);
|
|
456
|
+
|
|
457
|
+
this.paginators.forEach((paginator) => {
|
|
458
|
+
const isMatch = matchingIds.has(paginator.id);
|
|
459
|
+
const isOwner = ownerIds.size === 0 || ownerIds.has(paginator.id);
|
|
460
|
+
if (isMatch && isOwner) {
|
|
461
|
+
paginator.ingestItem(channel);
|
|
462
|
+
} else {
|
|
463
|
+
// Not a match, or matched but not the selected owner — enforce exclusivity.
|
|
464
|
+
paginator.removeItem({ item: channel });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Filter a page of query results for a specific paginator according to ownership rules.
|
|
471
|
+
* If no owners are specified by the resolver, all matching paginators keep the item.
|
|
472
|
+
*/
|
|
473
|
+
protected filterItemsByOwnership({
|
|
474
|
+
paginator,
|
|
475
|
+
items,
|
|
476
|
+
}: {
|
|
477
|
+
paginator: ChannelPaginator;
|
|
478
|
+
items: Channel[];
|
|
479
|
+
}): Channel[] {
|
|
480
|
+
if (!items.length) return items;
|
|
481
|
+
const result: Channel[] = [];
|
|
482
|
+
for (const ch of items) {
|
|
483
|
+
const matchingPaginators = this.paginators.filter((p) => p.matchesFilter(ch));
|
|
484
|
+
const ownerIds = this.resolveOwnership(ch, matchingPaginators);
|
|
485
|
+
const noOwnersOrPaginatorIsOwner =
|
|
486
|
+
ownerIds.size === 0 || ownerIds.has(paginator.id);
|
|
487
|
+
|
|
488
|
+
if (noOwnersOrPaginatorIsOwner) {
|
|
489
|
+
result.push(ch);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Makes a registered paginator apply this manager's ownership rules to the pages it fetches
|
|
497
|
+
* itself, by composing `filterItemsByOwnership` onto its `filterQueryResults`.
|
|
498
|
+
*
|
|
499
|
+
* A channel can enter a list through two doors, and only one of them goes through the manager:
|
|
500
|
+
* - **WS events / `ingestChannel`** — the manager routes the channel and consults
|
|
501
|
+
* `resolveOwnership` before ingesting, so exclusivity holds by construction.
|
|
502
|
+
* - **A server query** — `paginator.next()` (a `ChannelList` scrolling, `reload()`) queries and
|
|
503
|
+
* ingests on its own; the manager is not involved. Without this wrapper a page would land in
|
|
504
|
+
* every list whose *server-side* filter matched it, so a channel matching both
|
|
505
|
+
* `channels:archived` and `channels:default` would show up in both lists until some later event
|
|
506
|
+
* re-routed it — the exact duplication the resolver exists to prevent.
|
|
507
|
+
*
|
|
508
|
+
* Wrapping (rather than subclassing or a constructor hook) is what lets the manager inject this
|
|
509
|
+
* into paginators it does not construct: `filterQueryResults` is an instance property, so the
|
|
510
|
+
* original is captured here and handed back by `unwrapPaginatorFiltering` when the paginator is
|
|
511
|
+
* detached. The paginator's own filtering runs first and stays authoritative — ownership only ever
|
|
512
|
+
* removes further items, never adds any back.
|
|
513
|
+
*
|
|
514
|
+
* Idempotent: the `WeakMap` guard keeps a re-inserted paginator from being wrapped twice, which
|
|
515
|
+
* would filter the same page repeatedly and, worse, lose the reference to the true original.
|
|
516
|
+
*/
|
|
517
|
+
protected wrapPaginatorFiltering(paginator: ChannelPaginator) {
|
|
518
|
+
if (this.filterQueryResultsBeforeWrapping.has(paginator)) return;
|
|
519
|
+
const original = paginator.filterQueryResults.bind(paginator);
|
|
520
|
+
paginator.filterQueryResults = (items: Channel[]) => {
|
|
521
|
+
const filtered = original(items) as Channel[];
|
|
522
|
+
return this.filterItemsByOwnership({ paginator, items: filtered });
|
|
523
|
+
};
|
|
524
|
+
this.filterQueryResultsBeforeWrapping.set(paginator, original);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Undo `wrapPaginatorFiltering` — restore the `filterQueryResults` the paginator had before this
|
|
529
|
+
* manager wrapped it. A paginator that has left the manager must not keep applying its ownership
|
|
530
|
+
* rules: those are resolved against the paginators the manager still holds, so a detached
|
|
531
|
+
* paginator would drop channels owned by lists it is no longer part of.
|
|
532
|
+
*/
|
|
533
|
+
protected unwrapPaginatorFiltering(paginator: ChannelPaginator) {
|
|
534
|
+
const original = this.filterQueryResultsBeforeWrapping.get(paginator);
|
|
535
|
+
if (!original) return;
|
|
536
|
+
paginator.filterQueryResults = original;
|
|
537
|
+
this.filterQueryResultsBeforeWrapping.delete(paginator);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
getPaginatorById(id: string) {
|
|
541
|
+
return this.paginators.find((p) => p.id === id);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Replace the whole set of lists in a single state update — the primitive `insertPaginator` and
|
|
546
|
+
* `removePaginator` build on, and what to use for a wholesale swap instead of publishing an
|
|
547
|
+
* intermediate state per step. Paginators dropped from the set are detached exactly as
|
|
548
|
+
* `removePaginator` detaches them; a repeated id keeps only its first occurrence.
|
|
549
|
+
*
|
|
550
|
+
* @param paginators - The lists this manager should hold, in render order.
|
|
551
|
+
*/
|
|
552
|
+
setPaginators(paginators: ChannelPaginator[]) {
|
|
553
|
+
const nextPaginators: ChannelPaginator[] = [];
|
|
554
|
+
const seenIds = new Set<string>();
|
|
555
|
+
for (const paginator of paginators) {
|
|
556
|
+
if (seenIds.has(paginator.id)) continue;
|
|
557
|
+
seenIds.add(paginator.id);
|
|
558
|
+
nextPaginators.push(paginator);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const currentPaginators = this.paginators;
|
|
562
|
+
// Publishing an equivalent set would hand subscribers a new array for no reason — a
|
|
563
|
+
// `state.paginators` selector shallow-compares by reference, so every list would re-render.
|
|
564
|
+
const isUnchanged =
|
|
565
|
+
currentPaginators.length === nextPaginators.length &&
|
|
566
|
+
currentPaginators.every((paginator, i) => paginator === nextPaginators[i]);
|
|
567
|
+
if (isUnchanged) return;
|
|
568
|
+
|
|
569
|
+
const detached = currentPaginators.filter(
|
|
570
|
+
(paginator) => !nextPaginators.includes(paginator),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
this.state.partialNext({ paginators: nextPaginators });
|
|
574
|
+
|
|
575
|
+
detached.forEach((paginator) => {
|
|
576
|
+
this.unwrapPaginatorFiltering(paginator);
|
|
577
|
+
paginator.cancelScheduledQuery();
|
|
578
|
+
});
|
|
579
|
+
// Wrap the current set to enforce ownership on their query results
|
|
580
|
+
nextPaginators.forEach((paginator) => this.wrapPaginatorFiltering(paginator));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Detach every list at once — the teardown counterpart of `setPaginators`. Each paginator is
|
|
585
|
+
* released exactly as `removePaginator` releases it (ownership-filtering wrapper removed,
|
|
586
|
+
* scheduled query canceled, loaded items kept), in a single state update.
|
|
587
|
+
*
|
|
588
|
+
* @returns The paginators the manager held, in the order it held them.
|
|
589
|
+
*/
|
|
590
|
+
clearPaginators(): ChannelPaginator[] {
|
|
591
|
+
const cleared = this.paginators;
|
|
592
|
+
this.setPaginators([]);
|
|
593
|
+
return cleared;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* If paginator already exists → remove old, reinsert at new index.
|
|
598
|
+
* If index not provided → append at the end.
|
|
599
|
+
* If index provided → insert (or move) at that index.
|
|
600
|
+
*
|
|
601
|
+
* @param params - The insertion parameters.
|
|
602
|
+
* @param params.paginator - The paginator to insert or move.
|
|
603
|
+
* @param params.index - Target index; when omitted the paginator is appended.
|
|
604
|
+
*/
|
|
605
|
+
insertPaginator({ paginator, index }: { paginator: ChannelPaginator; index?: number }) {
|
|
606
|
+
const paginators = [...this.paginators];
|
|
607
|
+
const existingIndex = paginators.findIndex((p) => p.id === paginator.id);
|
|
608
|
+
if (existingIndex > -1) {
|
|
609
|
+
paginators.splice(existingIndex, 1);
|
|
610
|
+
}
|
|
611
|
+
const validIndex = Math.max(
|
|
612
|
+
0,
|
|
613
|
+
Math.min(index ?? paginators.length, paginators.length),
|
|
614
|
+
);
|
|
615
|
+
paginators.splice(validIndex, 0, paginator);
|
|
616
|
+
this.setPaginators(paginators);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Remove a paginator from the manager. The paginator stops receiving WS-driven updates and
|
|
621
|
+
* disappears from `state.paginators` (so UIs rendering one list per paginator drop its list), and
|
|
622
|
+
* gets its own `filterQueryResults` back — from here on it is an independent paginator again.
|
|
623
|
+
*
|
|
624
|
+
* Its loaded items are left untouched: the paginator can be re-inserted later, or kept and
|
|
625
|
+
* queried on its own. Any query scheduled through `nextDebounced` is canceled, so a list that was
|
|
626
|
+
* just removed does not fire one more request.
|
|
627
|
+
*
|
|
628
|
+
* @param paginatorOrId - The paginator to remove, or its id.
|
|
629
|
+
* @returns The removed paginator, or `undefined` when this manager did not hold it.
|
|
630
|
+
*/
|
|
631
|
+
removePaginator(
|
|
632
|
+
paginatorOrId: ChannelPaginator | string,
|
|
633
|
+
): ChannelPaginator | undefined {
|
|
634
|
+
const id = typeof paginatorOrId === 'string' ? paginatorOrId : paginatorOrId.id;
|
|
635
|
+
const removed = this.getPaginatorById(id);
|
|
636
|
+
if (!removed) return undefined;
|
|
637
|
+
|
|
638
|
+
this.setPaginators(this.paginators.filter((paginator) => paginator !== removed));
|
|
639
|
+
return removed;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Discard the loaded data of every registered list, keeping the registrations themselves: which
|
|
644
|
+
* lists exist is application configuration, while the channels in them belong to the connected
|
|
645
|
+
* user and become invalid on `disconnectUser`. Each paginator returns to "never queried"
|
|
646
|
+
* (`items: undefined`) with its debounced query canceled. Deliberately not named `resetState` —
|
|
647
|
+
* this manager's own `state` *is* the paginator list, which this must not touch.
|
|
648
|
+
*/
|
|
649
|
+
resetPaginatorStates() {
|
|
650
|
+
this.paginators.forEach((paginator) => {
|
|
651
|
+
paginator.cancelScheduledQuery();
|
|
652
|
+
paginator.resetState();
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
addEventHandler({
|
|
657
|
+
eventType,
|
|
658
|
+
...payload
|
|
659
|
+
}: {
|
|
660
|
+
eventType: SupportedEventType;
|
|
661
|
+
} & InsertEventHandlerPayload<EventHandlerContext>): Unsubscribe {
|
|
662
|
+
return this.ensurePipeline(eventType).insert(payload);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
setEventHandlers({
|
|
666
|
+
eventType,
|
|
667
|
+
handlers,
|
|
668
|
+
}: {
|
|
669
|
+
eventType: SupportedEventType;
|
|
670
|
+
handlers: LabeledEventHandler<EventHandlerContext>[];
|
|
671
|
+
}) {
|
|
672
|
+
return this.ensurePipeline(eventType).replaceAll(handlers);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
removeEventHandlers({
|
|
676
|
+
eventType,
|
|
677
|
+
handlers,
|
|
678
|
+
}: {
|
|
679
|
+
eventType: SupportedEventType;
|
|
680
|
+
handlers: FindEventHandlerParams<EventHandlerContext>[];
|
|
681
|
+
}) {
|
|
682
|
+
const pipeline = this._pipelines.get(eventType);
|
|
683
|
+
if (!pipeline) return;
|
|
684
|
+
handlers.forEach((params) => pipeline.remove(params));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Subscribe to WS (and more buses via attachBus) */
|
|
688
|
+
registerSubscriptions(): Unsubscribe {
|
|
689
|
+
if (!this.hasSubscriptions) {
|
|
690
|
+
this.addUnsubscribeFunction(
|
|
691
|
+
// todo: maybe we should have a wrapper here to decide, whether the event is a LocalEventBus event or else supported by client
|
|
692
|
+
this.client.on((event) => {
|
|
693
|
+
const pipe = this._pipelines.get(event.type);
|
|
694
|
+
if (pipe) {
|
|
695
|
+
pipe.run(event, this.ctx);
|
|
696
|
+
}
|
|
697
|
+
}).unsubscribe,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
this.incrementRefCount();
|
|
702
|
+
return () => this.unregisterSubscriptions();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
ensurePipeline(
|
|
706
|
+
eventType: SupportedEventType,
|
|
707
|
+
): EventHandlerPipeline<EventHandlerContext> {
|
|
708
|
+
let pipe = this._pipelines.get(eventType);
|
|
709
|
+
if (!pipe) {
|
|
710
|
+
pipe = new EventHandlerPipeline<EventHandlerContext>({
|
|
711
|
+
id: `ChannelManager:${eventType}`,
|
|
712
|
+
});
|
|
713
|
+
this._pipelines.set(eventType, pipe);
|
|
714
|
+
}
|
|
715
|
+
return pipe;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
reload = async () =>
|
|
719
|
+
await Promise.allSettled(
|
|
720
|
+
this.paginators.map(async (paginator) => {
|
|
721
|
+
await paginator.reload();
|
|
722
|
+
}),
|
|
723
|
+
);
|
|
724
|
+
}
|