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/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
+ }