stream-chat 9.52.1 → 9.54.0
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 +388 -90
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +388 -90
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +388 -90
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/channel_batch_updater.d.ts +9 -2
- package/dist/types/client.d.ts +19 -6
- package/dist/types/poll.d.ts +22 -4
- package/dist/types/poll_optimistic_votes.d.ts +63 -0
- package/dist/types/types.d.ts +43 -3
- package/dist/types/utils.d.ts +1 -3
- package/package.json +1 -1
- package/src/channel_batch_updater.ts +28 -2
- package/src/client.ts +84 -10
- package/src/messageComposer/middleware/messageComposer/linkPreviews.ts +20 -10
- package/src/poll.ts +86 -60
- package/src/poll_optimistic_votes.ts +321 -0
- package/src/types.ts +51 -3
|
@@ -382,6 +382,7 @@ var Campaign = class {
|
|
|
382
382
|
};
|
|
383
383
|
|
|
384
384
|
// src/channel_batch_updater.ts
|
|
385
|
+
var isChannelBatchDataUpdateOptions = (update) => "data" in update || "custom_set" in update || "custom_unset" in update;
|
|
385
386
|
var ChannelBatchUpdater = class {
|
|
386
387
|
constructor(client) {
|
|
387
388
|
this.client = client;
|
|
@@ -520,19 +521,14 @@ var ChannelBatchUpdater = class {
|
|
|
520
521
|
filter
|
|
521
522
|
});
|
|
522
523
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
* updateData - Update data on channels matching the filter
|
|
526
|
-
*
|
|
527
|
-
* @param {UpdateChannelsBatchFilters} filter Filter to select channels
|
|
528
|
-
* @param {BatchChannelDataUpdate} data Data to update
|
|
529
|
-
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
530
|
-
*/
|
|
531
|
-
async updateData(filter, data) {
|
|
524
|
+
async updateData(filter, update) {
|
|
525
|
+
const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update) ? update : { data: update };
|
|
532
526
|
return await this.client.updateChannelsBatch({
|
|
533
527
|
operation: "updateData",
|
|
534
528
|
filter,
|
|
535
|
-
data
|
|
529
|
+
...data && { data },
|
|
530
|
+
...custom_set && { custom_set },
|
|
531
|
+
...custom_unset && { custom_unset }
|
|
536
532
|
});
|
|
537
533
|
}
|
|
538
534
|
};
|
|
@@ -5914,10 +5910,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5914
5910
|
const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
|
|
5915
5911
|
(preview) => LinkPreviewsManager.getPreviewData(preview)
|
|
5916
5912
|
);
|
|
5917
|
-
const
|
|
5913
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
5918
5914
|
linkPreviews
|
|
5919
5915
|
);
|
|
5920
|
-
|
|
5916
|
+
const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
|
|
5917
|
+
if (!messageAttachments.length && !localAttachments.length) return forward();
|
|
5921
5918
|
const sendOptions = { ...state.sendOptions };
|
|
5922
5919
|
const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
|
|
5923
5920
|
if (skip_enrich_url) {
|
|
@@ -5925,14 +5922,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5925
5922
|
}
|
|
5926
5923
|
return next({
|
|
5927
5924
|
...state,
|
|
5928
|
-
message: {
|
|
5925
|
+
message: messageAttachments.length ? {
|
|
5929
5926
|
...state.message,
|
|
5930
|
-
attachments
|
|
5931
|
-
},
|
|
5932
|
-
localMessage: {
|
|
5927
|
+
attachments: messageAttachments
|
|
5928
|
+
} : state.message,
|
|
5929
|
+
localMessage: localAttachments.length ? {
|
|
5933
5930
|
...state.localMessage,
|
|
5934
|
-
attachments
|
|
5935
|
-
},
|
|
5931
|
+
attachments: localAttachments
|
|
5932
|
+
} : state.localMessage,
|
|
5936
5933
|
sendOptions
|
|
5937
5934
|
});
|
|
5938
5935
|
}
|
|
@@ -13161,6 +13158,242 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
13161
13158
|
}
|
|
13162
13159
|
};
|
|
13163
13160
|
|
|
13161
|
+
// src/poll_optimistic_votes.ts
|
|
13162
|
+
var CATCH_UP_TIMEOUT_MS = 3e3;
|
|
13163
|
+
var OptimisticPollVotes = class {
|
|
13164
|
+
constructor({ client, poll }) {
|
|
13165
|
+
// Own vote changes not confirmed by a WS event yet
|
|
13166
|
+
this.pendingCount = 0;
|
|
13167
|
+
// Own vote changes whose requests succeeded since the server's vote state was last shown
|
|
13168
|
+
this.succeededChanges = [];
|
|
13169
|
+
// Vote requests with this tag are sent one after another. A symbol, so it is unique per instance.
|
|
13170
|
+
this.requestTag = /* @__PURE__ */ Symbol("poll-votes");
|
|
13171
|
+
this.withServerVotes = (pollState) => ({
|
|
13172
|
+
...pollState,
|
|
13173
|
+
...this.serverVoteState
|
|
13174
|
+
});
|
|
13175
|
+
/**
|
|
13176
|
+
* Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
|
|
13177
|
+
* votes, it replaces the existing own votes.
|
|
13178
|
+
*/
|
|
13179
|
+
this.castVote = (optionId, messageId) => {
|
|
13180
|
+
const sendRequest = () => this.client.castPollVote(messageId, this.poll.id, { option_id: optionId });
|
|
13181
|
+
const state = this.poll.data;
|
|
13182
|
+
if (state.ownVotesByOptionId[optionId]) return this.send(sendRequest);
|
|
13183
|
+
const replacedVotes = state.enforce_unique_vote ? Object.values(state.ownVotesByOptionId) : [];
|
|
13184
|
+
this.poll.state.partialNext(
|
|
13185
|
+
applyOwnVoteDelta(state, {
|
|
13186
|
+
add: [this.createPlaceholderVote(optionId)],
|
|
13187
|
+
remove: replacedVotes
|
|
13188
|
+
})
|
|
13189
|
+
);
|
|
13190
|
+
return this.track(sendRequest, (response) => ({
|
|
13191
|
+
optionId,
|
|
13192
|
+
vote: response.vote
|
|
13193
|
+
}));
|
|
13194
|
+
};
|
|
13195
|
+
// Removes a vote, right away.
|
|
13196
|
+
this.removeVote = (voteId, messageId) => {
|
|
13197
|
+
const sendRequest = (id) => this.client.removePollVote(messageId, this.poll.id, id);
|
|
13198
|
+
const state = this.poll.data;
|
|
13199
|
+
const vote = Object.values(state.ownVotesByOptionId).find(({ id }) => id === voteId);
|
|
13200
|
+
if (!vote?.option_id) return this.send(() => sendRequest(voteId));
|
|
13201
|
+
const optionId = vote.option_id;
|
|
13202
|
+
this.poll.state.partialNext(applyOwnVoteDelta(state, { remove: [vote] }));
|
|
13203
|
+
return this.track(
|
|
13204
|
+
// runs when the removal's turn comes, so a preceding cast of the vote has finished
|
|
13205
|
+
() => {
|
|
13206
|
+
const serverVoteId = this.getServerVoteId(vote);
|
|
13207
|
+
return serverVoteId ? sendRequest(serverVoteId) : Promise.resolve(void 0);
|
|
13208
|
+
},
|
|
13209
|
+
() => ({ optionId })
|
|
13210
|
+
);
|
|
13211
|
+
};
|
|
13212
|
+
// Sends a vote request once the previous one has finished, whether it succeeded or not.
|
|
13213
|
+
this.send = async (sendRequest) => {
|
|
13214
|
+
try {
|
|
13215
|
+
return await withoutConcurrency(this.requestTag, sendRequest);
|
|
13216
|
+
} finally {
|
|
13217
|
+
if (!hasPending(this.requestTag)) this.scheduleCatchUp();
|
|
13218
|
+
}
|
|
13219
|
+
};
|
|
13220
|
+
// Stores the vote state of a WS vote event and returns the vote fields to show, if any.
|
|
13221
|
+
this.handleVoteEvent = (poll, {
|
|
13222
|
+
confirmsOwnVote,
|
|
13223
|
+
ownVotesByOptionId
|
|
13224
|
+
}) => {
|
|
13225
|
+
this.serverVoteState = {
|
|
13226
|
+
latest_votes_by_option: poll.latest_votes_by_option,
|
|
13227
|
+
ownVotesByOptionId,
|
|
13228
|
+
vote_count: poll.vote_count,
|
|
13229
|
+
vote_counts_by_option: poll.vote_counts_by_option
|
|
13230
|
+
};
|
|
13231
|
+
if (confirmsOwnVote) this.pendingCount = Math.max(this.pendingCount - 1, 0);
|
|
13232
|
+
return this.pendingCount > 0 ? {} : this.takeServerVoteState();
|
|
13233
|
+
};
|
|
13234
|
+
// Fresh server state (e.g. a refetch) replaces everything, pending changes are dropped.
|
|
13235
|
+
this.reset = (pollState) => {
|
|
13236
|
+
this.serverVoteState = pickVoteState(pollState);
|
|
13237
|
+
this.pendingCount = 0;
|
|
13238
|
+
this.succeededChanges = [];
|
|
13239
|
+
clearTimeout(this.catchUpTimeout);
|
|
13240
|
+
};
|
|
13241
|
+
// Tracks an own vote change until its request settles. `sendRequest` resolves to undefined
|
|
13242
|
+
// when no request was needed.
|
|
13243
|
+
this.track = async (sendRequest, toChange) => {
|
|
13244
|
+
this.pendingCount += 1;
|
|
13245
|
+
clearTimeout(this.catchUpTimeout);
|
|
13246
|
+
let response;
|
|
13247
|
+
try {
|
|
13248
|
+
response = await this.send(async () => {
|
|
13249
|
+
const result = await sendRequest();
|
|
13250
|
+
if (result !== void 0) this.succeededChanges.push(toChange(result));
|
|
13251
|
+
return result;
|
|
13252
|
+
});
|
|
13253
|
+
} catch (error) {
|
|
13254
|
+
this.settleWithoutEvent();
|
|
13255
|
+
throw error;
|
|
13256
|
+
}
|
|
13257
|
+
if (response === void 0) this.settleWithoutEvent();
|
|
13258
|
+
return response;
|
|
13259
|
+
};
|
|
13260
|
+
// A failed or skipped request gets no WS event, so it counts as confirmed right away.
|
|
13261
|
+
this.settleWithoutEvent = () => {
|
|
13262
|
+
this.pendingCount = Math.max(this.pendingCount - 1, 0);
|
|
13263
|
+
if (this.pendingCount === 0) this.poll.state.partialNext(this.takeServerVoteState());
|
|
13264
|
+
};
|
|
13265
|
+
// The vote shown for a cast until the server's vote arrives
|
|
13266
|
+
this.createPlaceholderVote = (optionId) => {
|
|
13267
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13268
|
+
return {
|
|
13269
|
+
created_at: now,
|
|
13270
|
+
id: `local-${generateUUIDv4()}`,
|
|
13271
|
+
option_id: optionId,
|
|
13272
|
+
poll_id: this.poll.id,
|
|
13273
|
+
updated_at: now,
|
|
13274
|
+
user: this.client.user,
|
|
13275
|
+
user_id: this.client.userID
|
|
13276
|
+
};
|
|
13277
|
+
};
|
|
13278
|
+
/**
|
|
13279
|
+
* The server id of a vote to remove, once everything sent before the removal has finished. Casts
|
|
13280
|
+
* and removals of an option alternate, so the option's last succeeded change tells: a cast is the
|
|
13281
|
+
* vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
|
|
13282
|
+
* one, the server's own vote of the option is the vote being removed.
|
|
13283
|
+
*/
|
|
13284
|
+
this.getServerVoteId = (vote) => {
|
|
13285
|
+
for (let i = this.succeededChanges.length - 1; i >= 0; i--) {
|
|
13286
|
+
const change = this.succeededChanges[i];
|
|
13287
|
+
if (change.optionId === vote.option_id) return change.vote?.id;
|
|
13288
|
+
}
|
|
13289
|
+
return this.serverVoteState.ownVotesByOptionId[vote.option_id]?.id;
|
|
13290
|
+
};
|
|
13291
|
+
this.takeServerVoteState = () => {
|
|
13292
|
+
clearTimeout(this.catchUpTimeout);
|
|
13293
|
+
this.succeededChanges = [];
|
|
13294
|
+
return {
|
|
13295
|
+
...this.serverVoteState,
|
|
13296
|
+
maxVotedOptionIds: getMaxVotedOptionIds(this.serverVoteState.vote_counts_by_option)
|
|
13297
|
+
};
|
|
13298
|
+
};
|
|
13299
|
+
this.scheduleCatchUp = () => {
|
|
13300
|
+
clearTimeout(this.catchUpTimeout);
|
|
13301
|
+
if (this.pendingCount === 0) return;
|
|
13302
|
+
this.catchUpTimeout = setTimeout(this.applySucceededChanges, CATCH_UP_TIMEOUT_MS);
|
|
13303
|
+
};
|
|
13304
|
+
/**
|
|
13305
|
+
* A WS event of an own vote change got lost, so the server's own votes are stale. All requests
|
|
13306
|
+
* have finished (the timeout starts once nothing is left to send and a new request cancels it),
|
|
13307
|
+
* so applying the succeeded changes in order gives the server's own votes (changes whose events
|
|
13308
|
+
* did arrive end up the same). They are shown with the vote counts of the last WS event, which
|
|
13309
|
+
* contain the other users' votes too.
|
|
13310
|
+
*/
|
|
13311
|
+
this.applySucceededChanges = () => {
|
|
13312
|
+
const { enforce_unique_vote } = this.poll.data;
|
|
13313
|
+
let ownVotesByOptionId = { ...this.serverVoteState.ownVotesByOptionId };
|
|
13314
|
+
for (const { optionId, vote } of this.succeededChanges) {
|
|
13315
|
+
if (!vote) {
|
|
13316
|
+
delete ownVotesByOptionId[optionId];
|
|
13317
|
+
} else if (enforce_unique_vote) {
|
|
13318
|
+
ownVotesByOptionId = { [optionId]: vote };
|
|
13319
|
+
} else {
|
|
13320
|
+
ownVotesByOptionId[optionId] = vote;
|
|
13321
|
+
}
|
|
13322
|
+
}
|
|
13323
|
+
this.serverVoteState = { ...this.serverVoteState, ownVotesByOptionId };
|
|
13324
|
+
this.pendingCount = 0;
|
|
13325
|
+
this.poll.state.partialNext(this.takeServerVoteState());
|
|
13326
|
+
};
|
|
13327
|
+
this.client = client;
|
|
13328
|
+
this.poll = poll;
|
|
13329
|
+
this.serverVoteState = pickVoteState(poll.data);
|
|
13330
|
+
}
|
|
13331
|
+
get serverOwnVotes() {
|
|
13332
|
+
return this.serverVoteState.ownVotesByOptionId;
|
|
13333
|
+
}
|
|
13334
|
+
};
|
|
13335
|
+
function pickVoteState(state) {
|
|
13336
|
+
return {
|
|
13337
|
+
latest_votes_by_option: state.latest_votes_by_option,
|
|
13338
|
+
ownVotesByOptionId: state.ownVotesByOptionId,
|
|
13339
|
+
vote_count: state.vote_count,
|
|
13340
|
+
vote_counts_by_option: state.vote_counts_by_option
|
|
13341
|
+
};
|
|
13342
|
+
}
|
|
13343
|
+
function applyOwnVoteDelta(state, { add = [], remove = [] }) {
|
|
13344
|
+
const ownVotesByOptionId = { ...state.ownVotesByOptionId };
|
|
13345
|
+
const voteCountsByOption = { ...state.vote_counts_by_option };
|
|
13346
|
+
const latestVotesByOption = state.latest_votes_by_option ? { ...state.latest_votes_by_option } : state.latest_votes_by_option;
|
|
13347
|
+
let voteCount = state.vote_count ?? 0;
|
|
13348
|
+
for (const vote of remove) {
|
|
13349
|
+
const optionId = vote.option_id;
|
|
13350
|
+
if (!optionId) continue;
|
|
13351
|
+
delete ownVotesByOptionId[optionId];
|
|
13352
|
+
const optionVoteCount = (voteCountsByOption[optionId] ?? 0) - 1;
|
|
13353
|
+
if (optionVoteCount > 0) {
|
|
13354
|
+
voteCountsByOption[optionId] = optionVoteCount;
|
|
13355
|
+
} else {
|
|
13356
|
+
delete voteCountsByOption[optionId];
|
|
13357
|
+
}
|
|
13358
|
+
voteCount = Math.max(voteCount - 1, 0);
|
|
13359
|
+
if (latestVotesByOption?.[optionId]) {
|
|
13360
|
+
latestVotesByOption[optionId] = latestVotesByOption[optionId].filter(
|
|
13361
|
+
(latestVote) => latestVote.id !== vote.id
|
|
13362
|
+
);
|
|
13363
|
+
}
|
|
13364
|
+
}
|
|
13365
|
+
for (const vote of add) {
|
|
13366
|
+
const optionId = vote.option_id;
|
|
13367
|
+
if (!optionId) continue;
|
|
13368
|
+
ownVotesByOptionId[optionId] = vote;
|
|
13369
|
+
voteCountsByOption[optionId] = (voteCountsByOption[optionId] ?? 0) + 1;
|
|
13370
|
+
voteCount += 1;
|
|
13371
|
+
if (latestVotesByOption && state.voting_visibility !== "anonymous") {
|
|
13372
|
+
latestVotesByOption[optionId] = [vote, ...latestVotesByOption[optionId] ?? []];
|
|
13373
|
+
}
|
|
13374
|
+
}
|
|
13375
|
+
return {
|
|
13376
|
+
latest_votes_by_option: latestVotesByOption,
|
|
13377
|
+
maxVotedOptionIds: getMaxVotedOptionIds(voteCountsByOption),
|
|
13378
|
+
ownVotesByOptionId,
|
|
13379
|
+
vote_count: voteCount,
|
|
13380
|
+
vote_counts_by_option: voteCountsByOption
|
|
13381
|
+
};
|
|
13382
|
+
}
|
|
13383
|
+
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13384
|
+
let maxVotes = 0;
|
|
13385
|
+
let winningOptions = [];
|
|
13386
|
+
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13387
|
+
if (count > maxVotes) {
|
|
13388
|
+
winningOptions = [id];
|
|
13389
|
+
maxVotes = count;
|
|
13390
|
+
} else if (count === maxVotes) {
|
|
13391
|
+
winningOptions.push(id);
|
|
13392
|
+
}
|
|
13393
|
+
}
|
|
13394
|
+
return winningOptions;
|
|
13395
|
+
}
|
|
13396
|
+
|
|
13164
13397
|
// src/poll.ts
|
|
13165
13398
|
var isPollUpdatedEvent = (e) => e.type === "poll.updated";
|
|
13166
13399
|
var isPollClosedEventEvent = (e) => e.type === "poll.closed";
|
|
@@ -13170,6 +13403,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
|
|
|
13170
13403
|
var isVoteAnswer = (vote) => !!vote?.answer_text;
|
|
13171
13404
|
var Poll = class {
|
|
13172
13405
|
constructor({ client, poll }) {
|
|
13406
|
+
// Set whenever server data (WS event or rehydration) confirms that the poll is closed,
|
|
13407
|
+
// so that a failed optimistic close does not reopen a poll that is actually closed.
|
|
13408
|
+
this.closeConfirmed = false;
|
|
13173
13409
|
this.getInitialStateFromPollResponse = (poll) => {
|
|
13174
13410
|
const { own_votes, id, ...pollResponseForState } = poll;
|
|
13175
13411
|
const { ownAnswer, ownVotes } = own_votes?.reduce(
|
|
@@ -13193,25 +13429,47 @@ var Poll = class {
|
|
|
13193
13429
|
ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
|
|
13194
13430
|
};
|
|
13195
13431
|
};
|
|
13432
|
+
// persists only the server's votes, never the optimistic ones
|
|
13196
13433
|
this.upsertOfflineDb = () => {
|
|
13434
|
+
const serverState = this.optimisticVotes.withServerVotes(this.data);
|
|
13197
13435
|
this.client.offlineDb?.executeQuerySafely(
|
|
13198
|
-
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
|
|
13436
|
+
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
|
|
13199
13437
|
{ method: "upsertPoll" }
|
|
13200
13438
|
);
|
|
13201
13439
|
};
|
|
13202
13440
|
this.reinitializeState = (poll) => {
|
|
13203
|
-
|
|
13441
|
+
if (poll.is_closed) this.closeConfirmed = true;
|
|
13442
|
+
const initialState = this.getInitialStateFromPollResponse(poll);
|
|
13443
|
+
this.optimisticVotes.reset(initialState);
|
|
13444
|
+
this.state.partialNext(initialState);
|
|
13445
|
+
};
|
|
13446
|
+
// Hands the vote state of a WS vote event to the optimistic votes, which decide whether to show it.
|
|
13447
|
+
this.updateFromVoteEvent = (event, ownVotesByOptionId, answerState) => {
|
|
13448
|
+
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13449
|
+
const shownVoteState = this.optimisticVotes.handleVoteEvent(event.poll, {
|
|
13450
|
+
confirmsOwnVote: isOwnVote && !isVoteAnswer(event.poll_vote) && !!event.poll_vote.option_id,
|
|
13451
|
+
ownVotesByOptionId
|
|
13452
|
+
});
|
|
13453
|
+
this.state.partialNext({
|
|
13454
|
+
...answerState,
|
|
13455
|
+
answers_count: event.poll.answers_count,
|
|
13456
|
+
lastActivityAt: new Date(event.created_at),
|
|
13457
|
+
...shownVoteState
|
|
13458
|
+
});
|
|
13459
|
+
this.upsertOfflineDb();
|
|
13204
13460
|
};
|
|
13205
13461
|
this.handlePollUpdated = (event) => {
|
|
13206
13462
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
13207
13463
|
if (!isPollUpdatedEvent(event)) return;
|
|
13208
13464
|
const { id, ...pollData } = extractPollData(event.poll);
|
|
13465
|
+
if (pollData.is_closed) this.closeConfirmed = true;
|
|
13209
13466
|
this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
|
|
13210
13467
|
this.upsertOfflineDb();
|
|
13211
13468
|
};
|
|
13212
13469
|
this.handlePollClosed = (event) => {
|
|
13213
13470
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
13214
13471
|
if (!isPollClosedEventEvent(event)) return;
|
|
13472
|
+
this.closeConfirmed = true;
|
|
13215
13473
|
this.state.partialNext({
|
|
13216
13474
|
is_closed: true,
|
|
13217
13475
|
lastActivityAt: new Date(event.created_at)
|
|
@@ -13225,8 +13483,7 @@ var Poll = class {
|
|
|
13225
13483
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13226
13484
|
let latestAnswers = [...currentState.latest_answers];
|
|
13227
13485
|
let ownAnswer = currentState.ownAnswer;
|
|
13228
|
-
const ownVotesByOptionId =
|
|
13229
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13486
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13230
13487
|
if (isOwnVote) {
|
|
13231
13488
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13232
13489
|
ownAnswer = event.poll_vote;
|
|
@@ -13236,19 +13493,11 @@ var Poll = class {
|
|
|
13236
13493
|
}
|
|
13237
13494
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13238
13495
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13239
|
-
} else {
|
|
13240
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13241
13496
|
}
|
|
13242
|
-
|
|
13243
|
-
this.state.partialNext({
|
|
13244
|
-
...pollEnrichData,
|
|
13497
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13245
13498
|
latest_answers: latestAnswers,
|
|
13246
|
-
|
|
13247
|
-
ownAnswer,
|
|
13248
|
-
ownVotesByOptionId,
|
|
13249
|
-
maxVotedOptionIds
|
|
13499
|
+
ownAnswer
|
|
13250
13500
|
});
|
|
13251
|
-
this.upsertOfflineDb();
|
|
13252
13501
|
};
|
|
13253
13502
|
this.handleVoteChanged = (event) => {
|
|
13254
13503
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13257,8 +13506,7 @@ var Poll = class {
|
|
|
13257
13506
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13258
13507
|
let latestAnswers = [...currentState.latest_answers];
|
|
13259
13508
|
let ownAnswer = currentState.ownAnswer;
|
|
13260
|
-
let ownVotesByOptionId =
|
|
13261
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13509
|
+
let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
|
|
13262
13510
|
if (isOwnVote) {
|
|
13263
13511
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13264
13512
|
latestAnswers = [
|
|
@@ -13282,23 +13530,14 @@ var Poll = class {
|
|
|
13282
13530
|
if (ownAnswer?.id === event.poll_vote.id) {
|
|
13283
13531
|
ownAnswer = void 0;
|
|
13284
13532
|
}
|
|
13285
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13286
13533
|
}
|
|
13287
13534
|
} else if (isVoteAnswer(event.poll_vote)) {
|
|
13288
13535
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13289
|
-
} else {
|
|
13290
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13291
13536
|
}
|
|
13292
|
-
|
|
13293
|
-
this.state.partialNext({
|
|
13294
|
-
...pollEnrichData,
|
|
13537
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13295
13538
|
latest_answers: latestAnswers,
|
|
13296
|
-
|
|
13297
|
-
ownAnswer,
|
|
13298
|
-
ownVotesByOptionId,
|
|
13299
|
-
maxVotedOptionIds
|
|
13539
|
+
ownAnswer
|
|
13300
13540
|
});
|
|
13301
|
-
this.upsertOfflineDb();
|
|
13302
13541
|
};
|
|
13303
13542
|
this.handleVoteRemoved = (event) => {
|
|
13304
13543
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13307,29 +13546,19 @@ var Poll = class {
|
|
|
13307
13546
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13308
13547
|
let latestAnswers = [...currentState.latest_answers];
|
|
13309
13548
|
let ownAnswer = currentState.ownAnswer;
|
|
13310
|
-
const ownVotesByOptionId = { ...
|
|
13311
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13549
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13312
13550
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13313
13551
|
latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
|
|
13314
13552
|
if (isOwnVote) {
|
|
13315
13553
|
ownAnswer = void 0;
|
|
13316
13554
|
}
|
|
13317
|
-
} else {
|
|
13318
|
-
|
|
13319
|
-
if (isOwnVote && event.poll_vote.option_id) {
|
|
13320
|
-
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13321
|
-
}
|
|
13555
|
+
} else if (isOwnVote && event.poll_vote.option_id) {
|
|
13556
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13322
13557
|
}
|
|
13323
|
-
|
|
13324
|
-
this.state.partialNext({
|
|
13325
|
-
...pollEnrichData,
|
|
13558
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13326
13559
|
latest_answers: latestAnswers,
|
|
13327
|
-
|
|
13328
|
-
ownAnswer,
|
|
13329
|
-
ownVotesByOptionId,
|
|
13330
|
-
maxVotedOptionIds
|
|
13560
|
+
ownAnswer
|
|
13331
13561
|
});
|
|
13332
|
-
this.upsertOfflineDb();
|
|
13333
13562
|
};
|
|
13334
13563
|
this.query = async (id) => {
|
|
13335
13564
|
const { poll } = await this.client.getPoll(id);
|
|
@@ -13338,11 +13567,33 @@ var Poll = class {
|
|
|
13338
13567
|
};
|
|
13339
13568
|
this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
|
|
13340
13569
|
this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
|
|
13341
|
-
|
|
13570
|
+
/**
|
|
13571
|
+
* Closes the poll. The poll is marked as closed optimistically and the change is
|
|
13572
|
+
* reverted if the request fails, unless the server confirmed the close meanwhile.
|
|
13573
|
+
* Does nothing if the poll is already closed.
|
|
13574
|
+
*/
|
|
13575
|
+
this.close = async () => {
|
|
13576
|
+
if (this.data.is_closed) return;
|
|
13577
|
+
this.closeConfirmed = false;
|
|
13578
|
+
this.state.partialNext({ is_closed: true });
|
|
13579
|
+
try {
|
|
13580
|
+
return await this.client.closePoll(this.id);
|
|
13581
|
+
} catch (error) {
|
|
13582
|
+
if (!this.closeConfirmed) {
|
|
13583
|
+
this.state.partialNext({ is_closed: false });
|
|
13584
|
+
}
|
|
13585
|
+
throw error;
|
|
13586
|
+
}
|
|
13587
|
+
};
|
|
13342
13588
|
this.delete = async () => await this.client.deletePoll(this.id);
|
|
13343
13589
|
this.createOption = async (option) => await this.client.createPollOption(this.id, option);
|
|
13344
13590
|
this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
|
|
13345
13591
|
this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
|
|
13592
|
+
/**
|
|
13593
|
+
* Casts a vote for an option. The vote is shown right away through a local placeholder vote,
|
|
13594
|
+
* which the server's vote replaces once the WS events of all pending vote changes arrived.
|
|
13595
|
+
* If the request fails, the change is reverted.
|
|
13596
|
+
*/
|
|
13346
13597
|
this.castVote = async (optionId, messageId) => {
|
|
13347
13598
|
const { max_votes_allowed, ownVotesByOptionId } = this.data;
|
|
13348
13599
|
const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
|
|
@@ -13359,11 +13610,14 @@ var Poll = class {
|
|
|
13359
13610
|
});
|
|
13360
13611
|
return;
|
|
13361
13612
|
}
|
|
13362
|
-
return await this.
|
|
13363
|
-
option_id: optionId
|
|
13364
|
-
});
|
|
13613
|
+
return await this.optimisticVotes.castVote(optionId, messageId);
|
|
13365
13614
|
};
|
|
13366
|
-
|
|
13615
|
+
/**
|
|
13616
|
+
* Removes a vote. The vote is removed right away and restored if the request fails. Vote
|
|
13617
|
+
* requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
|
|
13618
|
+
* id from the cast's response; if that cast failed, there is nothing to remove.
|
|
13619
|
+
*/
|
|
13620
|
+
this.removeVote = async (voteId, messageId) => await this.optimisticVotes.removeVote(voteId, messageId);
|
|
13367
13621
|
this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
|
|
13368
13622
|
this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
|
|
13369
13623
|
this.queryAnswers = async (params) => await this.client.queryPollAnswers(
|
|
@@ -13381,24 +13635,12 @@ var Poll = class {
|
|
|
13381
13635
|
this.client = client;
|
|
13382
13636
|
this.id = poll.id;
|
|
13383
13637
|
this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
|
|
13638
|
+
this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
|
|
13384
13639
|
}
|
|
13385
13640
|
get data() {
|
|
13386
13641
|
return this.state.getLatestValue();
|
|
13387
13642
|
}
|
|
13388
13643
|
};
|
|
13389
|
-
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13390
|
-
let maxVotes = 0;
|
|
13391
|
-
let winningOptions = [];
|
|
13392
|
-
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13393
|
-
if (count > maxVotes) {
|
|
13394
|
-
winningOptions = [id];
|
|
13395
|
-
maxVotes = count;
|
|
13396
|
-
} else if (count === maxVotes) {
|
|
13397
|
-
winningOptions.push(id);
|
|
13398
|
-
}
|
|
13399
|
-
}
|
|
13400
|
-
return winningOptions;
|
|
13401
|
-
}
|
|
13402
13644
|
function getOwnVotesByOptionId(ownVotes) {
|
|
13403
13645
|
return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
|
|
13404
13646
|
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
@@ -13420,7 +13662,7 @@ function extractPollData(pollResponse) {
|
|
|
13420
13662
|
voting_visibility: pollResponse.voting_visibility
|
|
13421
13663
|
};
|
|
13422
13664
|
}
|
|
13423
|
-
function mapPollStateToResponse(poll) {
|
|
13665
|
+
function mapPollStateToResponse(poll, state = poll.data) {
|
|
13424
13666
|
const {
|
|
13425
13667
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13426
13668
|
lastActivityAt,
|
|
@@ -13429,7 +13671,7 @@ function mapPollStateToResponse(poll) {
|
|
|
13429
13671
|
ownVotesByOptionId,
|
|
13430
13672
|
ownAnswer,
|
|
13431
13673
|
...restState
|
|
13432
|
-
} =
|
|
13674
|
+
} = state;
|
|
13433
13675
|
const ownVotes = [
|
|
13434
13676
|
...Object.values(ownVotesByOptionId),
|
|
13435
13677
|
...ownAnswer ? [ownAnswer] : []
|
|
@@ -14587,6 +14829,8 @@ var StreamChat = class _StreamChat {
|
|
|
14587
14829
|
* The main use case for SDKs who do independent state recovery for channels.
|
|
14588
14830
|
*/
|
|
14589
14831
|
this.preventThreadCleanup = false;
|
|
14832
|
+
this._wsPromiseSettled = true;
|
|
14833
|
+
this._wsConnectId = 0;
|
|
14590
14834
|
this.nextRequestAbortController = null;
|
|
14591
14835
|
/**
|
|
14592
14836
|
* @private
|
|
@@ -14632,17 +14876,20 @@ var StreamChat = class _StreamChat {
|
|
|
14632
14876
|
const setTokenPromise = this._setToken(user, userTokenOrProvider);
|
|
14633
14877
|
this._setUser(user);
|
|
14634
14878
|
const wsPromise = this.openConnection();
|
|
14635
|
-
|
|
14879
|
+
const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
|
|
14636
14880
|
(result) => result[1]
|
|
14637
14881
|
// We only return connection promise;
|
|
14638
14882
|
);
|
|
14883
|
+
this.setUserPromise = setUserPromise;
|
|
14639
14884
|
try {
|
|
14640
|
-
return await
|
|
14885
|
+
return await setUserPromise;
|
|
14641
14886
|
} catch (err) {
|
|
14642
|
-
if (this.
|
|
14643
|
-
this.
|
|
14644
|
-
|
|
14645
|
-
|
|
14887
|
+
if (this.setUserPromise === setUserPromise && this.userID === user.id) {
|
|
14888
|
+
if (this.persistUserOnConnectionFailure) {
|
|
14889
|
+
this.closeConnection();
|
|
14890
|
+
} else {
|
|
14891
|
+
this.disconnectUser();
|
|
14892
|
+
}
|
|
14646
14893
|
}
|
|
14647
14894
|
throw err;
|
|
14648
14895
|
}
|
|
@@ -14738,9 +14985,50 @@ var StreamChat = class _StreamChat {
|
|
|
14738
14985
|
return;
|
|
14739
14986
|
}
|
|
14740
14987
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
14741
|
-
|
|
14988
|
+
const wsPromise = this._bindWsPromise(this.connect());
|
|
14742
14989
|
this._startCleaning();
|
|
14743
|
-
return
|
|
14990
|
+
return wsPromise;
|
|
14991
|
+
};
|
|
14992
|
+
/**
|
|
14993
|
+
* Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
|
|
14994
|
+
*/
|
|
14995
|
+
this._rejectPendingWsPromise = (reason) => {
|
|
14996
|
+
if (!this._wsPromiseSettled) {
|
|
14997
|
+
this._wsConnectId += 1;
|
|
14998
|
+
this._wsPromiseSettled = true;
|
|
14999
|
+
this.wsPromise?.catch(() => {
|
|
15000
|
+
});
|
|
15001
|
+
this._rejectWsPromise?.(reason);
|
|
15002
|
+
}
|
|
15003
|
+
};
|
|
15004
|
+
/**
|
|
15005
|
+
* Keep a single pending `wsPromise` across close/reopen so callers that already
|
|
15006
|
+
* `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
|
|
15007
|
+
*/
|
|
15008
|
+
this._bindWsPromise = (connectPromise) => {
|
|
15009
|
+
const connectId = ++this._wsConnectId;
|
|
15010
|
+
let pending = this.wsPromise;
|
|
15011
|
+
if (this._wsPromiseSettled || !pending) {
|
|
15012
|
+
this._wsPromiseSettled = false;
|
|
15013
|
+
pending = new Promise((resolve, reject) => {
|
|
15014
|
+
this._resolveWsPromise = resolve;
|
|
15015
|
+
this._rejectWsPromise = reject;
|
|
15016
|
+
});
|
|
15017
|
+
this.wsPromise = pending;
|
|
15018
|
+
}
|
|
15019
|
+
connectPromise.then(
|
|
15020
|
+
(value) => {
|
|
15021
|
+
if (connectId !== this._wsConnectId) return;
|
|
15022
|
+
this._wsPromiseSettled = true;
|
|
15023
|
+
this._resolveWsPromise?.(value);
|
|
15024
|
+
},
|
|
15025
|
+
(error) => {
|
|
15026
|
+
if (connectId !== this._wsConnectId) return;
|
|
15027
|
+
this._wsPromiseSettled = true;
|
|
15028
|
+
this._rejectWsPromise?.(error);
|
|
15029
|
+
}
|
|
15030
|
+
);
|
|
15031
|
+
return pending;
|
|
14744
15032
|
};
|
|
14745
15033
|
/**
|
|
14746
15034
|
* @deprecated Please use client.openConnction instead.
|
|
@@ -14774,6 +15062,10 @@ var StreamChat = class _StreamChat {
|
|
|
14774
15062
|
delete this._user;
|
|
14775
15063
|
delete this.userID;
|
|
14776
15064
|
this.anonymous = false;
|
|
15065
|
+
this._rejectPendingWsPromise(
|
|
15066
|
+
new Error("Connection was closed because disconnectUser() was called")
|
|
15067
|
+
);
|
|
15068
|
+
this.wsPromise = null;
|
|
14777
15069
|
const closePromise = this.closeConnection(timeout);
|
|
14778
15070
|
for (const channel of Object.values(this.activeChannels)) {
|
|
14779
15071
|
channel._disconnect();
|
|
@@ -15044,7 +15336,9 @@ var StreamChat = class _StreamChat {
|
|
|
15044
15336
|
type: "connection.recovered"
|
|
15045
15337
|
});
|
|
15046
15338
|
}
|
|
15047
|
-
this.
|
|
15339
|
+
if (this._wsPromiseSettled) {
|
|
15340
|
+
this.wsPromise = Promise.resolve();
|
|
15341
|
+
}
|
|
15048
15342
|
this.setUserPromise = Promise.resolve();
|
|
15049
15343
|
};
|
|
15050
15344
|
/**
|
|
@@ -15162,7 +15456,7 @@ var StreamChat = class _StreamChat {
|
|
|
15162
15456
|
return this.userAgent;
|
|
15163
15457
|
}
|
|
15164
15458
|
if (!this.cachedUserAgent) {
|
|
15165
|
-
const version = "9.
|
|
15459
|
+
const version = "9.54.0";
|
|
15166
15460
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
15167
15461
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
15168
15462
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -18404,6 +18698,10 @@ var StreamChat = class _StreamChat {
|
|
|
18404
18698
|
/**
|
|
18405
18699
|
* Update Channels Batch
|
|
18406
18700
|
*
|
|
18701
|
+
* For the `updateData` operation, `data.custom` replaces the channel's whole
|
|
18702
|
+
* custom object, while the root-level `custom_set` / `custom_unset` patch
|
|
18703
|
+
* individual keys and leave the rest untouched.
|
|
18704
|
+
*
|
|
18407
18705
|
* @param {UpdateChannelsBatchOptions} payload for updating channels in batch
|
|
18408
18706
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
18409
18707
|
*/
|