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
package/dist/cjs/index.node.js
CHANGED
|
@@ -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
|
|
5882
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
5883
5883
|
linkPreviews
|
|
5884
5884
|
);
|
|
5885
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
13208
|
-
this.state.partialNext({
|
|
13209
|
-
...pollEnrichData,
|
|
13466
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13210
13467
|
latest_answers: latestAnswers,
|
|
13211
|
-
|
|
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 =
|
|
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
|
-
|
|
13258
|
-
this.state.partialNext({
|
|
13259
|
-
...pollEnrichData,
|
|
13506
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13260
13507
|
latest_answers: latestAnswers,
|
|
13261
|
-
|
|
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 = { ...
|
|
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
|
-
|
|
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
|
-
|
|
13289
|
-
this.state.partialNext({
|
|
13290
|
-
...pollEnrichData,
|
|
13527
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13291
13528
|
latest_answers: latestAnswers,
|
|
13292
|
-
|
|
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
|
-
|
|
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.
|
|
13328
|
-
option_id: optionId
|
|
13329
|
-
});
|
|
13582
|
+
return await this.optimisticVotes.castVote(optionId, messageId);
|
|
13330
13583
|
};
|
|
13331
|
-
|
|
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
|
-
} =
|
|
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.
|
|
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 ?? {};
|