stream-chat 8.41.0 → 8.42.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/browser.es.js +1729 -804
- package/dist/browser.es.js.map +1 -1
- package/dist/browser.full-bundle.min.js +1 -1
- package/dist/browser.full-bundle.min.js.map +1 -1
- package/dist/browser.js +1730 -800
- package/dist/browser.js.map +1 -1
- package/dist/index.es.js +1729 -804
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1730 -800
- package/dist/index.js.map +1 -1
- package/dist/types/channel.d.ts +1 -1
- package/dist/types/channel.d.ts.map +1 -1
- package/dist/types/channel_state.d.ts +1 -5
- package/dist/types/channel_state.d.ts.map +1 -1
- package/dist/types/client.d.ts +27 -15
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/poll.d.ts +64 -0
- package/dist/types/poll.d.ts.map +1 -0
- package/dist/types/poll_manager.d.ts +31 -0
- package/dist/types/poll_manager.d.ts.map +1 -0
- package/dist/types/store.d.ts +2 -1
- package/dist/types/store.d.ts.map +1 -1
- package/dist/types/thread.d.ts.map +1 -1
- package/dist/types/types.d.ts +33 -20
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/channel.ts +2 -25
- package/src/channel_state.ts +0 -92
- package/src/client.ts +112 -70
- package/src/index.ts +8 -6
- package/src/poll.ts +414 -0
- package/src/poll_manager.ts +162 -0
- package/src/store.ts +22 -2
- package/src/thread.ts +7 -4
- package/src/thread_manager.ts +5 -5
- package/src/types.ts +60 -36
package/src/poll.ts
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { StateStore } from './store';
|
|
2
|
+
import type { StreamChat } from './client';
|
|
3
|
+
import type {
|
|
4
|
+
DefaultGenerics,
|
|
5
|
+
Event,
|
|
6
|
+
ExtendableGenerics,
|
|
7
|
+
PartialPollUpdate,
|
|
8
|
+
PollAnswer,
|
|
9
|
+
PollData,
|
|
10
|
+
PollEnrichData,
|
|
11
|
+
PollOptionData,
|
|
12
|
+
PollResponse,
|
|
13
|
+
PollVote,
|
|
14
|
+
QueryVotesFilters,
|
|
15
|
+
QueryVotesOptions,
|
|
16
|
+
VoteSort,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
type PollEvent<SCG extends ExtendableGenerics = DefaultGenerics> = {
|
|
20
|
+
cid: string;
|
|
21
|
+
created_at: string;
|
|
22
|
+
poll: PollResponse<SCG>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type PollUpdatedEvent<SCG extends ExtendableGenerics = DefaultGenerics> = PollEvent<SCG> & {
|
|
26
|
+
type: 'poll.updated';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type PollClosedEvent<SCG extends ExtendableGenerics = DefaultGenerics> = PollEvent<SCG> & {
|
|
30
|
+
type: 'poll.closed';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type PollVoteEvent<SCG extends ExtendableGenerics = DefaultGenerics> = {
|
|
34
|
+
cid: string;
|
|
35
|
+
created_at: string;
|
|
36
|
+
poll: PollResponse<SCG>;
|
|
37
|
+
poll_vote: PollVote<SCG> | PollAnswer<SCG>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type PollVoteCastedEvent<SCG extends ExtendableGenerics = DefaultGenerics> = PollVoteEvent<SCG> & {
|
|
41
|
+
type: 'poll.vote_casted';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type PollVoteCastedChanged<SCG extends ExtendableGenerics = DefaultGenerics> = PollVoteEvent<SCG> & {
|
|
45
|
+
type: 'poll.vote_removed';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type PollVoteCastedRemoved<SCG extends ExtendableGenerics = DefaultGenerics> = PollVoteEvent<SCG> & {
|
|
49
|
+
type: 'poll.vote_removed';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isPollUpdatedEvent = <SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
53
|
+
e: Event<SCG>,
|
|
54
|
+
): e is PollUpdatedEvent<SCG> => e.type === 'poll.updated';
|
|
55
|
+
const isPollClosedEventEvent = <SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
56
|
+
e: Event<SCG>,
|
|
57
|
+
): e is PollClosedEvent<SCG> => e.type === 'poll.closed';
|
|
58
|
+
const isPollVoteCastedEvent = <SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
59
|
+
e: Event<SCG>,
|
|
60
|
+
): e is PollVoteCastedEvent<SCG> => e.type === 'poll.vote_casted';
|
|
61
|
+
const isPollVoteChangedEvent = <SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
62
|
+
e: Event<SCG>,
|
|
63
|
+
): e is PollVoteCastedChanged<SCG> => e.type === 'poll.vote_changed';
|
|
64
|
+
const isPollVoteRemovedEvent = <SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
65
|
+
e: Event<SCG>,
|
|
66
|
+
): e is PollVoteCastedRemoved<SCG> => e.type === 'poll.vote_removed';
|
|
67
|
+
|
|
68
|
+
export const isVoteAnswer = <SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
69
|
+
vote: PollVote<SCG> | PollAnswer<SCG>,
|
|
70
|
+
): vote is PollAnswer<SCG> => !!(vote as PollAnswer<SCG>)?.answer_text;
|
|
71
|
+
|
|
72
|
+
export type PollAnswersQueryParams = {
|
|
73
|
+
filter?: QueryVotesFilters;
|
|
74
|
+
options?: QueryVotesOptions;
|
|
75
|
+
sort?: VoteSort;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type PollOptionVotesQueryParams = {
|
|
79
|
+
filter: { option_id: string } & QueryVotesFilters;
|
|
80
|
+
options?: QueryVotesOptions;
|
|
81
|
+
sort?: VoteSort;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type OptionId = string;
|
|
85
|
+
|
|
86
|
+
export type PollState<SCG extends ExtendableGenerics = DefaultGenerics> = SCG['pollType'] &
|
|
87
|
+
Omit<PollResponse<SCG>, 'own_votes' | 'id'> & {
|
|
88
|
+
lastActivityAt: Date; // todo: would be ideal to get this from the BE
|
|
89
|
+
maxVotedOptionIds: OptionId[];
|
|
90
|
+
ownVotesByOptionId: Record<OptionId, PollVote<SCG>>;
|
|
91
|
+
ownAnswer?: PollAnswer; // each user can have only one answer
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type PollInitOptions<SCG extends ExtendableGenerics = DefaultGenerics> = {
|
|
95
|
+
client: StreamChat<SCG>;
|
|
96
|
+
poll: PollResponse<SCG>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export class Poll<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
100
|
+
public readonly state: StateStore<PollState<SCG>>;
|
|
101
|
+
public id: string;
|
|
102
|
+
private client: StreamChat<SCG>;
|
|
103
|
+
private unsubscribeFunctions: Set<() => void> = new Set();
|
|
104
|
+
|
|
105
|
+
constructor({ client, poll }: PollInitOptions<SCG>) {
|
|
106
|
+
this.client = client;
|
|
107
|
+
this.id = poll.id;
|
|
108
|
+
|
|
109
|
+
this.state = new StateStore<PollState<SCG>>(this.getInitialStateFromPollResponse(poll));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getInitialStateFromPollResponse = (poll: PollInitOptions<SCG>['poll']) => {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
114
|
+
const { own_votes, id, ...pollResponseForState } = poll;
|
|
115
|
+
const { ownAnswer, ownVotes } = own_votes?.reduce<{ ownVotes: PollVote<SCG>[]; ownAnswer?: PollAnswer }>(
|
|
116
|
+
(acc, voteOrAnswer) => {
|
|
117
|
+
if (isVoteAnswer(voteOrAnswer)) {
|
|
118
|
+
acc.ownAnswer = voteOrAnswer;
|
|
119
|
+
} else {
|
|
120
|
+
acc.ownVotes.push(voteOrAnswer);
|
|
121
|
+
}
|
|
122
|
+
return acc;
|
|
123
|
+
},
|
|
124
|
+
{ ownVotes: [] },
|
|
125
|
+
) ?? { ownVotes: [] };
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...pollResponseForState,
|
|
129
|
+
lastActivityAt: new Date(),
|
|
130
|
+
maxVotedOptionIds: getMaxVotedOptionIds(
|
|
131
|
+
pollResponseForState.vote_counts_by_option as PollResponse<SCG>['vote_counts_by_option'],
|
|
132
|
+
),
|
|
133
|
+
ownAnswer,
|
|
134
|
+
ownVotesByOptionId: getOwnVotesByOptionId(ownVotes),
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
public reinitializeState = (poll: PollInitOptions<SCG>['poll']) => {
|
|
139
|
+
this.state.partialNext(this.getInitialStateFromPollResponse(poll));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
get data(): PollState<SCG> {
|
|
143
|
+
return this.state.getLatestValue();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public handlePollUpdated = (event: Event<SCG>) => {
|
|
147
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
148
|
+
if (!isPollUpdatedEvent(event)) return;
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
150
|
+
const { id, ...pollData } = extractPollData(event.poll);
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
this.state.partialNext({ ...pollData, lastActivityAt: new Date(event.created_at) });
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
public handlePollClosed = (event: Event<SCG>) => {
|
|
156
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
157
|
+
if (!isPollClosedEventEvent(event)) return;
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
this.state.partialNext({ is_closed: true, lastActivityAt: new Date(event.created_at) });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
public handleVoteCasted = (event: Event<SCG>) => {
|
|
163
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
164
|
+
if (!isPollVoteCastedEvent(event)) return;
|
|
165
|
+
const currentState = this.data;
|
|
166
|
+
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
167
|
+
let latestAnswers = [...(currentState.latest_answers as PollAnswer[])];
|
|
168
|
+
let ownAnswer = currentState.ownAnswer;
|
|
169
|
+
const ownVotesByOptionId = currentState.ownVotesByOptionId;
|
|
170
|
+
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
171
|
+
|
|
172
|
+
if (isOwnVote) {
|
|
173
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
174
|
+
ownAnswer = event.poll_vote;
|
|
175
|
+
} else if (event.poll_vote.option_id) {
|
|
176
|
+
ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
181
|
+
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
182
|
+
} else {
|
|
183
|
+
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pollEnrichData = extractPollEnrichedData(event.poll);
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
this.state.partialNext({
|
|
189
|
+
...pollEnrichData,
|
|
190
|
+
latest_answers: latestAnswers,
|
|
191
|
+
lastActivityAt: new Date(event.created_at),
|
|
192
|
+
ownAnswer,
|
|
193
|
+
ownVotesByOptionId,
|
|
194
|
+
maxVotedOptionIds,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
public handleVoteChanged = (event: Event<SCG>) => {
|
|
199
|
+
// this event is triggered only when event.poll.enforce_unique_vote === true
|
|
200
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
201
|
+
if (!isPollVoteChangedEvent(event)) return;
|
|
202
|
+
const currentState = this.data;
|
|
203
|
+
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
204
|
+
let latestAnswers = [...(currentState.latest_answers as PollAnswer[])];
|
|
205
|
+
let ownAnswer = currentState.ownAnswer;
|
|
206
|
+
let ownVotesByOptionId = currentState.ownVotesByOptionId;
|
|
207
|
+
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
208
|
+
|
|
209
|
+
if (isOwnVote) {
|
|
210
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
211
|
+
latestAnswers = [event.poll_vote, ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id)];
|
|
212
|
+
ownAnswer = event.poll_vote;
|
|
213
|
+
} else if (event.poll_vote.option_id) {
|
|
214
|
+
if (event.poll.enforce_unique_votes) {
|
|
215
|
+
ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
|
|
216
|
+
} else {
|
|
217
|
+
ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce<Record<OptionId, PollVote<SCG>>>(
|
|
218
|
+
(acc, [optionId, vote]) => {
|
|
219
|
+
if (optionId !== event.poll_vote.option_id && vote.id === event.poll_vote.id) {
|
|
220
|
+
return acc;
|
|
221
|
+
}
|
|
222
|
+
acc[optionId] = vote;
|
|
223
|
+
return acc;
|
|
224
|
+
},
|
|
225
|
+
{},
|
|
226
|
+
);
|
|
227
|
+
ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (ownAnswer?.id === event.poll_vote.id) {
|
|
231
|
+
ownAnswer = undefined;
|
|
232
|
+
}
|
|
233
|
+
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
234
|
+
}
|
|
235
|
+
} else if (isVoteAnswer(event.poll_vote)) {
|
|
236
|
+
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
237
|
+
} else {
|
|
238
|
+
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const pollEnrichData = extractPollEnrichedData(event.poll);
|
|
242
|
+
// @ts-ignore
|
|
243
|
+
this.state.partialNext({
|
|
244
|
+
...pollEnrichData,
|
|
245
|
+
latest_answers: latestAnswers,
|
|
246
|
+
lastActivityAt: new Date(event.created_at),
|
|
247
|
+
ownAnswer,
|
|
248
|
+
ownVotesByOptionId,
|
|
249
|
+
maxVotedOptionIds,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
public handleVoteRemoved = (event: Event<SCG>) => {
|
|
254
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
255
|
+
if (!isPollVoteRemovedEvent(event)) return;
|
|
256
|
+
const currentState = this.data;
|
|
257
|
+
const isOwnVote = event.poll_vote.user_id === this.client.userID;
|
|
258
|
+
let latestAnswers = [...(currentState.latest_answers as PollAnswer[])];
|
|
259
|
+
let ownAnswer = currentState.ownAnswer;
|
|
260
|
+
const ownVotesByOptionId = { ...currentState.ownVotesByOptionId };
|
|
261
|
+
let maxVotedOptionIds = currentState.maxVotedOptionIds;
|
|
262
|
+
|
|
263
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
264
|
+
latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
|
|
265
|
+
if (isOwnVote) {
|
|
266
|
+
ownAnswer = undefined;
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
|
|
270
|
+
if (isOwnVote && event.poll_vote.option_id) {
|
|
271
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const pollEnrichData = extractPollEnrichedData(event.poll);
|
|
276
|
+
// @ts-ignore
|
|
277
|
+
this.state.partialNext({
|
|
278
|
+
...pollEnrichData,
|
|
279
|
+
latest_answers: latestAnswers,
|
|
280
|
+
lastActivityAt: new Date(event.created_at),
|
|
281
|
+
ownAnswer,
|
|
282
|
+
ownVotesByOptionId,
|
|
283
|
+
maxVotedOptionIds,
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
query = async (id: string) => {
|
|
288
|
+
const { poll } = await this.client.getPoll(id);
|
|
289
|
+
// @ts-ignore
|
|
290
|
+
this.state.partialNext({ ...poll, lastActivityAt: new Date() });
|
|
291
|
+
return poll;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
update = async (data: Exclude<PollData<SCG>, 'id'>) => {
|
|
295
|
+
return await this.client.updatePoll({ ...data, id: this.id });
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
partialUpdate = async (partialPollObject: PartialPollUpdate<SCG>) => {
|
|
299
|
+
return await this.client.partialUpdatePoll(this.id as string, partialPollObject);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
close = async () => {
|
|
303
|
+
return await this.client.closePoll(this.id as string);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
delete = async () => {
|
|
307
|
+
return await this.client.deletePoll(this.id as string);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
createOption = async (option: PollOptionData) => {
|
|
311
|
+
return await this.client.createPollOption(this.id as string, option);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
updateOption = async (option: PollOptionData) => {
|
|
315
|
+
return await this.client.updatePollOption(this.id as string, option);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
deleteOption = async (optionId: string) => {
|
|
319
|
+
return await this.client.deletePollOption(this.id as string, optionId);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
castVote = async (optionId: string, messageId: string) => {
|
|
323
|
+
const { max_votes_allowed, ownVotesByOptionId } = this.data;
|
|
324
|
+
|
|
325
|
+
const reachedVoteLimit = max_votes_allowed && max_votes_allowed === Object.keys(ownVotesByOptionId).length;
|
|
326
|
+
|
|
327
|
+
if (reachedVoteLimit) {
|
|
328
|
+
let oldestVote = Object.values(ownVotesByOptionId)[0];
|
|
329
|
+
Object.values(ownVotesByOptionId)
|
|
330
|
+
.slice(1)
|
|
331
|
+
.forEach((vote) => {
|
|
332
|
+
if (!oldestVote?.created_at || new Date(vote.created_at) < new Date(oldestVote.created_at)) {
|
|
333
|
+
oldestVote = vote;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if (oldestVote?.id) {
|
|
337
|
+
await this.removeVote(oldestVote.id, messageId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return await this.client.castPollVote(messageId, this.id as string, { option_id: optionId });
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
removeVote = async (voteId: string, messageId: string) => {
|
|
344
|
+
return await this.client.removePollVote(messageId, this.id as string, voteId);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
addAnswer = async (answerText: string, messageId: string) => {
|
|
348
|
+
return await this.client.addPollAnswer(messageId, this.id as string, answerText);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
removeAnswer = async (answerId: string, messageId: string) => {
|
|
352
|
+
return await this.client.removePollVote(messageId, this.id as string, answerId);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
queryAnswers = async (params: PollAnswersQueryParams) => {
|
|
356
|
+
return await this.client.queryPollAnswers(this.id as string, params.filter, params.sort, params.options);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
queryOptionVotes = async (params: PollOptionVotesQueryParams) => {
|
|
360
|
+
return await this.client.queryPollVotes(this.id as string, params.filter, params.sort, params.options);
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function getMaxVotedOptionIds(voteCountsByOption: PollResponse['vote_counts_by_option']) {
|
|
365
|
+
let maxVotes = 0;
|
|
366
|
+
let winningOptions: string[] = [];
|
|
367
|
+
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
368
|
+
if (count > maxVotes) {
|
|
369
|
+
winningOptions = [id];
|
|
370
|
+
maxVotes = count;
|
|
371
|
+
} else if (count === maxVotes) {
|
|
372
|
+
winningOptions.push(id);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return winningOptions;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getOwnVotesByOptionId<SCG extends ExtendableGenerics = DefaultGenerics>(ownVotes: PollVote<SCG>[]) {
|
|
379
|
+
return !ownVotes
|
|
380
|
+
? ({} as Record<OptionId, PollVote<SCG>>)
|
|
381
|
+
: ownVotes.reduce<Record<OptionId, PollVote<SCG>>>((acc, vote) => {
|
|
382
|
+
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
383
|
+
acc[vote.option_id] = vote;
|
|
384
|
+
return acc;
|
|
385
|
+
}, {});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function extractPollData<SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
389
|
+
pollResponse: PollResponse<SCG>,
|
|
390
|
+
): PollData<SCG> {
|
|
391
|
+
return {
|
|
392
|
+
allow_answers: pollResponse.allow_answers,
|
|
393
|
+
allow_user_suggested_options: pollResponse.allow_user_suggested_options,
|
|
394
|
+
description: pollResponse.description,
|
|
395
|
+
enforce_unique_vote: pollResponse.enforce_unique_vote,
|
|
396
|
+
id: pollResponse.id,
|
|
397
|
+
is_closed: pollResponse.is_closed,
|
|
398
|
+
max_votes_allowed: pollResponse.max_votes_allowed,
|
|
399
|
+
name: pollResponse.name,
|
|
400
|
+
options: pollResponse.options,
|
|
401
|
+
voting_visibility: pollResponse.voting_visibility,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function extractPollEnrichedData<SCG extends ExtendableGenerics = DefaultGenerics>(
|
|
406
|
+
pollResponse: PollResponse<SCG>,
|
|
407
|
+
): Omit<PollEnrichData<SCG>, 'own_votes' | 'latest_answers'> {
|
|
408
|
+
return {
|
|
409
|
+
answers_count: pollResponse.answers_count,
|
|
410
|
+
latest_votes_by_option: pollResponse.latest_votes_by_option,
|
|
411
|
+
vote_count: pollResponse.vote_count,
|
|
412
|
+
vote_counts_by_option: pollResponse.vote_counts_by_option,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { StreamChat } from './client';
|
|
2
|
+
import type {
|
|
3
|
+
CreatePollData,
|
|
4
|
+
DefaultGenerics,
|
|
5
|
+
ExtendableGenerics,
|
|
6
|
+
PollResponse,
|
|
7
|
+
PollSort,
|
|
8
|
+
QueryPollsFilters,
|
|
9
|
+
QueryPollsOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
import { Poll } from './poll';
|
|
12
|
+
import { FormatMessageResponse } from './types';
|
|
13
|
+
import { formatMessage } from './utils';
|
|
14
|
+
|
|
15
|
+
export class PollManager<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
16
|
+
private client: StreamChat<SCG>;
|
|
17
|
+
// The pollCache contains only polls that have been created and sent as messages
|
|
18
|
+
// (i.e only polls that are coupled with a message, can be voted on and require a
|
|
19
|
+
// reactive state). It shall work as a basic look-up table for our SDK to be able
|
|
20
|
+
// to quickly consume poll state that will be reactive even without the polls being
|
|
21
|
+
// rendered within the UI.
|
|
22
|
+
private pollCache = new Map<string, Poll<SCG>>();
|
|
23
|
+
private unsubscribeFunctions: Set<() => void> = new Set();
|
|
24
|
+
|
|
25
|
+
constructor({ client }: { client: StreamChat<SCG> }) {
|
|
26
|
+
this.client = client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get data(): Map<string, Poll<SCG>> {
|
|
30
|
+
return this.pollCache;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public fromState = (id: string) => {
|
|
34
|
+
return this.pollCache.get(id);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
public registerSubscriptions = () => {
|
|
38
|
+
if (this.unsubscribeFunctions.size) {
|
|
39
|
+
// Already listening for events and changes
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.unsubscribeFunctions.add(this.subscribeMessageNew());
|
|
44
|
+
this.unsubscribeFunctions.add(this.subscribePollUpdated());
|
|
45
|
+
this.unsubscribeFunctions.add(this.subscribePollClosed());
|
|
46
|
+
this.unsubscribeFunctions.add(this.subscribeVoteCasted());
|
|
47
|
+
this.unsubscribeFunctions.add(this.subscribeVoteChanged());
|
|
48
|
+
this.unsubscribeFunctions.add(this.subscribeVoteRemoved());
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
public unregisterSubscriptions = () => {
|
|
52
|
+
this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction());
|
|
53
|
+
this.unsubscribeFunctions.clear();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
public createPoll = async (poll: CreatePollData<SCG>) => {
|
|
57
|
+
const { poll: createdPoll } = await this.client.createPoll(poll);
|
|
58
|
+
|
|
59
|
+
return new Poll({ client: this.client, poll: createdPoll });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
public getPoll = async (id: string) => {
|
|
63
|
+
const cachedPoll = this.fromState(id);
|
|
64
|
+
|
|
65
|
+
// optimistically return the cached poll if it exists and update in the background
|
|
66
|
+
if (cachedPoll) {
|
|
67
|
+
this.client.getPoll(id).then(({ poll }) => this.setOrOverwriteInCache(poll, true));
|
|
68
|
+
return cachedPoll;
|
|
69
|
+
}
|
|
70
|
+
// fetch it, write to the cache and return otherwise
|
|
71
|
+
const { poll } = await this.client.getPoll(id);
|
|
72
|
+
|
|
73
|
+
this.setOrOverwriteInCache(poll);
|
|
74
|
+
|
|
75
|
+
return this.fromState(id);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
public queryPolls = async (filter: QueryPollsFilters, sort: PollSort = [], options: QueryPollsOptions = {}) => {
|
|
79
|
+
const { polls, next } = await this.client.queryPolls(filter, sort, options);
|
|
80
|
+
|
|
81
|
+
const pollInstances = polls.map((poll) => {
|
|
82
|
+
this.setOrOverwriteInCache(poll, true);
|
|
83
|
+
|
|
84
|
+
return this.fromState(poll.id);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
polls: pollInstances,
|
|
89
|
+
next,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
public hydratePollCache = (messages: FormatMessageResponse<SCG>[], overwriteState?: boolean) => {
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
if (!message.poll) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const pollResponse = message.poll as PollResponse<SCG>;
|
|
99
|
+
this.setOrOverwriteInCache(pollResponse, overwriteState);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
private setOrOverwriteInCache = (pollResponse: PollResponse<SCG>, overwriteState?: boolean) => {
|
|
104
|
+
const pollFromCache = this.fromState(pollResponse.id);
|
|
105
|
+
if (!pollFromCache) {
|
|
106
|
+
const poll = new Poll<SCG>({ client: this.client, poll: pollResponse });
|
|
107
|
+
this.pollCache.set(poll.id, poll);
|
|
108
|
+
} else if (overwriteState) {
|
|
109
|
+
pollFromCache.reinitializeState(pollResponse);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private subscribePollUpdated = () => {
|
|
114
|
+
return this.client.on('poll.updated', (event) => {
|
|
115
|
+
if (event.poll?.id) {
|
|
116
|
+
this.fromState(event.poll.id)?.handlePollUpdated(event);
|
|
117
|
+
}
|
|
118
|
+
}).unsubscribe;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
private subscribePollClosed = () => {
|
|
122
|
+
return this.client.on('poll.closed', (event) => {
|
|
123
|
+
if (event.poll?.id) {
|
|
124
|
+
this.fromState(event.poll.id)?.handlePollClosed(event);
|
|
125
|
+
}
|
|
126
|
+
}).unsubscribe;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
private subscribeVoteCasted = () => {
|
|
130
|
+
return this.client.on('poll.vote_casted', (event) => {
|
|
131
|
+
if (event.poll?.id) {
|
|
132
|
+
this.fromState(event.poll.id)?.handleVoteCasted(event);
|
|
133
|
+
}
|
|
134
|
+
}).unsubscribe;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
private subscribeVoteChanged = () => {
|
|
138
|
+
return this.client.on('poll.vote_changed', (event) => {
|
|
139
|
+
if (event.poll?.id) {
|
|
140
|
+
this.fromState(event.poll.id)?.handleVoteChanged(event);
|
|
141
|
+
}
|
|
142
|
+
}).unsubscribe;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
private subscribeVoteRemoved = () => {
|
|
146
|
+
return this.client.on('poll.vote_removed', (event) => {
|
|
147
|
+
if (event.poll?.id) {
|
|
148
|
+
this.fromState(event.poll.id)?.handleVoteRemoved(event);
|
|
149
|
+
}
|
|
150
|
+
}).unsubscribe;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
private subscribeMessageNew = () => {
|
|
154
|
+
return this.client.on('message.new', (event) => {
|
|
155
|
+
const { message } = event;
|
|
156
|
+
if (message) {
|
|
157
|
+
const formattedMessage = formatMessage(message);
|
|
158
|
+
this.hydratePollCache([formattedMessage]);
|
|
159
|
+
}
|
|
160
|
+
}).unsubscribe;
|
|
161
|
+
};
|
|
162
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -9,6 +9,8 @@ function isPatch<T>(value: T | Patch<T>): value is Patch<T> {
|
|
|
9
9
|
export class StateStore<T extends Record<string, unknown>> {
|
|
10
10
|
private handlerSet = new Set<Handler<T>>();
|
|
11
11
|
|
|
12
|
+
private static logCount = 5;
|
|
13
|
+
|
|
12
14
|
constructor(private value: T) {}
|
|
13
15
|
|
|
14
16
|
public next = (newValueOrPatch: T | Patch<T>): void => {
|
|
@@ -36,13 +38,31 @@ export class StateStore<T extends Record<string, unknown>> {
|
|
|
36
38
|
};
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
public subscribeWithSelector = <O extends
|
|
41
|
+
public subscribeWithSelector = <O extends Readonly<Record<string, unknown>> | Readonly<unknown[]>>(
|
|
42
|
+
selector: (nextValue: T) => O,
|
|
43
|
+
handler: Handler<O>,
|
|
44
|
+
) => {
|
|
40
45
|
// begin with undefined to reduce amount of selector calls
|
|
41
46
|
let selectedValues: O | undefined;
|
|
42
47
|
|
|
43
48
|
const wrappedHandler: Handler<T> = (nextValue) => {
|
|
44
49
|
const newlySelectedValues = selector(nextValue);
|
|
45
|
-
|
|
50
|
+
|
|
51
|
+
let hasUpdatedValues = !selectedValues;
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(newlySelectedValues) && StateStore.logCount > 0) {
|
|
54
|
+
console.warn(
|
|
55
|
+
'[StreamChat]: The API of our StateStore has changed. Instead of returning an array in the selector, please return a named object of properties.',
|
|
56
|
+
);
|
|
57
|
+
StateStore.logCount--;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const key in selectedValues) {
|
|
61
|
+
// @ts-ignore TODO: remove array support (Readonly<unknown[]>)
|
|
62
|
+
if (selectedValues[key] === newlySelectedValues[key]) continue;
|
|
63
|
+
hasUpdatedValues = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
46
66
|
|
|
47
67
|
if (!hasUpdatedValues) return;
|
|
48
68
|
|
package/src/thread.ts
CHANGED
|
@@ -197,8 +197,11 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
|
197
197
|
|
|
198
198
|
private subscribeMarkActiveThreadRead = () => {
|
|
199
199
|
return this.state.subscribeWithSelector(
|
|
200
|
-
(nextValue) =>
|
|
201
|
-
|
|
200
|
+
(nextValue) => ({
|
|
201
|
+
active: nextValue.active,
|
|
202
|
+
unreadMessageCount: ownUnreadCountSelector(this.client.userID)(nextValue),
|
|
203
|
+
}),
|
|
204
|
+
({ active, unreadMessageCount }) => {
|
|
202
205
|
if (!active || !unreadMessageCount) return;
|
|
203
206
|
this.throttledMarkAsRead();
|
|
204
207
|
},
|
|
@@ -207,8 +210,8 @@ export class Thread<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
|
207
210
|
|
|
208
211
|
private subscribeReloadActiveStaleThread = () =>
|
|
209
212
|
this.state.subscribeWithSelector(
|
|
210
|
-
(nextValue) =>
|
|
211
|
-
(
|
|
213
|
+
(nextValue) => ({ active: nextValue.active, isStateStale: nextValue.isStateStale }),
|
|
214
|
+
({ active, isStateStale }) => {
|
|
212
215
|
if (active && isStateStale) {
|
|
213
216
|
this.reload();
|
|
214
217
|
}
|
package/src/thread_manager.ts
CHANGED
|
@@ -119,9 +119,9 @@ export class ThreadManager<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
|
119
119
|
|
|
120
120
|
private subscribeManageThreadSubscriptions = () =>
|
|
121
121
|
this.state.subscribeWithSelector(
|
|
122
|
-
(nextValue) =>
|
|
123
|
-
(
|
|
124
|
-
const
|
|
122
|
+
(nextValue) => ({ threads: nextValue.threads }),
|
|
123
|
+
({ threads: nextThreads }, prev) => {
|
|
124
|
+
const { threads: prevThreads = [] } = prev ?? {};
|
|
125
125
|
// Thread instance was removed if there's no thread with the given id at all,
|
|
126
126
|
// or it was replaced with a new instance
|
|
127
127
|
const removedThreads = prevThreads.filter((thread) => thread !== this.threadsById[thread.id]);
|
|
@@ -133,8 +133,8 @@ export class ThreadManager<SCG extends ExtendableGenerics = DefaultGenerics> {
|
|
|
133
133
|
|
|
134
134
|
private subscribeReloadOnActivation = () =>
|
|
135
135
|
this.state.subscribeWithSelector(
|
|
136
|
-
(nextValue) =>
|
|
137
|
-
(
|
|
136
|
+
(nextValue) => ({ active: nextValue.active }),
|
|
137
|
+
({ active }) => {
|
|
138
138
|
if (active) this.reload();
|
|
139
139
|
},
|
|
140
140
|
);
|