stream-chat 9.53.0 → 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.
@@ -5879,10 +5879,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5879
5879
  const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
5880
5880
  (preview) => LinkPreviewsManager.getPreviewData(preview)
5881
5881
  );
5882
- const attachments = (state.message.attachments ?? []).concat(
5882
+ const messageAttachments = (state.message.attachments ?? []).concat(
5883
5883
  linkPreviews
5884
5884
  );
5885
- if (!attachments.length) return forward();
5885
+ const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
5886
+ if (!messageAttachments.length && !localAttachments.length) return forward();
5886
5887
  const sendOptions = { ...state.sendOptions };
5887
5888
  const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
5888
5889
  if (skip_enrich_url) {
@@ -5890,14 +5891,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
5890
5891
  }
5891
5892
  return next({
5892
5893
  ...state,
5893
- message: {
5894
+ message: messageAttachments.length ? {
5894
5895
  ...state.message,
5895
- attachments
5896
- },
5897
- localMessage: {
5896
+ attachments: messageAttachments
5897
+ } : state.message,
5898
+ localMessage: localAttachments.length ? {
5898
5899
  ...state.localMessage,
5899
- attachments
5900
- },
5900
+ attachments: localAttachments
5901
+ } : state.localMessage,
5901
5902
  sendOptions
5902
5903
  });
5903
5904
  }
@@ -13126,6 +13127,242 @@ var ThreadManager = class extends WithSubscriptions {
13126
13127
  }
13127
13128
  };
13128
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
+
13129
13366
  // src/poll.ts
13130
13367
  var isPollUpdatedEvent = (e) => e.type === "poll.updated";
13131
13368
  var isPollClosedEventEvent = (e) => e.type === "poll.closed";
@@ -13135,6 +13372,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
13135
13372
  var isVoteAnswer = (vote) => !!vote?.answer_text;
13136
13373
  var Poll = class {
13137
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;
13138
13378
  this.getInitialStateFromPollResponse = (poll) => {
13139
13379
  const { own_votes, id, ...pollResponseForState } = poll;
13140
13380
  const { ownAnswer, ownVotes } = own_votes?.reduce(
@@ -13158,25 +13398,47 @@ var Poll = class {
13158
13398
  ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
13159
13399
  };
13160
13400
  };
13401
+ // persists only the server's votes, never the optimistic ones
13161
13402
  this.upsertOfflineDb = () => {
13403
+ const serverState = this.optimisticVotes.withServerVotes(this.data);
13162
13404
  this.client.offlineDb?.executeQuerySafely(
13163
- (db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
13405
+ (db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
13164
13406
  { method: "upsertPoll" }
13165
13407
  );
13166
13408
  };
13167
13409
  this.reinitializeState = (poll) => {
13168
- this.state.partialNext(this.getInitialStateFromPollResponse(poll));
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();
13169
13429
  };
13170
13430
  this.handlePollUpdated = (event) => {
13171
13431
  if (event.poll?.id && event.poll.id !== this.id) return;
13172
13432
  if (!isPollUpdatedEvent(event)) return;
13173
13433
  const { id, ...pollData } = extractPollData(event.poll);
13434
+ if (pollData.is_closed) this.closeConfirmed = true;
13174
13435
  this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
13175
13436
  this.upsertOfflineDb();
13176
13437
  };
13177
13438
  this.handlePollClosed = (event) => {
13178
13439
  if (event.poll?.id && event.poll.id !== this.id) return;
13179
13440
  if (!isPollClosedEventEvent(event)) return;
13441
+ this.closeConfirmed = true;
13180
13442
  this.state.partialNext({
13181
13443
  is_closed: true,
13182
13444
  lastActivityAt: new Date(event.created_at)
@@ -13190,8 +13452,7 @@ var Poll = class {
13190
13452
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13191
13453
  let latestAnswers = [...currentState.latest_answers];
13192
13454
  let ownAnswer = currentState.ownAnswer;
13193
- const ownVotesByOptionId = currentState.ownVotesByOptionId;
13194
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13455
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13195
13456
  if (isOwnVote) {
13196
13457
  if (isVoteAnswer(event.poll_vote)) {
13197
13458
  ownAnswer = event.poll_vote;
@@ -13201,19 +13462,11 @@ var Poll = class {
13201
13462
  }
13202
13463
  if (isVoteAnswer(event.poll_vote)) {
13203
13464
  latestAnswers = [event.poll_vote, ...latestAnswers];
13204
- } else {
13205
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13206
13465
  }
13207
- const pollEnrichData = extractPollEnrichedData(event.poll);
13208
- this.state.partialNext({
13209
- ...pollEnrichData,
13466
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13210
13467
  latest_answers: latestAnswers,
13211
- lastActivityAt: new Date(event.created_at),
13212
- ownAnswer,
13213
- ownVotesByOptionId,
13214
- maxVotedOptionIds
13468
+ ownAnswer
13215
13469
  });
13216
- this.upsertOfflineDb();
13217
13470
  };
13218
13471
  this.handleVoteChanged = (event) => {
13219
13472
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13222,8 +13475,7 @@ var Poll = class {
13222
13475
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13223
13476
  let latestAnswers = [...currentState.latest_answers];
13224
13477
  let ownAnswer = currentState.ownAnswer;
13225
- let ownVotesByOptionId = currentState.ownVotesByOptionId;
13226
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13478
+ let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
13227
13479
  if (isOwnVote) {
13228
13480
  if (isVoteAnswer(event.poll_vote)) {
13229
13481
  latestAnswers = [
@@ -13247,23 +13499,14 @@ var Poll = class {
13247
13499
  if (ownAnswer?.id === event.poll_vote.id) {
13248
13500
  ownAnswer = void 0;
13249
13501
  }
13250
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13251
13502
  }
13252
13503
  } else if (isVoteAnswer(event.poll_vote)) {
13253
13504
  latestAnswers = [event.poll_vote, ...latestAnswers];
13254
- } else {
13255
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13256
13505
  }
13257
- const pollEnrichData = extractPollEnrichedData(event.poll);
13258
- this.state.partialNext({
13259
- ...pollEnrichData,
13506
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13260
13507
  latest_answers: latestAnswers,
13261
- lastActivityAt: new Date(event.created_at),
13262
- ownAnswer,
13263
- ownVotesByOptionId,
13264
- maxVotedOptionIds
13508
+ ownAnswer
13265
13509
  });
13266
- this.upsertOfflineDb();
13267
13510
  };
13268
13511
  this.handleVoteRemoved = (event) => {
13269
13512
  if (event.poll?.id && event.poll.id !== this.id) return;
@@ -13272,29 +13515,19 @@ var Poll = class {
13272
13515
  const isOwnVote = event.poll_vote.user_id === this.client.userID;
13273
13516
  let latestAnswers = [...currentState.latest_answers];
13274
13517
  let ownAnswer = currentState.ownAnswer;
13275
- const ownVotesByOptionId = { ...currentState.ownVotesByOptionId };
13276
- let maxVotedOptionIds = currentState.maxVotedOptionIds;
13518
+ const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
13277
13519
  if (isVoteAnswer(event.poll_vote)) {
13278
13520
  latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
13279
13521
  if (isOwnVote) {
13280
13522
  ownAnswer = void 0;
13281
13523
  }
13282
- } else {
13283
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
13284
- if (isOwnVote && event.poll_vote.option_id) {
13285
- delete ownVotesByOptionId[event.poll_vote.option_id];
13286
- }
13524
+ } else if (isOwnVote && event.poll_vote.option_id) {
13525
+ delete ownVotesByOptionId[event.poll_vote.option_id];
13287
13526
  }
13288
- const pollEnrichData = extractPollEnrichedData(event.poll);
13289
- this.state.partialNext({
13290
- ...pollEnrichData,
13527
+ this.updateFromVoteEvent(event, ownVotesByOptionId, {
13291
13528
  latest_answers: latestAnswers,
13292
- lastActivityAt: new Date(event.created_at),
13293
- ownAnswer,
13294
- ownVotesByOptionId,
13295
- maxVotedOptionIds
13529
+ ownAnswer
13296
13530
  });
13297
- this.upsertOfflineDb();
13298
13531
  };
13299
13532
  this.query = async (id) => {
13300
13533
  const { poll } = await this.client.getPoll(id);
@@ -13303,11 +13536,33 @@ var Poll = class {
13303
13536
  };
13304
13537
  this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
13305
13538
  this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
13306
- this.close = async () => await this.client.closePoll(this.id);
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
+ };
13307
13557
  this.delete = async () => await this.client.deletePoll(this.id);
13308
13558
  this.createOption = async (option) => await this.client.createPollOption(this.id, option);
13309
13559
  this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
13310
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
+ */
13311
13566
  this.castVote = async (optionId, messageId) => {
13312
13567
  const { max_votes_allowed, ownVotesByOptionId } = this.data;
13313
13568
  const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
@@ -13324,11 +13579,14 @@ var Poll = class {
13324
13579
  });
13325
13580
  return;
13326
13581
  }
13327
- return await this.client.castPollVote(messageId, this.id, {
13328
- option_id: optionId
13329
- });
13582
+ return await this.optimisticVotes.castVote(optionId, messageId);
13330
13583
  };
13331
- this.removeVote = async (voteId, messageId) => await this.client.removePollVote(messageId, this.id, voteId);
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);
13332
13590
  this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
13333
13591
  this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
13334
13592
  this.queryAnswers = async (params) => await this.client.queryPollAnswers(
@@ -13346,24 +13604,12 @@ var Poll = class {
13346
13604
  this.client = client;
13347
13605
  this.id = poll.id;
13348
13606
  this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
13607
+ this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
13349
13608
  }
13350
13609
  get data() {
13351
13610
  return this.state.getLatestValue();
13352
13611
  }
13353
13612
  };
13354
- function getMaxVotedOptionIds(voteCountsByOption) {
13355
- let maxVotes = 0;
13356
- let winningOptions = [];
13357
- for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
13358
- if (count > maxVotes) {
13359
- winningOptions = [id];
13360
- maxVotes = count;
13361
- } else if (count === maxVotes) {
13362
- winningOptions.push(id);
13363
- }
13364
- }
13365
- return winningOptions;
13366
- }
13367
13613
  function getOwnVotesByOptionId(ownVotes) {
13368
13614
  return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
13369
13615
  if (isVoteAnswer(vote) || !vote.option_id) return acc;
@@ -13385,7 +13631,7 @@ function extractPollData(pollResponse) {
13385
13631
  voting_visibility: pollResponse.voting_visibility
13386
13632
  };
13387
13633
  }
13388
- function mapPollStateToResponse(poll) {
13634
+ function mapPollStateToResponse(poll, state = poll.data) {
13389
13635
  const {
13390
13636
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
13391
13637
  lastActivityAt,
@@ -13394,7 +13640,7 @@ function mapPollStateToResponse(poll) {
13394
13640
  ownVotesByOptionId,
13395
13641
  ownAnswer,
13396
13642
  ...restState
13397
- } = poll.data;
13643
+ } = state;
13398
13644
  const ownVotes = [
13399
13645
  ...Object.values(ownVotesByOptionId),
13400
13646
  ...ownAnswer ? [ownAnswer] : []
@@ -15179,7 +15425,7 @@ var StreamChat = class _StreamChat {
15179
15425
  return this.userAgent;
15180
15426
  }
15181
15427
  if (!this.cachedUserAgent) {
15182
- const version = "9.53.0";
15428
+ const version = "9.54.0";
15183
15429
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
15184
15430
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
15185
15431
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};