@stream-io/feeds-client 0.1.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/@react-bindings/index.ts +2 -0
- package/CHANGELOG.md +44 -0
- package/LICENSE +219 -0
- package/README.md +9 -0
- package/dist/@react-bindings/hooks/useComments.d.ts +12 -0
- package/dist/@react-bindings/hooks/useStateStore.d.ts +3 -0
- package/dist/@react-bindings/index.d.ts +2 -0
- package/dist/index-react-bindings.browser.cjs +56 -0
- package/dist/index-react-bindings.browser.cjs.map +1 -0
- package/dist/index-react-bindings.browser.js +53 -0
- package/dist/index-react-bindings.browser.js.map +1 -0
- package/dist/index-react-bindings.node.cjs +56 -0
- package/dist/index-react-bindings.node.cjs.map +1 -0
- package/dist/index-react-bindings.node.js +53 -0
- package/dist/index-react-bindings.node.js.map +1 -0
- package/dist/index.browser.cjs +5799 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.js +5782 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.node.cjs +5799 -0
- package/dist/index.node.cjs.map +1 -0
- package/dist/index.node.js +5782 -0
- package/dist/index.node.js.map +1 -0
- package/dist/src/Feed.d.ts +109 -0
- package/dist/src/FeedsClient.d.ts +63 -0
- package/dist/src/ModerationClient.d.ts +3 -0
- package/dist/src/common/ActivitySearchSource.d.ts +17 -0
- package/dist/src/common/ApiClient.d.ts +20 -0
- package/dist/src/common/BaseSearchSource.d.ts +87 -0
- package/dist/src/common/ConnectionIdManager.d.ts +11 -0
- package/dist/src/common/EventDispatcher.d.ts +11 -0
- package/dist/src/common/FeedSearchSource.d.ts +17 -0
- package/dist/src/common/Poll.d.ts +34 -0
- package/dist/src/common/SearchController.d.ts +41 -0
- package/dist/src/common/StateStore.d.ts +124 -0
- package/dist/src/common/TokenManager.d.ts +29 -0
- package/dist/src/common/UserSearchSource.d.ts +17 -0
- package/dist/src/common/gen-imports.d.ts +2 -0
- package/dist/src/common/rate-limit.d.ts +2 -0
- package/dist/src/common/real-time/StableWSConnection.d.ts +144 -0
- package/dist/src/common/real-time/event-models.d.ts +36 -0
- package/dist/src/common/types.d.ts +29 -0
- package/dist/src/common/utils.d.ts +54 -0
- package/dist/src/gen/feeds/FeedApi.d.ts +26 -0
- package/dist/src/gen/feeds/FeedsApi.d.ts +237 -0
- package/dist/src/gen/model-decoders/decoders.d.ts +3 -0
- package/dist/src/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
- package/dist/src/gen/models/index.d.ts +3437 -0
- package/dist/src/gen/moderation/ModerationApi.d.ts +21 -0
- package/dist/src/gen-imports.d.ts +3 -0
- package/dist/src/state-updates/activity-reaction-utils.d.ts +10 -0
- package/dist/src/state-updates/activity-utils.d.ts +13 -0
- package/dist/src/state-updates/bookmark-utils.d.ts +14 -0
- package/dist/src/types-internal.d.ts +4 -0
- package/dist/src/types.d.ts +13 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/index.ts +13 -0
- package/package.json +85 -0
- package/src/Feed.ts +1070 -0
- package/src/FeedsClient.ts +352 -0
- package/src/ModerationClient.ts +3 -0
- package/src/common/ActivitySearchSource.ts +46 -0
- package/src/common/ApiClient.ts +197 -0
- package/src/common/BaseSearchSource.ts +238 -0
- package/src/common/ConnectionIdManager.ts +51 -0
- package/src/common/EventDispatcher.ts +52 -0
- package/src/common/FeedSearchSource.ts +94 -0
- package/src/common/Poll.ts +313 -0
- package/src/common/SearchController.ts +152 -0
- package/src/common/StateStore.ts +314 -0
- package/src/common/TokenManager.ts +112 -0
- package/src/common/UserSearchSource.ts +93 -0
- package/src/common/gen-imports.ts +2 -0
- package/src/common/rate-limit.ts +23 -0
- package/src/common/real-time/StableWSConnection.ts +761 -0
- package/src/common/real-time/event-models.ts +38 -0
- package/src/common/types.ts +40 -0
- package/src/common/utils.ts +194 -0
- package/src/gen/feeds/FeedApi.ts +129 -0
- package/src/gen/feeds/FeedsApi.ts +2192 -0
- package/src/gen/model-decoders/decoders.ts +1877 -0
- package/src/gen/model-decoders/event-decoder-mapping.ts +150 -0
- package/src/gen/models/index.ts +5882 -0
- package/src/gen/moderation/ModerationApi.ts +270 -0
- package/src/gen-imports.ts +3 -0
- package/src/state-updates/activity-reaction-utils.test.ts +348 -0
- package/src/state-updates/activity-reaction-utils.ts +107 -0
- package/src/state-updates/activity-utils.test.ts +257 -0
- package/src/state-updates/activity-utils.ts +80 -0
- package/src/state-updates/bookmark-utils.test.ts +383 -0
- package/src/state-updates/bookmark-utils.ts +157 -0
- package/src/types-internal.ts +5 -0
- package/src/types.ts +20 -0
- package/src/utils.ts +4 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { StateStore } from './StateStore';
|
|
2
|
+
import type { FeedsClient } from '../FeedsClient';
|
|
3
|
+
import type {
|
|
4
|
+
PollVote,
|
|
5
|
+
QueryPollVotesRequest,
|
|
6
|
+
PollUpdatedFeedEvent,
|
|
7
|
+
WSEvent,
|
|
8
|
+
PollClosedFeedEvent,
|
|
9
|
+
PollVoteCastedFeedEvent,
|
|
10
|
+
PollVoteChangedFeedEvent,
|
|
11
|
+
PollVoteRemovedFeedEvent,
|
|
12
|
+
PollResponseData,
|
|
13
|
+
Poll as PollType,
|
|
14
|
+
} from '../gen/models';
|
|
15
|
+
|
|
16
|
+
const isPollUpdatedEvent = (
|
|
17
|
+
e: WSEvent,
|
|
18
|
+
): e is { type: 'feeds.poll.updated' } & PollUpdatedFeedEvent =>
|
|
19
|
+
e.type === 'feeds.poll.updated';
|
|
20
|
+
const isPollClosedEventEvent = (
|
|
21
|
+
e: WSEvent,
|
|
22
|
+
): e is { type: 'feeds.poll.closed' } & PollClosedFeedEvent =>
|
|
23
|
+
e.type === 'feeds.poll.closed';
|
|
24
|
+
const isPollVoteCastedEvent = (
|
|
25
|
+
e: WSEvent,
|
|
26
|
+
): e is { type: 'feeds.poll.vote_casted' } & PollVoteCastedFeedEvent =>
|
|
27
|
+
e.type === 'feeds.poll.vote_casted';
|
|
28
|
+
const isPollVoteChangedEvent = (
|
|
29
|
+
e: WSEvent,
|
|
30
|
+
): e is { type: 'feeds.poll.vote_changed' } & PollVoteChangedFeedEvent =>
|
|
31
|
+
e.type === 'feeds.poll.vote_changed';
|
|
32
|
+
const isPollVoteRemovedEvent = (
|
|
33
|
+
e: WSEvent,
|
|
34
|
+
): e is { type: 'feeds.poll.vote_removed' } & PollVoteRemovedFeedEvent =>
|
|
35
|
+
e.type === 'feeds.poll.vote_removed';
|
|
36
|
+
|
|
37
|
+
export const isVoteAnswer = (vote: PollVote) => !!vote?.answer_text;
|
|
38
|
+
|
|
39
|
+
export type PollAnswersQueryParams = QueryPollVotesRequest & { poll_id: string; user_id?: string };
|
|
40
|
+
|
|
41
|
+
type OptionId = string;
|
|
42
|
+
|
|
43
|
+
export type PollState = Omit<PollType, 'own_votes' | 'id'> & {
|
|
44
|
+
last_activity_at: Date;
|
|
45
|
+
max_voted_option_ids: OptionId[];
|
|
46
|
+
own_votes_by_option_id: Record<OptionId, PollVote>;
|
|
47
|
+
own_answer?: PollVote; // each user can have only one answer
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type PollInitOptions = {
|
|
51
|
+
client: FeedsClient;
|
|
52
|
+
poll: PollType;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export class StreamPoll {
|
|
56
|
+
public readonly state: StateStore<PollState>;
|
|
57
|
+
public id: string;
|
|
58
|
+
private readonly client: FeedsClient;
|
|
59
|
+
|
|
60
|
+
constructor({ client, poll }: PollInitOptions) {
|
|
61
|
+
this.client = client;
|
|
62
|
+
this.id = poll.id;
|
|
63
|
+
|
|
64
|
+
this.state = new StateStore<PollState>(
|
|
65
|
+
this.getInitialStateFromPollResponse(poll),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private readonly getInitialStateFromPollResponse = (poll: PollInitOptions['poll']) => {
|
|
70
|
+
const { own_votes, id, ...pollResponseForState } = poll;
|
|
71
|
+
const { ownAnswer, ownVotes } = own_votes?.reduce<{
|
|
72
|
+
ownVotes: PollVote[];
|
|
73
|
+
ownAnswer?: PollVote;
|
|
74
|
+
}>(
|
|
75
|
+
(acc, voteOrAnswer) => {
|
|
76
|
+
if (isVoteAnswer(voteOrAnswer)) {
|
|
77
|
+
acc.ownAnswer = voteOrAnswer;
|
|
78
|
+
} else {
|
|
79
|
+
acc.ownVotes.push(voteOrAnswer);
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
},
|
|
83
|
+
{ ownVotes: [] },
|
|
84
|
+
) ?? { ownVotes: [] };
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...pollResponseForState,
|
|
88
|
+
last_activity_at: new Date(),
|
|
89
|
+
max_voted_option_ids: getMaxVotedOptionIds(
|
|
90
|
+
pollResponseForState.vote_counts_by_option,
|
|
91
|
+
),
|
|
92
|
+
own_answer: ownAnswer,
|
|
93
|
+
own_votes_by_option_id: getOwnVotesByOptionId(ownVotes),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
public reinitializeState = (poll: PollInitOptions['poll']) => {
|
|
98
|
+
this.state.partialNext(this.getInitialStateFromPollResponse(poll));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
get data(): PollState {
|
|
102
|
+
return this.state.getLatestValue();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public handlePollUpdated = (event: PollUpdatedFeedEvent) => {
|
|
106
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
107
|
+
if (!isPollUpdatedEvent(event as WSEvent)) return;
|
|
108
|
+
const { id, ...pollData } = event.poll;
|
|
109
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
110
|
+
this.state.partialNext({
|
|
111
|
+
...pollData,
|
|
112
|
+
last_activity_at: new Date(event.created_at),
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
public handlePollClosed = (event: PollClosedFeedEvent) => {
|
|
117
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
118
|
+
if (!isPollClosedEventEvent(event as WSEvent)) return;
|
|
119
|
+
this.state.partialNext({
|
|
120
|
+
is_closed: true,
|
|
121
|
+
last_activity_at: new Date(event.created_at),
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
public handleVoteCasted = (event: PollVoteCastedFeedEvent) => {
|
|
126
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
127
|
+
if (!isPollVoteCastedEvent(event as WSEvent)) return;
|
|
128
|
+
const currentState = this.data;
|
|
129
|
+
const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
|
|
130
|
+
let latestAnswers = [...currentState.latest_answers];
|
|
131
|
+
let ownAnswer = currentState.own_answer;
|
|
132
|
+
const ownVotesByOptionId = currentState.own_votes_by_option_id;
|
|
133
|
+
let maxVotedOptionIds = currentState.max_voted_option_ids;
|
|
134
|
+
|
|
135
|
+
if (isOwnVote) {
|
|
136
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
137
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
138
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
139
|
+
ownAnswer = event.poll_vote;
|
|
140
|
+
} else if (event.poll_vote.option_id) {
|
|
141
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
142
|
+
ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
147
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
148
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
149
|
+
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
150
|
+
} else {
|
|
151
|
+
maxVotedOptionIds = getMaxVotedOptionIds(
|
|
152
|
+
event.poll.vote_counts_by_option,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
|
|
157
|
+
this.state.partialNext({
|
|
158
|
+
answers_count,
|
|
159
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
160
|
+
latest_votes_by_option,
|
|
161
|
+
vote_count,
|
|
162
|
+
vote_counts_by_option,
|
|
163
|
+
latest_answers: latestAnswers,
|
|
164
|
+
last_activity_at: new Date(event.created_at),
|
|
165
|
+
own_answer: ownAnswer,
|
|
166
|
+
own_votes_by_option_id: ownVotesByOptionId,
|
|
167
|
+
max_voted_option_ids: maxVotedOptionIds,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
public handleVoteChanged = (event: PollVoteChangedFeedEvent) => {
|
|
172
|
+
// this event is triggered only when event.poll.enforce_unique_vote === true
|
|
173
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
174
|
+
if (!isPollVoteChangedEvent(event as WSEvent)) return;
|
|
175
|
+
const currentState = this.data;
|
|
176
|
+
const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
|
|
177
|
+
let latestAnswers = [...currentState.latest_answers];
|
|
178
|
+
let ownAnswer = currentState.own_answer;
|
|
179
|
+
let ownVotesByOptionId = currentState.own_votes_by_option_id;
|
|
180
|
+
let maxVotedOptionIds = currentState.max_voted_option_ids;
|
|
181
|
+
|
|
182
|
+
if (isOwnVote) {
|
|
183
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
184
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
185
|
+
latestAnswers = [
|
|
186
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
187
|
+
event.poll_vote,
|
|
188
|
+
...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id),
|
|
189
|
+
];
|
|
190
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
191
|
+
ownAnswer = event.poll_vote;
|
|
192
|
+
} else if (event.poll_vote.option_id) {
|
|
193
|
+
if (event.poll.enforce_unique_vote) {
|
|
194
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
195
|
+
ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
|
|
196
|
+
} else {
|
|
197
|
+
ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce<
|
|
198
|
+
Record<OptionId, PollVote>
|
|
199
|
+
>((acc, [optionId, vote]) => {
|
|
200
|
+
if (
|
|
201
|
+
optionId !== event.poll_vote.option_id &&
|
|
202
|
+
vote.id === event.poll_vote.id
|
|
203
|
+
) {
|
|
204
|
+
return acc;
|
|
205
|
+
}
|
|
206
|
+
acc[optionId] = vote;
|
|
207
|
+
return acc;
|
|
208
|
+
}, {});
|
|
209
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
210
|
+
ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (ownAnswer?.id === event.poll_vote.id) {
|
|
214
|
+
ownAnswer = undefined;
|
|
215
|
+
}
|
|
216
|
+
maxVotedOptionIds = getMaxVotedOptionIds(
|
|
217
|
+
event.poll.vote_counts_by_option,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
221
|
+
} else if (isVoteAnswer(event.poll_vote)) {
|
|
222
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
223
|
+
latestAnswers = [event.poll_vote, ...latestAnswers];
|
|
224
|
+
} else {
|
|
225
|
+
maxVotedOptionIds = getMaxVotedOptionIds(
|
|
226
|
+
event.poll.vote_counts_by_option,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
|
|
231
|
+
this.state.partialNext({
|
|
232
|
+
answers_count,
|
|
233
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
234
|
+
latest_votes_by_option,
|
|
235
|
+
vote_count,
|
|
236
|
+
vote_counts_by_option,
|
|
237
|
+
latest_answers: latestAnswers,
|
|
238
|
+
last_activity_at: new Date(event.created_at),
|
|
239
|
+
own_answer: ownAnswer,
|
|
240
|
+
own_votes_by_option_id:ownVotesByOptionId,
|
|
241
|
+
max_voted_option_ids: maxVotedOptionIds,
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
public handleVoteRemoved = (event: PollVoteRemovedFeedEvent) => {
|
|
246
|
+
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
247
|
+
if (!isPollVoteRemovedEvent(event as WSEvent)) return;
|
|
248
|
+
const currentState = this.data;
|
|
249
|
+
const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
|
|
250
|
+
let latestAnswers = [...currentState.latest_answers];
|
|
251
|
+
let ownAnswer = currentState.own_answer;
|
|
252
|
+
const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
|
|
253
|
+
let maxVotedOptionIds = currentState.max_voted_option_ids;
|
|
254
|
+
|
|
255
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
256
|
+
if (isVoteAnswer(event.poll_vote)) {
|
|
257
|
+
latestAnswers = latestAnswers.filter(
|
|
258
|
+
(answer) => answer.id !== event.poll_vote.id,
|
|
259
|
+
);
|
|
260
|
+
if (isOwnVote) {
|
|
261
|
+
ownAnswer = undefined;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
maxVotedOptionIds = getMaxVotedOptionIds(
|
|
265
|
+
event.poll.vote_counts_by_option,
|
|
266
|
+
);
|
|
267
|
+
if (isOwnVote && event.poll_vote.option_id) {
|
|
268
|
+
|
|
269
|
+
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
|
|
274
|
+
this.state.partialNext({
|
|
275
|
+
answers_count,
|
|
276
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
277
|
+
latest_votes_by_option,
|
|
278
|
+
vote_count,
|
|
279
|
+
vote_counts_by_option,
|
|
280
|
+
latest_answers: latestAnswers,
|
|
281
|
+
last_activity_at: new Date(event.created_at),
|
|
282
|
+
own_answer: ownAnswer,
|
|
283
|
+
own_votes_by_option_id: ownVotesByOptionId,
|
|
284
|
+
max_voted_option_ids: maxVotedOptionIds,
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getMaxVotedOptionIds(
|
|
290
|
+
voteCountsByOption: PollResponseData['vote_counts_by_option'],
|
|
291
|
+
) {
|
|
292
|
+
let maxVotes = 0;
|
|
293
|
+
let winningOptions: string[] = [];
|
|
294
|
+
for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
|
|
295
|
+
if (count > maxVotes) {
|
|
296
|
+
winningOptions = [id];
|
|
297
|
+
maxVotes = count;
|
|
298
|
+
} else if (count === maxVotes) {
|
|
299
|
+
winningOptions.push(id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return winningOptions;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getOwnVotesByOptionId(ownVotes: PollVote[]) {
|
|
306
|
+
return !ownVotes
|
|
307
|
+
? ({} satisfies Record<OptionId, PollVote>)
|
|
308
|
+
: ownVotes.reduce<Record<OptionId, PollVote>>((acc, vote) => {
|
|
309
|
+
if (isVoteAnswer(vote) || !vote.option_id) return acc;
|
|
310
|
+
acc[vote.option_id] = vote;
|
|
311
|
+
return acc;
|
|
312
|
+
}, {});
|
|
313
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { StateStore } from './StateStore';
|
|
2
|
+
import type { SearchSource } from './BaseSearchSource';
|
|
3
|
+
|
|
4
|
+
export type SearchControllerState = {
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
searchQuery: string;
|
|
7
|
+
sources: SearchSource[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type InternalSearchControllerState = {};
|
|
11
|
+
|
|
12
|
+
export type SearchControllerConfig = {
|
|
13
|
+
// The controller will make sure there is always exactly one active source. Enabled by default.
|
|
14
|
+
keepSingleActiveSource: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SearchControllerOptions = {
|
|
18
|
+
config?: Partial<SearchControllerConfig>;
|
|
19
|
+
sources?: SearchSource[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class SearchController {
|
|
23
|
+
/**
|
|
24
|
+
* Not intended for direct use by integrators, might be removed without notice resulting in
|
|
25
|
+
* broken integrations.
|
|
26
|
+
*/
|
|
27
|
+
_internalState: StateStore<InternalSearchControllerState>;
|
|
28
|
+
state: StateStore<SearchControllerState>;
|
|
29
|
+
config: SearchControllerConfig;
|
|
30
|
+
|
|
31
|
+
constructor({ config, sources }: SearchControllerOptions = {}) {
|
|
32
|
+
this.state = new StateStore<SearchControllerState>({
|
|
33
|
+
isActive: false,
|
|
34
|
+
searchQuery: '',
|
|
35
|
+
sources: sources ?? [],
|
|
36
|
+
});
|
|
37
|
+
this._internalState = new StateStore<InternalSearchControllerState>({});
|
|
38
|
+
this.config = { keepSingleActiveSource: true, ...config };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get hasNext() {
|
|
42
|
+
return this.sources.some((source) => source.hasNext);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get sources() {
|
|
46
|
+
return this.state.getLatestValue().sources;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get activeSources() {
|
|
50
|
+
return this.state.getLatestValue().sources.filter((s) => s.isActive);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get isActive() {
|
|
54
|
+
return this.state.getLatestValue().isActive;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get searchQuery() {
|
|
58
|
+
return this.state.getLatestValue().searchQuery;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get searchSourceTypes(): Array<SearchSource['type']> {
|
|
62
|
+
return this.sources.map((s) => s.type);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addSource = (source: SearchSource) => {
|
|
66
|
+
this.state.partialNext({
|
|
67
|
+
sources: [...this.sources, source],
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
getSource = (sourceType: SearchSource['type']) =>
|
|
72
|
+
this.sources.find((s) => s.type === sourceType);
|
|
73
|
+
|
|
74
|
+
removeSource = (sourceType: SearchSource['type']) => {
|
|
75
|
+
const newSources = this.sources.filter((s) => s.type !== sourceType);
|
|
76
|
+
if (newSources.length === this.sources.length) return;
|
|
77
|
+
this.state.partialNext({ sources: newSources });
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
activateSource = (sourceType: SearchSource['type']) => {
|
|
81
|
+
const source = this.getSource(sourceType);
|
|
82
|
+
if (!source || source.isActive) return;
|
|
83
|
+
if (this.config.keepSingleActiveSource) {
|
|
84
|
+
this.sources.forEach((s) => {
|
|
85
|
+
if (s.type !== sourceType) {
|
|
86
|
+
s.deactivate();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
source.activate();
|
|
91
|
+
this.state.partialNext({ sources: [...this.sources] });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
deactivateSource = (sourceType: SearchSource['type']) => {
|
|
95
|
+
const source = this.getSource(sourceType);
|
|
96
|
+
if (!source?.isActive) return;
|
|
97
|
+
if (this.activeSources.length === 1) return;
|
|
98
|
+
source.deactivate();
|
|
99
|
+
this.state.partialNext({ sources: [...this.sources] });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
activate = () => {
|
|
103
|
+
if (!this.activeSources.length) {
|
|
104
|
+
const sourcesToActivate = this.config.keepSingleActiveSource
|
|
105
|
+
? this.sources.slice(0, 1)
|
|
106
|
+
: this.sources;
|
|
107
|
+
sourcesToActivate.forEach((s) => s.activate());
|
|
108
|
+
}
|
|
109
|
+
if (this.isActive) return;
|
|
110
|
+
this.state.partialNext({ isActive: true });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
search = async (searchQuery?: string) => {
|
|
114
|
+
const searchedSources = this.activeSources;
|
|
115
|
+
this.state.partialNext({
|
|
116
|
+
searchQuery,
|
|
117
|
+
});
|
|
118
|
+
await Promise.all(
|
|
119
|
+
searchedSources.map((source) => source.search(searchQuery)),
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
cancelSearchQueries = () => {
|
|
124
|
+
this.activeSources.forEach((s) => s.cancelScheduledQuery());
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
clear = () => {
|
|
128
|
+
this.cancelSearchQueries();
|
|
129
|
+
this.sources.forEach((source) =>
|
|
130
|
+
source.state.next({ ...source.initialState, isActive: source.isActive }),
|
|
131
|
+
);
|
|
132
|
+
this.state.next((current) => ({
|
|
133
|
+
...current,
|
|
134
|
+
isActive: true,
|
|
135
|
+
queriesInProgress: [],
|
|
136
|
+
searchQuery: '',
|
|
137
|
+
}));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
exit = () => {
|
|
141
|
+
this.cancelSearchQueries();
|
|
142
|
+
this.sources.forEach((source) =>
|
|
143
|
+
source.state.next({ ...source.initialState, isActive: source.isActive }),
|
|
144
|
+
);
|
|
145
|
+
this.state.next((current) => ({
|
|
146
|
+
...current,
|
|
147
|
+
isActive: false,
|
|
148
|
+
queriesInProgress: [],
|
|
149
|
+
searchQuery: '',
|
|
150
|
+
}));
|
|
151
|
+
};
|
|
152
|
+
}
|