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.
- package/dist/cjs/index.browser.js +317 -71
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +317 -71
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +317 -71
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/client.d.ts +2 -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 +10 -3
- package/dist/types/utils.d.ts +1 -3
- package/package.json +1 -1
- 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 +14 -3
|
@@ -5910,10 +5910,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5910
5910
|
const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
|
|
5911
5911
|
(preview) => LinkPreviewsManager.getPreviewData(preview)
|
|
5912
5912
|
);
|
|
5913
|
-
const
|
|
5913
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
5914
5914
|
linkPreviews
|
|
5915
5915
|
);
|
|
5916
|
-
|
|
5916
|
+
const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
|
|
5917
|
+
if (!messageAttachments.length && !localAttachments.length) return forward();
|
|
5917
5918
|
const sendOptions = { ...state.sendOptions };
|
|
5918
5919
|
const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
|
|
5919
5920
|
if (skip_enrich_url) {
|
|
@@ -5921,14 +5922,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5921
5922
|
}
|
|
5922
5923
|
return next({
|
|
5923
5924
|
...state,
|
|
5924
|
-
message: {
|
|
5925
|
+
message: messageAttachments.length ? {
|
|
5925
5926
|
...state.message,
|
|
5926
|
-
attachments
|
|
5927
|
-
},
|
|
5928
|
-
localMessage: {
|
|
5927
|
+
attachments: messageAttachments
|
|
5928
|
+
} : state.message,
|
|
5929
|
+
localMessage: localAttachments.length ? {
|
|
5929
5930
|
...state.localMessage,
|
|
5930
|
-
attachments
|
|
5931
|
-
},
|
|
5931
|
+
attachments: localAttachments
|
|
5932
|
+
} : state.localMessage,
|
|
5932
5933
|
sendOptions
|
|
5933
5934
|
});
|
|
5934
5935
|
}
|
|
@@ -13157,6 +13158,242 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
13157
13158
|
}
|
|
13158
13159
|
};
|
|
13159
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
|
+
|
|
13160
13397
|
// src/poll.ts
|
|
13161
13398
|
var isPollUpdatedEvent = (e) => e.type === "poll.updated";
|
|
13162
13399
|
var isPollClosedEventEvent = (e) => e.type === "poll.closed";
|
|
@@ -13166,6 +13403,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
|
|
|
13166
13403
|
var isVoteAnswer = (vote) => !!vote?.answer_text;
|
|
13167
13404
|
var Poll = class {
|
|
13168
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;
|
|
13169
13409
|
this.getInitialStateFromPollResponse = (poll) => {
|
|
13170
13410
|
const { own_votes, id, ...pollResponseForState } = poll;
|
|
13171
13411
|
const { ownAnswer, ownVotes } = own_votes?.reduce(
|
|
@@ -13189,25 +13429,47 @@ var Poll = class {
|
|
|
13189
13429
|
ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
|
|
13190
13430
|
};
|
|
13191
13431
|
};
|
|
13432
|
+
// persists only the server's votes, never the optimistic ones
|
|
13192
13433
|
this.upsertOfflineDb = () => {
|
|
13434
|
+
const serverState = this.optimisticVotes.withServerVotes(this.data);
|
|
13193
13435
|
this.client.offlineDb?.executeQuerySafely(
|
|
13194
|
-
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
|
|
13436
|
+
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
|
|
13195
13437
|
{ method: "upsertPoll" }
|
|
13196
13438
|
);
|
|
13197
13439
|
};
|
|
13198
13440
|
this.reinitializeState = (poll) => {
|
|
13199
|
-
|
|
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();
|
|
13200
13460
|
};
|
|
13201
13461
|
this.handlePollUpdated = (event) => {
|
|
13202
13462
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
13203
13463
|
if (!isPollUpdatedEvent(event)) return;
|
|
13204
13464
|
const { id, ...pollData } = extractPollData(event.poll);
|
|
13465
|
+
if (pollData.is_closed) this.closeConfirmed = true;
|
|
13205
13466
|
this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
|
|
13206
13467
|
this.upsertOfflineDb();
|
|
13207
13468
|
};
|
|
13208
13469
|
this.handlePollClosed = (event) => {
|
|
13209
13470
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
13210
13471
|
if (!isPollClosedEventEvent(event)) return;
|
|
13472
|
+
this.closeConfirmed = true;
|
|
13211
13473
|
this.state.partialNext({
|
|
13212
13474
|
is_closed: true,
|
|
13213
13475
|
lastActivityAt: new Date(event.created_at)
|
|
@@ -13221,8 +13483,7 @@ var Poll = class {
|
|
|
13221
13483
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13222
13484
|
let latestAnswers = [...currentState.latest_answers];
|
|
13223
13485
|
let ownAnswer = currentState.ownAnswer;
|
|
13224
|
-
const ownVotesByOptionId =
|
|
13225
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13486
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13226
13487
|
if (isOwnVote) {
|
|
13227
13488
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13228
13489
|
ownAnswer = event.poll_vote;
|
|
@@ -13232,19 +13493,11 @@ var Poll = class {
|
|
|
13232
13493
|
}
|
|
13233
13494
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13234
13495
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13235
|
-
} else {
|
|
13236
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13237
13496
|
}
|
|
13238
|
-
|
|
13239
|
-
this.state.partialNext({
|
|
13240
|
-
...pollEnrichData,
|
|
13497
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13241
13498
|
latest_answers: latestAnswers,
|
|
13242
|
-
|
|
13243
|
-
ownAnswer,
|
|
13244
|
-
ownVotesByOptionId,
|
|
13245
|
-
maxVotedOptionIds
|
|
13499
|
+
ownAnswer
|
|
13246
13500
|
});
|
|
13247
|
-
this.upsertOfflineDb();
|
|
13248
13501
|
};
|
|
13249
13502
|
this.handleVoteChanged = (event) => {
|
|
13250
13503
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13253,8 +13506,7 @@ var Poll = class {
|
|
|
13253
13506
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13254
13507
|
let latestAnswers = [...currentState.latest_answers];
|
|
13255
13508
|
let ownAnswer = currentState.ownAnswer;
|
|
13256
|
-
let ownVotesByOptionId =
|
|
13257
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13509
|
+
let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
|
|
13258
13510
|
if (isOwnVote) {
|
|
13259
13511
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13260
13512
|
latestAnswers = [
|
|
@@ -13278,23 +13530,14 @@ var Poll = class {
|
|
|
13278
13530
|
if (ownAnswer?.id === event.poll_vote.id) {
|
|
13279
13531
|
ownAnswer = void 0;
|
|
13280
13532
|
}
|
|
13281
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13282
13533
|
}
|
|
13283
13534
|
} else if (isVoteAnswer(event.poll_vote)) {
|
|
13284
13535
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13285
|
-
} else {
|
|
13286
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13287
13536
|
}
|
|
13288
|
-
|
|
13289
|
-
this.state.partialNext({
|
|
13290
|
-
...pollEnrichData,
|
|
13537
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13291
13538
|
latest_answers: latestAnswers,
|
|
13292
|
-
|
|
13293
|
-
ownAnswer,
|
|
13294
|
-
ownVotesByOptionId,
|
|
13295
|
-
maxVotedOptionIds
|
|
13539
|
+
ownAnswer
|
|
13296
13540
|
});
|
|
13297
|
-
this.upsertOfflineDb();
|
|
13298
13541
|
};
|
|
13299
13542
|
this.handleVoteRemoved = (event) => {
|
|
13300
13543
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13303,29 +13546,19 @@ var Poll = class {
|
|
|
13303
13546
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13304
13547
|
let latestAnswers = [...currentState.latest_answers];
|
|
13305
13548
|
let ownAnswer = currentState.ownAnswer;
|
|
13306
|
-
const ownVotesByOptionId = { ...
|
|
13307
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13549
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13308
13550
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13309
13551
|
latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
|
|
13310
13552
|
if (isOwnVote) {
|
|
13311
13553
|
ownAnswer = void 0;
|
|
13312
13554
|
}
|
|
13313
|
-
} else {
|
|
13314
|
-
|
|
13315
|
-
if (isOwnVote && event.poll_vote.option_id) {
|
|
13316
|
-
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13317
|
-
}
|
|
13555
|
+
} else if (isOwnVote && event.poll_vote.option_id) {
|
|
13556
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13318
13557
|
}
|
|
13319
|
-
|
|
13320
|
-
this.state.partialNext({
|
|
13321
|
-
...pollEnrichData,
|
|
13558
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13322
13559
|
latest_answers: latestAnswers,
|
|
13323
|
-
|
|
13324
|
-
ownAnswer,
|
|
13325
|
-
ownVotesByOptionId,
|
|
13326
|
-
maxVotedOptionIds
|
|
13560
|
+
ownAnswer
|
|
13327
13561
|
});
|
|
13328
|
-
this.upsertOfflineDb();
|
|
13329
13562
|
};
|
|
13330
13563
|
this.query = async (id) => {
|
|
13331
13564
|
const { poll } = await this.client.getPoll(id);
|
|
@@ -13334,11 +13567,33 @@ var Poll = class {
|
|
|
13334
13567
|
};
|
|
13335
13568
|
this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
|
|
13336
13569
|
this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
|
|
13337
|
-
|
|
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
|
+
};
|
|
13338
13588
|
this.delete = async () => await this.client.deletePoll(this.id);
|
|
13339
13589
|
this.createOption = async (option) => await this.client.createPollOption(this.id, option);
|
|
13340
13590
|
this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
|
|
13341
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
|
+
*/
|
|
13342
13597
|
this.castVote = async (optionId, messageId) => {
|
|
13343
13598
|
const { max_votes_allowed, ownVotesByOptionId } = this.data;
|
|
13344
13599
|
const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
|
|
@@ -13355,11 +13610,14 @@ var Poll = class {
|
|
|
13355
13610
|
});
|
|
13356
13611
|
return;
|
|
13357
13612
|
}
|
|
13358
|
-
return await this.
|
|
13359
|
-
option_id: optionId
|
|
13360
|
-
});
|
|
13613
|
+
return await this.optimisticVotes.castVote(optionId, messageId);
|
|
13361
13614
|
};
|
|
13362
|
-
|
|
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);
|
|
13363
13621
|
this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
|
|
13364
13622
|
this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
|
|
13365
13623
|
this.queryAnswers = async (params) => await this.client.queryPollAnswers(
|
|
@@ -13377,24 +13635,12 @@ var Poll = class {
|
|
|
13377
13635
|
this.client = client;
|
|
13378
13636
|
this.id = poll.id;
|
|
13379
13637
|
this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
|
|
13638
|
+
this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
|
|
13380
13639
|
}
|
|
13381
13640
|
get data() {
|
|
13382
13641
|
return this.state.getLatestValue();
|
|
13383
13642
|
}
|
|
13384
13643
|
};
|
|
13385
|
-
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13386
|
-
let maxVotes = 0;
|
|
13387
|
-
let winningOptions = [];
|
|
13388
|
-
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13389
|
-
if (count > maxVotes) {
|
|
13390
|
-
winningOptions = [id];
|
|
13391
|
-
maxVotes = count;
|
|
13392
|
-
} else if (count === maxVotes) {
|
|
13393
|
-
winningOptions.push(id);
|
|
13394
|
-
}
|
|
13395
|
-
}
|
|
13396
|
-
return winningOptions;
|
|
13397
|
-
}
|
|
13398
13644
|
function getOwnVotesByOptionId(ownVotes) {
|
|
13399
13645
|
return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
|
|
13400
13646
|
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
@@ -13416,7 +13662,7 @@ function extractPollData(pollResponse) {
|
|
|
13416
13662
|
voting_visibility: pollResponse.voting_visibility
|
|
13417
13663
|
};
|
|
13418
13664
|
}
|
|
13419
|
-
function mapPollStateToResponse(poll) {
|
|
13665
|
+
function mapPollStateToResponse(poll, state = poll.data) {
|
|
13420
13666
|
const {
|
|
13421
13667
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13422
13668
|
lastActivityAt,
|
|
@@ -13425,7 +13671,7 @@ function mapPollStateToResponse(poll) {
|
|
|
13425
13671
|
ownVotesByOptionId,
|
|
13426
13672
|
ownAnswer,
|
|
13427
13673
|
...restState
|
|
13428
|
-
} =
|
|
13674
|
+
} = state;
|
|
13429
13675
|
const ownVotes = [
|
|
13430
13676
|
...Object.values(ownVotesByOptionId),
|
|
13431
13677
|
...ownAnswer ? [ownAnswer] : []
|
|
@@ -15210,7 +15456,7 @@ var StreamChat = class _StreamChat {
|
|
|
15210
15456
|
return this.userAgent;
|
|
15211
15457
|
}
|
|
15212
15458
|
if (!this.cachedUserAgent) {
|
|
15213
|
-
const version = "9.
|
|
15459
|
+
const version = "9.54.0";
|
|
15214
15460
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
15215
15461
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
15216
15462
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|