stream-chat 10.0.0-rc.15 → 10.0.0-rc.16
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 +485 -250
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +488 -250
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +485 -250
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/connection/ConnectionRecoveryManager.d.ts +11 -22
- package/dist/types/entityStore/EntityStore.d.ts +4 -0
- package/dist/types/messageComposer/linkPreviewsManager.d.ts +3 -3
- package/dist/types/messageComposer/messageComposer.d.ts +2 -2
- package/dist/types/offline-support/offline_support_api.d.ts +5 -6
- package/dist/types/pagination/paginators/BasePaginator.d.ts +8 -1
- package/dist/types/pagination/paginators/ChannelPaginator.d.ts +5 -1
- package/dist/types/pagination/paginators/ThreadPaginator.d.ts +57 -0
- package/dist/types/pagination/paginators/index.d.ts +1 -0
- package/dist/types/thread.d.ts +30 -21
- package/dist/types/thread_manager.d.ts +74 -32
- package/package.json +1 -1
- package/src/client.ts +18 -7
- package/src/connection/ConnectionRecoveryManager.ts +19 -24
- package/src/entityStore/EntityStore.ts +10 -0
- package/src/messageComposer/messageComposer.ts +3 -3
- package/src/messageOperations/createMessageOperations.ts +3 -3
- package/src/offline-support/offline_support_api.ts +5 -6
- package/src/offline-support/queueableOperations.ts +1 -1
- package/src/pagination/paginators/BasePaginator.ts +45 -8
- package/src/pagination/paginators/ChannelPaginator.ts +17 -3
- package/src/pagination/paginators/ThreadPaginator.ts +180 -0
- package/src/pagination/paginators/index.ts +1 -0
- package/src/thread.ts +188 -220
- package/src/thread_manager.ts +257 -173
|
@@ -71,6 +71,8 @@ __export(src_exports, {
|
|
|
71
71
|
DEFAULT_STOP_REFRESH_BOUNDARY_MS: () => DEFAULT_STOP_REFRESH_BOUNDARY_MS,
|
|
72
72
|
DEFAULT_TEXT_COMPOSER_CONFIG: () => DEFAULT_TEXT_COMPOSER_CONFIG,
|
|
73
73
|
DEFAULT_THREAD_MANAGER_CONFIG: () => DEFAULT_THREAD_MANAGER_CONFIG,
|
|
74
|
+
DEFAULT_THREAD_PAGE_SIZE: () => DEFAULT_THREAD_PAGE_SIZE,
|
|
75
|
+
DEFAULT_THREAD_QUERY: () => DEFAULT_THREAD_QUERY,
|
|
74
76
|
DEFAULT_WS_CONNECTION_CONFIG: () => DEFAULT_WS_CONNECTION_CONFIG,
|
|
75
77
|
EntityStore: () => EntityStore,
|
|
76
78
|
EventHandlerPipeline: () => EventHandlerPipeline,
|
|
@@ -125,6 +127,7 @@ __export(src_exports, {
|
|
|
125
127
|
TextComposerMiddlewareExecutor: () => TextComposerMiddlewareExecutor,
|
|
126
128
|
Thread: () => Thread,
|
|
127
129
|
ThreadManager: () => ThreadManager,
|
|
130
|
+
ThreadPaginator: () => ThreadPaginator,
|
|
128
131
|
TokenManager: () => TokenManager,
|
|
129
132
|
UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT: () => UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT,
|
|
130
133
|
UploadManager: () => UploadManager,
|
|
@@ -1446,6 +1449,14 @@ var EntityStore = class {
|
|
|
1446
1449
|
has(id) {
|
|
1447
1450
|
return this.byId.has(id);
|
|
1448
1451
|
}
|
|
1452
|
+
/** Whether `subscriber` is one of the holders of `id`. */
|
|
1453
|
+
isHeldBy(id, subscriber) {
|
|
1454
|
+
return this.subscribers.get(id)?.has(subscriber) ?? false;
|
|
1455
|
+
}
|
|
1456
|
+
/** Every entity currently in the store. */
|
|
1457
|
+
values() {
|
|
1458
|
+
return Array.from(this.byId.values());
|
|
1459
|
+
}
|
|
1449
1460
|
// ---- writes ----
|
|
1450
1461
|
/**
|
|
1451
1462
|
* Replaces the canonical copy of `entity` and notifies every subscriber watching its id —
|
|
@@ -2613,6 +2624,16 @@ var BasePaginator = class {
|
|
|
2613
2624
|
* {@link setIntervals}), which bypasses the per-interval {@link commitInterval}/{@link dropInterval}
|
|
2614
2625
|
* publishing. No-ops when the views are already empty so a reset does not emit needlessly.
|
|
2615
2626
|
*/
|
|
2627
|
+
/**
|
|
2628
|
+
* Drops every loaded interval and item without publishing anything, so the next ingest starts from
|
|
2629
|
+
* empty storage while `state.items` keeps showing the previous window until the caller publishes.
|
|
2630
|
+
*/
|
|
2631
|
+
clearItemStorage() {
|
|
2632
|
+
this.setIntervals([]);
|
|
2633
|
+
this.setActiveInterval(void 0);
|
|
2634
|
+
this._itemIndex.clear();
|
|
2635
|
+
this.clearIntervalViews();
|
|
2636
|
+
}
|
|
2616
2637
|
clearIntervalViews() {
|
|
2617
2638
|
this._pendingViewChangedIds.clear();
|
|
2618
2639
|
const { logicalHead, logicalTail, anchoredHead } = this.intervalViews.getLatestValue();
|
|
@@ -4080,10 +4101,7 @@ var BasePaginator = class {
|
|
|
4080
4101
|
if (!this.canExecuteQuery({ direction, reset })) return;
|
|
4081
4102
|
const isForcedReset = reset === "yes" && !keepPreviousItems;
|
|
4082
4103
|
if (isForcedReset) {
|
|
4083
|
-
this.
|
|
4084
|
-
this.setActiveInterval(void 0);
|
|
4085
|
-
this._itemIndex.clear();
|
|
4086
|
-
this.clearIntervalViews();
|
|
4104
|
+
this.clearItemStorage();
|
|
4087
4105
|
this.state.next(this.getStateBeforeFirstQuery());
|
|
4088
4106
|
} else if (reset === "yes" && !forcedQueryShape) {
|
|
4089
4107
|
this.state.partialNext({
|
|
@@ -4091,7 +4109,9 @@ var BasePaginator = class {
|
|
|
4091
4109
|
offset: this.config.initialOffset ?? 0
|
|
4092
4110
|
});
|
|
4093
4111
|
}
|
|
4094
|
-
const
|
|
4112
|
+
const isRefresh = reset === "yes" && keepPreviousItems && !forcedQueryShape;
|
|
4113
|
+
const pageSize = isRefresh ? Math.max(this.pageSize, this.items?.length ?? 0) : this.pageSize;
|
|
4114
|
+
const queryShape = forcedQueryShape ?? this.getNextQueryShape({ direction, pageSize });
|
|
4095
4115
|
const isFirstPage = this.isFirstPageQuery({ queryShape, reset });
|
|
4096
4116
|
if (isFirstPage && !keepPreviousItems) {
|
|
4097
4117
|
const state = this.getStateBeforeFirstQuery();
|
|
@@ -4155,6 +4175,7 @@ var BasePaginator = class {
|
|
|
4155
4175
|
const filteredItems = this.filterQueryResults(items);
|
|
4156
4176
|
stateUpdate.items = filteredItems;
|
|
4157
4177
|
const isJumpQuery = !!queryShape && this.isJumpQueryShape(queryShape);
|
|
4178
|
+
const keptItemIds = isFirstPage && keepPreviousItems && !this.isCursorPagination && this.items?.length ? new Set(this.items.map((item) => this.getItemId(item))) : void 0;
|
|
4158
4179
|
const interval = this.ingestPage({
|
|
4159
4180
|
page: stateUpdate.items,
|
|
4160
4181
|
policy: isJumpQuery ? "strict-overlap-only" : "auto",
|
|
@@ -4196,7 +4217,15 @@ var BasePaginator = class {
|
|
|
4196
4217
|
} else {
|
|
4197
4218
|
const startOffset = this.offset ?? 0;
|
|
4198
4219
|
stateUpdate.offset = startOffset + items.length;
|
|
4199
|
-
|
|
4220
|
+
const lastPageItem = items[items.length - 1];
|
|
4221
|
+
if (interval && keptItemIds && lastPageItem && keptItemIds.has(this.getItemId(lastPageItem))) {
|
|
4222
|
+
stateUpdate.offset = Math.max(
|
|
4223
|
+
stateUpdate.offset,
|
|
4224
|
+
startOffset + this.intervalToItems(interval).length
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4227
|
+
stateUpdate.hasMoreTail = items.length >= this.pageSize;
|
|
4228
|
+
if (isFirstPage) stateUpdate.hasMoreHead = (this.config.initialOffset ?? 0) > 0;
|
|
4200
4229
|
}
|
|
4201
4230
|
if (interval) {
|
|
4202
4231
|
const current = this.state.getLatestValue();
|
|
@@ -4224,7 +4253,7 @@ var BasePaginator = class {
|
|
|
4224
4253
|
}
|
|
4225
4254
|
const state = this.getStateAfterQuery(stateUpdate, isFirstPage);
|
|
4226
4255
|
if (updateState) this.state.next(state);
|
|
4227
|
-
this.populateOfflineDbAfterQuery({ items: state.items, queryShape });
|
|
4256
|
+
if (isFirstPage) this.populateOfflineDbAfterQuery({ items: state.items, queryShape });
|
|
4228
4257
|
return {
|
|
4229
4258
|
stateCandidate: state,
|
|
4230
4259
|
targetInterval: interval
|
|
@@ -5854,7 +5883,7 @@ var QUEUEABLE_OPERATIONS = {
|
|
|
5854
5883
|
const message = result?.message;
|
|
5855
5884
|
if (!message) return;
|
|
5856
5885
|
if (message.parent_id) {
|
|
5857
|
-
client.threads.
|
|
5886
|
+
client.threads.get(message.parent_id)?.upsertReplyLocally({
|
|
5858
5887
|
message,
|
|
5859
5888
|
timestampChanged: true
|
|
5860
5889
|
});
|
|
@@ -8153,6 +8182,8 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8153
8182
|
* continuation rather than a first page. See `executeQuery`.
|
|
8154
8183
|
*/
|
|
8155
8184
|
this._itemsLoadedFromOfflineDB = false;
|
|
8185
|
+
/** The query in flight refreshes a cache-only window, so its response replaces the window. */
|
|
8186
|
+
this._replaceOfflineSeedOnResponse = false;
|
|
8156
8187
|
/**
|
|
8157
8188
|
* Filters sent to the server with the next query (and used as the offline-db query key). Deliberately
|
|
8158
8189
|
* built from the locally configured filters only — feeding the backend-resolved filter back in would
|
|
@@ -8209,6 +8240,7 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8209
8240
|
});
|
|
8210
8241
|
};
|
|
8211
8242
|
this.query = async () => {
|
|
8243
|
+
if (this._itemsLoadedFromOfflineDB) this._replaceOfflineSeedOnResponse = true;
|
|
8212
8244
|
this._itemsLoadedFromOfflineDB = false;
|
|
8213
8245
|
if (!this._nextQueryShape) {
|
|
8214
8246
|
this._nextQueryShape = this.getNextQueryShape();
|
|
@@ -8376,14 +8408,22 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8376
8408
|
if (params.isFirstPage && params.results) {
|
|
8377
8409
|
this.applyPredefinedFilterResponse(pendingPredefinedFilter);
|
|
8378
8410
|
}
|
|
8411
|
+
const replaceOfflineSeed = this._replaceOfflineSeedOnResponse;
|
|
8412
|
+
this._replaceOfflineSeedOnResponse = false;
|
|
8413
|
+
if (replaceOfflineSeed && params.results) {
|
|
8414
|
+
this.clearItemStorage();
|
|
8415
|
+
return super.postQueryReconcile({ ...params, keepPreviousItems: false });
|
|
8416
|
+
}
|
|
8379
8417
|
return super.postQueryReconcile(params);
|
|
8380
8418
|
}
|
|
8381
8419
|
// invoked inside BasePaginator.executeQuery() to keep it as a query descriptor;
|
|
8382
|
-
getNextQueryShape(
|
|
8420
|
+
getNextQueryShape({
|
|
8421
|
+
pageSize
|
|
8422
|
+
} = {}) {
|
|
8383
8423
|
const shape = {
|
|
8384
8424
|
filter_conditions: this.buildQueryFilters(),
|
|
8385
8425
|
...this.options,
|
|
8386
|
-
limit: this.pageSize,
|
|
8426
|
+
limit: pageSize ?? this.pageSize,
|
|
8387
8427
|
offset: this.offset
|
|
8388
8428
|
};
|
|
8389
8429
|
if (this.sort) {
|
|
@@ -8420,7 +8460,7 @@ var ChannelPaginator = class extends BasePaginator {
|
|
|
8420
8460
|
}) {
|
|
8421
8461
|
this.client.offlineDb?.executeQuerySafely(
|
|
8422
8462
|
(db) => db.upsertCidsForQuery({
|
|
8423
|
-
cids,
|
|
8463
|
+
cids: cids.slice(0, this.pageSize),
|
|
8424
8464
|
filters: request.filter_conditions,
|
|
8425
8465
|
options: request,
|
|
8426
8466
|
predefinedFilter: this._predefinedFilter,
|
|
@@ -9022,6 +9062,130 @@ var UserGroupPaginator = class extends BasePaginator {
|
|
|
9022
9062
|
}
|
|
9023
9063
|
};
|
|
9024
9064
|
|
|
9065
|
+
// src/pagination/paginators/ThreadPaginator.ts
|
|
9066
|
+
var DEFAULT_THREAD_PAGE_SIZE = 25;
|
|
9067
|
+
var DEFAULT_THREAD_QUERY = {
|
|
9068
|
+
participant_limit: 10,
|
|
9069
|
+
reply_limit: 10,
|
|
9070
|
+
watch: true
|
|
9071
|
+
};
|
|
9072
|
+
var getQueryIdentity2 = (queryShape) => {
|
|
9073
|
+
if (!queryShape) return queryShape;
|
|
9074
|
+
const { limit: _, next: __, prev: ___, ...identity } = queryShape;
|
|
9075
|
+
return identity;
|
|
9076
|
+
};
|
|
9077
|
+
var hasPaginationQueryShapeChanged3 = (prevQueryShape, nextQueryShape) => !isEqual(getQueryIdentity2(prevQueryShape), getQueryIdentity2(nextQueryShape));
|
|
9078
|
+
var ThreadPaginator = class extends BasePaginator {
|
|
9079
|
+
constructor({
|
|
9080
|
+
client,
|
|
9081
|
+
store = new EntityStore({ getEntityId: (thread) => thread.id }),
|
|
9082
|
+
paginatorOptions
|
|
9083
|
+
}) {
|
|
9084
|
+
super({
|
|
9085
|
+
hasPaginationQueryShapeChanged: hasPaginationQueryShapeChanged3,
|
|
9086
|
+
initialCursor: ZERO_PAGE_CURSOR,
|
|
9087
|
+
itemIndex: new StoreBackedItemIndex({
|
|
9088
|
+
getEntityId: (thread) => thread.id,
|
|
9089
|
+
store
|
|
9090
|
+
}),
|
|
9091
|
+
pageSize: DEFAULT_THREAD_PAGE_SIZE,
|
|
9092
|
+
...paginatorOptions
|
|
9093
|
+
});
|
|
9094
|
+
this.query = async ({
|
|
9095
|
+
queryShape
|
|
9096
|
+
}) => {
|
|
9097
|
+
const { threads, next } = await this.client.queryThreadsAndHydrate(queryShape);
|
|
9098
|
+
return { items: threads.map(this.toStoredInstance), tailward: next };
|
|
9099
|
+
};
|
|
9100
|
+
/**
|
|
9101
|
+
* The stored instance for a queried thread, so there's one per id. The index upserts, so a second
|
|
9102
|
+
* instance would replace the stored one. A stale stored thread is refreshed from the query, unless
|
|
9103
|
+
* it's open (active) at which point it reloads itself and this would replace the replies on screen.
|
|
9104
|
+
*/
|
|
9105
|
+
this.toStoredInstance = (incoming) => {
|
|
9106
|
+
const stored = this.store.get(incoming.id);
|
|
9107
|
+
if (!stored) return incoming;
|
|
9108
|
+
if (stored.hasStaleState && !stored.state.getLatestValue().active) {
|
|
9109
|
+
stored.hydrateState(incoming);
|
|
9110
|
+
}
|
|
9111
|
+
return stored;
|
|
9112
|
+
};
|
|
9113
|
+
this.filterQueryResults = (threads) => threads;
|
|
9114
|
+
/**
|
|
9115
|
+
* Re-queries the first page and swaps the list in place, in server order; also the first load. The
|
|
9116
|
+
* current threads stay until the response lands, and a failed query leaves them untouched and
|
|
9117
|
+
* records `lastQueryError`; a successful one clears it. Like any paginator, the first load publishes
|
|
9118
|
+
* `isLoading`; a reload of a loaded list does not, since `isLoading` with items means the next page.
|
|
9119
|
+
*/
|
|
9120
|
+
this.reload = async ({ limit = this.pageSize } = {}) => {
|
|
9121
|
+
const isFirstLoad = !this.hasResults;
|
|
9122
|
+
if (isFirstLoad) this.state.partialNext({ isLoading: true });
|
|
9123
|
+
let replaced = false;
|
|
9124
|
+
try {
|
|
9125
|
+
const results = await this.runQueryRetryable({
|
|
9126
|
+
queryShape: { ...DEFAULT_THREAD_QUERY, limit },
|
|
9127
|
+
retryCount: this.config.retryCount
|
|
9128
|
+
});
|
|
9129
|
+
if (!results) return;
|
|
9130
|
+
const nextIds = new Set(results.items.map((thread) => thread.id));
|
|
9131
|
+
this.setIntervals([]);
|
|
9132
|
+
this.setActiveInterval(void 0);
|
|
9133
|
+
for (const [id] of this._itemIndex.entries()) {
|
|
9134
|
+
if (!nextIds.has(id)) this._itemIndex.remove(id);
|
|
9135
|
+
}
|
|
9136
|
+
this.setItems({
|
|
9137
|
+
cursor: { headward: void 0, tailward: results.tailward },
|
|
9138
|
+
isFirstPage: true,
|
|
9139
|
+
isLastPage: !results.tailward,
|
|
9140
|
+
valueOrFactory: results.items
|
|
9141
|
+
});
|
|
9142
|
+
replaced = true;
|
|
9143
|
+
} finally {
|
|
9144
|
+
const clearError = replaced && this.lastQueryError;
|
|
9145
|
+
if (isFirstLoad || clearError) {
|
|
9146
|
+
this.state.partialNext({
|
|
9147
|
+
...isFirstLoad ? { isLoading: false } : {},
|
|
9148
|
+
...clearError ? { lastQueryError: void 0 } : {}
|
|
9149
|
+
});
|
|
9150
|
+
}
|
|
9151
|
+
}
|
|
9152
|
+
};
|
|
9153
|
+
this.client = client;
|
|
9154
|
+
this.store = store;
|
|
9155
|
+
}
|
|
9156
|
+
/**
|
|
9157
|
+
* No next page until a load has landed (`master`'s `nextCursor: null`), so `toTail()` is a no-op
|
|
9158
|
+
* before the first successful reload, e.g. when an empty list fires `onEndReached` on mount.
|
|
9159
|
+
*/
|
|
9160
|
+
get initialState() {
|
|
9161
|
+
return {
|
|
9162
|
+
...super.initialState,
|
|
9163
|
+
hasMoreTail: false
|
|
9164
|
+
};
|
|
9165
|
+
}
|
|
9166
|
+
getItemId(thread) {
|
|
9167
|
+
return thread.id;
|
|
9168
|
+
}
|
|
9169
|
+
getNextQueryShape({
|
|
9170
|
+
direction
|
|
9171
|
+
}) {
|
|
9172
|
+
const cursor = this.cursor?.[direction] ?? void 0;
|
|
9173
|
+
return {
|
|
9174
|
+
...DEFAULT_THREAD_QUERY,
|
|
9175
|
+
limit: this.pageSize,
|
|
9176
|
+
...cursor ? { [direction === "tailward" ? "next" : "prev"]: cursor } : {}
|
|
9177
|
+
};
|
|
9178
|
+
}
|
|
9179
|
+
/**
|
|
9180
|
+
* Also releases the list's hold on its threads, which the base reset leaves in the index. Released
|
|
9181
|
+
* before the base publishes, so a subscriber sees the threads already gone from the registry.
|
|
9182
|
+
*/
|
|
9183
|
+
resetState() {
|
|
9184
|
+
this._itemIndex.clear();
|
|
9185
|
+
super.resetState();
|
|
9186
|
+
}
|
|
9187
|
+
};
|
|
9188
|
+
|
|
9025
9189
|
// src/search/ChannelMemberSearchSource.ts
|
|
9026
9190
|
var ChannelMemberSearchSource = class extends BaseSearchSource {
|
|
9027
9191
|
constructor(channel, options, filterBuilderOptions = {}) {
|
|
@@ -12621,7 +12785,7 @@ var createMessageOperations = (collection) => {
|
|
|
12621
12785
|
channel.messagePaginator.removeItem({ id });
|
|
12622
12786
|
channel.pinnedMessagesPaginator.removeItem({ id });
|
|
12623
12787
|
if (parentId) {
|
|
12624
|
-
client.threads.
|
|
12788
|
+
client.threads.get(parentId)?.messagePaginator.removeItem({ id });
|
|
12625
12789
|
}
|
|
12626
12790
|
},
|
|
12627
12791
|
...parentMessageId ? {
|
|
@@ -12666,7 +12830,7 @@ var createMessageOperations = (collection) => {
|
|
|
12666
12830
|
update: async (m, o) => {
|
|
12667
12831
|
const result = await channel.getClient().updateMessage({
|
|
12668
12832
|
id: m.id,
|
|
12669
|
-
message:
|
|
12833
|
+
message: toUpdatedMessagePayload(m),
|
|
12670
12834
|
...o
|
|
12671
12835
|
});
|
|
12672
12836
|
return { message: result.message };
|
|
@@ -12777,10 +12941,15 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12777
12941
|
* reload on reconnect, so an unbalanced `deactivate()` now costs more than a missed auto-read —
|
|
12778
12942
|
* hence the refcount, matching `channel.activate()`. A thread held by more than one mount stays
|
|
12779
12943
|
* active until the last holder releases it.
|
|
12944
|
+
*
|
|
12945
|
+
* The first activation also keeps the thread in `client.threads` for the rest of the session:
|
|
12946
|
+
* it stays subscribed and resolvable through `client.threads.get(id)` after it is deactivated,
|
|
12947
|
+
* whether or not the thread list holds it. Reopening it therefore needs no fetch unless it went stale.
|
|
12780
12948
|
*/
|
|
12781
12949
|
this.activate = () => {
|
|
12782
12950
|
this._activeRefCount += 1;
|
|
12783
12951
|
if (this._activeRefCount === 1) {
|
|
12952
|
+
this.client.threads.register(this);
|
|
12784
12953
|
this.state.partialNext({ active: true });
|
|
12785
12954
|
}
|
|
12786
12955
|
};
|
|
@@ -12802,10 +12971,9 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12802
12971
|
*
|
|
12803
12972
|
* Preserves failed (unsent) replies. They are read out of the reply paginator rather than out of
|
|
12804
12973
|
* `failedRepliesMap`, because that map is only written by `upsertReplyLocally`, whose callers are
|
|
12805
|
-
* this thread's own subscriptions and the offline-DB path
|
|
12806
|
-
* neither of which covers a thread
|
|
12807
|
-
*
|
|
12808
|
-
* is true for managed and unmanaged threads alike. An overlap merge keeps them anyway (the
|
|
12974
|
+
* this thread's own subscriptions and the offline-DB path resolved through `client.threads.get` —
|
|
12975
|
+
* neither of which covers a thread that was never activated or listed. Reading the paginator
|
|
12976
|
+
* is true for registered and unregistered threads alike. An overlap merge keeps them anyway (the
|
|
12809
12977
|
* reconcile's provenance guard never prunes a non-server message); only a disjoint rebuild can drop
|
|
12810
12978
|
* them, so any that actually fell out are re-ingested below.
|
|
12811
12979
|
*
|
|
@@ -12889,6 +13057,7 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12889
13057
|
this.client.messageStore.upsert(parentMessage);
|
|
12890
13058
|
}
|
|
12891
13059
|
const incomingReplies = thread.messagePaginator.state.getLatestValue().items ?? [];
|
|
13060
|
+
if (!options?.reconcile) this.messagePaginator.clearStateAndCache();
|
|
12892
13061
|
if (typeof this.messagePaginator.items === "undefined") {
|
|
12893
13062
|
this.messagePaginator.setItems({
|
|
12894
13063
|
valueOrFactory: incomingReplies,
|
|
@@ -12907,16 +13076,9 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12907
13076
|
}
|
|
12908
13077
|
this.addUnsubscribeFunction(this.subscribeThreadSetupStateChange());
|
|
12909
13078
|
this.addUnsubscribeFunction(this.subscribeParentMessageFromStore());
|
|
12910
|
-
this.addUnsubscribeFunction(this.subscribeThreadUpdated());
|
|
12911
13079
|
this.addUnsubscribeFunction(this.subscribeMarkActiveThreadRead());
|
|
12912
13080
|
this.addUnsubscribeFunction(this.subscribeReloadActiveStaleThread());
|
|
12913
|
-
this.addUnsubscribeFunction(this.
|
|
12914
|
-
this.addUnsubscribeFunction(this.subscribeNewReplies());
|
|
12915
|
-
this.addUnsubscribeFunction(this.subscribeRepliesRead());
|
|
12916
|
-
this.addUnsubscribeFunction(this.subscribeRepliesUnread());
|
|
12917
|
-
this.addUnsubscribeFunction(this.subscribeMessageDeleted());
|
|
12918
|
-
this.addUnsubscribeFunction(this.subscribeMessageUpdated());
|
|
12919
|
-
this.addUnsubscribeFunction(this.subscribeUserMessagesDeleted());
|
|
13081
|
+
this.addUnsubscribeFunction(this.subscribeMarkStaleOnStopWatching());
|
|
12920
13082
|
};
|
|
12921
13083
|
/**
|
|
12922
13084
|
* Subscribes this thread to the `'thread'` configuration key. Registered through
|
|
@@ -12933,10 +13095,10 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12933
13095
|
* `hasLiveInstances('thread')` does not count it when deciding whether to warn about a
|
|
12934
13096
|
* construction-only path registered too late.
|
|
12935
13097
|
*
|
|
12936
|
-
* So read "declarative configuration is unaffected" as *at construction only*. A thread
|
|
12937
|
-
* `ThreadManager` that has itself registered is covered —
|
|
12938
|
-
* `registerSubscriptions()` on every thread
|
|
12939
|
-
*
|
|
13098
|
+
* So read "declarative configuration is unaffected" as *at construction only*. A thread registered
|
|
13099
|
+
* with a `ThreadManager` that has itself registered is covered — the manager calls
|
|
13100
|
+
* `registerSubscriptions()` on every thread the list holds or that is active — so the common path
|
|
13101
|
+
* is fine. A thread that is neither, or held by an unregistered manager, is not.
|
|
12940
13102
|
*
|
|
12941
13103
|
* The alternative — applying the setup function at construction — would break the teardown symmetry
|
|
12942
13104
|
* `WithSubscriptions` provides, which is why the asymmetry stands.
|
|
@@ -12952,7 +13114,7 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12952
13114
|
// on a change there, so the setup function's overrides survive.
|
|
12953
13115
|
alsoWatch: ["messagePaginator", "messageOperations"]
|
|
12954
13116
|
});
|
|
12955
|
-
this.
|
|
13117
|
+
this.handleThreadUpdated = (event) => {
|
|
12956
13118
|
if (!event.thread || event.thread.parent_message_id !== this.id) {
|
|
12957
13119
|
return;
|
|
12958
13120
|
}
|
|
@@ -12963,7 +13125,7 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12963
13125
|
deletedAt: threadData.deleted_at ?? null,
|
|
12964
13126
|
custom: threadData.custom ?? {}
|
|
12965
13127
|
});
|
|
12966
|
-
}
|
|
13128
|
+
};
|
|
12967
13129
|
this.subscribeMarkActiveThreadRead = () => this.state.subscribeWithSelector(
|
|
12968
13130
|
(nextValue) => ({
|
|
12969
13131
|
active: nextValue.active,
|
|
@@ -12982,14 +13144,19 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
12982
13144
|
}
|
|
12983
13145
|
}
|
|
12984
13146
|
);
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
13147
|
+
/**
|
|
13148
|
+
* A watch stopped while the connection stays up (`NotWatching`) ends the channel's events with no
|
|
13149
|
+
* reconnect to catch it. A dropped connection (`WasWatching`) is left to recovery.
|
|
13150
|
+
*/
|
|
13151
|
+
this.subscribeMarkStaleOnStopWatching = () => this.channel.state.subscribeWithSelector(
|
|
13152
|
+
({ watchStatus }) => ({ watchStatus }),
|
|
13153
|
+
({ watchStatus }, previous) => {
|
|
13154
|
+
if (previous?.watchStatus === ChannelWatchStatus.Watching && watchStatus === ChannelWatchStatus.NotWatching) {
|
|
13155
|
+
this.state.partialNext({ isStateStale: true });
|
|
13156
|
+
}
|
|
12989
13157
|
}
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
this.subscribeRepliesUnread = () => this.client.on("notification.mark_unread", (event) => {
|
|
13158
|
+
);
|
|
13159
|
+
this.handleRepliesUnread = (event) => {
|
|
12993
13160
|
if (!event.user || event.created_at == null || !event.thread_id) return;
|
|
12994
13161
|
if (event.thread_id !== this.id) return;
|
|
12995
13162
|
const userId = event.user.id;
|
|
@@ -13008,8 +13175,8 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13008
13175
|
}
|
|
13009
13176
|
}
|
|
13010
13177
|
}));
|
|
13011
|
-
}
|
|
13012
|
-
this.
|
|
13178
|
+
};
|
|
13179
|
+
this.handleNewReply = (event) => {
|
|
13013
13180
|
if (!this.client.userId || event.message?.parent_id !== this.id) {
|
|
13014
13181
|
return;
|
|
13015
13182
|
}
|
|
@@ -13047,8 +13214,8 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13047
13214
|
}
|
|
13048
13215
|
}
|
|
13049
13216
|
this.state.partialNext({ read: nextRead });
|
|
13050
|
-
}
|
|
13051
|
-
this.
|
|
13217
|
+
};
|
|
13218
|
+
this.handleRepliesRead = (event) => {
|
|
13052
13219
|
if (!event.user || event.created_at == null || !event.thread) return;
|
|
13053
13220
|
if (event.thread.parent_message_id !== this.id) return;
|
|
13054
13221
|
const userId = event.user.id;
|
|
@@ -13066,8 +13233,8 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13066
13233
|
}
|
|
13067
13234
|
}
|
|
13068
13235
|
}));
|
|
13069
|
-
}
|
|
13070
|
-
this.
|
|
13236
|
+
};
|
|
13237
|
+
this.handleMessageDeleted = (event) => {
|
|
13071
13238
|
if (!event.message) return;
|
|
13072
13239
|
const formattedMessage = formatMessage(event.message);
|
|
13073
13240
|
if (event.message.parent_id === this.id) {
|
|
@@ -13081,52 +13248,35 @@ var Thread = class extends WithMessageOperations(WithSubscriptions) {
|
|
|
13081
13248
|
this.updateParentMessageLocally({ message: event.message });
|
|
13082
13249
|
}
|
|
13083
13250
|
this.messagePaginator.reflectQuotedMessageUpdate(formattedMessage);
|
|
13084
|
-
}).unsubscribe;
|
|
13085
|
-
this.subscribeMessageUpdated = () => {
|
|
13086
|
-
const messageUpdateTypes = ["message.updated", "message.undeleted"];
|
|
13087
|
-
const reactionTypes = [
|
|
13088
|
-
"reaction.new",
|
|
13089
|
-
"reaction.deleted",
|
|
13090
|
-
"reaction.updated"
|
|
13091
|
-
];
|
|
13092
|
-
const unsubscribeMessageUpdated = messageUpdateTypes.map(
|
|
13093
|
-
(eventType) => this.client.on(eventType, (event) => {
|
|
13094
|
-
if (!event.message) return;
|
|
13095
|
-
const message = event.message.parent_id === this.id ? {
|
|
13096
|
-
...event.message,
|
|
13097
|
-
own_reactions: this.messagePaginator.getItem(event.message.id)?.own_reactions ?? event.message.own_reactions
|
|
13098
|
-
} : !event.message.parent_id && event.message.id === this.id ? {
|
|
13099
|
-
...event.message,
|
|
13100
|
-
own_reactions: this.state.getLatestValue().parentMessage?.own_reactions ?? event.message.own_reactions
|
|
13101
|
-
} : event.message;
|
|
13102
|
-
this.updateParentMessageOrReplyLocally(message);
|
|
13103
|
-
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(event.message));
|
|
13104
|
-
}).unsubscribe
|
|
13105
|
-
);
|
|
13106
|
-
const unsubscribeReactions = reactionTypes.map(
|
|
13107
|
-
(eventType) => this.client.on(eventType, (event) => {
|
|
13108
|
-
if (!event.message || !event.reaction) return;
|
|
13109
|
-
const { message } = event;
|
|
13110
|
-
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message));
|
|
13111
|
-
}).unsubscribe
|
|
13112
|
-
);
|
|
13113
|
-
const unsubscribeFunctions = [...unsubscribeMessageUpdated, ...unsubscribeReactions];
|
|
13114
|
-
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
13115
13251
|
};
|
|
13116
|
-
this.
|
|
13117
|
-
|
|
13118
|
-
const
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
|
|
13125
|
-
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13252
|
+
this.handleMessageUpdated = (event) => {
|
|
13253
|
+
if (!event.message) return;
|
|
13254
|
+
const message = event.message.parent_id === this.id ? {
|
|
13255
|
+
...event.message,
|
|
13256
|
+
own_reactions: this.messagePaginator.getItem(event.message.id)?.own_reactions ?? event.message.own_reactions
|
|
13257
|
+
} : !event.message.parent_id && event.message.id === this.id ? {
|
|
13258
|
+
...event.message,
|
|
13259
|
+
own_reactions: this.state.getLatestValue().parentMessage?.own_reactions ?? event.message.own_reactions
|
|
13260
|
+
} : event.message;
|
|
13261
|
+
this.updateParentMessageOrReplyLocally(message);
|
|
13262
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(event.message));
|
|
13263
|
+
};
|
|
13264
|
+
this.handleReactionChanged = (event) => {
|
|
13265
|
+
if (!event.message || !event.reaction) return;
|
|
13266
|
+
const { message } = event;
|
|
13267
|
+
this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message));
|
|
13268
|
+
};
|
|
13269
|
+
// Apply a user ban / deletion to this thread's own reply list. Previously
|
|
13270
|
+
// channel.state.deleteUserMessages marked banned-user replies deleted in the (now removed)
|
|
13271
|
+
// channel.state.threads shadow; the reply paginator is the thread's source of truth now.
|
|
13272
|
+
this.handleUserMessagesDeleted = (event) => {
|
|
13273
|
+
if (!event.user) return;
|
|
13274
|
+
const deletedAtSource = event.type === "user.deleted" ? event.user.deleted_at : event.created_at;
|
|
13275
|
+
this.messagePaginator.applyMessageDeletionForUser({
|
|
13276
|
+
userId: event.user.id,
|
|
13277
|
+
hardDelete: !!event.hard_delete,
|
|
13278
|
+
deletedAt: deletedAtSource ?? nowNs()
|
|
13279
|
+
});
|
|
13130
13280
|
};
|
|
13131
13281
|
// The parent message lives in the client-global message store (one canonical POJO per id);
|
|
13132
13282
|
// `state.parentMessage` and the fields derived from it are a projection of that copy. Every update
|
|
@@ -14221,13 +14371,13 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
|
|
|
14221
14371
|
*
|
|
14222
14372
|
* Thread replies live in `thread.messagePaginator`, which is independent of the channel's, so
|
|
14223
14373
|
* dispatching to the wrong one leaves the reply invisible where it was written. An edit composer
|
|
14224
|
-
* is built from the message rather than from its thread, so it
|
|
14225
|
-
*
|
|
14374
|
+
* is built from the message rather than from its thread, so it finds the thread only when
|
|
14375
|
+
* `client.threads` has it registered — listed, or active (open somewhere).
|
|
14226
14376
|
*/
|
|
14227
14377
|
get defaultSubmitTarget() {
|
|
14228
14378
|
if (this.compositionContext instanceof Thread) return this.compositionContext;
|
|
14229
14379
|
const { threadId } = this;
|
|
14230
|
-
return threadId && this.client.threads.
|
|
14380
|
+
return threadId && this.client.threads.get(threadId) || this.channel;
|
|
14231
14381
|
}
|
|
14232
14382
|
/**
|
|
14233
14383
|
* Whether submitting keeps the composer's contents rather than clearing them - see
|
|
@@ -18457,26 +18607,126 @@ var eventCarriesOwnUser = (event) => Object.hasOwn(event, "me");
|
|
|
18457
18607
|
var DEFAULT_THREAD_MANAGER_CONFIG = deepFreezeConfig({
|
|
18458
18608
|
connectionRecoveryThrottleMs: 1e3
|
|
18459
18609
|
});
|
|
18460
|
-
var MAX_QUERY_THREADS_LIMIT = 25;
|
|
18461
18610
|
var THREAD_MANAGER_INITIAL_STATE = {
|
|
18462
18611
|
active: false,
|
|
18463
|
-
wasActivatedAtLeastOnce: false,
|
|
18464
18612
|
isThreadOrderStale: false,
|
|
18465
|
-
threads: [],
|
|
18466
18613
|
unreadThreadCount: 0,
|
|
18467
18614
|
unseenThreadIds: [],
|
|
18468
|
-
|
|
18469
|
-
isLoading: false,
|
|
18470
|
-
isLoadingNext: false,
|
|
18471
|
-
nextCursor: null
|
|
18472
|
-
},
|
|
18473
|
-
ready: false
|
|
18615
|
+
wasActivatedAtLeastOnce: false
|
|
18474
18616
|
};
|
|
18475
18617
|
var logger10 = chatLoggerSystem.getLogger("thread-manager");
|
|
18618
|
+
var getThreadId = (thread) => thread.id;
|
|
18619
|
+
var REGISTERED_HOLDER = { onEntitiesChanged: () => void 0 };
|
|
18476
18620
|
var ThreadManager = class extends WithSubscriptions {
|
|
18477
18621
|
constructor({ client }) {
|
|
18478
18622
|
super();
|
|
18623
|
+
/**
|
|
18624
|
+
* Every live thread, held by the list (the paginator's own index over this store) and/or
|
|
18625
|
+
* `REGISTERED_HOLDER` (opened this session). A thread leaves once neither holds it.
|
|
18626
|
+
*/
|
|
18627
|
+
this.threadStore = new EntityStore({ getEntityId: getThreadId });
|
|
18628
|
+
/** A reload is in flight; a second one is dropped (`master`'s `pagination.isLoading` guard). */
|
|
18629
|
+
this.isReloadInFlight = false;
|
|
18630
|
+
/**
|
|
18631
|
+
* A local `pendingDisposal` listener (a state subscription, not a server watch) per channel instance
|
|
18632
|
+
* that has had a listed or opened thread; removed when that channel is disposed or on reset.
|
|
18633
|
+
*/
|
|
18634
|
+
this.disposalListeners = /* @__PURE__ */ new Map();
|
|
18635
|
+
/**
|
|
18636
|
+
* The live thread for `id`: one in the list, or one opened this session (e.g. from a message
|
|
18637
|
+
* list). Resolve threads through this, not `paginator.items`, which is the list only; list
|
|
18638
|
+
* membership is `paginator.getItem(id)`.
|
|
18639
|
+
*/
|
|
18640
|
+
this.get = (id) => this.threadStore.get(id);
|
|
18641
|
+
/**
|
|
18642
|
+
* Returns the thread for `parentMessage`, creating it if it doesn't exist yet. Use this to open a
|
|
18643
|
+
* thread instead of calling `new Thread()`.
|
|
18644
|
+
*
|
|
18645
|
+
* The returned thread is registered right away. If the thread list loads it before the UI
|
|
18646
|
+
* activates it, the list reuses this instance instead of creating a duplicate.
|
|
18647
|
+
*
|
|
18648
|
+
* A thread created here only has its parent message, so it starts stale and it loads its thread data
|
|
18649
|
+
* (participants, read state, replies) once, the first time it's opened.
|
|
18650
|
+
*/
|
|
18651
|
+
this.ensure = ({
|
|
18652
|
+
channel,
|
|
18653
|
+
parentMessage
|
|
18654
|
+
}) => {
|
|
18655
|
+
let thread = this.threadStore.get(parentMessage.id);
|
|
18656
|
+
if (!thread) {
|
|
18657
|
+
thread = new Thread({ channel, client: this.client, parentMessage });
|
|
18658
|
+
thread.state.partialNext({ isStateStale: true });
|
|
18659
|
+
}
|
|
18660
|
+
this.register(thread);
|
|
18661
|
+
return thread;
|
|
18662
|
+
};
|
|
18663
|
+
/**
|
|
18664
|
+
* Called by `thread.activate()`. Keeps `thread` for the session: it resolves through
|
|
18665
|
+
* {@link ThreadManager.get} and stays subscribed whether or not the list holds it, until its
|
|
18666
|
+
* channel is disposed or the user disconnects.
|
|
18667
|
+
*
|
|
18668
|
+
* @internal
|
|
18669
|
+
*/
|
|
18670
|
+
this.register = (thread) => {
|
|
18671
|
+
const { id } = thread;
|
|
18672
|
+
const registered = this.threadStore.get(id);
|
|
18673
|
+
if (registered && registered !== thread) {
|
|
18674
|
+
logger10.withExtraTags("register").warn(
|
|
18675
|
+
"Another instance of this thread is registered; this one stays unmanaged.",
|
|
18676
|
+
{
|
|
18677
|
+
threadId: id
|
|
18678
|
+
}
|
|
18679
|
+
);
|
|
18680
|
+
return;
|
|
18681
|
+
}
|
|
18682
|
+
this.threadStore.link(id, REGISTERED_HOLDER);
|
|
18683
|
+
this.threadStore.upsert(thread, REGISTERED_HOLDER);
|
|
18684
|
+
if (this.hasSubscriptions) thread.registerSubscriptions();
|
|
18685
|
+
this.listenForDisposal(thread);
|
|
18686
|
+
};
|
|
18687
|
+
/** Releases the `REGISTERED_HOLDER` hold on `thread`; the list's own diff unsubscribes it if it is still listed. */
|
|
18688
|
+
this.release = (thread) => {
|
|
18689
|
+
this.threadStore.unlink(thread.id, REGISTERED_HOLDER);
|
|
18690
|
+
if (this.hasSubscriptions && !this.paginator.items?.includes(thread)) {
|
|
18691
|
+
thread.unregisterSubscriptions();
|
|
18692
|
+
}
|
|
18693
|
+
};
|
|
18694
|
+
/** Listens for its channel's disposal the first time a thread on that channel is seen. */
|
|
18695
|
+
this.listenForDisposal = (thread) => {
|
|
18696
|
+
const { channel } = thread;
|
|
18697
|
+
if (this.disposalListeners.has(channel)) return;
|
|
18698
|
+
if (channel.pendingDisposal) {
|
|
18699
|
+
this.releaseChannel(channel);
|
|
18700
|
+
return;
|
|
18701
|
+
}
|
|
18702
|
+
this.disposalListeners.set(
|
|
18703
|
+
channel,
|
|
18704
|
+
channel.state.subscribeWithSelector(
|
|
18705
|
+
({ pendingDisposal }) => ({ pendingDisposal }),
|
|
18706
|
+
({ pendingDisposal }) => {
|
|
18707
|
+
if (pendingDisposal) this.releaseChannel(channel);
|
|
18708
|
+
}
|
|
18709
|
+
)
|
|
18710
|
+
);
|
|
18711
|
+
};
|
|
18712
|
+
/** The channel was disposed, so its threads are unusable: release every one of them. */
|
|
18713
|
+
this.releaseChannel = (channel) => {
|
|
18714
|
+
this.disposalListeners.get(channel)?.();
|
|
18715
|
+
this.disposalListeners.delete(channel);
|
|
18716
|
+
const onChannel = (thread) => thread.channel === channel;
|
|
18717
|
+
this.threadStore.values().filter(onChannel).forEach(this.release);
|
|
18718
|
+
const listed = (this.paginator.items ?? []).filter(onChannel);
|
|
18719
|
+
if (!listed.length) return;
|
|
18720
|
+
this.paginator.batch(
|
|
18721
|
+
() => listed.forEach(({ id }) => this.paginator.removeItem({ id })),
|
|
18722
|
+
{ coalesce: true }
|
|
18723
|
+
);
|
|
18724
|
+
};
|
|
18479
18725
|
this.resetState = () => {
|
|
18726
|
+
this.threadStore.values().forEach(this.release);
|
|
18727
|
+
this.paginator.resetState();
|
|
18728
|
+
this.disposalListeners.forEach((unsubscribe) => unsubscribe());
|
|
18729
|
+
this.disposalListeners.clear();
|
|
18480
18730
|
this.state.next(THREAD_MANAGER_INITIAL_STATE);
|
|
18481
18731
|
};
|
|
18482
18732
|
this.activate = () => {
|
|
@@ -18492,7 +18742,65 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18492
18742
|
this.addUnsubscribeFunction(this.subscribeReloadOnActivation());
|
|
18493
18743
|
this.addUnsubscribeFunction(this.subscribeNewReplies());
|
|
18494
18744
|
this.addUnsubscribeFunction(this.subscribeReloadOnConnectionRecovered());
|
|
18495
|
-
this.addUnsubscribeFunction(this.
|
|
18745
|
+
this.addUnsubscribeFunction(this.subscribeThreadEvents());
|
|
18746
|
+
this.threadStore.values().forEach((thread) => thread.registerSubscriptions());
|
|
18747
|
+
};
|
|
18748
|
+
/**
|
|
18749
|
+
* Hands client events to the threads in the store by id, as `PollManager` does for polls: a thread
|
|
18750
|
+
* doesn't listen to the client itself. A message event goes to the thread the message is a reply
|
|
18751
|
+
* in and the thread it starts; quoted-message updates stay within the reply's own thread, as on
|
|
18752
|
+
* `master`. A thread follows its channel's `watchStatus` itself, so watch events need no routing.
|
|
18753
|
+
*/
|
|
18754
|
+
this.subscribeThreadEvents = () => {
|
|
18755
|
+
const threadsOf = (message) => message ? [
|
|
18756
|
+
message.parent_id ? this.get(message.parent_id) : void 0,
|
|
18757
|
+
this.get(message.id)
|
|
18758
|
+
] : [];
|
|
18759
|
+
const unsubscribeFunctions = [
|
|
18760
|
+
this.client.on("message.new", (event) => {
|
|
18761
|
+
if (event.message?.parent_id)
|
|
18762
|
+
this.get(event.message.parent_id)?.handleNewReply(event);
|
|
18763
|
+
}),
|
|
18764
|
+
this.client.on("message.read", (event) => {
|
|
18765
|
+
if (event.thread)
|
|
18766
|
+
this.get(event.thread.parent_message_id)?.handleRepliesRead(event);
|
|
18767
|
+
}),
|
|
18768
|
+
this.client.on("notification.mark_unread", (event) => {
|
|
18769
|
+
if (event.thread_id) this.get(event.thread_id)?.handleRepliesUnread(event);
|
|
18770
|
+
}),
|
|
18771
|
+
this.client.on("thread.updated", (event) => {
|
|
18772
|
+
if (event.thread)
|
|
18773
|
+
this.get(event.thread.parent_message_id)?.handleThreadUpdated(event);
|
|
18774
|
+
}),
|
|
18775
|
+
this.client.on(
|
|
18776
|
+
"message.deleted",
|
|
18777
|
+
(event) => threadsOf(event.message).forEach((thread) => thread?.handleMessageDeleted(event))
|
|
18778
|
+
),
|
|
18779
|
+
...["message.updated", "message.undeleted"].map(
|
|
18780
|
+
(eventType) => this.client.on(
|
|
18781
|
+
eventType,
|
|
18782
|
+
(event) => threadsOf(event.message).forEach(
|
|
18783
|
+
(thread) => thread?.handleMessageUpdated(event)
|
|
18784
|
+
)
|
|
18785
|
+
)
|
|
18786
|
+
),
|
|
18787
|
+
...["reaction.new", "reaction.deleted", "reaction.updated"].map(
|
|
18788
|
+
(eventType) => this.client.on(
|
|
18789
|
+
eventType,
|
|
18790
|
+
(event) => threadsOf(event.message).forEach(
|
|
18791
|
+
(thread) => thread?.handleReactionChanged(event)
|
|
18792
|
+
)
|
|
18793
|
+
)
|
|
18794
|
+
),
|
|
18795
|
+
// A banned or deleted user's replies can be in any thread, so these two are the exception.
|
|
18796
|
+
...["user.messages.deleted", "user.deleted"].map(
|
|
18797
|
+
(eventType) => this.client.on(
|
|
18798
|
+
eventType,
|
|
18799
|
+
(event) => this.threadStore.values().forEach((thread) => thread.handleUserMessagesDeleted(event))
|
|
18800
|
+
)
|
|
18801
|
+
)
|
|
18802
|
+
].map(({ unsubscribe }) => unsubscribe);
|
|
18803
|
+
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
18496
18804
|
};
|
|
18497
18805
|
this.subscribeUnreadThreadsCountChange = () => {
|
|
18498
18806
|
const { unread_threads: unreadThreadCount = 0 } = this.client.user ?? {};
|
|
@@ -18514,18 +18822,13 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18514
18822
|
);
|
|
18515
18823
|
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
18516
18824
|
};
|
|
18517
|
-
this.
|
|
18518
|
-
|
|
18519
|
-
const { threads } = this.state.getLatestValue();
|
|
18520
|
-
const newThreads = threads.filter((thread) => thread.channel.cid !== cid);
|
|
18521
|
-
this.state.partialNext({ threads: newThreads });
|
|
18522
|
-
}).unsubscribe;
|
|
18523
|
-
this.subscribeManageThreadSubscriptions = () => this.state.subscribeWithSelector(
|
|
18524
|
-
(nextValue) => ({ threads: nextValue.threads }),
|
|
18825
|
+
this.subscribeManageThreadSubscriptions = () => this.paginator.state.subscribeWithSelector(
|
|
18826
|
+
(nextValue) => ({ threads: nextValue.items ?? [] }),
|
|
18525
18827
|
({ threads: nextThreads }, prev) => {
|
|
18526
18828
|
const { threads: prevThreads = [] } = prev ?? {};
|
|
18829
|
+
const listed = new Set(nextThreads);
|
|
18527
18830
|
const removedThreads = prevThreads.filter(
|
|
18528
|
-
(thread) => thread
|
|
18831
|
+
(thread) => !listed.has(thread) && !this.threadStore.isHeldBy(thread.id, REGISTERED_HOLDER)
|
|
18529
18832
|
);
|
|
18530
18833
|
nextThreads.forEach((thread) => thread.registerSubscriptions());
|
|
18531
18834
|
removedThreads.forEach((thread) => thread.unregisterSubscriptions());
|
|
@@ -18540,9 +18843,9 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18540
18843
|
this.subscribeNewReplies = () => this.client.on("notification.thread_message_new", (event) => {
|
|
18541
18844
|
const parentId = event.message?.parent_id;
|
|
18542
18845
|
if (!parentId) return;
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
if (this.
|
|
18846
|
+
if (!this.paginator.hasResults) return;
|
|
18847
|
+
const { unseenThreadIds } = this.state.getLatestValue();
|
|
18848
|
+
if (this.paginator.getItem(parentId)) {
|
|
18546
18849
|
this.state.partialNext({ isThreadOrderStale: true });
|
|
18547
18850
|
} else if (!unseenThreadIds.includes(parentId)) {
|
|
18548
18851
|
this.state.partialNext({ unseenThreadIds: unseenThreadIds.concat(parentId) });
|
|
@@ -18569,94 +18872,33 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18569
18872
|
return this.client.on("connection.recovered", throttledHandleConnectionRecovered).unsubscribe;
|
|
18570
18873
|
};
|
|
18571
18874
|
this.unregisterSubscriptions = () => {
|
|
18572
|
-
this.
|
|
18875
|
+
this.threadStore.values().forEach((thread) => thread.unregisterSubscriptions());
|
|
18573
18876
|
return super.unregisterSubscriptions();
|
|
18574
18877
|
};
|
|
18878
|
+
/**
|
|
18879
|
+
* Loads the first page, or re-queries a loaded list in place, sized to what is loaded plus the
|
|
18880
|
+
* unseen threads. Skipped once loaded unless forced or something changed; the list survives a
|
|
18881
|
+
* failure. Guarded only against another reload, as on `master`. A reload of a loaded list shows
|
|
18882
|
+
* no loading state; the first load shows through `paginator.isLoading`.
|
|
18883
|
+
*/
|
|
18575
18884
|
this.reload = async ({ force = false } = {}) => {
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
if (!force &&
|
|
18579
|
-
|
|
18885
|
+
if (this.isReloadInFlight) return;
|
|
18886
|
+
const { isThreadOrderStale, unseenThreadIds } = this.state.getLatestValue();
|
|
18887
|
+
if (!force && this.paginator.hasResults && !unseenThreadIds.length && !isThreadOrderStale)
|
|
18888
|
+
return;
|
|
18889
|
+
const { items, pageSize } = this.paginator;
|
|
18890
|
+
const limit = (items?.length ?? 0) + unseenThreadIds.length;
|
|
18891
|
+
this.isReloadInFlight = true;
|
|
18580
18892
|
try {
|
|
18581
|
-
this.
|
|
18582
|
-
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18586
|
-
}
|
|
18587
|
-
}
|
|
18588
|
-
|
|
18589
|
-
limit: Math.min(limit, MAX_QUERY_THREADS_LIMIT) || MAX_QUERY_THREADS_LIMIT
|
|
18590
|
-
});
|
|
18591
|
-
const nextThreads = [];
|
|
18592
|
-
for (const incomingThread of response.threads) {
|
|
18593
|
-
const existingThread = this.threadsById[incomingThread.id];
|
|
18594
|
-
if (existingThread) {
|
|
18595
|
-
nextThreads.push(existingThread);
|
|
18596
|
-
if (existingThread.hasStaleState) {
|
|
18597
|
-
existingThread.hydrateState(incomingThread);
|
|
18598
|
-
}
|
|
18599
|
-
} else {
|
|
18600
|
-
nextThreads.push(incomingThread);
|
|
18601
|
-
}
|
|
18893
|
+
await this.paginator.reload({ limit: Math.min(limit, pageSize) || pageSize });
|
|
18894
|
+
} finally {
|
|
18895
|
+
this.isReloadInFlight = false;
|
|
18896
|
+
const error = this.paginator.lastQueryError;
|
|
18897
|
+
if (error) {
|
|
18898
|
+
logger10.withExtraTags("reload").error("Failed to reload the thread list.", { error });
|
|
18899
|
+
} else {
|
|
18900
|
+
this.state.partialNext({ isThreadOrderStale: false, unseenThreadIds: [] });
|
|
18602
18901
|
}
|
|
18603
|
-
this.state.next((current) => ({
|
|
18604
|
-
...current,
|
|
18605
|
-
threads: nextThreads,
|
|
18606
|
-
unseenThreadIds: [],
|
|
18607
|
-
isThreadOrderStale: false,
|
|
18608
|
-
pagination: {
|
|
18609
|
-
...current.pagination,
|
|
18610
|
-
isLoading: false,
|
|
18611
|
-
nextCursor: response.next ?? null
|
|
18612
|
-
},
|
|
18613
|
-
ready: true
|
|
18614
|
-
}));
|
|
18615
|
-
} catch (error) {
|
|
18616
|
-
logger10.withExtraTags("reload").error("Failed to reload the thread list.", { error });
|
|
18617
|
-
this.state.next((current) => ({
|
|
18618
|
-
...current,
|
|
18619
|
-
pagination: {
|
|
18620
|
-
...current.pagination,
|
|
18621
|
-
isLoading: false
|
|
18622
|
-
}
|
|
18623
|
-
}));
|
|
18624
|
-
}
|
|
18625
|
-
};
|
|
18626
|
-
this.queryThreads = (options = {}) => this.client.queryThreadsAndHydrate({
|
|
18627
|
-
limit: 25,
|
|
18628
|
-
participant_limit: 10,
|
|
18629
|
-
reply_limit: 10,
|
|
18630
|
-
watch: true,
|
|
18631
|
-
...options
|
|
18632
|
-
});
|
|
18633
|
-
this.loadNextPage = async (options = {}) => {
|
|
18634
|
-
const { pagination } = this.state.getLatestValue();
|
|
18635
|
-
if (pagination.isLoadingNext || !pagination.nextCursor) return;
|
|
18636
|
-
try {
|
|
18637
|
-
this.state.partialNext({ pagination: { ...pagination, isLoadingNext: true } });
|
|
18638
|
-
const response = await this.queryThreads({
|
|
18639
|
-
...options,
|
|
18640
|
-
next: pagination.nextCursor
|
|
18641
|
-
});
|
|
18642
|
-
this.state.next((current) => ({
|
|
18643
|
-
...current,
|
|
18644
|
-
threads: response.threads.length ? current.threads.concat(response.threads) : current.threads,
|
|
18645
|
-
pagination: {
|
|
18646
|
-
...current.pagination,
|
|
18647
|
-
nextCursor: response.next ?? null,
|
|
18648
|
-
isLoadingNext: false
|
|
18649
|
-
}
|
|
18650
|
-
}));
|
|
18651
|
-
} catch (error) {
|
|
18652
|
-
logger10.withExtraTags("loadNextPage").error("Failed to load the next page of threads.", { error });
|
|
18653
|
-
this.state.next((current) => ({
|
|
18654
|
-
...current,
|
|
18655
|
-
pagination: {
|
|
18656
|
-
...current.pagination,
|
|
18657
|
-
isLoadingNext: false
|
|
18658
|
-
}
|
|
18659
|
-
}));
|
|
18660
18902
|
}
|
|
18661
18903
|
};
|
|
18662
18904
|
this.configController = new ConfigController({
|
|
@@ -18664,7 +18906,11 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18664
18906
|
});
|
|
18665
18907
|
this.client = client;
|
|
18666
18908
|
this.state = new import_state_store21.StateStore(THREAD_MANAGER_INITIAL_STATE);
|
|
18667
|
-
this.
|
|
18909
|
+
this.paginator = new ThreadPaginator({ client, store: this.threadStore });
|
|
18910
|
+
this.paginator.state.subscribeWithSelector(
|
|
18911
|
+
({ items }) => ({ items }),
|
|
18912
|
+
({ items }) => items?.forEach(this.listenForDisposal)
|
|
18913
|
+
);
|
|
18668
18914
|
}
|
|
18669
18915
|
/**
|
|
18670
18916
|
* Resolved configuration, as a store — the shape every configurable class exposes
|
|
@@ -18698,21 +18944,13 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18698
18944
|
initializeConfig(config) {
|
|
18699
18945
|
this.configController.initialize(config);
|
|
18700
18946
|
}
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
newThreadsById[thread.id] = thread;
|
|
18709
|
-
return newThreadsById;
|
|
18710
|
-
},
|
|
18711
|
-
{}
|
|
18712
|
-
);
|
|
18713
|
-
this.threadsByIdGetterCache.threads = threads;
|
|
18714
|
-
this.threadsByIdGetterCache.threadsById = threadsById;
|
|
18715
|
-
return threadsById;
|
|
18947
|
+
/**
|
|
18948
|
+
* Every registered thread — the list's plus the opened ones.
|
|
18949
|
+
*
|
|
18950
|
+
* @internal
|
|
18951
|
+
*/
|
|
18952
|
+
get registeredThreads() {
|
|
18953
|
+
return this.threadStore.values();
|
|
18716
18954
|
}
|
|
18717
18955
|
};
|
|
18718
18956
|
|
|
@@ -20096,6 +20334,7 @@ var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
|
20096
20334
|
}
|
|
20097
20335
|
const channels = this.recoverableActiveChannels;
|
|
20098
20336
|
const threads = this.recoverableActiveThreads;
|
|
20337
|
+
this.markInactiveThreadsStale();
|
|
20099
20338
|
const socketDropBefore = this.client.wsConnection.state.getLatestValue().lastUnhealthyAt;
|
|
20100
20339
|
this.isRecovering = true;
|
|
20101
20340
|
logger12.withExtraTags("connectionRecovery").info(
|
|
@@ -20182,41 +20421,34 @@ var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
|
20182
20421
|
* Threads need their own pass because nothing else brings an open thread's replies back — a
|
|
20183
20422
|
* channel reload refreshes the main message list, `ChannelManager.recover()` refreshes the channel
|
|
20184
20423
|
* lists*, and `ThreadManager`'s own recovery refreshes the thread *list*, reusing existing
|
|
20185
|
-
* instances without rehydrating them unless they were separately marked stale
|
|
20186
|
-
* `user.watching.stop` does, never a reconnect).
|
|
20424
|
+
* instances without rehydrating them unless they were separately marked stale.
|
|
20187
20425
|
*
|
|
20188
|
-
* `active` is the filter that matters:
|
|
20189
|
-
* which is not what should be re-fetched on a reconnect — only what someone is actually
|
|
20426
|
+
* `active` is the filter that matters: the registry also holds every thread the list has paged
|
|
20427
|
+
* in, which is not what should be re-fetched on a reconnect — only what someone is actually
|
|
20428
|
+
* reading. An active thread is always registered, listed or not (`thread.activate()` registers it).
|
|
20190
20429
|
* Guarded on the owning channel the same way active channels are: a thread whose channel is being
|
|
20191
20430
|
* torn down has nothing to recover into.
|
|
20192
|
-
*
|
|
20193
|
-
* NOTE: `threadsById` is the thread LIST, not a thread cache. A thread opened from a message list
|
|
20194
|
-
* is constructed directly (`threadsById[id] ?? new Thread(...)`) and only reaches the list because
|
|
20195
|
-
* the UI SDKs adopt it there — prepended, once its replies have loaded — which is a workaround, not
|
|
20196
|
-
* a contract. Two consequences, both accepted for now and both narrow:
|
|
20197
|
-
*
|
|
20198
|
-
* - A reconnect landing before that adoption misses the thread. Self-limiting: the UI SDKs re-issue
|
|
20199
|
-
* the load while the reply list is still unloaded, so a thread that failed to load gets adopted on
|
|
20200
|
-
* the retry.
|
|
20201
|
-
* - `ThreadManager.reload()` rebuilds `state.threads` purely from the query response, so an adopted
|
|
20202
|
-
* thread absent from that response is evicted while still active. It cannot bite within the
|
|
20203
|
-
* reconnect that caused it: this getter is read before `connection.recovered` is dispatched, and
|
|
20204
|
-
* that event is what triggers the UI SDKs' list reload. It would take a LATER reconnect, after an
|
|
20205
|
-
* eviction, to miss the thread.
|
|
20206
|
-
*
|
|
20207
|
-
* The fix is a real off-list registry — see the commented-out `threadCache` in `ThreadManager` — at
|
|
20208
|
-
* which point this getter reads from that instead, with no change to the `active` filter.
|
|
20209
20431
|
*/
|
|
20210
20432
|
get recoverableActiveThreads() {
|
|
20211
20433
|
const threads = [];
|
|
20212
|
-
for (const thread of
|
|
20213
|
-
if (!thread) continue;
|
|
20434
|
+
for (const thread of this.client.threads.registeredThreads) {
|
|
20214
20435
|
const { active } = thread.state.getLatestValue();
|
|
20215
20436
|
if (!active || thread.channel.pendingDisposal) continue;
|
|
20216
20437
|
threads.push(thread);
|
|
20217
20438
|
}
|
|
20218
20439
|
return threads;
|
|
20219
20440
|
}
|
|
20441
|
+
/**
|
|
20442
|
+
* An opened thread stays registered and subscribed after it closes, but a drop loses events it
|
|
20443
|
+
* cannot replay. Flag only, no request: a listed one is rehydrated by the thread list's reload,
|
|
20444
|
+
* and any other reloads on its next `activate()`.
|
|
20445
|
+
*/
|
|
20446
|
+
markInactiveThreadsStale() {
|
|
20447
|
+
for (const thread of this.client.threads.registeredThreads) {
|
|
20448
|
+
const { active, isStateStale } = thread.state.getLatestValue();
|
|
20449
|
+
if (!active && !isStateStale) thread.state.partialNext({ isStateStale: true });
|
|
20450
|
+
}
|
|
20451
|
+
}
|
|
20220
20452
|
};
|
|
20221
20453
|
|
|
20222
20454
|
// src/connection/networkConnection/NetworkConnectionObserver.ts
|
|
@@ -23907,6 +24139,9 @@ function isString(value) {
|
|
|
23907
24139
|
}
|
|
23908
24140
|
var logger17 = chatLoggerSystem.getLogger("client");
|
|
23909
24141
|
var offlineDbLogger2 = chatLoggerSystem.getLogger("offline-db");
|
|
24142
|
+
var markThreadChannelWatched = ({ channel }) => {
|
|
24143
|
+
channel.watchStatus = ChannelWatchStatus.Watching;
|
|
24144
|
+
};
|
|
23910
24145
|
var StreamChat = class _StreamChat extends ChatApi {
|
|
23911
24146
|
/**
|
|
23912
24147
|
* Initializes a client.
|
|
@@ -24420,7 +24655,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24420
24655
|
return this.userAgent;
|
|
24421
24656
|
}
|
|
24422
24657
|
if (!this.cachedUserAgent) {
|
|
24423
|
-
const version = "10.0.0-rc.
|
|
24658
|
+
const version = "10.0.0-rc.16";
|
|
24424
24659
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
24425
24660
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
24426
24661
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -25369,12 +25604,11 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25369
25604
|
const response = await this.queryThreads(requestBody, requestOptions);
|
|
25370
25605
|
const parentMessages = response.threads.map((thread) => thread.parent_message);
|
|
25371
25606
|
this.polls.hydratePollCache(parentMessages);
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
};
|
|
25607
|
+
const threads = response.threads.map(
|
|
25608
|
+
(thread) => new Thread({ client: this, threadData: thread })
|
|
25609
|
+
);
|
|
25610
|
+
if (optionsWithDefaults.watch) threads.forEach(markThreadChannelWatched);
|
|
25611
|
+
return { threads, next: response.next };
|
|
25378
25612
|
}
|
|
25379
25613
|
/**
|
|
25380
25614
|
* Returns the thread of a message by its ID, wrapped in a hydrated `Thread` instance.
|
|
@@ -25406,7 +25640,9 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25406
25640
|
},
|
|
25407
25641
|
requestOptions
|
|
25408
25642
|
);
|
|
25409
|
-
|
|
25643
|
+
const thread = new Thread({ client: this, threadData: response.thread });
|
|
25644
|
+
if (optionsWithDefaults.watch) markThreadChannelWatched(thread);
|
|
25645
|
+
return thread;
|
|
25410
25646
|
}
|
|
25411
25647
|
/**
|
|
25412
25648
|
* Sets the user agent string.
|
|
@@ -27057,13 +27293,12 @@ var AbstractOfflineDB = class {
|
|
|
27057
27293
|
* message ordered against everything queued after it.
|
|
27058
27294
|
*
|
|
27059
27295
|
* Keyed on a queued `send-message` task existing for this id - the only reliable statement of "the
|
|
27060
|
-
* server has never seen this message". NOT on the edited message's status:
|
|
27061
|
-
*
|
|
27062
|
-
* field, so nothing arriving here carries one.
|
|
27296
|
+
* server has never seen this message". NOT on the edited message's status: `failed` is also what a
|
|
27297
|
+
* rejected edit of a message the server has carries.
|
|
27063
27298
|
*
|
|
27064
|
-
*
|
|
27065
|
-
*
|
|
27066
|
-
* `[undefined]`.
|
|
27299
|
+
* Both payloads are already `MessageRequest`s (the send's from `localMessageToNewMessagePayload`, the
|
|
27300
|
+
* edit's from `toUpdatedMessagePayload`), which is why they merge by a plain spread: normalizing again
|
|
27301
|
+
* would map an already-flattened `mentioned_users` back over itself as `[undefined]`.
|
|
27067
27302
|
*
|
|
27068
27303
|
* With nothing to fold into, the edit is queued on its own and replays as an ordinary update.
|
|
27069
27304
|
*/
|