stream-chat 10.0.0-rc.14 → 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 +507 -262
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +510 -262
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +507 -262
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/channel.d.ts +10 -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/messageDelivery/MessageDeliveryReporter.d.ts +3 -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/dist/types/types.d.ts +0 -6
- package/package.json +1 -1
- package/src/channel.ts +15 -12
- package/src/client.ts +19 -11
- package/src/configuration/shape.ts +6 -0
- package/src/connection/ConnectionRecoveryManager.ts +19 -24
- package/src/entityStore/EntityStore.ts +10 -0
- package/src/messageComposer/messageComposer.ts +3 -3
- package/src/messageDelivery/MessageDeliveryReporter.ts +18 -7
- 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
- package/src/types.ts +0 -6
- package/src/utils.ts +1 -2
|
@@ -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,
|
|
@@ -552,7 +555,7 @@ var channelHasReadEvents = (channel) => {
|
|
|
552
555
|
const ownCapabilities = channel?.data?.own_capabilities;
|
|
553
556
|
return !(Array.isArray(ownCapabilities) && !ownCapabilities.includes("read-events"));
|
|
554
557
|
};
|
|
555
|
-
var channelTracksReadLocally = (channel) => !channelHasReadEvents(channel) && !!channel?.
|
|
558
|
+
var channelTracksReadLocally = (channel) => !channelHasReadEvents(channel) && !!channel?.config.readEvents.localUnreadCountEnabled;
|
|
556
559
|
function retryInterval(numberOfFailures) {
|
|
557
560
|
const max = Math.min(500 + numberOfFailures * 2e3, 25e3);
|
|
558
561
|
const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2e3), 25e3);
|
|
@@ -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
|
|
@@ -14478,11 +14628,12 @@ var MessageDeliveryReporter = class _MessageDeliveryReporter {
|
|
|
14478
14628
|
}, this.requestTimeoutMs);
|
|
14479
14629
|
};
|
|
14480
14630
|
/**
|
|
14481
|
-
* Delegates the mark-read call to the Channel or Thread instance.
|
|
14631
|
+
* Delegates the mark-read call to the Channel or Thread instance. A channel whose read events are
|
|
14632
|
+
* disabled is marked read locally instead, when the client opted into counting unread itself.
|
|
14482
14633
|
*
|
|
14483
14634
|
* @param collection - The channel or thread to mark as read.
|
|
14484
14635
|
* @param options - Flags forwarded to the underlying `markRead` call (optional).
|
|
14485
|
-
* @returns The server response, or `null` when
|
|
14636
|
+
* @returns The server response, or `null` when no request went out.
|
|
14486
14637
|
*/
|
|
14487
14638
|
this.markRead = async (collection, options) => {
|
|
14488
14639
|
const isThreadCollection = isThread(collection);
|
|
@@ -14496,8 +14647,15 @@ var MessageDeliveryReporter = class _MessageDeliveryReporter {
|
|
|
14496
14647
|
thread: collection
|
|
14497
14648
|
}) ?? null : await channel.markRead(requestOptions);
|
|
14498
14649
|
} else {
|
|
14499
|
-
const
|
|
14500
|
-
|
|
14650
|
+
const { readEvents, requestHandlers } = channel.configState.getLatestValue();
|
|
14651
|
+
const markReadRequestHandler = requestHandlers?.markReadRequest;
|
|
14652
|
+
if (markReadRequestHandler) {
|
|
14653
|
+
result = await markReadRequestHandler({ channel, options: requestOptions }) ?? null;
|
|
14654
|
+
} else if (!readEvents.enabled && readEvents.localUnreadCountEnabled) {
|
|
14655
|
+
channel.markReadLocally();
|
|
14656
|
+
} else {
|
|
14657
|
+
result = await channel.markRead(requestOptions);
|
|
14658
|
+
}
|
|
14501
14659
|
}
|
|
14502
14660
|
if (result) this.removeCandidateFor(collection);
|
|
14503
14661
|
return result;
|
|
@@ -15451,7 +15609,7 @@ var ownDeclarativeConfig = (slice) => {
|
|
|
15451
15609
|
var DEFAULT_CHANNEL_CONFIG = deepFreezeConfig({
|
|
15452
15610
|
availableCommands: [],
|
|
15453
15611
|
deliveryEvents: { enabled: true },
|
|
15454
|
-
readEvents: { enabled: true },
|
|
15612
|
+
readEvents: { enabled: true, localUnreadCountEnabled: false },
|
|
15455
15613
|
replies: { enabled: true },
|
|
15456
15614
|
typingEvents: { enabled: true },
|
|
15457
15615
|
userMessageReminders: { enabled: true }
|
|
@@ -16478,7 +16636,7 @@ var Channel = class extends WithMessageOperations(ChannelApi) {
|
|
|
16478
16636
|
/**
|
|
16479
16637
|
* Resets this user's unread count locally, without any backend call. Intended for
|
|
16480
16638
|
* channels that have read events disabled (e.g. livestreams) when the client is created with the
|
|
16481
|
-
* `
|
|
16639
|
+
* `readEvents.localUnreadCountEnabled` configuration. Dispatches a dedicated, client-only `message.read_locally` event
|
|
16482
16640
|
* that runs through the same `_handleChannelEvent` read logic as a real `message.read` (minus the
|
|
16483
16641
|
* delivery-report network sync), so the read-state update lives in one place. When offline support
|
|
16484
16642
|
* is enabled, the offline DB persists the reset for read-events-disabled channels, so the local
|
|
@@ -16640,7 +16798,7 @@ var Channel = class extends WithMessageOperations(ChannelApi) {
|
|
|
16640
16798
|
if (message.user?.id === this.getClient().userId) return false;
|
|
16641
16799
|
if (message.user?.id && this.getClient().userMuteStatus(message.user.id))
|
|
16642
16800
|
return false;
|
|
16643
|
-
if (!this.
|
|
16801
|
+
if (!this.config.readEvents.localUnreadCountEnabled && !channelHasReadEvents(this)) {
|
|
16644
16802
|
return false;
|
|
16645
16803
|
}
|
|
16646
16804
|
if (this.muteStatus().muted) return false;
|
|
@@ -18449,26 +18607,126 @@ var eventCarriesOwnUser = (event) => Object.hasOwn(event, "me");
|
|
|
18449
18607
|
var DEFAULT_THREAD_MANAGER_CONFIG = deepFreezeConfig({
|
|
18450
18608
|
connectionRecoveryThrottleMs: 1e3
|
|
18451
18609
|
});
|
|
18452
|
-
var MAX_QUERY_THREADS_LIMIT = 25;
|
|
18453
18610
|
var THREAD_MANAGER_INITIAL_STATE = {
|
|
18454
18611
|
active: false,
|
|
18455
|
-
wasActivatedAtLeastOnce: false,
|
|
18456
18612
|
isThreadOrderStale: false,
|
|
18457
|
-
threads: [],
|
|
18458
18613
|
unreadThreadCount: 0,
|
|
18459
18614
|
unseenThreadIds: [],
|
|
18460
|
-
|
|
18461
|
-
isLoading: false,
|
|
18462
|
-
isLoadingNext: false,
|
|
18463
|
-
nextCursor: null
|
|
18464
|
-
},
|
|
18465
|
-
ready: false
|
|
18615
|
+
wasActivatedAtLeastOnce: false
|
|
18466
18616
|
};
|
|
18467
18617
|
var logger10 = chatLoggerSystem.getLogger("thread-manager");
|
|
18618
|
+
var getThreadId = (thread) => thread.id;
|
|
18619
|
+
var REGISTERED_HOLDER = { onEntitiesChanged: () => void 0 };
|
|
18468
18620
|
var ThreadManager = class extends WithSubscriptions {
|
|
18469
18621
|
constructor({ client }) {
|
|
18470
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
|
+
};
|
|
18471
18725
|
this.resetState = () => {
|
|
18726
|
+
this.threadStore.values().forEach(this.release);
|
|
18727
|
+
this.paginator.resetState();
|
|
18728
|
+
this.disposalListeners.forEach((unsubscribe) => unsubscribe());
|
|
18729
|
+
this.disposalListeners.clear();
|
|
18472
18730
|
this.state.next(THREAD_MANAGER_INITIAL_STATE);
|
|
18473
18731
|
};
|
|
18474
18732
|
this.activate = () => {
|
|
@@ -18484,7 +18742,65 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18484
18742
|
this.addUnsubscribeFunction(this.subscribeReloadOnActivation());
|
|
18485
18743
|
this.addUnsubscribeFunction(this.subscribeNewReplies());
|
|
18486
18744
|
this.addUnsubscribeFunction(this.subscribeReloadOnConnectionRecovered());
|
|
18487
|
-
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());
|
|
18488
18804
|
};
|
|
18489
18805
|
this.subscribeUnreadThreadsCountChange = () => {
|
|
18490
18806
|
const { unread_threads: unreadThreadCount = 0 } = this.client.user ?? {};
|
|
@@ -18506,18 +18822,13 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18506
18822
|
);
|
|
18507
18823
|
return () => unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
|
|
18508
18824
|
};
|
|
18509
|
-
this.
|
|
18510
|
-
|
|
18511
|
-
const { threads } = this.state.getLatestValue();
|
|
18512
|
-
const newThreads = threads.filter((thread) => thread.channel.cid !== cid);
|
|
18513
|
-
this.state.partialNext({ threads: newThreads });
|
|
18514
|
-
}).unsubscribe;
|
|
18515
|
-
this.subscribeManageThreadSubscriptions = () => this.state.subscribeWithSelector(
|
|
18516
|
-
(nextValue) => ({ threads: nextValue.threads }),
|
|
18825
|
+
this.subscribeManageThreadSubscriptions = () => this.paginator.state.subscribeWithSelector(
|
|
18826
|
+
(nextValue) => ({ threads: nextValue.items ?? [] }),
|
|
18517
18827
|
({ threads: nextThreads }, prev) => {
|
|
18518
18828
|
const { threads: prevThreads = [] } = prev ?? {};
|
|
18829
|
+
const listed = new Set(nextThreads);
|
|
18519
18830
|
const removedThreads = prevThreads.filter(
|
|
18520
|
-
(thread) => thread
|
|
18831
|
+
(thread) => !listed.has(thread) && !this.threadStore.isHeldBy(thread.id, REGISTERED_HOLDER)
|
|
18521
18832
|
);
|
|
18522
18833
|
nextThreads.forEach((thread) => thread.registerSubscriptions());
|
|
18523
18834
|
removedThreads.forEach((thread) => thread.unregisterSubscriptions());
|
|
@@ -18532,9 +18843,9 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18532
18843
|
this.subscribeNewReplies = () => this.client.on("notification.thread_message_new", (event) => {
|
|
18533
18844
|
const parentId = event.message?.parent_id;
|
|
18534
18845
|
if (!parentId) return;
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
if (this.
|
|
18846
|
+
if (!this.paginator.hasResults) return;
|
|
18847
|
+
const { unseenThreadIds } = this.state.getLatestValue();
|
|
18848
|
+
if (this.paginator.getItem(parentId)) {
|
|
18538
18849
|
this.state.partialNext({ isThreadOrderStale: true });
|
|
18539
18850
|
} else if (!unseenThreadIds.includes(parentId)) {
|
|
18540
18851
|
this.state.partialNext({ unseenThreadIds: unseenThreadIds.concat(parentId) });
|
|
@@ -18561,94 +18872,33 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18561
18872
|
return this.client.on("connection.recovered", throttledHandleConnectionRecovered).unsubscribe;
|
|
18562
18873
|
};
|
|
18563
18874
|
this.unregisterSubscriptions = () => {
|
|
18564
|
-
this.
|
|
18875
|
+
this.threadStore.values().forEach((thread) => thread.unregisterSubscriptions());
|
|
18565
18876
|
return super.unregisterSubscriptions();
|
|
18566
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
|
+
*/
|
|
18567
18884
|
this.reload = async ({ force = false } = {}) => {
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
if (!force &&
|
|
18571
|
-
|
|
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;
|
|
18572
18892
|
try {
|
|
18573
|
-
this.
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
}
|
|
18579
|
-
}
|
|
18580
|
-
|
|
18581
|
-
limit: Math.min(limit, MAX_QUERY_THREADS_LIMIT) || MAX_QUERY_THREADS_LIMIT
|
|
18582
|
-
});
|
|
18583
|
-
const nextThreads = [];
|
|
18584
|
-
for (const incomingThread of response.threads) {
|
|
18585
|
-
const existingThread = this.threadsById[incomingThread.id];
|
|
18586
|
-
if (existingThread) {
|
|
18587
|
-
nextThreads.push(existingThread);
|
|
18588
|
-
if (existingThread.hasStaleState) {
|
|
18589
|
-
existingThread.hydrateState(incomingThread);
|
|
18590
|
-
}
|
|
18591
|
-
} else {
|
|
18592
|
-
nextThreads.push(incomingThread);
|
|
18593
|
-
}
|
|
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: [] });
|
|
18594
18901
|
}
|
|
18595
|
-
this.state.next((current) => ({
|
|
18596
|
-
...current,
|
|
18597
|
-
threads: nextThreads,
|
|
18598
|
-
unseenThreadIds: [],
|
|
18599
|
-
isThreadOrderStale: false,
|
|
18600
|
-
pagination: {
|
|
18601
|
-
...current.pagination,
|
|
18602
|
-
isLoading: false,
|
|
18603
|
-
nextCursor: response.next ?? null
|
|
18604
|
-
},
|
|
18605
|
-
ready: true
|
|
18606
|
-
}));
|
|
18607
|
-
} catch (error) {
|
|
18608
|
-
logger10.withExtraTags("reload").error("Failed to reload the thread list.", { error });
|
|
18609
|
-
this.state.next((current) => ({
|
|
18610
|
-
...current,
|
|
18611
|
-
pagination: {
|
|
18612
|
-
...current.pagination,
|
|
18613
|
-
isLoading: false
|
|
18614
|
-
}
|
|
18615
|
-
}));
|
|
18616
|
-
}
|
|
18617
|
-
};
|
|
18618
|
-
this.queryThreads = (options = {}) => this.client.queryThreadsAndHydrate({
|
|
18619
|
-
limit: 25,
|
|
18620
|
-
participant_limit: 10,
|
|
18621
|
-
reply_limit: 10,
|
|
18622
|
-
watch: true,
|
|
18623
|
-
...options
|
|
18624
|
-
});
|
|
18625
|
-
this.loadNextPage = async (options = {}) => {
|
|
18626
|
-
const { pagination } = this.state.getLatestValue();
|
|
18627
|
-
if (pagination.isLoadingNext || !pagination.nextCursor) return;
|
|
18628
|
-
try {
|
|
18629
|
-
this.state.partialNext({ pagination: { ...pagination, isLoadingNext: true } });
|
|
18630
|
-
const response = await this.queryThreads({
|
|
18631
|
-
...options,
|
|
18632
|
-
next: pagination.nextCursor
|
|
18633
|
-
});
|
|
18634
|
-
this.state.next((current) => ({
|
|
18635
|
-
...current,
|
|
18636
|
-
threads: response.threads.length ? current.threads.concat(response.threads) : current.threads,
|
|
18637
|
-
pagination: {
|
|
18638
|
-
...current.pagination,
|
|
18639
|
-
nextCursor: response.next ?? null,
|
|
18640
|
-
isLoadingNext: false
|
|
18641
|
-
}
|
|
18642
|
-
}));
|
|
18643
|
-
} catch (error) {
|
|
18644
|
-
logger10.withExtraTags("loadNextPage").error("Failed to load the next page of threads.", { error });
|
|
18645
|
-
this.state.next((current) => ({
|
|
18646
|
-
...current,
|
|
18647
|
-
pagination: {
|
|
18648
|
-
...current.pagination,
|
|
18649
|
-
isLoadingNext: false
|
|
18650
|
-
}
|
|
18651
|
-
}));
|
|
18652
18902
|
}
|
|
18653
18903
|
};
|
|
18654
18904
|
this.configController = new ConfigController({
|
|
@@ -18656,7 +18906,11 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18656
18906
|
});
|
|
18657
18907
|
this.client = client;
|
|
18658
18908
|
this.state = new import_state_store21.StateStore(THREAD_MANAGER_INITIAL_STATE);
|
|
18659
|
-
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
|
+
);
|
|
18660
18914
|
}
|
|
18661
18915
|
/**
|
|
18662
18916
|
* Resolved configuration, as a store — the shape every configurable class exposes
|
|
@@ -18690,21 +18944,13 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
18690
18944
|
initializeConfig(config) {
|
|
18691
18945
|
this.configController.initialize(config);
|
|
18692
18946
|
}
|
|
18693
|
-
|
|
18694
|
-
|
|
18695
|
-
|
|
18696
|
-
|
|
18697
|
-
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
newThreadsById[thread.id] = thread;
|
|
18701
|
-
return newThreadsById;
|
|
18702
|
-
},
|
|
18703
|
-
{}
|
|
18704
|
-
);
|
|
18705
|
-
this.threadsByIdGetterCache.threads = threads;
|
|
18706
|
-
this.threadsByIdGetterCache.threadsById = threadsById;
|
|
18707
|
-
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();
|
|
18708
18954
|
}
|
|
18709
18955
|
};
|
|
18710
18956
|
|
|
@@ -20088,6 +20334,7 @@ var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
|
20088
20334
|
}
|
|
20089
20335
|
const channels = this.recoverableActiveChannels;
|
|
20090
20336
|
const threads = this.recoverableActiveThreads;
|
|
20337
|
+
this.markInactiveThreadsStale();
|
|
20091
20338
|
const socketDropBefore = this.client.wsConnection.state.getLatestValue().lastUnhealthyAt;
|
|
20092
20339
|
this.isRecovering = true;
|
|
20093
20340
|
logger12.withExtraTags("connectionRecovery").info(
|
|
@@ -20174,41 +20421,34 @@ var ConnectionRecoveryManager = class extends WithSubscriptions {
|
|
|
20174
20421
|
* Threads need their own pass because nothing else brings an open thread's replies back — a
|
|
20175
20422
|
* channel reload refreshes the main message list, `ChannelManager.recover()` refreshes the channel
|
|
20176
20423
|
* lists*, and `ThreadManager`'s own recovery refreshes the thread *list*, reusing existing
|
|
20177
|
-
* instances without rehydrating them unless they were separately marked stale
|
|
20178
|
-
* `user.watching.stop` does, never a reconnect).
|
|
20424
|
+
* instances without rehydrating them unless they were separately marked stale.
|
|
20179
20425
|
*
|
|
20180
|
-
* `active` is the filter that matters:
|
|
20181
|
-
* 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).
|
|
20182
20429
|
* Guarded on the owning channel the same way active channels are: a thread whose channel is being
|
|
20183
20430
|
* torn down has nothing to recover into.
|
|
20184
|
-
*
|
|
20185
|
-
* NOTE: `threadsById` is the thread LIST, not a thread cache. A thread opened from a message list
|
|
20186
|
-
* is constructed directly (`threadsById[id] ?? new Thread(...)`) and only reaches the list because
|
|
20187
|
-
* the UI SDKs adopt it there — prepended, once its replies have loaded — which is a workaround, not
|
|
20188
|
-
* a contract. Two consequences, both accepted for now and both narrow:
|
|
20189
|
-
*
|
|
20190
|
-
* - A reconnect landing before that adoption misses the thread. Self-limiting: the UI SDKs re-issue
|
|
20191
|
-
* the load while the reply list is still unloaded, so a thread that failed to load gets adopted on
|
|
20192
|
-
* the retry.
|
|
20193
|
-
* - `ThreadManager.reload()` rebuilds `state.threads` purely from the query response, so an adopted
|
|
20194
|
-
* thread absent from that response is evicted while still active. It cannot bite within the
|
|
20195
|
-
* reconnect that caused it: this getter is read before `connection.recovered` is dispatched, and
|
|
20196
|
-
* that event is what triggers the UI SDKs' list reload. It would take a LATER reconnect, after an
|
|
20197
|
-
* eviction, to miss the thread.
|
|
20198
|
-
*
|
|
20199
|
-
* The fix is a real off-list registry — see the commented-out `threadCache` in `ThreadManager` — at
|
|
20200
|
-
* which point this getter reads from that instead, with no change to the `active` filter.
|
|
20201
20431
|
*/
|
|
20202
20432
|
get recoverableActiveThreads() {
|
|
20203
20433
|
const threads = [];
|
|
20204
|
-
for (const thread of
|
|
20205
|
-
if (!thread) continue;
|
|
20434
|
+
for (const thread of this.client.threads.registeredThreads) {
|
|
20206
20435
|
const { active } = thread.state.getLatestValue();
|
|
20207
20436
|
if (!active || thread.channel.pendingDisposal) continue;
|
|
20208
20437
|
threads.push(thread);
|
|
20209
20438
|
}
|
|
20210
20439
|
return threads;
|
|
20211
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
|
+
}
|
|
20212
20452
|
};
|
|
20213
20453
|
|
|
20214
20454
|
// src/connection/networkConnection/NetworkConnectionObserver.ts
|
|
@@ -23899,6 +24139,9 @@ function isString(value) {
|
|
|
23899
24139
|
}
|
|
23900
24140
|
var logger17 = chatLoggerSystem.getLogger("client");
|
|
23901
24141
|
var offlineDbLogger2 = chatLoggerSystem.getLogger("offline-db");
|
|
24142
|
+
var markThreadChannelWatched = ({ channel }) => {
|
|
24143
|
+
channel.watchStatus = ChannelWatchStatus.Watching;
|
|
24144
|
+
};
|
|
23902
24145
|
var StreamChat = class _StreamChat extends ChatApi {
|
|
23903
24146
|
/**
|
|
23904
24147
|
* Initializes a client.
|
|
@@ -24412,7 +24655,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24412
24655
|
return this.userAgent;
|
|
24413
24656
|
}
|
|
24414
24657
|
if (!this.cachedUserAgent) {
|
|
24415
|
-
const version = "10.0.0-rc.
|
|
24658
|
+
const version = "10.0.0-rc.16";
|
|
24416
24659
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
24417
24660
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
24418
24661
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -24468,10 +24711,7 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
24468
24711
|
this.uploadManager = new UploadManager(this);
|
|
24469
24712
|
this.browser = options.browser ?? typeof window !== "undefined";
|
|
24470
24713
|
this.node = !this.browser;
|
|
24471
|
-
this.options = {
|
|
24472
|
-
isLocalUnreadCountEnabled: false,
|
|
24473
|
-
...options
|
|
24474
|
-
};
|
|
24714
|
+
this.options = { ...options };
|
|
24475
24715
|
this.axiosInstance = import_axios4.default.create({
|
|
24476
24716
|
timeout: 3e3,
|
|
24477
24717
|
withCredentials: false,
|
|
@@ -25364,12 +25604,11 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25364
25604
|
const response = await this.queryThreads(requestBody, requestOptions);
|
|
25365
25605
|
const parentMessages = response.threads.map((thread) => thread.parent_message);
|
|
25366
25606
|
this.polls.hydratePollCache(parentMessages);
|
|
25367
|
-
|
|
25368
|
-
|
|
25369
|
-
|
|
25370
|
-
|
|
25371
|
-
|
|
25372
|
-
};
|
|
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 };
|
|
25373
25612
|
}
|
|
25374
25613
|
/**
|
|
25375
25614
|
* Returns the thread of a message by its ID, wrapped in a hydrated `Thread` instance.
|
|
@@ -25401,7 +25640,9 @@ var StreamChat = class _StreamChat extends ChatApi {
|
|
|
25401
25640
|
},
|
|
25402
25641
|
requestOptions
|
|
25403
25642
|
);
|
|
25404
|
-
|
|
25643
|
+
const thread = new Thread({ client: this, threadData: response.thread });
|
|
25644
|
+
if (optionsWithDefaults.watch) markThreadChannelWatched(thread);
|
|
25645
|
+
return thread;
|
|
25405
25646
|
}
|
|
25406
25647
|
/**
|
|
25407
25648
|
* Sets the user agent string.
|
|
@@ -25961,6 +26202,11 @@ var CHANNEL_FIELDS = {
|
|
|
25961
26202
|
description: 'Allows marking the channel read or unread. The server must also allow it per channel type (`read_events`), and a server "no" wins.',
|
|
25962
26203
|
kind: "value",
|
|
25963
26204
|
type: "boolean"
|
|
26205
|
+
},
|
|
26206
|
+
localUnreadCountEnabled: {
|
|
26207
|
+
description: "Counts unread messages client-side when `enabled` is false, reset by `channel.markReadLocally()` instead of a request. Not subject to server authority.",
|
|
26208
|
+
kind: "value",
|
|
26209
|
+
type: "boolean"
|
|
25964
26210
|
}
|
|
25965
26211
|
},
|
|
25966
26212
|
kind: "group"
|
|
@@ -27047,13 +27293,12 @@ var AbstractOfflineDB = class {
|
|
|
27047
27293
|
* message ordered against everything queued after it.
|
|
27048
27294
|
*
|
|
27049
27295
|
* Keyed on a queued `send-message` task existing for this id - the only reliable statement of "the
|
|
27050
|
-
* server has never seen this message". NOT on the edited message's status:
|
|
27051
|
-
*
|
|
27052
|
-
* 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.
|
|
27053
27298
|
*
|
|
27054
|
-
*
|
|
27055
|
-
*
|
|
27056
|
-
* `[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]`.
|
|
27057
27302
|
*
|
|
27058
27303
|
* With nothing to fold into, the edit is queued on its own and replays as an ordinary update.
|
|
27059
27304
|
*/
|