@stream-io/feeds-client 0.1.5 → 0.1.6
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/hooks/feed-state-hooks/index.ts +2 -0
- package/CHANGELOG.md +15 -0
- package/dist/@react-bindings/hooks/client-state-hooks/useWsConnectionState.d.ts +1 -1
- package/dist/@react-bindings/hooks/feed-state-hooks/index.d.ts +2 -0
- package/dist/@react-bindings/hooks/feed-state-hooks/useComments.d.ts +2 -2
- package/dist/@react-bindings/hooks/feed-state-hooks/useFeedActivities.d.ts +2 -2
- package/dist/@react-bindings/hooks/feed-state-hooks/useFeedMetadata.d.ts +12 -0
- package/dist/@react-bindings/hooks/feed-state-hooks/useFollowers.d.ts +2 -2
- package/dist/@react-bindings/hooks/feed-state-hooks/useFollowing.d.ts +2 -2
- package/dist/@react-bindings/hooks/feed-state-hooks/useOwnCapabilities.d.ts +30 -30
- package/dist/@react-bindings/hooks/feed-state-hooks/useOwnFollows.d.ts +8 -0
- package/dist/index-react-bindings.browser.cjs +128 -85
- package/dist/index-react-bindings.browser.cjs.map +1 -1
- package/dist/index-react-bindings.browser.js +127 -86
- package/dist/index-react-bindings.browser.js.map +1 -1
- package/dist/index-react-bindings.node.cjs +128 -85
- package/dist/index-react-bindings.node.cjs.map +1 -1
- package/dist/index-react-bindings.node.js +127 -86
- package/dist/index-react-bindings.node.js.map +1 -1
- package/dist/index.browser.cjs +48 -37
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +48 -37
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +48 -37
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +48 -37
- package/dist/index.node.js.map +1 -1
- package/dist/src/FeedsClient.d.ts +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -3
- package/src/Feed.ts +17 -11
- package/src/FeedsClient.ts +15 -10
- package/src/common/ActivitySearchSource.ts +5 -5
- package/src/common/ApiClient.ts +1 -1
- package/src/common/FeedSearchSource.ts +1 -1
- package/src/common/Poll.ts +35 -10
- package/src/common/TokenManager.ts +2 -3
- package/src/common/UserSearchSource.ts +1 -1
- package/src/state-updates/bookmark-utils.test.ts +134 -8
- package/src/state-updates/bookmark-utils.ts +17 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/feeds-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"packageManager": "yarn@3.2.4",
|
|
5
5
|
"main": "./dist/index.node.js",
|
|
6
6
|
"exports": {
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
"start": "rollup -w -c",
|
|
45
45
|
"build": "yarn clean && rollup -c",
|
|
46
46
|
"test": "vitest",
|
|
47
|
-
"test
|
|
47
|
+
"test:unit": "vitest --exclude '__integration-tests__/**'",
|
|
48
|
+
"test-ci": "vitest --exclude '__integration-tests__/docs-snippets/**' --coverage",
|
|
49
|
+
"test-docs-snippets": "vitest __integration-tests__/docs-snippets/**"
|
|
48
50
|
},
|
|
49
51
|
"files": [
|
|
50
52
|
"dist",
|
|
@@ -61,8 +63,9 @@
|
|
|
61
63
|
"devDependencies": {
|
|
62
64
|
"@rollup/plugin-replace": "^6.0.1",
|
|
63
65
|
"@rollup/plugin-typescript": "^12.1.0",
|
|
64
|
-
"@stream-io/node-sdk": "
|
|
66
|
+
"@stream-io/node-sdk": "0.5.0",
|
|
65
67
|
"@types/react": "^19.1.8",
|
|
68
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
66
69
|
"dotenv": "^16.4.5",
|
|
67
70
|
"react": "19.0.0",
|
|
68
71
|
"rimraf": "^6.0.1",
|
package/src/Feed.ts
CHANGED
|
@@ -155,7 +155,7 @@ export class Feed extends FeedApi {
|
|
|
155
155
|
},
|
|
156
156
|
'feeds.activity.reaction.added': (event) => {
|
|
157
157
|
const currentActivities = this.currentState.activities;
|
|
158
|
-
const connectedUser = this.client.state.getLatestValue().
|
|
158
|
+
const connectedUser = this.client.state.getLatestValue().connected_user;
|
|
159
159
|
const isCurrentUser = Boolean(
|
|
160
160
|
connectedUser && event.reaction.user.id === connectedUser.id,
|
|
161
161
|
);
|
|
@@ -171,7 +171,7 @@ export class Feed extends FeedApi {
|
|
|
171
171
|
},
|
|
172
172
|
'feeds.activity.reaction.deleted': (event) => {
|
|
173
173
|
const currentActivities = this.currentState.activities;
|
|
174
|
-
const connectedUser = this.client.state.getLatestValue().
|
|
174
|
+
const connectedUser = this.client.state.getLatestValue().connected_user;
|
|
175
175
|
const isCurrentUser = Boolean(
|
|
176
176
|
connectedUser && event.reaction.user.id === connectedUser.id,
|
|
177
177
|
);
|
|
@@ -344,7 +344,7 @@ export class Feed extends FeedApi {
|
|
|
344
344
|
event.follow.target_feed.fid === this.fid
|
|
345
345
|
) {
|
|
346
346
|
const source = event.follow.source_feed;
|
|
347
|
-
const connectedUser = this.client.state.getLatestValue().
|
|
347
|
+
const connectedUser = this.client.state.getLatestValue().connected_user;
|
|
348
348
|
|
|
349
349
|
this.state.next((currentState) => {
|
|
350
350
|
const newState = { ...currentState, ...event.follow.target_feed };
|
|
@@ -389,7 +389,7 @@ export class Feed extends FeedApi {
|
|
|
389
389
|
event.follow.target_feed.fid === this.fid
|
|
390
390
|
) {
|
|
391
391
|
const source = event.follow.source_feed;
|
|
392
|
-
const connectedUser = this.client.state.getLatestValue().
|
|
392
|
+
const connectedUser = this.client.state.getLatestValue().connected_user;
|
|
393
393
|
|
|
394
394
|
this.state.next((currentState) => {
|
|
395
395
|
const newState = { ...currentState, ...event.follow.target_feed };
|
|
@@ -415,7 +415,8 @@ export class Feed extends FeedApi {
|
|
|
415
415
|
this.handleCommentReactionEvent.bind(this),
|
|
416
416
|
'feeds.comment.reaction.updated': Feed.noop,
|
|
417
417
|
'feeds.feed_member.added': (event) => {
|
|
418
|
-
const { connectedUser } =
|
|
418
|
+
const { connected_user: connectedUser } =
|
|
419
|
+
this.client.state.getLatestValue();
|
|
419
420
|
|
|
420
421
|
this.state.next((currentState) => {
|
|
421
422
|
let newState: FeedState | undefined;
|
|
@@ -447,7 +448,8 @@ export class Feed extends FeedApi {
|
|
|
447
448
|
});
|
|
448
449
|
},
|
|
449
450
|
'feeds.feed_member.removed': (event) => {
|
|
450
|
-
const { connectedUser } =
|
|
451
|
+
const { connected_user: connectedUser } =
|
|
452
|
+
this.client.state.getLatestValue();
|
|
451
453
|
|
|
452
454
|
this.state.next((currentState) => {
|
|
453
455
|
const newState = {
|
|
@@ -465,7 +467,8 @@ export class Feed extends FeedApi {
|
|
|
465
467
|
});
|
|
466
468
|
},
|
|
467
469
|
'feeds.feed_member.updated': (event) => {
|
|
468
|
-
const { connectedUser } =
|
|
470
|
+
const { connected_user: connectedUser } =
|
|
471
|
+
this.client.state.getLatestValue();
|
|
469
472
|
|
|
470
473
|
this.state.next((currentState) => {
|
|
471
474
|
const memberIndex =
|
|
@@ -559,7 +562,7 @@ export class Feed extends FeedApi {
|
|
|
559
562
|
},
|
|
560
563
|
) {
|
|
561
564
|
const { comment, reaction } = event;
|
|
562
|
-
const connectedUser = this.client.state.getLatestValue().
|
|
565
|
+
const connectedUser = this.client.state.getLatestValue().connected_user;
|
|
563
566
|
|
|
564
567
|
this.state.next((currentState) => {
|
|
565
568
|
const forId = comment.parent_id ?? comment.object_id;
|
|
@@ -693,7 +696,8 @@ export class Feed extends FeedApi {
|
|
|
693
696
|
|
|
694
697
|
private handleBookmarkAdded(event: BookmarkAddedEvent) {
|
|
695
698
|
const currentActivities = this.currentState.activities;
|
|
696
|
-
const { connectedUser } =
|
|
699
|
+
const { connected_user: connectedUser } =
|
|
700
|
+
this.client.state.getLatestValue();
|
|
697
701
|
const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
|
|
698
702
|
|
|
699
703
|
const result = addBookmarkToActivities(
|
|
@@ -708,7 +712,8 @@ export class Feed extends FeedApi {
|
|
|
708
712
|
|
|
709
713
|
private handleBookmarkDeleted(event: BookmarkDeletedEvent) {
|
|
710
714
|
const currentActivities = this.currentState.activities;
|
|
711
|
-
const { connectedUser } =
|
|
715
|
+
const { connected_user: connectedUser } =
|
|
716
|
+
this.client.state.getLatestValue();
|
|
712
717
|
const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
|
|
713
718
|
|
|
714
719
|
const result = removeBookmarkFromActivities(
|
|
@@ -723,7 +728,8 @@ export class Feed extends FeedApi {
|
|
|
723
728
|
|
|
724
729
|
private handleBookmarkUpdated(event: BookmarkUpdatedEvent) {
|
|
725
730
|
const currentActivities = this.currentState.activities;
|
|
726
|
-
const { connectedUser } =
|
|
731
|
+
const { connected_user: connectedUser } =
|
|
732
|
+
this.client.state.getLatestValue();
|
|
727
733
|
const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
|
|
728
734
|
|
|
729
735
|
const result = updateBookmarkInActivities(
|
package/src/FeedsClient.ts
CHANGED
|
@@ -35,8 +35,8 @@ import { ModerationClient } from './ModerationClient';
|
|
|
35
35
|
import { StreamPoll } from './common/Poll';
|
|
36
36
|
|
|
37
37
|
export type FeedsClientState = {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
connected_user: OwnUser | undefined;
|
|
39
|
+
is_ws_connection_healthy: boolean;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
type FID = string;
|
|
@@ -70,8 +70,8 @@ export class FeedsClient extends FeedsApi {
|
|
|
70
70
|
);
|
|
71
71
|
super(apiClient);
|
|
72
72
|
this.state = new StateStore<FeedsClientState>({
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
connected_user: undefined,
|
|
74
|
+
is_ws_connection_healthy: false,
|
|
75
75
|
});
|
|
76
76
|
this.moderation = new ModerationClient(apiClient);
|
|
77
77
|
this.tokenManager = tokenManager;
|
|
@@ -86,7 +86,7 @@ export class FeedsClient extends FeedsApi {
|
|
|
86
86
|
switch (event.type) {
|
|
87
87
|
case 'connection.changed': {
|
|
88
88
|
const { online } = event;
|
|
89
|
-
this.state.partialNext({
|
|
89
|
+
this.state.partialNext({ is_ws_connection_healthy: online });
|
|
90
90
|
|
|
91
91
|
if (online) {
|
|
92
92
|
this.healthyConnectionChangedEventCount++;
|
|
@@ -211,7 +211,7 @@ export class FeedsClient extends FeedsApi {
|
|
|
211
211
|
|
|
212
212
|
connectUser = async (user: UserRequest, tokenProvider: TokenOrProvider) => {
|
|
213
213
|
if (
|
|
214
|
-
this.state.getLatestValue().
|
|
214
|
+
this.state.getLatestValue().connected_user !== undefined ||
|
|
215
215
|
this.wsConnection
|
|
216
216
|
) {
|
|
217
217
|
throw new Error(`Can't connect a new user, call "disconnectUser" first`);
|
|
@@ -235,8 +235,8 @@ export class FeedsClient extends FeedsApi {
|
|
|
235
235
|
);
|
|
236
236
|
const connectedEvent = await this.wsConnection.connect();
|
|
237
237
|
this.state.partialNext({
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
connected_user: connectedEvent?.me,
|
|
239
|
+
is_ws_connection_healthy: !!this.wsConnection?.isHealthy,
|
|
240
240
|
});
|
|
241
241
|
} catch (err) {
|
|
242
242
|
await this.disconnectUser();
|
|
@@ -260,7 +260,9 @@ export class FeedsClient extends FeedsApi {
|
|
|
260
260
|
};
|
|
261
261
|
|
|
262
262
|
// @ts-expect-error API spec says file should be a string
|
|
263
|
-
uploadFile = (
|
|
263
|
+
uploadFile = (
|
|
264
|
+
request: Omit<FileUploadRequest, 'file'> & { file: StreamFile },
|
|
265
|
+
) => {
|
|
264
266
|
return super.uploadFile({
|
|
265
267
|
// @ts-expect-error API spec says file should be a string
|
|
266
268
|
file: request.file,
|
|
@@ -316,7 +318,10 @@ export class FeedsClient extends FeedsApi {
|
|
|
316
318
|
|
|
317
319
|
this.connectionIdManager.reset();
|
|
318
320
|
this.tokenManager.reset();
|
|
319
|
-
this.state.partialNext({
|
|
321
|
+
this.state.partialNext({
|
|
322
|
+
connected_user: undefined,
|
|
323
|
+
is_ws_connection_healthy: false,
|
|
324
|
+
});
|
|
320
325
|
};
|
|
321
326
|
|
|
322
327
|
on = this.eventDispatcher.on;
|
|
@@ -20,7 +20,7 @@ export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
protected async query(searchQuery: string) {
|
|
23
|
-
const { connectedUser } = this.client.state.getLatestValue();
|
|
23
|
+
const { connected_user: connectedUser } = this.client.state.getLatestValue();
|
|
24
24
|
if (!connectedUser) return { items: [] };
|
|
25
25
|
|
|
26
26
|
const { activities: items, next } = await this.client.queryActivities({
|
|
@@ -39,8 +39,8 @@ export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
// filter: {
|
|
43
|
-
// 'feed.name': { $autocomplete: searchQuery }
|
|
44
|
-
// 'feed.description': { $autocomplete: searchQuery }
|
|
45
|
-
// 'created_by.name': { $autocomplete: searchQuery }
|
|
42
|
+
// filter: {
|
|
43
|
+
// 'feed.name': { $autocomplete: searchQuery }
|
|
44
|
+
// 'feed.description': { $autocomplete: searchQuery }
|
|
45
|
+
// 'created_by.name': { $autocomplete: searchQuery }
|
|
46
46
|
// },
|
package/src/common/ApiClient.ts
CHANGED
|
@@ -21,7 +21,7 @@ export class ApiClient {
|
|
|
21
21
|
private readonly connectionIdManager: ConnectionIdManager,
|
|
22
22
|
options?: FeedsClientOptions,
|
|
23
23
|
) {
|
|
24
|
-
this.baseUrl = options?.base_url ?? 'https://
|
|
24
|
+
this.baseUrl = options?.base_url ?? 'https://feeds.stream-io-api.com';
|
|
25
25
|
this.timeout = options?.timeout ?? 3000;
|
|
26
26
|
this.axiosInstance = axios.create({
|
|
27
27
|
baseURL: this.baseUrl,
|
|
@@ -20,7 +20,7 @@ export class FeedSearchSource extends BaseSearchSource<Feed> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
protected async query(searchQuery: string) {
|
|
23
|
-
const { connectedUser } = this.client.state.getLatestValue();
|
|
23
|
+
const { connected_user: connectedUser } = this.client.state.getLatestValue();
|
|
24
24
|
if (!connectedUser) return { items: [] };
|
|
25
25
|
|
|
26
26
|
// const channelFilters: ChannelFilters = {
|
package/src/common/Poll.ts
CHANGED
|
@@ -36,7 +36,10 @@ const isPollVoteRemovedEvent = (
|
|
|
36
36
|
|
|
37
37
|
export const isVoteAnswer = (vote: PollVote) => !!vote?.answer_text;
|
|
38
38
|
|
|
39
|
-
export type PollAnswersQueryParams = QueryPollVotesRequest & {
|
|
39
|
+
export type PollAnswersQueryParams = QueryPollVotesRequest & {
|
|
40
|
+
poll_id: string;
|
|
41
|
+
user_id?: string;
|
|
42
|
+
};
|
|
40
43
|
|
|
41
44
|
type OptionId = string;
|
|
42
45
|
|
|
@@ -66,7 +69,9 @@ export class StreamPoll {
|
|
|
66
69
|
);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
private readonly getInitialStateFromPollResponse = (
|
|
72
|
+
private readonly getInitialStateFromPollResponse = (
|
|
73
|
+
poll: PollInitOptions['poll'],
|
|
74
|
+
) => {
|
|
70
75
|
const { own_votes, id, ...pollResponseForState } = poll;
|
|
71
76
|
const { ownAnswer, ownVotes } = own_votes?.reduce<{
|
|
72
77
|
ownVotes: PollVote[];
|
|
@@ -126,7 +131,9 @@ export class StreamPoll {
|
|
|
126
131
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
127
132
|
if (!isPollVoteCastedEvent(event as WSEvent)) return;
|
|
128
133
|
const currentState = this.data;
|
|
129
|
-
const isOwnVote =
|
|
134
|
+
const isOwnVote =
|
|
135
|
+
event.poll_vote.user_id ===
|
|
136
|
+
this.client.state.getLatestValue().connected_user?.id;
|
|
130
137
|
let latestAnswers = [...currentState.latest_answers];
|
|
131
138
|
let ownAnswer = currentState.own_answer;
|
|
132
139
|
const ownVotesByOptionId = currentState.own_votes_by_option_id;
|
|
@@ -153,7 +160,12 @@ export class StreamPoll {
|
|
|
153
160
|
);
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
const {
|
|
163
|
+
const {
|
|
164
|
+
answers_count,
|
|
165
|
+
latest_votes_by_option,
|
|
166
|
+
vote_count,
|
|
167
|
+
vote_counts_by_option,
|
|
168
|
+
} = event.poll;
|
|
157
169
|
this.state.partialNext({
|
|
158
170
|
answers_count,
|
|
159
171
|
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
@@ -173,7 +185,9 @@ export class StreamPoll {
|
|
|
173
185
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
174
186
|
if (!isPollVoteChangedEvent(event as WSEvent)) return;
|
|
175
187
|
const currentState = this.data;
|
|
176
|
-
const isOwnVote =
|
|
188
|
+
const isOwnVote =
|
|
189
|
+
event.poll_vote.user_id ===
|
|
190
|
+
this.client.state.getLatestValue().connected_user?.id;
|
|
177
191
|
let latestAnswers = [...currentState.latest_answers];
|
|
178
192
|
let ownAnswer = currentState.own_answer;
|
|
179
193
|
let ownVotesByOptionId = currentState.own_votes_by_option_id;
|
|
@@ -227,7 +241,12 @@ export class StreamPoll {
|
|
|
227
241
|
);
|
|
228
242
|
}
|
|
229
243
|
|
|
230
|
-
const {
|
|
244
|
+
const {
|
|
245
|
+
answers_count,
|
|
246
|
+
latest_votes_by_option,
|
|
247
|
+
vote_count,
|
|
248
|
+
vote_counts_by_option,
|
|
249
|
+
} = event.poll;
|
|
231
250
|
this.state.partialNext({
|
|
232
251
|
answers_count,
|
|
233
252
|
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
@@ -237,7 +256,7 @@ export class StreamPoll {
|
|
|
237
256
|
latest_answers: latestAnswers,
|
|
238
257
|
last_activity_at: new Date(event.created_at),
|
|
239
258
|
own_answer: ownAnswer,
|
|
240
|
-
own_votes_by_option_id:ownVotesByOptionId,
|
|
259
|
+
own_votes_by_option_id: ownVotesByOptionId,
|
|
241
260
|
max_voted_option_ids: maxVotedOptionIds,
|
|
242
261
|
});
|
|
243
262
|
};
|
|
@@ -246,7 +265,9 @@ export class StreamPoll {
|
|
|
246
265
|
if (event.poll?.id && event.poll.id !== this.id) return;
|
|
247
266
|
if (!isPollVoteRemovedEvent(event as WSEvent)) return;
|
|
248
267
|
const currentState = this.data;
|
|
249
|
-
const isOwnVote =
|
|
268
|
+
const isOwnVote =
|
|
269
|
+
event.poll_vote.user_id ===
|
|
270
|
+
this.client.state.getLatestValue().connected_user?.id;
|
|
250
271
|
let latestAnswers = [...currentState.latest_answers];
|
|
251
272
|
let ownAnswer = currentState.own_answer;
|
|
252
273
|
const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
|
|
@@ -265,12 +286,16 @@ export class StreamPoll {
|
|
|
265
286
|
event.poll.vote_counts_by_option,
|
|
266
287
|
);
|
|
267
288
|
if (isOwnVote && event.poll_vote.option_id) {
|
|
268
|
-
|
|
269
289
|
delete ownVotesByOptionId[event.poll_vote.option_id];
|
|
270
290
|
}
|
|
271
291
|
}
|
|
272
292
|
|
|
273
|
-
const {
|
|
293
|
+
const {
|
|
294
|
+
answers_count,
|
|
295
|
+
latest_votes_by_option,
|
|
296
|
+
vote_count,
|
|
297
|
+
vote_counts_by_option,
|
|
298
|
+
} = event.poll;
|
|
274
299
|
this.state.partialNext({
|
|
275
300
|
answers_count,
|
|
276
301
|
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFunction,
|
|
1
|
+
import { isFunction, sleep } from './utils';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* TokenManager
|
|
@@ -67,12 +67,11 @@ export class TokenManager {
|
|
|
67
67
|
this.token = await tokenProvider();
|
|
68
68
|
} catch (e) {
|
|
69
69
|
const numberOfFailures = ++previousFailuresCount;
|
|
70
|
-
await sleep(
|
|
70
|
+
await sleep(1000);
|
|
71
71
|
if (numberOfFailures === 3) {
|
|
72
72
|
this.loadTokenPromise = null;
|
|
73
73
|
return reject(
|
|
74
74
|
new Error(
|
|
75
|
-
|
|
76
75
|
`Stream error: tried to get token ${numberOfFailures} times, but it failed with ${e}. Check your token provider`,
|
|
77
76
|
{ cause: e },
|
|
78
77
|
),
|
|
@@ -20,7 +20,7 @@ export class UserSearchSource extends BaseSearchSource<UserResponse> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
protected async query(searchQuery: string) {
|
|
23
|
-
const { connectedUser } = this.client.state.getLatestValue();
|
|
23
|
+
const { connected_user: connectedUser } = this.client.state.getLatestValue();
|
|
24
24
|
if (!connectedUser) return { items: [] };
|
|
25
25
|
|
|
26
26
|
// const channelFilters: ChannelFilters = {
|
|
@@ -37,6 +37,7 @@ const createMockActivity = (id: string): ActivityResponse => ({
|
|
|
37
37
|
visibility: 'public',
|
|
38
38
|
bookmark_count: 0,
|
|
39
39
|
comment_count: 0,
|
|
40
|
+
reaction_count: 0,
|
|
40
41
|
share_count: 0,
|
|
41
42
|
attachments: [],
|
|
42
43
|
comments: [],
|
|
@@ -181,6 +182,71 @@ describe('bookmark-utils', () => {
|
|
|
181
182
|
expect(result.own_bookmarks).toHaveLength(1);
|
|
182
183
|
expect(result.own_bookmarks[0]).toEqual(bookmark2);
|
|
183
184
|
});
|
|
185
|
+
|
|
186
|
+
it('should correctly identify bookmarks by activity_id + folder_id + user_id', () => {
|
|
187
|
+
const activity = createMockActivity('activity1');
|
|
188
|
+
const user = createMockUser('user1');
|
|
189
|
+
|
|
190
|
+
// Create two bookmarks with same activity and user but different folders
|
|
191
|
+
const bookmark1 = {
|
|
192
|
+
...createMockBookmark(user, activity),
|
|
193
|
+
folder: {
|
|
194
|
+
id: 'folder1',
|
|
195
|
+
name: 'Folder 1',
|
|
196
|
+
created_at: new Date(),
|
|
197
|
+
updated_at: new Date(),
|
|
198
|
+
custom: {},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
const bookmark2 = {
|
|
202
|
+
...createMockBookmark(user, activity),
|
|
203
|
+
folder: {
|
|
204
|
+
id: 'folder2',
|
|
205
|
+
name: 'Folder 2',
|
|
206
|
+
created_at: new Date(),
|
|
207
|
+
updated_at: new Date(),
|
|
208
|
+
custom: {},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
activity.own_bookmarks = [bookmark1, bookmark2];
|
|
213
|
+
|
|
214
|
+
// Try to remove bookmark1
|
|
215
|
+
const event = createMockDeletedEvent(bookmark1);
|
|
216
|
+
const result = removeBookmarkFromActivity(event, activity, true);
|
|
217
|
+
|
|
218
|
+
expect(result.changed).toBe(true);
|
|
219
|
+
expect(result.own_bookmarks).toHaveLength(1);
|
|
220
|
+
expect(result.own_bookmarks[0]).toEqual(bookmark2);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should handle bookmarks without folders correctly', () => {
|
|
224
|
+
const activity = createMockActivity('activity1');
|
|
225
|
+
const user = createMockUser('user1');
|
|
226
|
+
|
|
227
|
+
// Create two bookmarks: one with folder, one without
|
|
228
|
+
const bookmarkWithFolder = {
|
|
229
|
+
...createMockBookmark(user, activity),
|
|
230
|
+
folder: {
|
|
231
|
+
id: 'folder1',
|
|
232
|
+
name: 'Folder 1',
|
|
233
|
+
created_at: new Date(),
|
|
234
|
+
updated_at: new Date(),
|
|
235
|
+
custom: {},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const bookmarkWithoutFolder = createMockBookmark(user, activity);
|
|
239
|
+
|
|
240
|
+
activity.own_bookmarks = [bookmarkWithFolder, bookmarkWithoutFolder];
|
|
241
|
+
|
|
242
|
+
// Try to remove bookmark without folder
|
|
243
|
+
const event = createMockDeletedEvent(bookmarkWithoutFolder);
|
|
244
|
+
const result = removeBookmarkFromActivity(event, activity, true);
|
|
245
|
+
|
|
246
|
+
expect(result.changed).toBe(true);
|
|
247
|
+
expect(result.own_bookmarks).toHaveLength(1);
|
|
248
|
+
expect(result.own_bookmarks[0]).toEqual(bookmarkWithFolder);
|
|
249
|
+
});
|
|
184
250
|
});
|
|
185
251
|
|
|
186
252
|
describe('updateBookmarkInActivity', () => {
|
|
@@ -220,20 +286,80 @@ describe('bookmark-utils', () => {
|
|
|
220
286
|
expect(result.own_bookmarks[0]).toEqual(bookmark); // unchanged
|
|
221
287
|
});
|
|
222
288
|
|
|
223
|
-
|
|
289
|
+
// Test for the bug: updating bookmarks with same activity_id but different folder_id
|
|
290
|
+
it('should correctly update bookmark by activity_id + folder_id + user_id', () => {
|
|
224
291
|
const activity = createMockActivity('activity1');
|
|
225
292
|
const user = createMockUser('user1');
|
|
226
|
-
const bookmark = createMockBookmark(user, activity);
|
|
227
|
-
activity.own_bookmarks = [bookmark];
|
|
228
293
|
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
294
|
+
// Create two bookmarks with same activity and user but different folders
|
|
295
|
+
const bookmark1 = {
|
|
296
|
+
...createMockBookmark(user, activity),
|
|
297
|
+
folder: {
|
|
298
|
+
id: 'folder1',
|
|
299
|
+
name: 'Folder 1',
|
|
300
|
+
created_at: new Date(),
|
|
301
|
+
updated_at: new Date(),
|
|
302
|
+
custom: {},
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
const bookmark2 = {
|
|
306
|
+
...createMockBookmark(user, activity),
|
|
307
|
+
folder: {
|
|
308
|
+
id: 'folder2',
|
|
309
|
+
name: 'Folder 2',
|
|
310
|
+
created_at: new Date(),
|
|
311
|
+
updated_at: new Date(),
|
|
312
|
+
custom: {},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
activity.own_bookmarks = [bookmark1, bookmark2];
|
|
317
|
+
|
|
318
|
+
// Update bookmark1
|
|
319
|
+
const updatedBookmark1 = {
|
|
320
|
+
...bookmark1,
|
|
321
|
+
custom: { updated: true },
|
|
322
|
+
};
|
|
323
|
+
const event = createMockUpdatedEvent(updatedBookmark1);
|
|
232
324
|
const result = updateBookmarkInActivity(event, activity, true);
|
|
233
325
|
|
|
234
326
|
expect(result.changed).toBe(true);
|
|
235
|
-
expect(result.own_bookmarks).toHaveLength(
|
|
236
|
-
expect(result.own_bookmarks
|
|
327
|
+
expect(result.own_bookmarks).toHaveLength(2);
|
|
328
|
+
expect(result.own_bookmarks).toContain(updatedBookmark1);
|
|
329
|
+
expect(result.own_bookmarks).toContain(bookmark2);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should handle updating bookmarks without folders correctly', () => {
|
|
333
|
+
const activity = createMockActivity('activity1');
|
|
334
|
+
const user = createMockUser('user1');
|
|
335
|
+
|
|
336
|
+
// Create two bookmarks: one with folder, one without
|
|
337
|
+
const bookmarkWithFolder = {
|
|
338
|
+
...createMockBookmark(user, activity),
|
|
339
|
+
folder: {
|
|
340
|
+
id: 'folder1',
|
|
341
|
+
name: 'Folder 1',
|
|
342
|
+
created_at: new Date(),
|
|
343
|
+
updated_at: new Date(),
|
|
344
|
+
custom: {},
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
const bookmarkWithoutFolder = createMockBookmark(user, activity);
|
|
348
|
+
|
|
349
|
+
activity.own_bookmarks = [bookmarkWithFolder, bookmarkWithoutFolder];
|
|
350
|
+
|
|
351
|
+
// Update bookmark without folder
|
|
352
|
+
const updatedBookmarkWithoutFolder = {
|
|
353
|
+
...bookmarkWithoutFolder,
|
|
354
|
+
custom: { updated: true },
|
|
355
|
+
};
|
|
356
|
+
const event = createMockUpdatedEvent(updatedBookmarkWithoutFolder);
|
|
357
|
+
const result = updateBookmarkInActivity(event, activity, true);
|
|
358
|
+
|
|
359
|
+
expect(result.changed).toBe(true);
|
|
360
|
+
expect(result.own_bookmarks).toHaveLength(2);
|
|
361
|
+
expect(result.own_bookmarks).toContain(bookmarkWithFolder);
|
|
362
|
+
expect(result.own_bookmarks).toContain(updatedBookmarkWithoutFolder);
|
|
237
363
|
});
|
|
238
364
|
});
|
|
239
365
|
|
|
@@ -3,9 +3,23 @@ import {
|
|
|
3
3
|
BookmarkDeletedEvent,
|
|
4
4
|
BookmarkUpdatedEvent,
|
|
5
5
|
ActivityResponse,
|
|
6
|
+
BookmarkResponse,
|
|
6
7
|
} from '../gen/models';
|
|
7
8
|
import { UpdateStateResult } from '../types-internal';
|
|
8
9
|
|
|
10
|
+
// Helper function to check if two bookmarks are the same
|
|
11
|
+
// A bookmark is identified by activity_id + folder_id + user_id
|
|
12
|
+
const isSameBookmark = (
|
|
13
|
+
bookmark1: BookmarkResponse,
|
|
14
|
+
bookmark2: BookmarkResponse,
|
|
15
|
+
): boolean => {
|
|
16
|
+
return (
|
|
17
|
+
bookmark1.user.id === bookmark2.user.id &&
|
|
18
|
+
bookmark1.activity.id === bookmark2.activity.id &&
|
|
19
|
+
bookmark1.folder?.id === bookmark2.folder?.id
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
9
23
|
const updateActivityInActivities = (
|
|
10
24
|
updatedActivity: ActivityResponse,
|
|
11
25
|
activities: ActivityResponse[],
|
|
@@ -46,9 +60,7 @@ export const removeBookmarkFromActivity = (
|
|
|
46
60
|
// Update own_bookmarks if the bookmark is from the current user
|
|
47
61
|
const ownBookmarks = isCurrentUser
|
|
48
62
|
? (activity.own_bookmarks || []).filter(
|
|
49
|
-
(bookmark) =>
|
|
50
|
-
bookmark.user.id !== event.bookmark.user.id ||
|
|
51
|
-
bookmark.activity.id !== event.bookmark.activity.id,
|
|
63
|
+
(bookmark) => !isSameBookmark(bookmark, event.bookmark),
|
|
52
64
|
)
|
|
53
65
|
: activity.own_bookmarks;
|
|
54
66
|
|
|
@@ -67,10 +79,8 @@ export const updateBookmarkInActivity = (
|
|
|
67
79
|
// Update own_bookmarks if the bookmark is from the current user
|
|
68
80
|
let ownBookmarks = activity.own_bookmarks || [];
|
|
69
81
|
if (isCurrentUser) {
|
|
70
|
-
const bookmarkIndex = ownBookmarks.findIndex(
|
|
71
|
-
(bookmark)
|
|
72
|
-
bookmark.user.id === event.bookmark.user.id &&
|
|
73
|
-
bookmark.activity.id === event.bookmark.activity.id,
|
|
82
|
+
const bookmarkIndex = ownBookmarks.findIndex((bookmark) =>
|
|
83
|
+
isSameBookmark(bookmark, event.bookmark),
|
|
74
84
|
);
|
|
75
85
|
if (bookmarkIndex !== -1) {
|
|
76
86
|
ownBookmarks = [...ownBookmarks];
|