stream-chat 9.52.1 → 9.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.browser.js +388 -90
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +388 -90
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +388 -90
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/channel_batch_updater.d.ts +9 -2
- package/dist/types/client.d.ts +19 -6
- package/dist/types/poll.d.ts +22 -4
- package/dist/types/poll_optimistic_votes.d.ts +63 -0
- package/dist/types/types.d.ts +43 -3
- package/dist/types/utils.d.ts +1 -3
- package/package.json +1 -1
- package/src/channel_batch_updater.ts +28 -2
- package/src/client.ts +84 -10
- package/src/messageComposer/middleware/messageComposer/linkPreviews.ts +20 -10
- package/src/poll.ts +86 -60
- package/src/poll_optimistic_votes.ts +321 -0
- package/src/types.ts +51 -3
package/dist/esm/index.mjs
CHANGED
|
@@ -159,6 +159,7 @@ var Campaign = class {
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
// src/channel_batch_updater.ts
|
|
162
|
+
var isChannelBatchDataUpdateOptions = (update) => "data" in update || "custom_set" in update || "custom_unset" in update;
|
|
162
163
|
var ChannelBatchUpdater = class {
|
|
163
164
|
constructor(client) {
|
|
164
165
|
this.client = client;
|
|
@@ -297,19 +298,14 @@ var ChannelBatchUpdater = class {
|
|
|
297
298
|
filter
|
|
298
299
|
});
|
|
299
300
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
* updateData - Update data on channels matching the filter
|
|
303
|
-
*
|
|
304
|
-
* @param {UpdateChannelsBatchFilters} filter Filter to select channels
|
|
305
|
-
* @param {BatchChannelDataUpdate} data Data to update
|
|
306
|
-
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
307
|
-
*/
|
|
308
|
-
async updateData(filter, data) {
|
|
301
|
+
async updateData(filter, update) {
|
|
302
|
+
const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update) ? update : { data: update };
|
|
309
303
|
return await this.client.updateChannelsBatch({
|
|
310
304
|
operation: "updateData",
|
|
311
305
|
filter,
|
|
312
|
-
data
|
|
306
|
+
...data && { data },
|
|
307
|
+
...custom_set && { custom_set },
|
|
308
|
+
...custom_unset && { custom_unset }
|
|
313
309
|
});
|
|
314
310
|
}
|
|
315
311
|
};
|
|
@@ -5691,10 +5687,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5691
5687
|
const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
|
|
5692
5688
|
(preview) => LinkPreviewsManager.getPreviewData(preview)
|
|
5693
5689
|
);
|
|
5694
|
-
const
|
|
5690
|
+
const messageAttachments = (state.message.attachments ?? []).concat(
|
|
5695
5691
|
linkPreviews
|
|
5696
5692
|
);
|
|
5697
|
-
|
|
5693
|
+
const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
|
|
5694
|
+
if (!messageAttachments.length && !localAttachments.length) return forward();
|
|
5698
5695
|
const sendOptions = { ...state.sendOptions };
|
|
5699
5696
|
const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
|
|
5700
5697
|
if (skip_enrich_url) {
|
|
@@ -5702,14 +5699,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
|
|
|
5702
5699
|
}
|
|
5703
5700
|
return next({
|
|
5704
5701
|
...state,
|
|
5705
|
-
message: {
|
|
5702
|
+
message: messageAttachments.length ? {
|
|
5706
5703
|
...state.message,
|
|
5707
|
-
attachments
|
|
5708
|
-
},
|
|
5709
|
-
localMessage: {
|
|
5704
|
+
attachments: messageAttachments
|
|
5705
|
+
} : state.message,
|
|
5706
|
+
localMessage: localAttachments.length ? {
|
|
5710
5707
|
...state.localMessage,
|
|
5711
|
-
attachments
|
|
5712
|
-
},
|
|
5708
|
+
attachments: localAttachments
|
|
5709
|
+
} : state.localMessage,
|
|
5713
5710
|
sendOptions
|
|
5714
5711
|
});
|
|
5715
5712
|
}
|
|
@@ -12938,6 +12935,242 @@ var ThreadManager = class extends WithSubscriptions {
|
|
|
12938
12935
|
}
|
|
12939
12936
|
};
|
|
12940
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
|
+
|
|
12941
13174
|
// src/poll.ts
|
|
12942
13175
|
var isPollUpdatedEvent = (e) => e.type === "poll.updated";
|
|
12943
13176
|
var isPollClosedEventEvent = (e) => e.type === "poll.closed";
|
|
@@ -12947,6 +13180,9 @@ var isPollVoteRemovedEvent = (e) => e.type === "poll.vote_removed";
|
|
|
12947
13180
|
var isVoteAnswer = (vote) => !!vote?.answer_text;
|
|
12948
13181
|
var Poll = class {
|
|
12949
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;
|
|
12950
13186
|
this.getInitialStateFromPollResponse = (poll) => {
|
|
12951
13187
|
const { own_votes, id, ...pollResponseForState } = poll;
|
|
12952
13188
|
const { ownAnswer, ownVotes } = own_votes?.reduce(
|
|
@@ -12970,25 +13206,47 @@ var Poll = class {
|
|
|
12970
13206
|
ownVotesByOptionId: getOwnVotesByOptionId(ownVotes)
|
|
12971
13207
|
};
|
|
12972
13208
|
};
|
|
13209
|
+
// persists only the server's votes, never the optimistic ones
|
|
12973
13210
|
this.upsertOfflineDb = () => {
|
|
13211
|
+
const serverState = this.optimisticVotes.withServerVotes(this.data);
|
|
12974
13212
|
this.client.offlineDb?.executeQuerySafely(
|
|
12975
|
-
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this) }),
|
|
13213
|
+
(db) => db.upsertPoll({ poll: mapPollStateToResponse(this, serverState) }),
|
|
12976
13214
|
{ method: "upsertPoll" }
|
|
12977
13215
|
);
|
|
12978
13216
|
};
|
|
12979
13217
|
this.reinitializeState = (poll) => {
|
|
12980
|
-
|
|
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();
|
|
12981
13237
|
};
|
|
12982
13238
|
this.handlePollUpdated = (event) => {
|
|
12983
13239
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
12984
13240
|
if (!isPollUpdatedEvent(event)) return;
|
|
12985
13241
|
const { id, ...pollData } = extractPollData(event.poll);
|
|
13242
|
+
if (pollData.is_closed) this.closeConfirmed = true;
|
|
12986
13243
|
this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
|
|
12987
13244
|
this.upsertOfflineDb();
|
|
12988
13245
|
};
|
|
12989
13246
|
this.handlePollClosed = (event) => {
|
|
12990
13247
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
12991
13248
|
if (!isPollClosedEventEvent(event)) return;
|
|
13249
|
+
this.closeConfirmed = true;
|
|
12992
13250
|
this.state.partialNext({
|
|
12993
13251
|
is_closed: true,
|
|
12994
13252
|
lastActivityAt: new Date(event.created_at)
|
|
@@ -13002,8 +13260,7 @@ var Poll = class {
|
|
|
13002
13260
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13003
13261
|
let latestAnswers = [...currentState.latest_answers];
|
|
13004
13262
|
let ownAnswer = currentState.ownAnswer;
|
|
13005
|
-
const ownVotesByOptionId =
|
|
13006
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13263
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13007
13264
|
if (isOwnVote) {
|
|
13008
13265
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13009
13266
|
ownAnswer = event.poll_vote;
|
|
@@ -13013,19 +13270,11 @@ var Poll = class {
|
|
|
13013
13270
|
}
|
|
13014
13271
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13015
13272
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13016
|
-
} else {
|
|
13017
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13018
13273
|
}
|
|
13019
|
-
|
|
13020
|
-
this.state.partialNext({
|
|
13021
|
-
...pollEnrichData,
|
|
13274
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13022
13275
|
latest_answers: latestAnswers,
|
|
13023
|
-
|
|
13024
|
-
ownAnswer,
|
|
13025
|
-
ownVotesByOptionId,
|
|
13026
|
-
maxVotedOptionIds
|
|
13276
|
+
ownAnswer
|
|
13027
13277
|
});
|
|
13028
|
-
this.upsertOfflineDb();
|
|
13029
13278
|
};
|
|
13030
13279
|
this.handleVoteChanged = (event) => {
|
|
13031
13280
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13034,8 +13283,7 @@ var Poll = class {
|
|
|
13034
13283
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13035
13284
|
let latestAnswers = [...currentState.latest_answers];
|
|
13036
13285
|
let ownAnswer = currentState.ownAnswer;
|
|
13037
|
-
let ownVotesByOptionId =
|
|
13038
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13286
|
+
let ownVotesByOptionId = this.optimisticVotes.serverOwnVotes;
|
|
13039
13287
|
if (isOwnVote) {
|
|
13040
13288
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13041
13289
|
latestAnswers = [
|
|
@@ -13059,23 +13307,14 @@ var Poll = class {
|
|
|
13059
13307
|
if (ownAnswer?.id === event.poll_vote.id) {
|
|
13060
13308
|
ownAnswer = void 0;
|
|
13061
13309
|
}
|
|
13062
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13063
13310
|
}
|
|
13064
13311
|
} else if (isVoteAnswer(event.poll_vote)) {
|
|
13065
13312
|
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
13066
|
-
} else {
|
|
13067
|
-
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
13068
13313
|
}
|
|
13069
|
-
|
|
13070
|
-
this.state.partialNext({
|
|
13071
|
-
...pollEnrichData,
|
|
13314
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13072
13315
|
latest_answers: latestAnswers,
|
|
13073
|
-
|
|
13074
|
-
ownAnswer,
|
|
13075
|
-
ownVotesByOptionId,
|
|
13076
|
-
maxVotedOptionIds
|
|
13316
|
+
ownAnswer
|
|
13077
13317
|
});
|
|
13078
|
-
this.upsertOfflineDb();
|
|
13079
13318
|
};
|
|
13080
13319
|
this.handleVoteRemoved = (event) => {
|
|
13081
13320
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
@@ -13084,29 +13323,19 @@ var Poll = class {
|
|
|
13084
13323
|
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
13085
13324
|
let latestAnswers = [...currentState.latest_answers];
|
|
13086
13325
|
let ownAnswer = currentState.ownAnswer;
|
|
13087
|
-
const ownVotesByOptionId = { ...
|
|
13088
|
-
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
13326
|
+
const ownVotesByOptionId = { ...this.optimisticVotes.serverOwnVotes };
|
|
13089
13327
|
if (isVoteAnswer(event.poll_vote)) {
|
|
13090
13328
|
latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
|
|
13091
13329
|
if (isOwnVote) {
|
|
13092
13330
|
ownAnswer = void 0;
|
|
13093
13331
|
}
|
|
13094
|
-
} else {
|
|
13095
|
-
|
|
13096
|
-
if (isOwnVote && event.poll_vote.option_id) {
|
|
13097
|
-
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13098
|
-
}
|
|
13332
|
+
} else if (isOwnVote && event.poll_vote.option_id) {
|
|
13333
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
13099
13334
|
}
|
|
13100
|
-
|
|
13101
|
-
this.state.partialNext({
|
|
13102
|
-
...pollEnrichData,
|
|
13335
|
+
this.updateFromVoteEvent(event, ownVotesByOptionId, {
|
|
13103
13336
|
latest_answers: latestAnswers,
|
|
13104
|
-
|
|
13105
|
-
ownAnswer,
|
|
13106
|
-
ownVotesByOptionId,
|
|
13107
|
-
maxVotedOptionIds
|
|
13337
|
+
ownAnswer
|
|
13108
13338
|
});
|
|
13109
|
-
this.upsertOfflineDb();
|
|
13110
13339
|
};
|
|
13111
13340
|
this.query = async (id) => {
|
|
13112
13341
|
const { poll } = await this.client.getPoll(id);
|
|
@@ -13115,11 +13344,33 @@ var Poll = class {
|
|
|
13115
13344
|
};
|
|
13116
13345
|
this.update = async (data) => await this.client.updatePoll({ ...data, id: this.id });
|
|
13117
13346
|
this.partialUpdate = async (partialPollObject) => await this.client.partialUpdatePoll(this.id, partialPollObject);
|
|
13118
|
-
|
|
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
|
+
};
|
|
13119
13365
|
this.delete = async () => await this.client.deletePoll(this.id);
|
|
13120
13366
|
this.createOption = async (option) => await this.client.createPollOption(this.id, option);
|
|
13121
13367
|
this.updateOption = async (option) => await this.client.updatePollOption(this.id, option);
|
|
13122
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
|
+
*/
|
|
13123
13374
|
this.castVote = async (optionId, messageId) => {
|
|
13124
13375
|
const { max_votes_allowed, ownVotesByOptionId } = this.data;
|
|
13125
13376
|
const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
|
|
@@ -13136,11 +13387,14 @@ var Poll = class {
|
|
|
13136
13387
|
});
|
|
13137
13388
|
return;
|
|
13138
13389
|
}
|
|
13139
|
-
return await this.
|
|
13140
|
-
option_id: optionId
|
|
13141
|
-
});
|
|
13390
|
+
return await this.optimisticVotes.castVote(optionId, messageId);
|
|
13142
13391
|
};
|
|
13143
|
-
|
|
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);
|
|
13144
13398
|
this.addAnswer = async (answerText, messageId) => await this.client.addPollAnswer(messageId, this.id, answerText);
|
|
13145
13399
|
this.removeAnswer = async (answerId, messageId) => await this.client.removePollVote(messageId, this.id, answerId);
|
|
13146
13400
|
this.queryAnswers = async (params) => await this.client.queryPollAnswers(
|
|
@@ -13158,24 +13412,12 @@ var Poll = class {
|
|
|
13158
13412
|
this.client = client;
|
|
13159
13413
|
this.id = poll.id;
|
|
13160
13414
|
this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
|
|
13415
|
+
this.optimisticVotes = new OptimisticPollVotes({ client: this.client, poll: this });
|
|
13161
13416
|
}
|
|
13162
13417
|
get data() {
|
|
13163
13418
|
return this.state.getLatestValue();
|
|
13164
13419
|
}
|
|
13165
13420
|
};
|
|
13166
|
-
function getMaxVotedOptionIds(voteCountsByOption) {
|
|
13167
|
-
let maxVotes = 0;
|
|
13168
|
-
let winningOptions = [];
|
|
13169
|
-
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
13170
|
-
if (count > maxVotes) {
|
|
13171
|
-
winningOptions = [id];
|
|
13172
|
-
maxVotes = count;
|
|
13173
|
-
} else if (count === maxVotes) {
|
|
13174
|
-
winningOptions.push(id);
|
|
13175
|
-
}
|
|
13176
|
-
}
|
|
13177
|
-
return winningOptions;
|
|
13178
|
-
}
|
|
13179
13421
|
function getOwnVotesByOptionId(ownVotes) {
|
|
13180
13422
|
return !ownVotes ? {} : ownVotes.reduce((acc, vote) => {
|
|
13181
13423
|
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
@@ -13197,7 +13439,7 @@ function extractPollData(pollResponse) {
|
|
|
13197
13439
|
voting_visibility: pollResponse.voting_visibility
|
|
13198
13440
|
};
|
|
13199
13441
|
}
|
|
13200
|
-
function mapPollStateToResponse(poll) {
|
|
13442
|
+
function mapPollStateToResponse(poll, state = poll.data) {
|
|
13201
13443
|
const {
|
|
13202
13444
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13203
13445
|
lastActivityAt,
|
|
@@ -13206,7 +13448,7 @@ function mapPollStateToResponse(poll) {
|
|
|
13206
13448
|
ownVotesByOptionId,
|
|
13207
13449
|
ownAnswer,
|
|
13208
13450
|
...restState
|
|
13209
|
-
} =
|
|
13451
|
+
} = state;
|
|
13210
13452
|
const ownVotes = [
|
|
13211
13453
|
...Object.values(ownVotesByOptionId),
|
|
13212
13454
|
...ownAnswer ? [ownAnswer] : []
|
|
@@ -14364,6 +14606,8 @@ var StreamChat = class _StreamChat {
|
|
|
14364
14606
|
* The main use case for SDKs who do independent state recovery for channels.
|
|
14365
14607
|
*/
|
|
14366
14608
|
this.preventThreadCleanup = false;
|
|
14609
|
+
this._wsPromiseSettled = true;
|
|
14610
|
+
this._wsConnectId = 0;
|
|
14367
14611
|
this.nextRequestAbortController = null;
|
|
14368
14612
|
/**
|
|
14369
14613
|
* @private
|
|
@@ -14409,17 +14653,20 @@ var StreamChat = class _StreamChat {
|
|
|
14409
14653
|
const setTokenPromise = this._setToken(user, userTokenOrProvider);
|
|
14410
14654
|
this._setUser(user);
|
|
14411
14655
|
const wsPromise = this.openConnection();
|
|
14412
|
-
|
|
14656
|
+
const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
|
|
14413
14657
|
(result) => result[1]
|
|
14414
14658
|
// We only return connection promise;
|
|
14415
14659
|
);
|
|
14660
|
+
this.setUserPromise = setUserPromise;
|
|
14416
14661
|
try {
|
|
14417
|
-
return await
|
|
14662
|
+
return await setUserPromise;
|
|
14418
14663
|
} catch (err) {
|
|
14419
|
-
if (this.
|
|
14420
|
-
this.
|
|
14421
|
-
|
|
14422
|
-
|
|
14664
|
+
if (this.setUserPromise === setUserPromise && this.userID === user.id) {
|
|
14665
|
+
if (this.persistUserOnConnectionFailure) {
|
|
14666
|
+
this.closeConnection();
|
|
14667
|
+
} else {
|
|
14668
|
+
this.disconnectUser();
|
|
14669
|
+
}
|
|
14423
14670
|
}
|
|
14424
14671
|
throw err;
|
|
14425
14672
|
}
|
|
@@ -14515,9 +14762,50 @@ var StreamChat = class _StreamChat {
|
|
|
14515
14762
|
return;
|
|
14516
14763
|
}
|
|
14517
14764
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
14518
|
-
|
|
14765
|
+
const wsPromise = this._bindWsPromise(this.connect());
|
|
14519
14766
|
this._startCleaning();
|
|
14520
|
-
return
|
|
14767
|
+
return wsPromise;
|
|
14768
|
+
};
|
|
14769
|
+
/**
|
|
14770
|
+
* Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
|
|
14771
|
+
*/
|
|
14772
|
+
this._rejectPendingWsPromise = (reason) => {
|
|
14773
|
+
if (!this._wsPromiseSettled) {
|
|
14774
|
+
this._wsConnectId += 1;
|
|
14775
|
+
this._wsPromiseSettled = true;
|
|
14776
|
+
this.wsPromise?.catch(() => {
|
|
14777
|
+
});
|
|
14778
|
+
this._rejectWsPromise?.(reason);
|
|
14779
|
+
}
|
|
14780
|
+
};
|
|
14781
|
+
/**
|
|
14782
|
+
* Keep a single pending `wsPromise` across close/reopen so callers that already
|
|
14783
|
+
* `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
|
|
14784
|
+
*/
|
|
14785
|
+
this._bindWsPromise = (connectPromise) => {
|
|
14786
|
+
const connectId = ++this._wsConnectId;
|
|
14787
|
+
let pending = this.wsPromise;
|
|
14788
|
+
if (this._wsPromiseSettled || !pending) {
|
|
14789
|
+
this._wsPromiseSettled = false;
|
|
14790
|
+
pending = new Promise((resolve, reject) => {
|
|
14791
|
+
this._resolveWsPromise = resolve;
|
|
14792
|
+
this._rejectWsPromise = reject;
|
|
14793
|
+
});
|
|
14794
|
+
this.wsPromise = pending;
|
|
14795
|
+
}
|
|
14796
|
+
connectPromise.then(
|
|
14797
|
+
(value) => {
|
|
14798
|
+
if (connectId !== this._wsConnectId) return;
|
|
14799
|
+
this._wsPromiseSettled = true;
|
|
14800
|
+
this._resolveWsPromise?.(value);
|
|
14801
|
+
},
|
|
14802
|
+
(error) => {
|
|
14803
|
+
if (connectId !== this._wsConnectId) return;
|
|
14804
|
+
this._wsPromiseSettled = true;
|
|
14805
|
+
this._rejectWsPromise?.(error);
|
|
14806
|
+
}
|
|
14807
|
+
);
|
|
14808
|
+
return pending;
|
|
14521
14809
|
};
|
|
14522
14810
|
/**
|
|
14523
14811
|
* @deprecated Please use client.openConnction instead.
|
|
@@ -14551,6 +14839,10 @@ var StreamChat = class _StreamChat {
|
|
|
14551
14839
|
delete this._user;
|
|
14552
14840
|
delete this.userID;
|
|
14553
14841
|
this.anonymous = false;
|
|
14842
|
+
this._rejectPendingWsPromise(
|
|
14843
|
+
new Error("Connection was closed because disconnectUser() was called")
|
|
14844
|
+
);
|
|
14845
|
+
this.wsPromise = null;
|
|
14554
14846
|
const closePromise = this.closeConnection(timeout);
|
|
14555
14847
|
for (const channel of Object.values(this.activeChannels)) {
|
|
14556
14848
|
channel._disconnect();
|
|
@@ -14821,7 +15113,9 @@ var StreamChat = class _StreamChat {
|
|
|
14821
15113
|
type: "connection.recovered"
|
|
14822
15114
|
});
|
|
14823
15115
|
}
|
|
14824
|
-
this.
|
|
15116
|
+
if (this._wsPromiseSettled) {
|
|
15117
|
+
this.wsPromise = Promise.resolve();
|
|
15118
|
+
}
|
|
14825
15119
|
this.setUserPromise = Promise.resolve();
|
|
14826
15120
|
};
|
|
14827
15121
|
/**
|
|
@@ -14939,7 +15233,7 @@ var StreamChat = class _StreamChat {
|
|
|
14939
15233
|
return this.userAgent;
|
|
14940
15234
|
}
|
|
14941
15235
|
if (!this.cachedUserAgent) {
|
|
14942
|
-
const version = "9.
|
|
15236
|
+
const version = "9.54.0";
|
|
14943
15237
|
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
14944
15238
|
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
14945
15239
|
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
@@ -18181,6 +18475,10 @@ var StreamChat = class _StreamChat {
|
|
|
18181
18475
|
/**
|
|
18182
18476
|
* Update Channels Batch
|
|
18183
18477
|
*
|
|
18478
|
+
* For the `updateData` operation, `data.custom` replaces the channel's whole
|
|
18479
|
+
* custom object, while the root-level `custom_set` / `custom_unset` patch
|
|
18480
|
+
* individual keys and leave the rest untouched.
|
|
18481
|
+
*
|
|
18184
18482
|
* @param {UpdateChannelsBatchOptions} payload for updating channels in batch
|
|
18185
18483
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
18186
18484
|
*/
|