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
package/dist/cjs/index.node.js
CHANGED
|
@@ -351,6 +351,7 @@ var Campaign = class {
|
|
|
351
351
|
};
|
|
352
352
|
|
|
353
353
|
// src/channel_batch_updater.ts
|
|
354
|
+
var isChannelBatchDataUpdateOptions = (update) => "data" in update || "custom_set" in update || "custom_unset" in update;
|
|
354
355
|
var ChannelBatchUpdater = class {
|
|
355
356
|
constructor(client) {
|
|
356
357
|
this.client = client;
|
|
@@ -489,19 +490,14 @@ var ChannelBatchUpdater = class {
|
|
|
489
490
|
filter
|
|
490
491
|
});
|
|
491
492
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
* updateData - Update data on channels matching the filter
|
|
495
|
-
*
|
|
496
|
-
* @param {UpdateChannelsBatchFilters} filter Filter to select channels
|
|
497
|
-
* @param {BatchChannelDataUpdate} data Data to update
|
|
498
|
-
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
499
|
-
*/
|
|
500
|
-
async updateData(filter, data) {
|
|
493
|
+
async updateData(filter, update) {
|
|
494
|
+
const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update) ? update : { data: update };
|
|
501
495
|
return await this.client.updateChannelsBatch({
|
|
502
496
|
operation: "updateData",
|
|
503
497
|
filter,
|
|
504
|
-
data
|
|
498
|
+
...data && { data },
|
|
499
|
+
...custom_set && { custom_set },
|
|
500
|
+
...custom_unset && { custom_unset }
|
|
505
501
|
});
|
|
506
502
|
}
|
|
507
503
|
};
|
|
@@ -5883,10 +5879,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5883
5879
|
const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
|
|
5884
5880
|
(preview) => LinkPreviewsManager.getPreviewData(preview)
|
|
5885
5881
|
);
|
|
5886
|
-
const
|
|
5882
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
5887
5883
|
linkPreviews
|
|
5888
5884
|
);
|
|
5889
|
-
|
|
5885
|
+
const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
|
|
5886
|
+
if (!messageAttachments.length && !localAttachments.length) return forward();
|
|
5890
5887
|
const sendOptions = { ...state.sendOptions };
|
|
5891
5888
|
const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
|
|
5892
5889
|
if (skip_enrich_url) {
|
|
@@ -5894,14 +5891,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5894
5891
|
}
|
|
5895
5892
|
return next({
|
|
5896
5893
|
...state,
|
|
5897
|
-
message: {
|
|
5894
|
+
message: messageAttachments.length ? {
|
|
5898
5895
|
...state.message,
|
|
5899
|
-
attachments
|
|
5900
|
-
},
|
|
5901
|
-
localMessage: {
|
|
5896
|
+
attachments: messageAttachments
|
|
5897
|
+
} : state.message,
|
|
5898
|
+
localMessage: localAttachments.length ? {
|
|
5902
5899
|
...state.localMessage,
|
|
5903
|
-
attachments
|
|
5904
|
-
},
|
|
5900
|
+
attachments: localAttachments
|
|
5901
|
+
} : state.localMessage,
|
|
5905
5902
|
sendOptions
|
|
5906
5903
|
});
|
|
5907
5904
|
}
|
|
@@ -13130,6 +13127,242 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
13130
13127
|
}
|
|
13131
13128
|
};
|
|
13132
13129
|
|
|
13130
|
+
// src/poll_optimistic_votes.ts
|
|
13131
|
+
var CATCH_UP_TIMEOUT_MS = 3e3;
|
|
13132
|
+
var OptimisticPollVotes = class {
|
|
13133
|
+
constructor({ client, poll }) {
|
|
13134
|
+
// Own vote changes not confirmed by a WS event yet
|
|
13135
|
+
this.pendingCount = 0;
|
|
13136
|
+
// Own vote changes whose requests succeeded since the server's vote state was last shown
|
|
13137
|
+
this.succeededChanges = [];
|
|
13138
|
+
// Vote requests with this tag are sent one after another. A symbol, so it is unique per instance.
|
|
13139
|
+
this.requestTag = /* @__PURE__ */ Symbol("poll-votes");
|
|
13140
|
+
this.withServerVotes = (pollState) => ({
|
|
13141
|
+
...pollState,
|
|
13142
|
+
...this.serverVoteState
|
|
13143
|
+
});
|
|
13144
|
+
/**
|
|
13145
|
+
* Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
|
|
13146
|
+
* votes, it replaces the existing own votes.
|
|
13147
|
+
*/
|
|
13148
|
+
this.castVote = (optionId, messageId) => {
|
|
13149
|
+
const sendRequest = () => this.client.castPollVote(messageId, this.poll.id, { option_id: optionId });
|
|
13150
|
+
const state = this.poll.data;
|
|
13151
|
+
if (state.ownVotesByOptionId[optionId]) return this.send(sendRequest);
|
|
13152
|
+
const replacedVotes = state.enforce_unique_vote ? Object.values(state.ownVotesByOptionId) : [];
|
|
13153
|
+
this.poll.state.partialNext(
|
|
13154
|
+
applyOwnVoteDelta(state, {
|
|
13155
|
+
add: [this.createPlaceholderVote(optionId)],
|
|
13156
|
+
remove: replacedVotes
|
|
13157
|
+
})
|
|
13158
|
+
);
|
|
13159
|
+
return this.track(sendRequest, (response) => ({
|
|
13160
|
+
optionId,
|
|
13161
|
+
vote: response.vote
|
|
13162
|
+
}));
|
|
13163
|
+
};
|
|
13164
|
+
// Removes a vote, right away.
|
|
13165
|
+
this.removeVote = (voteId, messageId) => {
|
|
13166
|
+
const sendRequest = (id) => this.client.removePollVote(messageId, this.poll.id, id);
|
|
13167
|
+
const state = this.poll.data;
|
|
13168
|
+
const vote = Object.values(state.ownVotesByOptionId).find(({ id }) => id === voteId);
|
|
13169
|
+
if (!vote?.option_id) return this.send(() => sendRequest(voteId));
|
|
13170
|
+
const optionId = vote.option_id;
|
|
13171
|
+
this.poll.state.partialNext(applyOwnVoteDelta(state, { remove: [vote] }));
|
|
13172
|
+
return this.track(
|
|
13173
|
+
// runs when the removal's turn comes, so a preceding cast of the vote has finished
|
|
13174
|
+
() => {
|
|
13175
|
+
const serverVoteId = this.getServerVoteId(vote);
|
|
13176
|
+
return serverVoteId ? sendRequest(serverVoteId) : Promise.resolve(void 0);
|
|
13177
|
+
},
|
|
13178
|
+
() => ({ optionId })
|
|
13179
|
+
);
|
|
13180
|
+
};
|
|
13181
|
+
// Sends a vote request once the previous one has finished, whether it succeeded or not.
|
|
13182
|
+
this.send = async (sendRequest) => {
|
|
13183
|
+
try {
|
|
13184
|
+
return await withoutConcurrency(this.requestTag, sendRequest);
|
|
13185
|
+
} finally {
|
|
13186
|
+
if (!hasPending(this.requestTag)) this.scheduleCatchUp();
|
|
13187
|
+
}
|
|
13188
|
+
};
|
|
13189
|
+
// Stores the vote state of a WS vote event and returns the vote fields to show, if any.
|
|
13190
|
+
this.handleVoteEvent = (poll, {
|
|
13191
|
+
confirmsOwnVote,
|
|
13192
|
+
ownVotesByOptionId
|
|
13193
|
+
}) => {
|
|
13194
|
+
this.serverVoteState = {
|
|
13195
|
+
latest_votes_by_option: poll.latest_votes_by_option,
|
|
13196
|
+
ownVotesByOptionId,
|
|
13197
|
+
vote_count: poll.vote_count,
|
|
13198
|
+
vote_counts_by_option: poll.vote_counts_by_option
|
|
13199
|
+
};
|
|
13200
|
+
if (confirmsOwnVote) this.pendingCount = Math.max(this.pendingCount - 1, 0);
|
|
13201
|
+
return this.pendingCount > 0 ? {} : this.takeServerVoteState();
|
|
13202
|
+
};
|
|
13203
|
+
// Fresh server state (e.g. a refetch) replaces everything, pending changes are dropped.
|
|
13204
|
+
this.reset = (pollState) => {
|
|
13205
|
+
this.serverVoteState = pickVoteState(pollState);
|
|
13206
|
+
this.pendingCount = 0;
|
|
13207
|
+
this.succeededChanges = [];
|
|
13208
|
+
clearTimeout(this.catchUpTimeout);
|
|
13209
|
+
};
|
|
13210
|
+
// Tracks an own vote change until its request settles. `sendRequest` resolves to undefined
|
|
13211
|
+
// when no request was needed.
|
|
13212
|
+
this.track = async (sendRequest, toChange) => {
|
|
13213
|
+
this.pendingCount += 1;
|
|
13214
|
+
clearTimeout(this.catchUpTimeout);
|
|
13215
|
+
let response;
|
|
13216
|
+
try {
|
|
13217
|
+
response = await this.send(async () => {
|
|
13218
|
+
const result = await sendRequest();
|
|
13219
|
+
if (result !== void 0) this.succeededChanges.push(toChange(result));
|
|
13220
|
+
return result;
|
|
13221
|
+
});
|
|
13222
|
+
} catch (error) {
|
|
13223
|
+
this.settleWithoutEvent();
|
|
13224
|
+
throw error;
|
|
13225
|
+
}
|
|
13226
|
+
if (response === void 0) this.settleWithoutEvent();
|
|
13227
|
+
return response;
|
|
13228
|
+
};
|
|
13229
|
+
// A failed or skipped request gets no WS event, so it counts as confirmed right away.
|
|
13230
|
+
this.settleWithoutEvent = () => {
|
|
13231
|
+
this.pendingCount = Math.max(this.pendingCount - 1, 0);
|
|
13232
|
+
if (this.pendingCount === 0) this.poll.state.partialNext(this.takeServerVoteState());
|
|
13233
|
+
};
|
|
13234
|
+
// The vote shown for a cast until the server's vote arrives
|
|
13235
|
+
this.createPlaceholderVote = (optionId) => {
|
|
13236
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13237
|
+
return {
|
|
13238
|
+
created_at: now,
|
|
13239
|
+
id: `local-${generateUUIDv4()}`,
|
|
13240
|
+
option_id: optionId,
|
|
13241
|
+
poll_id: this.poll.id,
|
|
13242
|
+
updated_at: now,
|
|
13243
|
+
user: this.client.user,
|
|
13244
|
+
user_id: this.client.userID
|
|
13245
|
+
};
|
|
13246
|
+
};
|
|
13247
|
+
/**
|
|
13248
|
+
* The server id of a vote to remove, once everything sent before the removal has finished. Casts
|
|
13249
|
+
* and removals of an option alternate, so the option's last succeeded change tells: a cast is the
|
|
13250
|
+
* vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
|
|
13251
|
+
* one, the server's own vote of the option is the vote being removed.
|
|
13252
|
+
*/
|
|
13253
|
+
this.getServerVoteId = (vote) => {
|
|
13254
|
+
for (let i = this.succeededChanges.length - 1; i >= 0; i--) {
|
|
13255
|
+
const change = this.succeededChanges[i];
|
|
13256
|
+
if (change.optionId === vote.option_id) return change.vote?.id;
|
|
13257
|
+
}
|
|
13258
|
+
return this.serverVoteState.ownVotesByOptionId[vote.option_id]?.id;
|
|
13259
|
+
};
|
|
13260
|
+
this.takeServerVoteState = () => {
|
|
13261
|
+
clearTimeout(this.catchUpTimeout);
|
|
13262
|
+
this.succeededChanges = [];
|
|
13263
|
+
return {
|
|
13264
|
+
...this.serverVoteState,
|
|
13265
|
+
maxVotedOptionIds: getMaxVotedOptionIds(this.serverVoteState.vote_counts_by_option)
|
|
13266
|
+
};
|
|
13267
|
+
};
|
|
13268
|
+
this.scheduleCatchUp = () => {
|
|
13269
|
+
clearTimeout(this.catchUpTimeout);
|
|
13270
|
+
if (this.pendingCount === 0) return;
|
|
13271
|
+
this.catchUpTimeout = setTimeout(this.applySucceededChanges, CATCH_UP_TIMEOUT_MS);
|
|
13272
|
+
};
|
|
13273
|
+
/**
|
|
13274
|
+
* A WS event of an own vote change got lost, so the server's own votes are stale. All requests
|
|
13275
|
+
* have finished (the timeout starts once nothing is left to send and a new request cancels it),
|
|
13276
|
+
* so applying the succeeded changes in order gives the server's own votes (changes whose events
|
|
13277
|
+
* did arrive end up the same). They are shown with the vote counts of the last WS event, which
|
|
13278
|
+
* contain the other users' votes too.
|
|
13279
|
+
*/
|
|
13280
|
+
this.applySucceededChanges = () => {
|
|
13281
|
+
const { enforce_unique_vote } = this.poll.data;
|
|
13282
|
+
let ownVotesByOptionId = { ...this.serverVoteState.ownVotesByOptionId };
|
|
13283
|
+
for (const { optionId, vote } of this.succeededChanges) {
|
|
13284
|
+
if (!vote) {
|
|
13285
|
+
delete ownVotesByOptionId[optionId];
|
|
13286
|
+
} else if (enforce_unique_vote) {
|
|
13287
|
+
ownVotesByOptionId = { [optionId]: vote };
|
|
13288
|
+
} else {
|
|
13289
|
+
ownVotesByOptionId[optionId] = vote;
|
|
13290
|
+
}
|
|
13291
|
+
}
|
|
13292
|
+
this.serverVoteState = { ...this.serverVoteState, ownVotesByOptionId };
|
|
13293
|
+
this.pendingCount = 0;
|
|
13294
|
+
this.poll.state.partialNext(this.takeServerVoteState());
|
|
13295
|
+
};
|
|
13296
|
+
this.client = client;
|
|
13297
|
+
this.poll = poll;
|
|
13298
|
+
this.serverVoteState = pickVoteState(poll.data);
|
|
13299
|
+
}
|
|
13300
|
+
get serverOwnVotes() {
|
|
13301
|
+
return this.serverVoteState.ownVotesByOptionId;
|
|
13302
|
+
}
|
|
13303
|
+
};
|
|
13304
|
+
function pickVoteState(state) {
|
|
13305
|
+
return {
|
|
13306
|
+
latest_votes_by_option: state.latest_votes_by_option,
|
|
13307
|
+
ownVotesByOptionId: state.ownVotesByOptionId,
|
|
13308
|
+
vote_count: state.vote_count,
|
|
13309
|
+
vote_counts_by_option: state.vote_counts_by_option
|
|
13310
|
+
};
|
|
13311
|
+
}
|
|
13312
|
+
function applyOwnVoteDelta(state, { add = [], remove = [] }) {
|
|
13313
|
+
const ownVotesByOptionId = { ...state.ownVotesByOptionId };
|
|
13314
|
+
const voteCountsByOption = { ...state.vote_counts_by_option };
|
|
13315
|
+
const latestVotesByOption = state.latest_votes_by_option ? { ...state.latest_votes_by_option } : state.latest_votes_by_option;
|
|
13316
|
+
let voteCount = state.vote_count ?? 0;
|
|
13317
|
+
for (const vote of remove) {
|
|
13318
|
+
const optionId = vote.option_id;
|
|
13319
|
+
if (!optionId) continue;
|
|
13320
|
+
delete ownVotesByOptionId[optionId];
|
|
13321
|
+
const optionVoteCount = (voteCountsByOption[optionId] ?? 0) - 1;
|
|
13322
|
+
if (optionVoteCount > 0) {
|
|
13323
|
+
voteCountsByOption[optionId] = optionVoteCount;
|
|
13324
|
+
} else {
|
|
13325
|
+
delete voteCountsByOption[optionId];
|
|
13326
|
+
}
|
|
13327
|
+
voteCount = Math.max(voteCount - 1, 0);
|
|
13328
|
+
if (latestVotesByOption?.[optionId]) {
|
|
13329
|
+
latestVotesByOption[optionId] = latestVotesByOption[optionId].filter(
|
|
13330
|
+
(latestVote) => latestVote.id !== vote.id
|
|
13331
|
+
);
|
|
13332
|
+
}
|
|
13333
|
+
}
|
|
13334
|
+
for (const vote of add) {
|
|
13335
|
+
const optionId = vote.option_id;
|
|
13336
|
+
if (!optionId) continue;
|
|
13337
|
+
ownVotesByOptionId[optionId] = vote;
|
|
13338
|
+
voteCountsByOption[optionId] = (voteCountsByOption[optionId] ?? 0) + 1;
|
|
13339
|
+
voteCount += 1;
|
|
13340
|
+
if (latestVotesByOption && state.voting_visibility !== "anonymous") {
|
|
13341
|
+
latestVotesByOption[optionId] = [vote, ...latestVotesByOption[optionId] ?? []];
|
|
13342
|
+
}
|
|
13343
|
+
}
|
|
13344
|
+
return {
|
|
13345
|
+
latest_votes_by_option: latestVotesByOption,
|
|
13346
|
+
maxVotedOptionIds: getMaxVotedOptionIds(voteCountsByOption),
|
|
13347
|
+
ownVotesByOptionId,
|
|
13348
|
+
vote_count: voteCount,
|
|
13349
|
+
vote_counts_by_option: voteCountsByOption
|
|
13350
|
+
};
|
|
13351
|
+
}
|
|
13352
|
+
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13353
|
+
let maxVotes = 0;
|
|
13354
|
+
let winningOptions = [];
|
|
13355
|
+
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13356
|
+
if (count > maxVotes) {
|
|
13357
|
+
winningOptions = [id];
|
|
13358
|
+
maxVotes = count;
|
|
13359
|
+
} else if (count === maxVotes) {
|
|
13360
|
+
winningOptions.push(id);
|
|
13361
|
+
}
|
|
13362
|
+
}
|
|
13363
|
+
return winningOptions;
|
|
13364
|
+
}
|
|
13365
|
+
|
|
13133
13366
|
// src/poll.ts
|
|
13134
13367
|
var isPollUpdatedEvent = (e) => e.type === "poll.updated";
|
|
13135
13368
|
var isPollClosedEventEvent = (e) => e.type === "poll.closed";
|
|
@@ -13139,6 +13372,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
|
|
|
13139
13372
|
var isVoteAnswer = (vote) => !!vote?.answer_text;
|
|
13140
13373
|
var Poll = class {
|
|
13141
13374
|
constructor({ client, poll }) {
|
|
13375
|
+
// Set whenever server data (WS event or rehydration) confirms that the poll is closed,
|
|
13376
|
+
// so that a failed optimistic close does not reopen a poll that is actually closed.
|
|
13377
|
+
this.closeConfirmed = false;
|
|
13142
13378
|
this.getInitialStateFromPollResponse = (poll) => {
|
|
13143
13379
|
const { own_votes, id, ...pollResponseForState } = poll;
|
|
13144
13380
|
const { ownAnswer, ownVotes } = own_votes?.reduce(
|
|
@@ -13162,25 +13398,47 @@ var Poll = class {
|
|
|
13162
13398
|
ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
|
|
13163
13399
|
};
|
|
13164
13400
|
};
|
|
13401
|
+
// persists only the server's votes, never the optimistic ones
|
|
13165
13402
|
this.upsertOfflineDb = () => {
|
|
13403
|
+
const serverState = this.optimisticVotes.withServerVotes(this.data);
|
|
13166
13404
|
this.client.offlineDb?.executeQuerySafely(
|
|
13167
|
-
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
|
|
13405
|
+
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
|
|
13168
13406
|
{ method: "upsertPoll" }
|
|
13169
13407
|
);
|
|
13170
13408
|
};
|
|
13171
13409
|
this.reinitializeState = (poll) => {
|
|
13172
|
-
|
|
13410
|
+
if (poll.is_closed) this.closeConfirmed = true;
|
|
13411
|
+
const initialState = this.getInitialStateFromPollResponse(poll);
|
|
13412
|
+
this.optimisticVotes.reset(initialState);
|
|
13413
|
+
this.state.partialNext(initialState);
|
|
13414
|
+
};
|
|
13415
|
+
// Hands the vote state of a WS vote event to the optimistic votes, which decide whether to show it.
|
|
13416
|
+
this.updateFromVoteEvent = (event, ownVotesByOptionId, answerState) => {
|
|
13417
|
+
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13418
|
+
const shownVoteState = this.optimisticVotes.handleVoteEvent(event.poll, {
|
|
13419
|
+
confirmsOwnVote: isOwnVote && !isVoteAnswer(event.poll_vote) && !!event.poll_vote.option_id,
|
|
13420
|
+
ownVotesByOptionId
|
|
13421
|
+
});
|
|
13422
|
+
this.state.partialNext({
|
|
13423
|
+
...answerState,
|
|
13424
|
+
answers_count: event.poll.answers_count,
|
|
13425
|
+
lastActivityAt: new Date(event.created_at),
|
|
13426
|
+
...shownVoteState
|
|
13427
|
+
});
|
|
13428
|
+
this.upsertOfflineDb();
|
|
13173
13429
|
};
|
|
13174
13430
|
this.handlePollUpdated = (event) => {
|
|
13175
13431
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
13176
13432
|
if (!isPollUpdatedEvent(event)) return;
|
|
13177
13433
|
const { id, ...pollData } = extractPollData(event.poll);
|
|
13434
|
+
if (pollData.is_closed) this.closeConfirmed = true;
|
|
13178
13435
|
this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
|
|
13179
13436
|
this.upsertOfflineDb();
|
|
13180
13437
|
};
|
|
13181
13438
|
this.handlePollClosed = (event) => {
|
|
13182
13439
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
13183
13440
|
if (!isPollClosedEventEvent(event)) return;
|
|
13441
|
+
this.closeConfirmed = true;
|
|
13184
13442
|
this.state.partialNext({
|
|
13185
13443
|
is_closed: true,
|
|
13186
13444
|
lastActivityAt: new Date(event.created_at)
|
|
@@ -13194,8 +13452,7 @@ var Poll = class {
|
|
|
13194
13452
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13195
13453
|
let latestAnswers = [...currentState.latest_answers];
|
|
13196
13454
|
let ownAnswer = currentState.ownAnswer;
|
|
13197
|
-
const ownVotesByOptionId =
|
|
13198
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13455
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13199
13456
|
if (isOwnVote) {
|
|
13200
13457
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13201
13458
|
ownAnswer = event.poll_vote;
|
|
@@ -13205,19 +13462,11 @@ var Poll = class {
|
|
|
13205
13462
|
}
|
|
13206
13463
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13207
13464
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13208
|
-
} else {
|
|
13209
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13210
13465
|
}
|
|
13211
|
-
|
|
13212
|
-
this.state.partialNext({
|
|
13213
|
-
...pollEnrichData,
|
|
13466
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13214
13467
|
latest_answers: latestAnswers,
|
|
13215
|
-
|
|
13216
|
-
ownAnswer,
|
|
13217
|
-
ownVotesByOptionId,
|
|
13218
|
-
maxVotedOptionIds
|
|
13468
|
+
ownAnswer
|
|
13219
13469
|
});
|
|
13220
|
-
this.upsertOfflineDb();
|
|
13221
13470
|
};
|
|
13222
13471
|
this.handleVoteChanged = (event) => {
|
|
13223
13472
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13226,8 +13475,7 @@ var Poll = class {
|
|
|
13226
13475
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13227
13476
|
let latestAnswers = [...currentState.latest_answers];
|
|
13228
13477
|
let ownAnswer = currentState.ownAnswer;
|
|
13229
|
-
let ownVotesByOptionId =
|
|
13230
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13478
|
+
let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
|
|
13231
13479
|
if (isOwnVote) {
|
|
13232
13480
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13233
13481
|
latestAnswers = [
|
|
@@ -13251,23 +13499,14 @@ var Poll = class {
|
|
|
13251
13499
|
if (ownAnswer?.id === event.poll_vote.id) {
|
|
13252
13500
|
ownAnswer = void 0;
|
|
13253
13501
|
}
|
|
13254
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13255
13502
|
}
|
|
13256
13503
|
} else if (isVoteAnswer(event.poll_vote)) {
|
|
13257
13504
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13258
|
-
} else {
|
|
13259
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13260
13505
|
}
|
|
13261
|
-
|
|
13262
|
-
this.state.partialNext({
|
|
13263
|
-
...pollEnrichData,
|
|
13506
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13264
13507
|
latest_answers: latestAnswers,
|
|
13265
|
-
|
|
13266
|
-
ownAnswer,
|
|
13267
|
-
ownVotesByOptionId,
|
|
13268
|
-
maxVotedOptionIds
|
|
13508
|
+
ownAnswer
|
|
13269
13509
|
});
|
|
13270
|
-
this.upsertOfflineDb();
|
|
13271
13510
|
};
|
|
13272
13511
|
this.handleVoteRemoved = (event) => {
|
|
13273
13512
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13276,29 +13515,19 @@ var Poll = class {
|
|
|
13276
13515
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13277
13516
|
let latestAnswers = [...currentState.latest_answers];
|
|
13278
13517
|
let ownAnswer = currentState.ownAnswer;
|
|
13279
|
-
const ownVotesByOptionId = { ...
|
|
13280
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13518
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13281
13519
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13282
13520
|
latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
|
|
13283
13521
|
if (isOwnVote) {
|
|
13284
13522
|
ownAnswer = void 0;
|
|
13285
13523
|
}
|
|
13286
|
-
} else {
|
|
13287
|
-
|
|
13288
|
-
if (isOwnVote && event.poll_vote.option_id) {
|
|
13289
|
-
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13290
|
-
}
|
|
13524
|
+
} else if (isOwnVote && event.poll_vote.option_id) {
|
|
13525
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13291
13526
|
}
|
|
13292
|
-
|
|
13293
|
-
this.state.partialNext({
|
|
13294
|
-
...pollEnrichData,
|
|
13527
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13295
13528
|
latest_answers: latestAnswers,
|
|
13296
|
-
|
|
13297
|
-
ownAnswer,
|
|
13298
|
-
ownVotesByOptionId,
|
|
13299
|
-
maxVotedOptionIds
|
|
13529
|
+
ownAnswer
|
|
13300
13530
|
});
|
|
13301
|
-
this.upsertOfflineDb();
|
|
13302
13531
|
};
|
|
13303
13532
|
this.query = async (id) => {
|
|
13304
13533
|
const { poll } = await this.client.getPoll(id);
|
|
@@ -13307,11 +13536,33 @@ var Poll = class {
|
|
|
13307
13536
|
};
|
|
13308
13537
|
this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
|
|
13309
13538
|
this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
|
|
13310
|
-
|
|
13539
|
+
/**
|
|
13540
|
+
* Closes the poll. The poll is marked as closed optimistically and the change is
|
|
13541
|
+
* reverted if the request fails, unless the server confirmed the close meanwhile.
|
|
13542
|
+
* Does nothing if the poll is already closed.
|
|
13543
|
+
*/
|
|
13544
|
+
this.close = async () => {
|
|
13545
|
+
if (this.data.is_closed) return;
|
|
13546
|
+
this.closeConfirmed = false;
|
|
13547
|
+
this.state.partialNext({ is_closed: true });
|
|
13548
|
+
try {
|
|
13549
|
+
return await this.client.closePoll(this.id);
|
|
13550
|
+
} catch (error) {
|
|
13551
|
+
if (!this.closeConfirmed) {
|
|
13552
|
+
this.state.partialNext({ is_closed: false });
|
|
13553
|
+
}
|
|
13554
|
+
throw error;
|
|
13555
|
+
}
|
|
13556
|
+
};
|
|
13311
13557
|
this.delete = async () => await this.client.deletePoll(this.id);
|
|
13312
13558
|
this.createOption = async (option) => await this.client.createPollOption(this.id, option);
|
|
13313
13559
|
this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
|
|
13314
13560
|
this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
|
|
13561
|
+
/**
|
|
13562
|
+
* Casts a vote for an option. The vote is shown right away through a local placeholder vote,
|
|
13563
|
+
* which the server's vote replaces once the WS events of all pending vote changes arrived.
|
|
13564
|
+
* If the request fails, the change is reverted.
|
|
13565
|
+
*/
|
|
13315
13566
|
this.castVote = async (optionId, messageId) => {
|
|
13316
13567
|
const { max_votes_allowed, ownVotesByOptionId } = this.data;
|
|
13317
13568
|
const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
|
|
@@ -13328,11 +13579,14 @@ var Poll = class {
|
|
|
13328
13579
|
});
|
|
13329
13580
|
return;
|
|
13330
13581
|
}
|
|
13331
|
-
return await this.
|
|
13332
|
-
option_id: optionId
|
|
13333
|
-
});
|
|
13582
|
+
return await this.optimisticVotes.castVote(optionId, messageId);
|
|
13334
13583
|
};
|
|
13335
|
-
|
|
13584
|
+
/**
|
|
13585
|
+
* Removes a vote. The vote is removed right away and restored if the request fails. Vote
|
|
13586
|
+
* requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
|
|
13587
|
+
* id from the cast's response; if that cast failed, there is nothing to remove.
|
|
13588
|
+
*/
|
|
13589
|
+
this.removeVote = async (voteId, messageId) => await this.optimisticVotes.removeVote(voteId, messageId);
|
|
13336
13590
|
this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
|
|
13337
13591
|
this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
|
|
13338
13592
|
this.queryAnswers = async (params) => await this.client.queryPollAnswers(
|
|
@@ -13350,24 +13604,12 @@ var Poll = class {
|
|
|
13350
13604
|
this.client = client;
|
|
13351
13605
|
this.id = poll.id;
|
|
13352
13606
|
this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
|
|
13607
|
+
this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
|
|
13353
13608
|
}
|
|
13354
13609
|
get data() {
|
|
13355
13610
|
return this.state.getLatestValue();
|
|
13356
13611
|
}
|
|
13357
13612
|
};
|
|
13358
|
-
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13359
|
-
let maxVotes = 0;
|
|
13360
|
-
let winningOptions = [];
|
|
13361
|
-
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13362
|
-
if (count > maxVotes) {
|
|
13363
|
-
winningOptions = [id];
|
|
13364
|
-
maxVotes = count;
|
|
13365
|
-
} else if (count === maxVotes) {
|
|
13366
|
-
winningOptions.push(id);
|
|
13367
|
-
}
|
|
13368
|
-
}
|
|
13369
|
-
return winningOptions;
|
|
13370
|
-
}
|
|
13371
13613
|
function getOwnVotesByOptionId(ownVotes) {
|
|
13372
13614
|
return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
|
|
13373
13615
|
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
@@ -13389,7 +13631,7 @@ function extractPollData(pollResponse) {
|
|
|
13389
13631
|
voting_visibility: pollResponse.voting_visibility
|
|
13390
13632
|
};
|
|
13391
13633
|
}
|
|
13392
|
-
function mapPollStateToResponse(poll) {
|
|
13634
|
+
function mapPollStateToResponse(poll, state = poll.data) {
|
|
13393
13635
|
const {
|
|
13394
13636
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13395
13637
|
lastActivityAt,
|
|
@@ -13398,7 +13640,7 @@ function mapPollStateToResponse(poll) {
|
|
|
13398
13640
|
ownVotesByOptionId,
|
|
13399
13641
|
ownAnswer,
|
|
13400
13642
|
...restState
|
|
13401
|
-
} =
|
|
13643
|
+
} = state;
|
|
13402
13644
|
const ownVotes = [
|
|
13403
13645
|
...Object.values(ownVotesByOptionId),
|
|
13404
13646
|
...ownAnswer ? [ownAnswer] : []
|
|
@@ -14556,6 +14798,8 @@ var StreamChat = class _StreamChat {
|
|
|
14556
14798
|
* The main use case for SDKs who do independent state recovery for channels.
|
|
14557
14799
|
*/
|
|
14558
14800
|
this.preventThreadCleanup = false;
|
|
14801
|
+
this._wsPromiseSettled = true;
|
|
14802
|
+
this._wsConnectId = 0;
|
|
14559
14803
|
this.nextRequestAbortController = null;
|
|
14560
14804
|
/**
|
|
14561
14805
|
* @private
|
|
@@ -14601,17 +14845,20 @@ var StreamChat = class _StreamChat {
|
|
|
14601
14845
|
const setTokenPromise = this._setToken(user, userTokenOrProvider);
|
|
14602
14846
|
this._setUser(user);
|
|
14603
14847
|
const wsPromise = this.openConnection();
|
|
14604
|
-
|
|
14848
|
+
const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
|
|
14605
14849
|
(result) => result[1]
|
|
14606
14850
|
// We only return connection promise;
|
|
14607
14851
|
);
|
|
14852
|
+
this.setUserPromise = setUserPromise;
|
|
14608
14853
|
try {
|
|
14609
|
-
return await
|
|
14854
|
+
return await setUserPromise;
|
|
14610
14855
|
} catch (err) {
|
|
14611
|
-
if (this.
|
|
14612
|
-
this.
|
|
14613
|
-
|
|
14614
|
-
|
|
14856
|
+
if (this.setUserPromise === setUserPromise && this.userID === user.id) {
|
|
14857
|
+
if (this.persistUserOnConnectionFailure) {
|
|
14858
|
+
this.closeConnection();
|
|
14859
|
+
} else {
|
|
14860
|
+
this.disconnectUser();
|
|
14861
|
+
}
|
|
14615
14862
|
}
|
|
14616
14863
|
throw err;
|
|
14617
14864
|
}
|
|
@@ -14707,9 +14954,50 @@ var StreamChat = class _StreamChat {
|
|
|
14707
14954
|
return;
|
|
14708
14955
|
}
|
|
14709
14956
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
14710
|
-
|
|
14957
|
+
const wsPromise = this._bindWsPromise(this.connect());
|
|
14711
14958
|
this._startCleaning();
|
|
14712
|
-
return
|
|
14959
|
+
return wsPromise;
|
|
14960
|
+
};
|
|
14961
|
+
/**
|
|
14962
|
+
* Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
|
|
14963
|
+
*/
|
|
14964
|
+
this._rejectPendingWsPromise = (reason) => {
|
|
14965
|
+
if (!this._wsPromiseSettled) {
|
|
14966
|
+
this._wsConnectId += 1;
|
|
14967
|
+
this._wsPromiseSettled = true;
|
|
14968
|
+
this.wsPromise?.catch(() => {
|
|
14969
|
+
});
|
|
14970
|
+
this._rejectWsPromise?.(reason);
|
|
14971
|
+
}
|
|
14972
|
+
};
|
|
14973
|
+
/**
|
|
14974
|
+
* Keep a single pending `wsPromise` across close/reopen so callers that already
|
|
14975
|
+
* `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
|
|
14976
|
+
*/
|
|
14977
|
+
this._bindWsPromise = (connectPromise) => {
|
|
14978
|
+
const connectId = ++this._wsConnectId;
|
|
14979
|
+
let pending = this.wsPromise;
|
|
14980
|
+
if (this._wsPromiseSettled || !pending) {
|
|
14981
|
+
this._wsPromiseSettled = false;
|
|
14982
|
+
pending = new Promise((resolve, reject) => {
|
|
14983
|
+
this._resolveWsPromise = resolve;
|
|
14984
|
+
this._rejectWsPromise = reject;
|
|
14985
|
+
});
|
|
14986
|
+
this.wsPromise = pending;
|
|
14987
|
+
}
|
|
14988
|
+
connectPromise.then(
|
|
14989
|
+
(value) => {
|
|
14990
|
+
if (connectId !== this._wsConnectId) return;
|
|
14991
|
+
this._wsPromiseSettled = true;
|
|
14992
|
+
this._resolveWsPromise?.(value);
|
|
14993
|
+
},
|
|
14994
|
+
(error) => {
|
|
14995
|
+
if (connectId !== this._wsConnectId) return;
|
|
14996
|
+
this._wsPromiseSettled = true;
|
|
14997
|
+
this._rejectWsPromise?.(error);
|
|
14998
|
+
}
|
|
14999
|
+
);
|
|
15000
|
+
return pending;
|
|
14713
15001
|
};
|
|
14714
15002
|
/**
|
|
14715
15003
|
* @deprecated Please use client.openConnction instead.
|
|
@@ -14743,6 +15031,10 @@ var StreamChat = class _StreamChat {
|
|
|
14743
15031
|
delete this._user;
|
|
14744
15032
|
delete this.userID;
|
|
14745
15033
|
this.anonymous = false;
|
|
15034
|
+
this._rejectPendingWsPromise(
|
|
15035
|
+
new Error("Connection was closed because disconnectUser() was called")
|
|
15036
|
+
);
|
|
15037
|
+
this.wsPromise = null;
|
|
14746
15038
|
const closePromise = this.closeConnection(timeout);
|
|
14747
15039
|
for (const channel of Object.values(this.activeChannels)) {
|
|
14748
15040
|
channel._disconnect();
|
|
@@ -15013,7 +15305,9 @@ var StreamChat = class _StreamChat {
|
|
|
15013
15305
|
type: "connection.recovered"
|
|
15014
15306
|
});
|
|
15015
15307
|
}
|
|
15016
|
-
this.
|
|
15308
|
+
if (this._wsPromiseSettled) {
|
|
15309
|
+
this.wsPromise = Promise.resolve();
|
|
15310
|
+
}
|
|
15017
15311
|
this.setUserPromise = Promise.resolve();
|
|
15018
15312
|
};
|
|
15019
15313
|
/**
|
|
@@ -15131,7 +15425,7 @@ var StreamChat = class _StreamChat {
|
|
|
15131
15425
|
return this.userAgent;
|
|
15132
15426
|
}
|
|
15133
15427
|
if (!this.cachedUserAgent) {
|
|
15134
|
-
const version = "9.
|
|
15428
|
+
const version = "9.54.0";
|
|
15135
15429
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
15136
15430
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
15137
15431
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -18373,6 +18667,10 @@ var StreamChat = class _StreamChat {
|
|
|
18373
18667
|
/**
|
|
18374
18668
|
* Update Channels Batch
|
|
18375
18669
|
*
|
|
18670
|
+
* For the `updateData` operation, `data.custom` replaces the channel's whole
|
|
18671
|
+
* custom object, while the root-level `custom_set` / `custom_unset` patch
|
|
18672
|
+
* individual keys and leave the rest untouched.
|
|
18673
|
+
*
|
|
18376
18674
|
* @param {UpdateChannelsBatchOptions} payload for updating channels in batch
|
|
18377
18675
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
18378
18676
|
*/
|