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/esm/index.mjs
CHANGED
|
@@ -5687,10 +5687,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5687
5687
|
const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
|
|
5688
5688
|
(preview) => LinkPreviewsManager.getPreviewData(preview)
|
|
5689
5689
|
);
|
|
5690
|
-
const
|
|
5690
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
5691
5691
|
linkPreviews
|
|
5692
5692
|
);
|
|
5693
|
-
|
|
5693
|
+
const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
|
|
5694
|
+
if (!messageAttachments.length && !localAttachments.length) return forward();
|
|
5694
5695
|
const sendOptions = { ...state.sendOptions };
|
|
5695
5696
|
const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
|
|
5696
5697
|
if (skip_enrich_url) {
|
|
@@ -5698,14 +5699,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5698
5699
|
}
|
|
5699
5700
|
return next({
|
|
5700
5701
|
...state,
|
|
5701
|
-
message: {
|
|
5702
|
+
message: messageAttachments.length ? {
|
|
5702
5703
|
...state.message,
|
|
5703
|
-
attachments
|
|
5704
|
-
},
|
|
5705
|
-
localMessage: {
|
|
5704
|
+
attachments: messageAttachments
|
|
5705
|
+
} : state.message,
|
|
5706
|
+
localMessage: localAttachments.length ? {
|
|
5706
5707
|
...state.localMessage,
|
|
5707
|
-
attachments
|
|
5708
|
-
},
|
|
5708
|
+
attachments: localAttachments
|
|
5709
|
+
} : state.localMessage,
|
|
5709
5710
|
sendOptions
|
|
5710
5711
|
});
|
|
5711
5712
|
}
|
|
@@ -12934,6 +12935,242 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
12934
12935
|
}
|
|
12935
12936
|
};
|
|
12936
12937
|
|
|
12938
|
+
// src/poll_optimistic_votes.ts
|
|
12939
|
+
var CATCH_UP_TIMEOUT_MS = 3e3;
|
|
12940
|
+
var OptimisticPollVotes = class {
|
|
12941
|
+
constructor({ client, poll }) {
|
|
12942
|
+
// Own vote changes not confirmed by a WS event yet
|
|
12943
|
+
this.pendingCount = 0;
|
|
12944
|
+
// Own vote changes whose requests succeeded since the server's vote state was last shown
|
|
12945
|
+
this.succeededChanges = [];
|
|
12946
|
+
// Vote requests with this tag are sent one after another. A symbol, so it is unique per instance.
|
|
12947
|
+
this.requestTag = /* @__PURE__ */ Symbol("poll-votes");
|
|
12948
|
+
this.withServerVotes = (pollState) => ({
|
|
12949
|
+
...pollState,
|
|
12950
|
+
...this.serverVoteState
|
|
12951
|
+
});
|
|
12952
|
+
/**
|
|
12953
|
+
* Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
|
|
12954
|
+
* votes, it replaces the existing own votes.
|
|
12955
|
+
*/
|
|
12956
|
+
this.castVote = (optionId, messageId) => {
|
|
12957
|
+
const sendRequest = () => this.client.castPollVote(messageId, this.poll.id, { option_id: optionId });
|
|
12958
|
+
const state = this.poll.data;
|
|
12959
|
+
if (state.ownVotesByOptionId[optionId]) return this.send(sendRequest);
|
|
12960
|
+
const replacedVotes = state.enforce_unique_vote ? Object.values(state.ownVotesByOptionId) : [];
|
|
12961
|
+
this.poll.state.partialNext(
|
|
12962
|
+
applyOwnVoteDelta(state, {
|
|
12963
|
+
add: [this.createPlaceholderVote(optionId)],
|
|
12964
|
+
remove: replacedVotes
|
|
12965
|
+
})
|
|
12966
|
+
);
|
|
12967
|
+
return this.track(sendRequest, (response) => ({
|
|
12968
|
+
optionId,
|
|
12969
|
+
vote: response.vote
|
|
12970
|
+
}));
|
|
12971
|
+
};
|
|
12972
|
+
// Removes a vote, right away.
|
|
12973
|
+
this.removeVote = (voteId, messageId) => {
|
|
12974
|
+
const sendRequest = (id) => this.client.removePollVote(messageId, this.poll.id, id);
|
|
12975
|
+
const state = this.poll.data;
|
|
12976
|
+
const vote = Object.values(state.ownVotesByOptionId).find(({ id }) => id === voteId);
|
|
12977
|
+
if (!vote?.option_id) return this.send(() => sendRequest(voteId));
|
|
12978
|
+
const optionId = vote.option_id;
|
|
12979
|
+
this.poll.state.partialNext(applyOwnVoteDelta(state, { remove: [vote] }));
|
|
12980
|
+
return this.track(
|
|
12981
|
+
// runs when the removal's turn comes, so a preceding cast of the vote has finished
|
|
12982
|
+
() => {
|
|
12983
|
+
const serverVoteId = this.getServerVoteId(vote);
|
|
12984
|
+
return serverVoteId ? sendRequest(serverVoteId) : Promise.resolve(void 0);
|
|
12985
|
+
},
|
|
12986
|
+
() => ({ optionId })
|
|
12987
|
+
);
|
|
12988
|
+
};
|
|
12989
|
+
// Sends a vote request once the previous one has finished, whether it succeeded or not.
|
|
12990
|
+
this.send = async (sendRequest) => {
|
|
12991
|
+
try {
|
|
12992
|
+
return await withoutConcurrency(this.requestTag, sendRequest);
|
|
12993
|
+
} finally {
|
|
12994
|
+
if (!hasPending(this.requestTag)) this.scheduleCatchUp();
|
|
12995
|
+
}
|
|
12996
|
+
};
|
|
12997
|
+
// Stores the vote state of a WS vote event and returns the vote fields to show, if any.
|
|
12998
|
+
this.handleVoteEvent = (poll, {
|
|
12999
|
+
confirmsOwnVote,
|
|
13000
|
+
ownVotesByOptionId
|
|
13001
|
+
}) => {
|
|
13002
|
+
this.serverVoteState = {
|
|
13003
|
+
latest_votes_by_option: poll.latest_votes_by_option,
|
|
13004
|
+
ownVotesByOptionId,
|
|
13005
|
+
vote_count: poll.vote_count,
|
|
13006
|
+
vote_counts_by_option: poll.vote_counts_by_option
|
|
13007
|
+
};
|
|
13008
|
+
if (confirmsOwnVote) this.pendingCount = Math.max(this.pendingCount - 1, 0);
|
|
13009
|
+
return this.pendingCount > 0 ? {} : this.takeServerVoteState();
|
|
13010
|
+
};
|
|
13011
|
+
// Fresh server state (e.g. a refetch) replaces everything, pending changes are dropped.
|
|
13012
|
+
this.reset = (pollState) => {
|
|
13013
|
+
this.serverVoteState = pickVoteState(pollState);
|
|
13014
|
+
this.pendingCount = 0;
|
|
13015
|
+
this.succeededChanges = [];
|
|
13016
|
+
clearTimeout(this.catchUpTimeout);
|
|
13017
|
+
};
|
|
13018
|
+
// Tracks an own vote change until its request settles. `sendRequest` resolves to undefined
|
|
13019
|
+
// when no request was needed.
|
|
13020
|
+
this.track = async (sendRequest, toChange) => {
|
|
13021
|
+
this.pendingCount += 1;
|
|
13022
|
+
clearTimeout(this.catchUpTimeout);
|
|
13023
|
+
let response;
|
|
13024
|
+
try {
|
|
13025
|
+
response = await this.send(async () => {
|
|
13026
|
+
const result = await sendRequest();
|
|
13027
|
+
if (result !== void 0) this.succeededChanges.push(toChange(result));
|
|
13028
|
+
return result;
|
|
13029
|
+
});
|
|
13030
|
+
} catch (error) {
|
|
13031
|
+
this.settleWithoutEvent();
|
|
13032
|
+
throw error;
|
|
13033
|
+
}
|
|
13034
|
+
if (response === void 0) this.settleWithoutEvent();
|
|
13035
|
+
return response;
|
|
13036
|
+
};
|
|
13037
|
+
// A failed or skipped request gets no WS event, so it counts as confirmed right away.
|
|
13038
|
+
this.settleWithoutEvent = () => {
|
|
13039
|
+
this.pendingCount = Math.max(this.pendingCount - 1, 0);
|
|
13040
|
+
if (this.pendingCount === 0) this.poll.state.partialNext(this.takeServerVoteState());
|
|
13041
|
+
};
|
|
13042
|
+
// The vote shown for a cast until the server's vote arrives
|
|
13043
|
+
this.createPlaceholderVote = (optionId) => {
|
|
13044
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13045
|
+
return {
|
|
13046
|
+
created_at: now,
|
|
13047
|
+
id: `local-${generateUUIDv4()}`,
|
|
13048
|
+
option_id: optionId,
|
|
13049
|
+
poll_id: this.poll.id,
|
|
13050
|
+
updated_at: now,
|
|
13051
|
+
user: this.client.user,
|
|
13052
|
+
user_id: this.client.userID
|
|
13053
|
+
};
|
|
13054
|
+
};
|
|
13055
|
+
/**
|
|
13056
|
+
* The server id of a vote to remove, once everything sent before the removal has finished. Casts
|
|
13057
|
+
* and removals of an option alternate, so the option's last succeeded change tells: a cast is the
|
|
13058
|
+
* vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
|
|
13059
|
+
* one, the server's own vote of the option is the vote being removed.
|
|
13060
|
+
*/
|
|
13061
|
+
this.getServerVoteId = (vote) => {
|
|
13062
|
+
for (let i = this.succeededChanges.length - 1; i >= 0; i--) {
|
|
13063
|
+
const change = this.succeededChanges[i];
|
|
13064
|
+
if (change.optionId === vote.option_id) return change.vote?.id;
|
|
13065
|
+
}
|
|
13066
|
+
return this.serverVoteState.ownVotesByOptionId[vote.option_id]?.id;
|
|
13067
|
+
};
|
|
13068
|
+
this.takeServerVoteState = () => {
|
|
13069
|
+
clearTimeout(this.catchUpTimeout);
|
|
13070
|
+
this.succeededChanges = [];
|
|
13071
|
+
return {
|
|
13072
|
+
...this.serverVoteState,
|
|
13073
|
+
maxVotedOptionIds: getMaxVotedOptionIds(this.serverVoteState.vote_counts_by_option)
|
|
13074
|
+
};
|
|
13075
|
+
};
|
|
13076
|
+
this.scheduleCatchUp = () => {
|
|
13077
|
+
clearTimeout(this.catchUpTimeout);
|
|
13078
|
+
if (this.pendingCount === 0) return;
|
|
13079
|
+
this.catchUpTimeout = setTimeout(this.applySucceededChanges, CATCH_UP_TIMEOUT_MS);
|
|
13080
|
+
};
|
|
13081
|
+
/**
|
|
13082
|
+
* A WS event of an own vote change got lost, so the server's own votes are stale. All requests
|
|
13083
|
+
* have finished (the timeout starts once nothing is left to send and a new request cancels it),
|
|
13084
|
+
* so applying the succeeded changes in order gives the server's own votes (changes whose events
|
|
13085
|
+
* did arrive end up the same). They are shown with the vote counts of the last WS event, which
|
|
13086
|
+
* contain the other users' votes too.
|
|
13087
|
+
*/
|
|
13088
|
+
this.applySucceededChanges = () => {
|
|
13089
|
+
const { enforce_unique_vote } = this.poll.data;
|
|
13090
|
+
let ownVotesByOptionId = { ...this.serverVoteState.ownVotesByOptionId };
|
|
13091
|
+
for (const { optionId, vote } of this.succeededChanges) {
|
|
13092
|
+
if (!vote) {
|
|
13093
|
+
delete ownVotesByOptionId[optionId];
|
|
13094
|
+
} else if (enforce_unique_vote) {
|
|
13095
|
+
ownVotesByOptionId = { [optionId]: vote };
|
|
13096
|
+
} else {
|
|
13097
|
+
ownVotesByOptionId[optionId] = vote;
|
|
13098
|
+
}
|
|
13099
|
+
}
|
|
13100
|
+
this.serverVoteState = { ...this.serverVoteState, ownVotesByOptionId };
|
|
13101
|
+
this.pendingCount = 0;
|
|
13102
|
+
this.poll.state.partialNext(this.takeServerVoteState());
|
|
13103
|
+
};
|
|
13104
|
+
this.client = client;
|
|
13105
|
+
this.poll = poll;
|
|
13106
|
+
this.serverVoteState = pickVoteState(poll.data);
|
|
13107
|
+
}
|
|
13108
|
+
get serverOwnVotes() {
|
|
13109
|
+
return this.serverVoteState.ownVotesByOptionId;
|
|
13110
|
+
}
|
|
13111
|
+
};
|
|
13112
|
+
function pickVoteState(state) {
|
|
13113
|
+
return {
|
|
13114
|
+
latest_votes_by_option: state.latest_votes_by_option,
|
|
13115
|
+
ownVotesByOptionId: state.ownVotesByOptionId,
|
|
13116
|
+
vote_count: state.vote_count,
|
|
13117
|
+
vote_counts_by_option: state.vote_counts_by_option
|
|
13118
|
+
};
|
|
13119
|
+
}
|
|
13120
|
+
function applyOwnVoteDelta(state, { add = [], remove = [] }) {
|
|
13121
|
+
const ownVotesByOptionId = { ...state.ownVotesByOptionId };
|
|
13122
|
+
const voteCountsByOption = { ...state.vote_counts_by_option };
|
|
13123
|
+
const latestVotesByOption = state.latest_votes_by_option ? { ...state.latest_votes_by_option } : state.latest_votes_by_option;
|
|
13124
|
+
let voteCount = state.vote_count ?? 0;
|
|
13125
|
+
for (const vote of remove) {
|
|
13126
|
+
const optionId = vote.option_id;
|
|
13127
|
+
if (!optionId) continue;
|
|
13128
|
+
delete ownVotesByOptionId[optionId];
|
|
13129
|
+
const optionVoteCount = (voteCountsByOption[optionId] ?? 0) - 1;
|
|
13130
|
+
if (optionVoteCount > 0) {
|
|
13131
|
+
voteCountsByOption[optionId] = optionVoteCount;
|
|
13132
|
+
} else {
|
|
13133
|
+
delete voteCountsByOption[optionId];
|
|
13134
|
+
}
|
|
13135
|
+
voteCount = Math.max(voteCount - 1, 0);
|
|
13136
|
+
if (latestVotesByOption?.[optionId]) {
|
|
13137
|
+
latestVotesByOption[optionId] = latestVotesByOption[optionId].filter(
|
|
13138
|
+
(latestVote) => latestVote.id !== vote.id
|
|
13139
|
+
);
|
|
13140
|
+
}
|
|
13141
|
+
}
|
|
13142
|
+
for (const vote of add) {
|
|
13143
|
+
const optionId = vote.option_id;
|
|
13144
|
+
if (!optionId) continue;
|
|
13145
|
+
ownVotesByOptionId[optionId] = vote;
|
|
13146
|
+
voteCountsByOption[optionId] = (voteCountsByOption[optionId] ?? 0) + 1;
|
|
13147
|
+
voteCount += 1;
|
|
13148
|
+
if (latestVotesByOption && state.voting_visibility !== "anonymous") {
|
|
13149
|
+
latestVotesByOption[optionId] = [vote, ...latestVotesByOption[optionId] ?? []];
|
|
13150
|
+
}
|
|
13151
|
+
}
|
|
13152
|
+
return {
|
|
13153
|
+
latest_votes_by_option: latestVotesByOption,
|
|
13154
|
+
maxVotedOptionIds: getMaxVotedOptionIds(voteCountsByOption),
|
|
13155
|
+
ownVotesByOptionId,
|
|
13156
|
+
vote_count: voteCount,
|
|
13157
|
+
vote_counts_by_option: voteCountsByOption
|
|
13158
|
+
};
|
|
13159
|
+
}
|
|
13160
|
+
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13161
|
+
let maxVotes = 0;
|
|
13162
|
+
let winningOptions = [];
|
|
13163
|
+
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13164
|
+
if (count > maxVotes) {
|
|
13165
|
+
winningOptions = [id];
|
|
13166
|
+
maxVotes = count;
|
|
13167
|
+
} else if (count === maxVotes) {
|
|
13168
|
+
winningOptions.push(id);
|
|
13169
|
+
}
|
|
13170
|
+
}
|
|
13171
|
+
return winningOptions;
|
|
13172
|
+
}
|
|
13173
|
+
|
|
12937
13174
|
// src/poll.ts
|
|
12938
13175
|
var isPollUpdatedEvent = (e) => e.type === "poll.updated";
|
|
12939
13176
|
var isPollClosedEventEvent = (e) => e.type === "poll.closed";
|
|
@@ -12943,6 +13180,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
|
|
|
12943
13180
|
var isVoteAnswer = (vote) => !!vote?.answer_text;
|
|
12944
13181
|
var Poll = class {
|
|
12945
13182
|
constructor({ client, poll }) {
|
|
13183
|
+
// Set whenever server data (WS event or rehydration) confirms that the poll is closed,
|
|
13184
|
+
// so that a failed optimistic close does not reopen a poll that is actually closed.
|
|
13185
|
+
this.closeConfirmed = false;
|
|
12946
13186
|
this.getInitialStateFromPollResponse = (poll) => {
|
|
12947
13187
|
const { own_votes, id, ...pollResponseForState } = poll;
|
|
12948
13188
|
const { ownAnswer, ownVotes } = own_votes?.reduce(
|
|
@@ -12966,25 +13206,47 @@ var Poll = class {
|
|
|
12966
13206
|
ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
|
|
12967
13207
|
};
|
|
12968
13208
|
};
|
|
13209
|
+
// persists only the server's votes, never the optimistic ones
|
|
12969
13210
|
this.upsertOfflineDb = () => {
|
|
13211
|
+
const serverState = this.optimisticVotes.withServerVotes(this.data);
|
|
12970
13212
|
this.client.offlineDb?.executeQuerySafely(
|
|
12971
|
-
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
|
|
13213
|
+
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
|
|
12972
13214
|
{ method: "upsertPoll" }
|
|
12973
13215
|
);
|
|
12974
13216
|
};
|
|
12975
13217
|
this.reinitializeState = (poll) => {
|
|
12976
|
-
|
|
13218
|
+
if (poll.is_closed) this.closeConfirmed = true;
|
|
13219
|
+
const initialState = this.getInitialStateFromPollResponse(poll);
|
|
13220
|
+
this.optimisticVotes.reset(initialState);
|
|
13221
|
+
this.state.partialNext(initialState);
|
|
13222
|
+
};
|
|
13223
|
+
// Hands the vote state of a WS vote event to the optimistic votes, which decide whether to show it.
|
|
13224
|
+
this.updateFromVoteEvent = (event, ownVotesByOptionId, answerState) => {
|
|
13225
|
+
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13226
|
+
const shownVoteState = this.optimisticVotes.handleVoteEvent(event.poll, {
|
|
13227
|
+
confirmsOwnVote: isOwnVote && !isVoteAnswer(event.poll_vote) && !!event.poll_vote.option_id,
|
|
13228
|
+
ownVotesByOptionId
|
|
13229
|
+
});
|
|
13230
|
+
this.state.partialNext({
|
|
13231
|
+
...answerState,
|
|
13232
|
+
answers_count: event.poll.answers_count,
|
|
13233
|
+
lastActivityAt: new Date(event.created_at),
|
|
13234
|
+
...shownVoteState
|
|
13235
|
+
});
|
|
13236
|
+
this.upsertOfflineDb();
|
|
12977
13237
|
};
|
|
12978
13238
|
this.handlePollUpdated = (event) => {
|
|
12979
13239
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
12980
13240
|
if (!isPollUpdatedEvent(event)) return;
|
|
12981
13241
|
const { id, ...pollData } = extractPollData(event.poll);
|
|
13242
|
+
if (pollData.is_closed) this.closeConfirmed = true;
|
|
12982
13243
|
this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
|
|
12983
13244
|
this.upsertOfflineDb();
|
|
12984
13245
|
};
|
|
12985
13246
|
this.handlePollClosed = (event) => {
|
|
12986
13247
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
12987
13248
|
if (!isPollClosedEventEvent(event)) return;
|
|
13249
|
+
this.closeConfirmed = true;
|
|
12988
13250
|
this.state.partialNext({
|
|
12989
13251
|
is_closed: true,
|
|
12990
13252
|
lastActivityAt: new Date(event.created_at)
|
|
@@ -12998,8 +13260,7 @@ var Poll = class {
|
|
|
12998
13260
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
12999
13261
|
let latestAnswers = [...currentState.latest_answers];
|
|
13000
13262
|
let ownAnswer = currentState.ownAnswer;
|
|
13001
|
-
const ownVotesByOptionId =
|
|
13002
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13263
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13003
13264
|
if (isOwnVote) {
|
|
13004
13265
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13005
13266
|
ownAnswer = event.poll_vote;
|
|
@@ -13009,19 +13270,11 @@ var Poll = class {
|
|
|
13009
13270
|
}
|
|
13010
13271
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13011
13272
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13012
|
-
} else {
|
|
13013
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13014
13273
|
}
|
|
13015
|
-
|
|
13016
|
-
this.state.partialNext({
|
|
13017
|
-
...pollEnrichData,
|
|
13274
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13018
13275
|
latest_answers: latestAnswers,
|
|
13019
|
-
|
|
13020
|
-
ownAnswer,
|
|
13021
|
-
ownVotesByOptionId,
|
|
13022
|
-
maxVotedOptionIds
|
|
13276
|
+
ownAnswer
|
|
13023
13277
|
});
|
|
13024
|
-
this.upsertOfflineDb();
|
|
13025
13278
|
};
|
|
13026
13279
|
this.handleVoteChanged = (event) => {
|
|
13027
13280
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13030,8 +13283,7 @@ var Poll = class {
|
|
|
13030
13283
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13031
13284
|
let latestAnswers = [...currentState.latest_answers];
|
|
13032
13285
|
let ownAnswer = currentState.ownAnswer;
|
|
13033
|
-
let ownVotesByOptionId =
|
|
13034
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13286
|
+
let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
|
|
13035
13287
|
if (isOwnVote) {
|
|
13036
13288
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13037
13289
|
latestAnswers = [
|
|
@@ -13055,23 +13307,14 @@ var Poll = class {
|
|
|
13055
13307
|
if (ownAnswer?.id === event.poll_vote.id) {
|
|
13056
13308
|
ownAnswer = void 0;
|
|
13057
13309
|
}
|
|
13058
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13059
13310
|
}
|
|
13060
13311
|
} else if (isVoteAnswer(event.poll_vote)) {
|
|
13061
13312
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13062
|
-
} else {
|
|
13063
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13064
13313
|
}
|
|
13065
|
-
|
|
13066
|
-
this.state.partialNext({
|
|
13067
|
-
...pollEnrichData,
|
|
13314
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13068
13315
|
latest_answers: latestAnswers,
|
|
13069
|
-
|
|
13070
|
-
ownAnswer,
|
|
13071
|
-
ownVotesByOptionId,
|
|
13072
|
-
maxVotedOptionIds
|
|
13316
|
+
ownAnswer
|
|
13073
13317
|
});
|
|
13074
|
-
this.upsertOfflineDb();
|
|
13075
13318
|
};
|
|
13076
13319
|
this.handleVoteRemoved = (event) => {
|
|
13077
13320
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13080,29 +13323,19 @@ var Poll = class {
|
|
|
13080
13323
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13081
13324
|
let latestAnswers = [...currentState.latest_answers];
|
|
13082
13325
|
let ownAnswer = currentState.ownAnswer;
|
|
13083
|
-
const ownVotesByOptionId = { ...
|
|
13084
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13326
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13085
13327
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13086
13328
|
latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
|
|
13087
13329
|
if (isOwnVote) {
|
|
13088
13330
|
ownAnswer = void 0;
|
|
13089
13331
|
}
|
|
13090
|
-
} else {
|
|
13091
|
-
|
|
13092
|
-
if (isOwnVote && event.poll_vote.option_id) {
|
|
13093
|
-
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13094
|
-
}
|
|
13332
|
+
} else if (isOwnVote && event.poll_vote.option_id) {
|
|
13333
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13095
13334
|
}
|
|
13096
|
-
|
|
13097
|
-
this.state.partialNext({
|
|
13098
|
-
...pollEnrichData,
|
|
13335
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13099
13336
|
latest_answers: latestAnswers,
|
|
13100
|
-
|
|
13101
|
-
ownAnswer,
|
|
13102
|
-
ownVotesByOptionId,
|
|
13103
|
-
maxVotedOptionIds
|
|
13337
|
+
ownAnswer
|
|
13104
13338
|
});
|
|
13105
|
-
this.upsertOfflineDb();
|
|
13106
13339
|
};
|
|
13107
13340
|
this.query = async (id) => {
|
|
13108
13341
|
const { poll } = await this.client.getPoll(id);
|
|
@@ -13111,11 +13344,33 @@ var Poll = class {
|
|
|
13111
13344
|
};
|
|
13112
13345
|
this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
|
|
13113
13346
|
this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
|
|
13114
|
-
|
|
13347
|
+
/**
|
|
13348
|
+
* Closes the poll. The poll is marked as closed optimistically and the change is
|
|
13349
|
+
* reverted if the request fails, unless the server confirmed the close meanwhile.
|
|
13350
|
+
* Does nothing if the poll is already closed.
|
|
13351
|
+
*/
|
|
13352
|
+
this.close = async () => {
|
|
13353
|
+
if (this.data.is_closed) return;
|
|
13354
|
+
this.closeConfirmed = false;
|
|
13355
|
+
this.state.partialNext({ is_closed: true });
|
|
13356
|
+
try {
|
|
13357
|
+
return await this.client.closePoll(this.id);
|
|
13358
|
+
} catch (error) {
|
|
13359
|
+
if (!this.closeConfirmed) {
|
|
13360
|
+
this.state.partialNext({ is_closed: false });
|
|
13361
|
+
}
|
|
13362
|
+
throw error;
|
|
13363
|
+
}
|
|
13364
|
+
};
|
|
13115
13365
|
this.delete = async () => await this.client.deletePoll(this.id);
|
|
13116
13366
|
this.createOption = async (option) => await this.client.createPollOption(this.id, option);
|
|
13117
13367
|
this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
|
|
13118
13368
|
this.deleteOption = async (optionId) => await this.client.deletePollOption(this.id, optionId);
|
|
13369
|
+
/**
|
|
13370
|
+
* Casts a vote for an option. The vote is shown right away through a local placeholder vote,
|
|
13371
|
+
* which the server's vote replaces once the WS events of all pending vote changes arrived.
|
|
13372
|
+
* If the request fails, the change is reverted.
|
|
13373
|
+
*/
|
|
13119
13374
|
this.castVote = async (optionId, messageId) => {
|
|
13120
13375
|
const { max_votes_allowed, ownVotesByOptionId } = this.data;
|
|
13121
13376
|
const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
|
|
@@ -13132,11 +13387,14 @@ var Poll = class {
|
|
|
13132
13387
|
});
|
|
13133
13388
|
return;
|
|
13134
13389
|
}
|
|
13135
|
-
return await this.
|
|
13136
|
-
option_id: optionId
|
|
13137
|
-
});
|
|
13390
|
+
return await this.optimisticVotes.castVote(optionId, messageId);
|
|
13138
13391
|
};
|
|
13139
|
-
|
|
13392
|
+
/**
|
|
13393
|
+
* Removes a vote. The vote is removed right away and restored if the request fails. Vote
|
|
13394
|
+
* requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
|
|
13395
|
+
* id from the cast's response; if that cast failed, there is nothing to remove.
|
|
13396
|
+
*/
|
|
13397
|
+
this.removeVote = async (voteId, messageId) => await this.optimisticVotes.removeVote(voteId, messageId);
|
|
13140
13398
|
this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
|
|
13141
13399
|
this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
|
|
13142
13400
|
this.queryAnswers = async (params) => await this.client.queryPollAnswers(
|
|
@@ -13154,24 +13412,12 @@ var Poll = class {
|
|
|
13154
13412
|
this.client = client;
|
|
13155
13413
|
this.id = poll.id;
|
|
13156
13414
|
this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
|
|
13415
|
+
this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
|
|
13157
13416
|
}
|
|
13158
13417
|
get data() {
|
|
13159
13418
|
return this.state.getLatestValue();
|
|
13160
13419
|
}
|
|
13161
13420
|
};
|
|
13162
|
-
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13163
|
-
let maxVotes = 0;
|
|
13164
|
-
let winningOptions = [];
|
|
13165
|
-
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13166
|
-
if (count > maxVotes) {
|
|
13167
|
-
winningOptions = [id];
|
|
13168
|
-
maxVotes = count;
|
|
13169
|
-
} else if (count === maxVotes) {
|
|
13170
|
-
winningOptions.push(id);
|
|
13171
|
-
}
|
|
13172
|
-
}
|
|
13173
|
-
return winningOptions;
|
|
13174
|
-
}
|
|
13175
13421
|
function getOwnVotesByOptionId(ownVotes) {
|
|
13176
13422
|
return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
|
|
13177
13423
|
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
@@ -13193,7 +13439,7 @@ function extractPollData(pollResponse) {
|
|
|
13193
13439
|
voting_visibility: pollResponse.voting_visibility
|
|
13194
13440
|
};
|
|
13195
13441
|
}
|
|
13196
|
-
function mapPollStateToResponse(poll) {
|
|
13442
|
+
function mapPollStateToResponse(poll, state = poll.data) {
|
|
13197
13443
|
const {
|
|
13198
13444
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13199
13445
|
lastActivityAt,
|
|
@@ -13202,7 +13448,7 @@ function mapPollStateToResponse(poll) {
|
|
|
13202
13448
|
ownVotesByOptionId,
|
|
13203
13449
|
ownAnswer,
|
|
13204
13450
|
...restState
|
|
13205
|
-
} =
|
|
13451
|
+
} = state;
|
|
13206
13452
|
const ownVotes = [
|
|
13207
13453
|
...Object.values(ownVotesByOptionId),
|
|
13208
13454
|
...ownAnswer ? [ownAnswer] : []
|
|
@@ -14987,7 +15233,7 @@ var StreamChat = class _StreamChat {
|
|
|
14987
15233
|
return this.userAgent;
|
|
14988
15234
|
}
|
|
14989
15235
|
if (!this.cachedUserAgent) {
|
|
14990
|
-
const version = "9.
|
|
15236
|
+
const version = "9.54.0";
|
|
14991
15237
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
14992
15238
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
14993
15239
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|