stream-chat 8.41.1 → 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 +1659 -748
- 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 +1660 -744
- package/dist/browser.js.map +1 -1
- package/dist/index.es.js +1659 -748
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1660 -744
- 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/types.d.ts +32 -19
- 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/types.ts +59 -35
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
|
+
}
|