@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
package/src/Feed.ts
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActivityRequest,
|
|
3
|
+
FeedResponse,
|
|
4
|
+
GetOrCreateFeedRequest,
|
|
5
|
+
GetOrCreateFeedResponse,
|
|
6
|
+
QueryFollowsRequest,
|
|
7
|
+
WSEvent,
|
|
8
|
+
ActivityResponse,
|
|
9
|
+
CommentResponse,
|
|
10
|
+
PagerResponse,
|
|
11
|
+
SingleFollowRequest,
|
|
12
|
+
CommentReactionAddedEvent,
|
|
13
|
+
CommentReactionDeletedEvent,
|
|
14
|
+
BookmarkAddedEvent,
|
|
15
|
+
BookmarkDeletedEvent,
|
|
16
|
+
BookmarkUpdatedEvent,
|
|
17
|
+
} from './gen/models';
|
|
18
|
+
import { Patch, StateStore } from './common/StateStore';
|
|
19
|
+
import { EventDispatcher } from './common/EventDispatcher';
|
|
20
|
+
import { FeedApi } from './gen/feeds/FeedApi';
|
|
21
|
+
import { FeedsClient } from './FeedsClient';
|
|
22
|
+
import {
|
|
23
|
+
addActivitiesToState,
|
|
24
|
+
updateActivityInState,
|
|
25
|
+
removeActivityFromState,
|
|
26
|
+
} from './state-updates/activity-utils';
|
|
27
|
+
import {
|
|
28
|
+
addReactionToActivities,
|
|
29
|
+
removeReactionFromActivities,
|
|
30
|
+
} from './state-updates/activity-reaction-utils';
|
|
31
|
+
import {
|
|
32
|
+
addBookmarkToActivities,
|
|
33
|
+
removeBookmarkFromActivities,
|
|
34
|
+
updateBookmarkInActivities,
|
|
35
|
+
} from './state-updates/bookmark-utils';
|
|
36
|
+
import { FeedsApi, StreamResponse } from './gen-imports';
|
|
37
|
+
import { capitalize } from './common/utils';
|
|
38
|
+
import type {
|
|
39
|
+
ActivityIdOrCommentId,
|
|
40
|
+
GetCommentsRepliesRequest,
|
|
41
|
+
GetCommentsRequest,
|
|
42
|
+
LoadingStates,
|
|
43
|
+
PagerResponseWithLoadingStates,
|
|
44
|
+
} from './types';
|
|
45
|
+
import type { FromArray } from './types-internal';
|
|
46
|
+
|
|
47
|
+
export type FeedState = Omit<
|
|
48
|
+
Partial<GetOrCreateFeedResponse & FeedResponse>,
|
|
49
|
+
'feed' | 'duration'
|
|
50
|
+
> & {
|
|
51
|
+
/**
|
|
52
|
+
* True when loading state using `getOrCreate`
|
|
53
|
+
*/
|
|
54
|
+
is_loading: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* True when loading activities using `getOrCreate` or `getNextPage`
|
|
57
|
+
*/
|
|
58
|
+
is_loading_activities: boolean;
|
|
59
|
+
|
|
60
|
+
comments_by_entity_id: Record<
|
|
61
|
+
ActivityIdOrCommentId,
|
|
62
|
+
| {
|
|
63
|
+
pagination?: PagerResponseWithLoadingStates & {
|
|
64
|
+
// registered on first pagination attempt and then used for real-time updates & subsequent pagination calls
|
|
65
|
+
sort?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Id of the "store" where the actual parent is stored in the comments array.
|
|
69
|
+
*
|
|
70
|
+
* Example:
|
|
71
|
+
* ```
|
|
72
|
+
* // top-level comment:
|
|
73
|
+
* const comment1 = {
|
|
74
|
+
* id: 'comment-1',
|
|
75
|
+
* object_id: 'activity-1',
|
|
76
|
+
* }
|
|
77
|
+
* // child comment:
|
|
78
|
+
* const comment2 = {
|
|
79
|
+
* id: 'comment-2',
|
|
80
|
+
* object_id: 'activity-1',
|
|
81
|
+
* parent_id: 'comment-1',
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
* When these comments are loaded, they're stored in the state like this:
|
|
85
|
+
* ```
|
|
86
|
+
* {
|
|
87
|
+
* comments_by_entity_id: {
|
|
88
|
+
* 'activity-1': {
|
|
89
|
+
* comments: [comment1],
|
|
90
|
+
* parent_id: undefined,
|
|
91
|
+
* },
|
|
92
|
+
* 'comment-1': {
|
|
93
|
+
* comments: [comment2],
|
|
94
|
+
* parent_id: 'activity-1', // parent store where "comment-1" is located in "comments" array
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
parent_id?: ActivityIdOrCommentId;
|
|
101
|
+
comments?: CommentResponse[];
|
|
102
|
+
}
|
|
103
|
+
| undefined
|
|
104
|
+
>;
|
|
105
|
+
|
|
106
|
+
followers_pagination?: LoadingStates & { sort?: string };
|
|
107
|
+
|
|
108
|
+
following_pagination?: LoadingStates & { sort?: string };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const END_OF_LIST = 'eol' as const;
|
|
112
|
+
const DEFAULT_COMMENT_PAGINATION = 'first' as const;
|
|
113
|
+
|
|
114
|
+
type EventHandlerByEventType = {
|
|
115
|
+
[Key in NonNullable<WSEvent['type']>]: Key extends Extract<
|
|
116
|
+
WSEvent,
|
|
117
|
+
{ type: Key }
|
|
118
|
+
>['type']
|
|
119
|
+
? ((event: Extract<WSEvent, { type: Key }>) => void) | undefined
|
|
120
|
+
: never;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export class Feed extends FeedApi {
|
|
124
|
+
readonly state: StateStore<FeedState>;
|
|
125
|
+
private static readonly noop = () => {};
|
|
126
|
+
|
|
127
|
+
private readonly eventHandlers: EventHandlerByEventType = {
|
|
128
|
+
'feeds.activity.added': (event) => {
|
|
129
|
+
const currentActivities = this.currentState.activities;
|
|
130
|
+
const result = addActivitiesToState(
|
|
131
|
+
[event.activity],
|
|
132
|
+
currentActivities,
|
|
133
|
+
'start',
|
|
134
|
+
);
|
|
135
|
+
if (result.changed) {
|
|
136
|
+
this.client.hydratePollCache([event.activity]);
|
|
137
|
+
this.state.partialNext({ activities: result.activities });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
'feeds.activity.deleted': (event) => {
|
|
141
|
+
const currentActivities = this.currentState.activities;
|
|
142
|
+
if (currentActivities) {
|
|
143
|
+
const result = removeActivityFromState(
|
|
144
|
+
event.activity,
|
|
145
|
+
currentActivities,
|
|
146
|
+
);
|
|
147
|
+
if (result.changed) {
|
|
148
|
+
this.state.partialNext({ activities: result.activities });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
'feeds.activity.reaction.added': (event) => {
|
|
153
|
+
const currentActivities = this.currentState.activities;
|
|
154
|
+
const connectedUser = this.client.state.getLatestValue().connectedUser;
|
|
155
|
+
const isCurrentUser = Boolean(
|
|
156
|
+
connectedUser && event.reaction.user.id === connectedUser.id,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const result = addReactionToActivities(
|
|
160
|
+
event,
|
|
161
|
+
currentActivities,
|
|
162
|
+
isCurrentUser,
|
|
163
|
+
);
|
|
164
|
+
if (result.changed) {
|
|
165
|
+
this.state.partialNext({ activities: result.activities });
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
'feeds.activity.reaction.deleted': (event) => {
|
|
169
|
+
const currentActivities = this.currentState.activities;
|
|
170
|
+
const connectedUser = this.client.state.getLatestValue().connectedUser;
|
|
171
|
+
const isCurrentUser = Boolean(
|
|
172
|
+
connectedUser && event.reaction.user.id === connectedUser.id,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result = removeReactionFromActivities(
|
|
176
|
+
event,
|
|
177
|
+
currentActivities,
|
|
178
|
+
isCurrentUser,
|
|
179
|
+
);
|
|
180
|
+
if (result.changed) {
|
|
181
|
+
this.state.partialNext({ activities: result.activities });
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
'feeds.activity.reaction.updated': Feed.noop,
|
|
185
|
+
'feeds.activity.removed_from_feed': (event) => {
|
|
186
|
+
const currentActivities = this.currentState.activities;
|
|
187
|
+
if (currentActivities) {
|
|
188
|
+
const result = removeActivityFromState(
|
|
189
|
+
event.activity,
|
|
190
|
+
currentActivities,
|
|
191
|
+
);
|
|
192
|
+
if (result.changed) {
|
|
193
|
+
this.state.partialNext({ activities: result.activities });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
'feeds.activity.updated': (event) => {
|
|
198
|
+
const currentActivities = this.currentState.activities;
|
|
199
|
+
if (currentActivities) {
|
|
200
|
+
const result = updateActivityInState(event.activity, currentActivities);
|
|
201
|
+
if (result.changed) {
|
|
202
|
+
this.client.hydratePollCache([event.activity]);
|
|
203
|
+
this.state.partialNext({ activities: result.activities });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
'feeds.bookmark.added': this.handleBookmarkAdded.bind(this),
|
|
208
|
+
'feeds.bookmark.deleted': this.handleBookmarkDeleted.bind(this),
|
|
209
|
+
'feeds.bookmark.updated': this.handleBookmarkUpdated.bind(this),
|
|
210
|
+
'feeds.comment.added': (event) => {
|
|
211
|
+
const { comment } = event;
|
|
212
|
+
const forId = comment.parent_id ?? comment.object_id;
|
|
213
|
+
|
|
214
|
+
this.state.next((currentState) => {
|
|
215
|
+
const entityState = currentState.comments_by_entity_id[forId];
|
|
216
|
+
const newComments = entityState?.comments?.concat([]) ?? [];
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
entityState?.pagination?.sort === 'last' &&
|
|
220
|
+
entityState?.pagination.next === END_OF_LIST
|
|
221
|
+
) {
|
|
222
|
+
newComments.unshift(comment);
|
|
223
|
+
} else if (entityState?.pagination?.sort === 'first') {
|
|
224
|
+
newComments.push(comment);
|
|
225
|
+
} else {
|
|
226
|
+
// no other sorting option is supported yet
|
|
227
|
+
return currentState;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...currentState,
|
|
232
|
+
comments_by_entity_id: {
|
|
233
|
+
...currentState.comments_by_entity_id,
|
|
234
|
+
[forId]: {
|
|
235
|
+
...currentState.comments_by_entity_id[forId],
|
|
236
|
+
comments: newComments,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
'feeds.comment.deleted': ({ comment }) => {
|
|
243
|
+
const forId = comment.parent_id ?? comment.object_id;
|
|
244
|
+
|
|
245
|
+
this.state.next((currentState) => {
|
|
246
|
+
const newCommentsByEntityId = {
|
|
247
|
+
...currentState.comments_by_entity_id,
|
|
248
|
+
[forId]: {
|
|
249
|
+
...currentState.comments_by_entity_id[forId],
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const index = this.getCommentIndex(comment, currentState);
|
|
254
|
+
|
|
255
|
+
if (newCommentsByEntityId?.[forId]?.comments?.length && index !== -1) {
|
|
256
|
+
newCommentsByEntityId[forId].comments = [
|
|
257
|
+
...newCommentsByEntityId[forId].comments,
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
newCommentsByEntityId[forId]?.comments?.splice(index, 1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
delete newCommentsByEntityId[comment.id];
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...currentState,
|
|
267
|
+
comments_by_entity_id: newCommentsByEntityId,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
'feeds.comment.updated': (event) => {
|
|
272
|
+
const { comment } = event;
|
|
273
|
+
const forId = comment.parent_id ?? comment.object_id;
|
|
274
|
+
|
|
275
|
+
this.state.next((currentState) => {
|
|
276
|
+
const entityState = currentState.comments_by_entity_id[forId];
|
|
277
|
+
|
|
278
|
+
if (!entityState?.comments?.length) return currentState;
|
|
279
|
+
|
|
280
|
+
const index = this.getCommentIndex(comment, currentState);
|
|
281
|
+
|
|
282
|
+
if (index === -1) return currentState;
|
|
283
|
+
|
|
284
|
+
const newComments = [...entityState.comments];
|
|
285
|
+
|
|
286
|
+
newComments[index] = comment;
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
...currentState,
|
|
290
|
+
comments_by_entity_id: {
|
|
291
|
+
...currentState.comments_by_entity_id,
|
|
292
|
+
[forId]: {
|
|
293
|
+
...currentState.comments_by_entity_id[forId],
|
|
294
|
+
comments: newComments,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
'feeds.feed.created': Feed.noop,
|
|
301
|
+
'feeds.feed.deleted': Feed.noop,
|
|
302
|
+
'feeds.feed.updated': (event) => {
|
|
303
|
+
this.state.partialNext({ ...event.feed });
|
|
304
|
+
},
|
|
305
|
+
'feeds.feed_group.changed': Feed.noop,
|
|
306
|
+
'feeds.feed_group.deleted': Feed.noop,
|
|
307
|
+
'feeds.follow.created': (event) => {
|
|
308
|
+
// filter non-accepted follows (the way getOrCreate does by default)
|
|
309
|
+
if (event.follow.status !== 'accepted') return;
|
|
310
|
+
|
|
311
|
+
// this feed followed someone
|
|
312
|
+
if (event.follow.source_feed.fid === this.fid) {
|
|
313
|
+
if (this.currentState.following_pagination?.next === END_OF_LIST) {
|
|
314
|
+
this.state.next((currentState) => ({
|
|
315
|
+
...currentState,
|
|
316
|
+
...event.follow.source_feed,
|
|
317
|
+
// TODO: respect sort
|
|
318
|
+
following: currentState.following
|
|
319
|
+
? currentState.following.concat(event.follow)
|
|
320
|
+
: [event.follow],
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
} else if (
|
|
324
|
+
// someone followed this feed
|
|
325
|
+
event.follow.target_feed.fid === this.fid
|
|
326
|
+
) {
|
|
327
|
+
const source = event.follow.source_feed;
|
|
328
|
+
const connectedUser = this.client.state.getLatestValue().connectedUser;
|
|
329
|
+
|
|
330
|
+
this.state.next((currentState) => {
|
|
331
|
+
const newState = { ...currentState, ...event.follow.target_feed };
|
|
332
|
+
|
|
333
|
+
if (source.created_by.id === connectedUser?.id) {
|
|
334
|
+
newState.own_follows = newState.own_follows
|
|
335
|
+
? newState.own_follows.concat(event.follow)
|
|
336
|
+
: [event.follow];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (currentState.followers_pagination?.next === END_OF_LIST) {
|
|
340
|
+
// TODO: respect sort
|
|
341
|
+
newState.followers = newState.followers
|
|
342
|
+
? newState.followers.concat(event.follow)
|
|
343
|
+
: [event.follow];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return newState;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
'feeds.follow.deleted': (event) => {
|
|
351
|
+
// this feed unfollowed someone
|
|
352
|
+
if (event.follow.source_feed.fid === this.fid) {
|
|
353
|
+
this.state.next((currentState) => {
|
|
354
|
+
return {
|
|
355
|
+
...currentState,
|
|
356
|
+
...event.follow.source_feed,
|
|
357
|
+
following: currentState.following?.filter(
|
|
358
|
+
(follow) =>
|
|
359
|
+
follow.target_feed.fid !== event.follow.target_feed.fid,
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
} else if (
|
|
364
|
+
// someone unfollowed this feed
|
|
365
|
+
event.follow.target_feed.fid === this.fid
|
|
366
|
+
) {
|
|
367
|
+
const source = event.follow.source_feed;
|
|
368
|
+
const connectedUser = this.client.state.getLatestValue().connectedUser;
|
|
369
|
+
|
|
370
|
+
this.state.next((currentState) => {
|
|
371
|
+
const newState = { ...currentState, ...event.follow.target_feed };
|
|
372
|
+
|
|
373
|
+
if (source.created_by.id === connectedUser?.id) {
|
|
374
|
+
newState.own_follows = newState.own_follows?.filter(
|
|
375
|
+
(follow) =>
|
|
376
|
+
follow.source_feed.fid !== event.follow.source_feed.fid,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
newState.followers = newState.followers?.filter(
|
|
381
|
+
(follow) => follow.source_feed.fid !== event.follow.source_feed.fid,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
return newState;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
'feeds.follow.updated': Feed.noop,
|
|
389
|
+
'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
|
|
390
|
+
'feeds.comment.reaction.deleted':
|
|
391
|
+
this.handleCommentReactionEvent.bind(this),
|
|
392
|
+
'feeds.comment.reaction.updated': Feed.noop,
|
|
393
|
+
'feeds.feed_member.added': (event) => {
|
|
394
|
+
const { member } = event;
|
|
395
|
+
|
|
396
|
+
// do not add a member if the pagination has reached the end of the list
|
|
397
|
+
if (this.currentState.member_pagination?.next !== END_OF_LIST) return;
|
|
398
|
+
|
|
399
|
+
this.state.next((currentState) => {
|
|
400
|
+
return {
|
|
401
|
+
...currentState,
|
|
402
|
+
// TODO: respect sort
|
|
403
|
+
members: currentState.members
|
|
404
|
+
? currentState.members.concat(member)
|
|
405
|
+
: [member],
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
'feeds.feed_member.removed': (event) => {
|
|
410
|
+
this.state.next((currentState) => {
|
|
411
|
+
return {
|
|
412
|
+
...currentState,
|
|
413
|
+
members: currentState.members?.filter(
|
|
414
|
+
(member) => member.user.id !== event.user?.id,
|
|
415
|
+
),
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
'feeds.feed_member.updated': (event) => {
|
|
420
|
+
this.state.next((currentState) => {
|
|
421
|
+
const memberIndex =
|
|
422
|
+
currentState.members?.findIndex(
|
|
423
|
+
(member) => member.user.id === event.member.user.id,
|
|
424
|
+
) ?? -1;
|
|
425
|
+
|
|
426
|
+
if (memberIndex !== -1) {
|
|
427
|
+
const newMembers = [...currentState.members!];
|
|
428
|
+
newMembers[memberIndex] = event.member;
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
...currentState,
|
|
432
|
+
members: newMembers,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return currentState;
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
// the poll events should be removed from here
|
|
440
|
+
'feeds.poll.closed': Feed.noop,
|
|
441
|
+
'feeds.poll.deleted': Feed.noop,
|
|
442
|
+
'feeds.poll.updated': Feed.noop,
|
|
443
|
+
'feeds.poll.vote_casted': Feed.noop,
|
|
444
|
+
'feeds.poll.vote_changed': Feed.noop,
|
|
445
|
+
'feeds.poll.vote_removed': Feed.noop,
|
|
446
|
+
'feeds.activity.pinned': Feed.noop,
|
|
447
|
+
'feeds.activity.unpinned': Feed.noop,
|
|
448
|
+
'feeds.activity.marked': Feed.noop,
|
|
449
|
+
'moderation.custom_action': Feed.noop,
|
|
450
|
+
'moderation.flagged': Feed.noop,
|
|
451
|
+
'moderation.mark_reviewed': Feed.noop,
|
|
452
|
+
'health.check': Feed.noop,
|
|
453
|
+
'app.updated': Feed.noop,
|
|
454
|
+
'user.banned': Feed.noop,
|
|
455
|
+
'user.deactivated': Feed.noop,
|
|
456
|
+
'user.muted': Feed.noop,
|
|
457
|
+
'user.reactivated': Feed.noop,
|
|
458
|
+
'user.updated': Feed.noop,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
protected eventDispatcher: EventDispatcher<WSEvent['type'], WSEvent> =
|
|
462
|
+
new EventDispatcher<WSEvent['type'], WSEvent>();
|
|
463
|
+
|
|
464
|
+
constructor(
|
|
465
|
+
client: FeedsClient,
|
|
466
|
+
groupId: 'user' | 'timeline' | (string & {}),
|
|
467
|
+
id: string,
|
|
468
|
+
data?: FeedResponse,
|
|
469
|
+
) {
|
|
470
|
+
// Need this ugly cast because fileUpload endpoints :(
|
|
471
|
+
super(client as unknown as FeedsApi, groupId, id);
|
|
472
|
+
this.state = new StateStore<FeedState>({
|
|
473
|
+
fid: `${groupId}:${id}`,
|
|
474
|
+
group_id: groupId,
|
|
475
|
+
id,
|
|
476
|
+
...(data ?? {}),
|
|
477
|
+
is_loading: false,
|
|
478
|
+
is_loading_activities: false,
|
|
479
|
+
comments_by_entity_id: {},
|
|
480
|
+
});
|
|
481
|
+
this.client = client;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private readonly client: FeedsClient;
|
|
485
|
+
|
|
486
|
+
get fid() {
|
|
487
|
+
return `${this.group}:${this.id}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
get currentState() {
|
|
491
|
+
return this.state.getLatestValue();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private handleCommentReactionEvent(
|
|
495
|
+
event: (CommentReactionAddedEvent | CommentReactionDeletedEvent) & {
|
|
496
|
+
type: 'feeds.comment.reaction.added' | 'feeds.comment.reaction.deleted';
|
|
497
|
+
},
|
|
498
|
+
) {
|
|
499
|
+
const { comment, reaction } = event;
|
|
500
|
+
const connectedUser = this.client.state.getLatestValue().connectedUser;
|
|
501
|
+
|
|
502
|
+
this.state.next((currentState) => {
|
|
503
|
+
const forId = comment.parent_id ?? comment.object_id;
|
|
504
|
+
const entityState = currentState.comments_by_entity_id[forId];
|
|
505
|
+
|
|
506
|
+
const commentIndex = this.getCommentIndex(comment, currentState);
|
|
507
|
+
|
|
508
|
+
if (commentIndex === -1) return currentState;
|
|
509
|
+
|
|
510
|
+
const newComments = entityState?.comments?.concat([]) ?? [];
|
|
511
|
+
|
|
512
|
+
const commentCopy: Partial<CommentResponse> = { ...comment };
|
|
513
|
+
|
|
514
|
+
delete commentCopy.own_reactions;
|
|
515
|
+
|
|
516
|
+
const newComment: CommentResponse = {
|
|
517
|
+
...newComments[commentIndex],
|
|
518
|
+
...commentCopy,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
newComments[commentIndex] = newComment;
|
|
522
|
+
|
|
523
|
+
if (reaction.user.id === connectedUser?.id) {
|
|
524
|
+
if (event.type === 'feeds.comment.reaction.added') {
|
|
525
|
+
newComment.own_reactions = newComment.own_reactions.concat(
|
|
526
|
+
reaction,
|
|
527
|
+
) ?? [reaction];
|
|
528
|
+
} else if (event.type === 'feeds.comment.reaction.deleted') {
|
|
529
|
+
newComment.own_reactions = newComment.own_reactions.filter(
|
|
530
|
+
(r) => r.type !== reaction.type,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
...currentState,
|
|
537
|
+
comments_by_entity_id: {
|
|
538
|
+
...currentState.comments_by_entity_id,
|
|
539
|
+
[forId]: {
|
|
540
|
+
...entityState,
|
|
541
|
+
comments: newComments,
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async getOrCreate(request?: GetOrCreateFeedRequest) {
|
|
549
|
+
if (this.currentState.is_loading_activities) {
|
|
550
|
+
throw new Error('Only one getOrCreate call is allowed at a time');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.state.partialNext({
|
|
554
|
+
is_loading: !request?.next,
|
|
555
|
+
is_loading_activities: true,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// TODO: pull comments/comment_pagination from activities and comment_sort from request
|
|
559
|
+
// and pre-populate comments_by_entity_id (once comment_sort and comment_limit are supported)
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const response = await super.getOrCreate(request);
|
|
563
|
+
if (request?.next) {
|
|
564
|
+
const { activities: currentActivities = [] } = this.currentState;
|
|
565
|
+
|
|
566
|
+
const result = addActivitiesToState(
|
|
567
|
+
response.activities,
|
|
568
|
+
currentActivities,
|
|
569
|
+
'end',
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (result.changed) {
|
|
573
|
+
this.state.partialNext({
|
|
574
|
+
activities: result.activities,
|
|
575
|
+
next: response.next,
|
|
576
|
+
prev: response.prev,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
const responseCopy: Partial<
|
|
581
|
+
StreamResponse<GetOrCreateFeedResponse>['feed'] &
|
|
582
|
+
StreamResponse<GetOrCreateFeedResponse>
|
|
583
|
+
> = {
|
|
584
|
+
...response,
|
|
585
|
+
...response.feed,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
delete responseCopy.feed;
|
|
589
|
+
delete responseCopy.metadata;
|
|
590
|
+
delete responseCopy.duration;
|
|
591
|
+
|
|
592
|
+
this.state.next((currentState) => {
|
|
593
|
+
const nextState: FeedState = {
|
|
594
|
+
...currentState,
|
|
595
|
+
...responseCopy,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// if there is no next cursor, set it to END_OF_LIST
|
|
599
|
+
// request has to have a limit set for this to work
|
|
600
|
+
if (
|
|
601
|
+
(request?.followers_pagination?.limit ?? 0) > 0 &&
|
|
602
|
+
typeof nextState.followers_pagination?.next === 'undefined'
|
|
603
|
+
) {
|
|
604
|
+
nextState.followers_pagination = {
|
|
605
|
+
...nextState.followers_pagination,
|
|
606
|
+
next: END_OF_LIST,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (
|
|
611
|
+
(request?.following_pagination?.limit ?? 0) > 0 &&
|
|
612
|
+
typeof nextState.following_pagination?.next === 'undefined'
|
|
613
|
+
) {
|
|
614
|
+
nextState.following_pagination = {
|
|
615
|
+
...nextState.following_pagination,
|
|
616
|
+
next: END_OF_LIST,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (
|
|
621
|
+
(request?.member_pagination?.limit ?? 0) > 0 &&
|
|
622
|
+
typeof nextState.member_pagination?.next === 'undefined'
|
|
623
|
+
) {
|
|
624
|
+
nextState.member_pagination = {
|
|
625
|
+
...nextState.member_pagination,
|
|
626
|
+
next: END_OF_LIST,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!request?.followers_pagination?.limit) {
|
|
631
|
+
delete nextState.followers;
|
|
632
|
+
}
|
|
633
|
+
if (!request?.following_pagination?.limit) {
|
|
634
|
+
delete nextState.following;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return nextState;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
this.client.hydratePollCache(response.activities);
|
|
642
|
+
|
|
643
|
+
return response;
|
|
644
|
+
} finally {
|
|
645
|
+
this.state.partialNext({
|
|
646
|
+
is_loading: false,
|
|
647
|
+
is_loading_activities: false,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private handleBookmarkAdded(event: BookmarkAddedEvent) {
|
|
653
|
+
const currentActivities = this.currentState.activities;
|
|
654
|
+
const { connectedUser } = this.client.state.getLatestValue();
|
|
655
|
+
const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
|
|
656
|
+
|
|
657
|
+
const result = addBookmarkToActivities(
|
|
658
|
+
event,
|
|
659
|
+
currentActivities,
|
|
660
|
+
isCurrentUser,
|
|
661
|
+
);
|
|
662
|
+
if (result.changed) {
|
|
663
|
+
this.state.partialNext({ activities: result.activities });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private handleBookmarkDeleted(event: BookmarkDeletedEvent) {
|
|
668
|
+
const currentActivities = this.currentState.activities;
|
|
669
|
+
const { connectedUser } = this.client.state.getLatestValue();
|
|
670
|
+
const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
|
|
671
|
+
|
|
672
|
+
const result = removeBookmarkFromActivities(
|
|
673
|
+
event,
|
|
674
|
+
currentActivities,
|
|
675
|
+
isCurrentUser,
|
|
676
|
+
);
|
|
677
|
+
if (result.changed) {
|
|
678
|
+
this.state.partialNext({ activities: result.activities });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private handleBookmarkUpdated(event: BookmarkUpdatedEvent) {
|
|
683
|
+
const currentActivities = this.currentState.activities;
|
|
684
|
+
const { connectedUser } = this.client.state.getLatestValue();
|
|
685
|
+
const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
|
|
686
|
+
|
|
687
|
+
const result = updateBookmarkInActivities(
|
|
688
|
+
event,
|
|
689
|
+
currentActivities,
|
|
690
|
+
isCurrentUser,
|
|
691
|
+
);
|
|
692
|
+
if (result.changed) {
|
|
693
|
+
this.state.partialNext({ activities: result.activities });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Returns index of the provided comment object.
|
|
699
|
+
*/
|
|
700
|
+
private getCommentIndex(
|
|
701
|
+
comment: Pick<CommentResponse, 'object_id' | 'parent_id' | 'id'>,
|
|
702
|
+
state?: FeedState,
|
|
703
|
+
) {
|
|
704
|
+
const { comments_by_entity_id = {} } = state ?? this.currentState;
|
|
705
|
+
|
|
706
|
+
const currentComments =
|
|
707
|
+
comments_by_entity_id[comment.parent_id ?? comment.object_id]?.comments;
|
|
708
|
+
|
|
709
|
+
if (!currentComments?.length) {
|
|
710
|
+
return -1;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// @ts-expect-error this will just fail if the comment is not object from state
|
|
714
|
+
let commentIndex = currentComments.indexOf(comment);
|
|
715
|
+
|
|
716
|
+
// fast lookup failed, try slower approach
|
|
717
|
+
if (commentIndex === -1) {
|
|
718
|
+
commentIndex = currentComments.findIndex(
|
|
719
|
+
(comment_) => comment_.id === comment.id,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return commentIndex;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private getActivityIndex(activity: ActivityResponse, state?: FeedState) {
|
|
727
|
+
const { activities } = state ?? this.currentState;
|
|
728
|
+
|
|
729
|
+
if (!activities) {
|
|
730
|
+
return -1;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
let activityIndex = activities.indexOf(activity);
|
|
734
|
+
|
|
735
|
+
// fast lookup failed, try slower approach
|
|
736
|
+
if (activityIndex === -1) {
|
|
737
|
+
activityIndex = activities.findIndex(
|
|
738
|
+
(activity_) => activity_.id === activity.id,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return activityIndex;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private updateActivityInState(
|
|
746
|
+
activity: ActivityResponse,
|
|
747
|
+
patch: Patch<FromArray<FeedState['activities']>>,
|
|
748
|
+
) {
|
|
749
|
+
this.state.next((currentState) => {
|
|
750
|
+
const activityIndex = this.getActivityIndex(activity, currentState);
|
|
751
|
+
|
|
752
|
+
if (activityIndex === -1) return currentState;
|
|
753
|
+
|
|
754
|
+
const nextActivities = [...currentState.activities!];
|
|
755
|
+
|
|
756
|
+
nextActivities[activityIndex] = patch(
|
|
757
|
+
currentState.activities![activityIndex],
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
...currentState,
|
|
762
|
+
activities: nextActivities,
|
|
763
|
+
};
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private async loadNextPageComments({
|
|
768
|
+
forId,
|
|
769
|
+
base,
|
|
770
|
+
sort,
|
|
771
|
+
parentId,
|
|
772
|
+
}: {
|
|
773
|
+
parentId?: string;
|
|
774
|
+
forId: string;
|
|
775
|
+
sort: string;
|
|
776
|
+
base: () => Promise<PagerResponse & { comments: CommentResponse[] }>;
|
|
777
|
+
}) {
|
|
778
|
+
try {
|
|
779
|
+
this.state.next((currentState) => ({
|
|
780
|
+
...currentState,
|
|
781
|
+
comments_by_entity_id: {
|
|
782
|
+
...currentState.comments_by_entity_id,
|
|
783
|
+
[forId]: {
|
|
784
|
+
...currentState.comments_by_entity_id[forId],
|
|
785
|
+
pagination: {
|
|
786
|
+
...currentState.comments_by_entity_id[forId]?.pagination,
|
|
787
|
+
loading_next_page: true,
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
}));
|
|
792
|
+
|
|
793
|
+
const { next: newNextCursor = END_OF_LIST, comments } = await base();
|
|
794
|
+
|
|
795
|
+
this.state.next((currentState) => {
|
|
796
|
+
const newPagination = {
|
|
797
|
+
...currentState.comments_by_entity_id[forId]?.pagination,
|
|
798
|
+
next: newNextCursor,
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
if (typeof newPagination.sort === 'undefined') {
|
|
802
|
+
newPagination.sort = sort;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
...currentState,
|
|
807
|
+
comments_by_entity_id: {
|
|
808
|
+
...currentState.comments_by_entity_id,
|
|
809
|
+
[forId]: {
|
|
810
|
+
...currentState.comments_by_entity_id[forId],
|
|
811
|
+
parent_id: parentId,
|
|
812
|
+
pagination: newPagination,
|
|
813
|
+
comments: currentState.comments_by_entity_id[forId]?.comments
|
|
814
|
+
? currentState.comments_by_entity_id[forId].comments?.concat(
|
|
815
|
+
comments,
|
|
816
|
+
)
|
|
817
|
+
: comments,
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
} catch (error) {
|
|
823
|
+
console.error(error);
|
|
824
|
+
// TODO: figure out how to handle errorss
|
|
825
|
+
} finally {
|
|
826
|
+
this.state.next((currentState) => ({
|
|
827
|
+
...currentState,
|
|
828
|
+
comments_by_entity_id: {
|
|
829
|
+
...currentState.comments_by_entity_id,
|
|
830
|
+
[forId]: {
|
|
831
|
+
...currentState.comments_by_entity_id[forId],
|
|
832
|
+
pagination: {
|
|
833
|
+
...currentState.comments_by_entity_id[forId]?.pagination,
|
|
834
|
+
loading_next_page: false,
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
}));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
public async loadNextPageActivityComments(
|
|
843
|
+
activity: ActivityResponse,
|
|
844
|
+
request?: Partial<
|
|
845
|
+
Omit<GetCommentsRequest, 'object_id' | 'object_type' | 'next'>
|
|
846
|
+
>,
|
|
847
|
+
) {
|
|
848
|
+
const pagination =
|
|
849
|
+
this.currentState.comments_by_entity_id[activity.id]?.pagination;
|
|
850
|
+
const currentNextCursor = pagination?.next;
|
|
851
|
+
const currentSort = pagination?.sort;
|
|
852
|
+
const isLoading = pagination?.loading_next_page;
|
|
853
|
+
|
|
854
|
+
const sort = currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION;
|
|
855
|
+
|
|
856
|
+
if (isLoading || currentNextCursor === END_OF_LIST) return;
|
|
857
|
+
|
|
858
|
+
await this.loadNextPageComments({
|
|
859
|
+
forId: activity.id,
|
|
860
|
+
base: () =>
|
|
861
|
+
this.client.getComments({
|
|
862
|
+
...request,
|
|
863
|
+
sort,
|
|
864
|
+
object_id: activity.id,
|
|
865
|
+
object_type: 'activity',
|
|
866
|
+
next: currentNextCursor,
|
|
867
|
+
}),
|
|
868
|
+
sort,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
public async loadNextPageCommentReplies(
|
|
873
|
+
comment: CommentResponse,
|
|
874
|
+
request?: Partial<Omit<GetCommentsRepliesRequest, 'comment_id' | 'next'>>,
|
|
875
|
+
) {
|
|
876
|
+
const pagination =
|
|
877
|
+
this.currentState.comments_by_entity_id[comment.id]?.pagination;
|
|
878
|
+
const currentNextCursor = pagination?.next;
|
|
879
|
+
const currentSort = pagination?.sort;
|
|
880
|
+
const isLoading = pagination?.loading_next_page;
|
|
881
|
+
|
|
882
|
+
const sort = currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION;
|
|
883
|
+
|
|
884
|
+
if (isLoading || currentNextCursor === END_OF_LIST) return;
|
|
885
|
+
|
|
886
|
+
await this.loadNextPageComments({
|
|
887
|
+
forId: comment.id,
|
|
888
|
+
base: () =>
|
|
889
|
+
this.client.getCommentReplies({
|
|
890
|
+
...request,
|
|
891
|
+
comment_id: comment.id,
|
|
892
|
+
// use known sort first (prevents broken pagination)
|
|
893
|
+
sort: currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION,
|
|
894
|
+
next: currentNextCursor,
|
|
895
|
+
}),
|
|
896
|
+
parentId: comment.parent_id ?? comment.object_id,
|
|
897
|
+
sort,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private async loadNextPageFollows(
|
|
902
|
+
type: 'followers' | 'following',
|
|
903
|
+
request: Pick<QueryFollowsRequest, 'limit'>,
|
|
904
|
+
) {
|
|
905
|
+
const paginationKey = `${type}_pagination` as const;
|
|
906
|
+
const method = `query${capitalize(type)}` as const;
|
|
907
|
+
|
|
908
|
+
const currentNextCursor = this.currentState[paginationKey]?.next;
|
|
909
|
+
const isLoading = this.currentState[paginationKey]?.loading_next_page;
|
|
910
|
+
|
|
911
|
+
if (isLoading || currentNextCursor === END_OF_LIST) return;
|
|
912
|
+
|
|
913
|
+
try {
|
|
914
|
+
this.state.next((currentState) => {
|
|
915
|
+
return {
|
|
916
|
+
...currentState,
|
|
917
|
+
[paginationKey]: {
|
|
918
|
+
...currentState[paginationKey],
|
|
919
|
+
loading_next_page: true,
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const { next: newNextCursor = END_OF_LIST, follows } = await this[method](
|
|
925
|
+
{
|
|
926
|
+
...request,
|
|
927
|
+
next: currentNextCursor,
|
|
928
|
+
},
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
this.state.next((currentState) => ({
|
|
932
|
+
...currentState,
|
|
933
|
+
[type]: currentState[type]
|
|
934
|
+
? currentState[type].concat(follows)
|
|
935
|
+
: follows,
|
|
936
|
+
[paginationKey]: {
|
|
937
|
+
...currentState[paginationKey],
|
|
938
|
+
next: newNextCursor,
|
|
939
|
+
},
|
|
940
|
+
}));
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.error(error);
|
|
943
|
+
// TODO: figure out how to handle errorss
|
|
944
|
+
} finally {
|
|
945
|
+
this.state.next((currentState) => {
|
|
946
|
+
return {
|
|
947
|
+
...currentState,
|
|
948
|
+
[paginationKey]: {
|
|
949
|
+
...currentState[paginationKey],
|
|
950
|
+
loading_next_page: false,
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async loadNextPageFollowers(request: Pick<QueryFollowsRequest, 'limit'>) {
|
|
958
|
+
await this.loadNextPageFollows('followers', request);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async loadNextPageFollowing(request: Pick<QueryFollowsRequest, 'limit'>) {
|
|
962
|
+
await this.loadNextPageFollows('following', request);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Method which queries followers of this feed (feeds which target this feed).
|
|
967
|
+
*
|
|
968
|
+
* _Note: Useful only for feeds with `groupId` of `user` value._
|
|
969
|
+
*/
|
|
970
|
+
async queryFollowers(request: Omit<QueryFollowsRequest, 'filter'>) {
|
|
971
|
+
const filter: QueryFollowsRequest['filter'] = {
|
|
972
|
+
target_feed: this.fid,
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const response = await this.client.queryFollows({
|
|
976
|
+
filter,
|
|
977
|
+
...request,
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return response;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Method which queries following of this feed (target feeds of this feed).
|
|
985
|
+
*
|
|
986
|
+
* _Note: Useful only for feeds with `groupId` of `timeline` value._
|
|
987
|
+
*/
|
|
988
|
+
async queryFollowing(request: Omit<QueryFollowsRequest, 'filter'>) {
|
|
989
|
+
const filter: QueryFollowsRequest['filter'] = {
|
|
990
|
+
source_feed: this.fid,
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
const response = await this.client.queryFollows({
|
|
994
|
+
filter,
|
|
995
|
+
...request,
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
return response;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async follow(
|
|
1002
|
+
feedOrFid: Feed | string,
|
|
1003
|
+
options?: Omit<SingleFollowRequest, 'source' | 'target'>,
|
|
1004
|
+
) {
|
|
1005
|
+
const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
|
|
1006
|
+
|
|
1007
|
+
const response = await this.client.follow({
|
|
1008
|
+
...options,
|
|
1009
|
+
source: this.fid,
|
|
1010
|
+
target: fid,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
return response;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async unfollow(feedOrFid: Feed | string) {
|
|
1017
|
+
const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
|
|
1018
|
+
|
|
1019
|
+
const response = await this.client.unfollow({
|
|
1020
|
+
source: this.fid,
|
|
1021
|
+
target: fid,
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
return response;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async getNextPage() {
|
|
1028
|
+
const currentState = this.currentState;
|
|
1029
|
+
const response = await this.getOrCreate({
|
|
1030
|
+
member_pagination: {
|
|
1031
|
+
limit: 0,
|
|
1032
|
+
},
|
|
1033
|
+
followers_pagination: {
|
|
1034
|
+
limit: 0,
|
|
1035
|
+
},
|
|
1036
|
+
following_pagination: {
|
|
1037
|
+
limit: 0,
|
|
1038
|
+
},
|
|
1039
|
+
next: currentState.next,
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
return response;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
addActivity(request: Omit<ActivityRequest, 'fids'>) {
|
|
1046
|
+
return this.feedsApi.addActivity({
|
|
1047
|
+
...request,
|
|
1048
|
+
fids: [this.fid],
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
on = this.eventDispatcher.on;
|
|
1053
|
+
off = this.eventDispatcher.off;
|
|
1054
|
+
|
|
1055
|
+
handleWSEvent(event: WSEvent) {
|
|
1056
|
+
const eventHandler = this.eventHandlers[event.type];
|
|
1057
|
+
|
|
1058
|
+
// no need to run noop function
|
|
1059
|
+
if (eventHandler !== Feed.noop) {
|
|
1060
|
+
// @ts-expect-error intersection of handler arguments results to never
|
|
1061
|
+
eventHandler?.(event);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (typeof eventHandler === 'undefined') {
|
|
1065
|
+
console.warn(`Received unknown event type: ${event.type}`, event);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
this.eventDispatcher.dispatch(event);
|
|
1069
|
+
}
|
|
1070
|
+
}
|