@stream-io/feeds-client 0.2.1 → 0.2.3

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.
Files changed (92) hide show
  1. package/@react-bindings/hooks/feed-state-hooks/index.ts +4 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/@react-bindings/contexts/StreamSearchContext.d.ts +1 -1
  4. package/dist/@react-bindings/contexts/StreamSearchResultsContext.d.ts +1 -1
  5. package/dist/@react-bindings/hooks/feed-state-hooks/index.d.ts +4 -0
  6. package/dist/@react-bindings/hooks/feed-state-hooks/useAggregatedActivities.d.ts +11 -0
  7. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivityRead.d.ts +6 -0
  8. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivitySeen.d.ts +6 -0
  9. package/dist/@react-bindings/hooks/feed-state-hooks/useNotificationStatus.d.ts +13 -0
  10. package/dist/@react-bindings/hooks/search-state-hooks/useSearchQuery.d.ts +1 -1
  11. package/dist/@react-bindings/hooks/search-state-hooks/useSearchResult.d.ts +1 -1
  12. package/dist/@react-bindings/hooks/search-state-hooks/useSearchSources.d.ts +2 -2
  13. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +1 -1
  14. package/dist/@react-bindings/wrappers/StreamSearch.d.ts +1 -1
  15. package/dist/@react-bindings/wrappers/StreamSearchResults.d.ts +1 -1
  16. package/dist/index-react-bindings.browser.cjs +178 -35
  17. package/dist/index-react-bindings.browser.cjs.map +1 -1
  18. package/dist/index-react-bindings.browser.js +175 -36
  19. package/dist/index-react-bindings.browser.js.map +1 -1
  20. package/dist/index-react-bindings.node.cjs +178 -35
  21. package/dist/index-react-bindings.node.cjs.map +1 -1
  22. package/dist/index-react-bindings.node.js +175 -36
  23. package/dist/index-react-bindings.node.js.map +1 -1
  24. package/dist/index.browser.cjs +328 -180
  25. package/dist/index.browser.cjs.map +1 -1
  26. package/dist/index.browser.js +328 -181
  27. package/dist/index.browser.js.map +1 -1
  28. package/dist/index.d.ts +1 -5
  29. package/dist/index.node.cjs +328 -180
  30. package/dist/index.node.cjs.map +1 -1
  31. package/dist/index.node.js +328 -181
  32. package/dist/index.node.js.map +1 -1
  33. package/dist/src/common/{ActivitySearchSource.d.ts → search/ActivitySearchSource.d.ts} +3 -3
  34. package/dist/src/common/{BaseSearchSource.d.ts → search/BaseSearchSource.d.ts} +41 -35
  35. package/dist/src/common/{FeedSearchSource.d.ts → search/FeedSearchSource.d.ts} +3 -3
  36. package/dist/src/common/{SearchController.d.ts → search/SearchController.d.ts} +1 -3
  37. package/dist/src/common/{UserSearchSource.d.ts → search/UserSearchSource.d.ts} +4 -4
  38. package/dist/src/common/search/index.d.ts +6 -0
  39. package/dist/src/common/search/types.d.ts +22 -0
  40. package/dist/src/common/types.d.ts +1 -0
  41. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +5 -12
  42. package/dist/src/feed/event-handlers/activity/handle-activity-marked.d.ts +11 -0
  43. package/dist/src/feed/event-handlers/activity/index.d.ts +1 -0
  44. package/dist/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +8 -1
  45. package/dist/src/feed/feed.d.ts +2 -2
  46. package/dist/src/gen/models/index.d.ts +58 -26
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/index.ts +1 -5
  49. package/package.json +1 -1
  50. package/src/common/{ActivitySearchSource.ts → search/ActivitySearchSource.ts} +3 -3
  51. package/src/common/{BaseSearchSource.ts → search/BaseSearchSource.ts} +137 -69
  52. package/src/common/{FeedSearchSource.ts → search/FeedSearchSource.ts} +3 -3
  53. package/src/common/{SearchController.ts → search/SearchController.ts} +2 -7
  54. package/src/common/{UserSearchSource.ts → search/UserSearchSource.ts} +3 -3
  55. package/src/common/search/index.ts +6 -0
  56. package/src/common/search/types.ts +21 -0
  57. package/src/common/types.ts +2 -0
  58. package/src/feed/event-handlers/activity/activity-marked-utils.test.ts +208 -0
  59. package/src/feed/event-handlers/activity/activity-utils.test.ts +2 -2
  60. package/src/feed/event-handlers/activity/handle-activity-added.test.ts +86 -0
  61. package/src/feed/event-handlers/activity/handle-activity-deleted.test.ts +117 -0
  62. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +8 -4
  63. package/src/feed/event-handlers/activity/handle-activity-marked.ts +68 -0
  64. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +15 -15
  65. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +14 -14
  66. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +4 -3
  67. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +4 -4
  68. package/src/feed/event-handlers/activity/index.ts +2 -1
  69. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +14 -14
  70. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +14 -14
  71. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +16 -16
  72. package/src/feed/event-handlers/comment/handle-comment-added.test.ts +147 -0
  73. package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +133 -0
  74. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +24 -10
  75. package/src/feed/event-handlers/comment/handle-comment-reaction.test.ts +315 -0
  76. package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +131 -0
  77. package/src/feed/event-handlers/feed-member/handle-feed-member-added.test.ts +75 -0
  78. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.test.ts +82 -0
  79. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +19 -9
  80. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.test.ts +84 -0
  81. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +7 -7
  82. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +2 -2
  83. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +1 -1
  84. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +120 -0
  85. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +47 -3
  86. package/src/feed/feed.ts +4 -2
  87. package/src/gen/feeds/FeedsApi.ts +6 -0
  88. package/src/gen/model-decoders/decoders.ts +12 -0
  89. package/src/gen/models/index.ts +90 -34
  90. package/src/test-utils/response-generators.ts +230 -0
  91. package/dist/src/test-utils/index.d.ts +0 -1
  92. package/dist/src/test-utils/response-generators.d.ts +0 -54
package/index.ts CHANGED
@@ -4,10 +4,6 @@ export * from './src/gen/models';
4
4
  export * from './src/types';
5
5
  export * from './src/common/types';
6
6
  export * from './src/common/StateStore';
7
- export * from './src/common/BaseSearchSource';
8
- export * from './src/common/SearchController';
9
- export * from './src/common/ActivitySearchSource';
10
- export * from './src/common/UserSearchSource';
11
- export * from './src/common/FeedSearchSource';
7
+ export * from './src/common/search';
12
8
  export * from './src/common/Poll';
13
9
  export * from './src/utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/feeds-client",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/index.node.js",
6
6
  "exports": {
@@ -1,8 +1,8 @@
1
1
  import { BaseSearchSource } from './BaseSearchSource';
2
- import type { SearchSourceOptions } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './types';
3
3
 
4
- import { FeedsClient } from '../feeds-client';
5
- import { ActivityResponse } from '../gen/models';
4
+ import { FeedsClient } from '../../feeds-client';
5
+ import { ActivityResponse } from '../../gen/models';
6
6
 
7
7
  export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
8
8
  readonly type = 'activity' as const;
@@ -1,22 +1,20 @@
1
- import { StateStore } from './StateStore';
2
- import { debounce, type DebouncedFunc } from './utils';
3
-
4
- export type SearchSourceType = 'activity' | 'user' | 'feed' | (string & {});
5
-
6
- export type QueryReturnValue<T> = { items: T[]; next?: string | null };
1
+ import { StateStore } from '../StateStore';
2
+ import { debounce, type DebouncedFunc } from '../utils';
3
+ import type {
4
+ QueryReturnValue,
5
+ SearchSourceOptions,
6
+ SearchSourceState,
7
+ SearchSourceType,
8
+ } from './types';
7
9
 
8
10
  export type DebounceOptions = {
9
11
  debounceMs: number;
10
12
  };
11
- type DebouncedExecQueryFunction = DebouncedFunc<
12
- (searchString?: string) => Promise<void>
13
- >;
13
+ type DebouncedExecQueryFunction = DebouncedFunc<(searchString?: string) => Promise<void>>;
14
14
 
15
- export interface SearchSource<T = any> {
15
+ interface ISearchSource<T = any> {
16
16
  activate(): void;
17
17
 
18
- cancelScheduledQuery(): void;
19
-
20
18
  canExecuteQuery(newSearchString?: string): boolean;
21
19
 
22
20
  deactivate(): void;
@@ -33,54 +31,44 @@ export interface SearchSource<T = any> {
33
31
 
34
32
  resetState(): void;
35
33
 
36
- search(text?: string): Promise<void> | undefined;
37
-
38
34
  readonly searchQuery: string;
39
35
 
40
- setDebounceOptions(options: DebounceOptions): void;
41
-
42
36
  readonly state: StateStore<SearchSourceState<T>>;
43
37
  readonly type: SearchSourceType;
44
38
  }
45
39
 
46
- export type SearchSourceState<T = any> = {
47
- hasNext: boolean;
48
- isActive: boolean;
49
- isLoading: boolean;
50
- items: T[] | undefined;
51
- searchQuery: string;
52
- lastQueryError?: Error;
53
- next?: string | null;
54
- offset?: number;
55
- };
56
- export type SearchSourceOptions = {
57
- /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
58
- debounceMs?: number;
59
- pageSize?: number;
60
- allowEmptySearchString?: boolean;
61
- };
40
+ export interface SearchSource<T = any> extends ISearchSource<T> {
41
+ cancelScheduledQuery(): void;
42
+ setDebounceOptions(options: DebounceOptions): void;
43
+ search(text?: string): Promise<void> | undefined;
44
+ }
45
+
46
+ export interface SearchSourceSync<T = any> extends ISearchSource<T> {
47
+ cancelScheduledQuery(): void;
48
+ setDebounceOptions(options: DebounceOptions): void;
49
+ search(text?: string): void;
50
+ }
51
+
62
52
  const DEFAULT_SEARCH_SOURCE_OPTIONS: Required<SearchSourceOptions> = {
63
53
  debounceMs: 300,
64
54
  pageSize: 10,
65
55
  allowEmptySearchString: false,
56
+ resetOnNewSearchQuery: true,
66
57
  } as const;
67
58
 
68
- export abstract class BaseSearchSource<T> implements SearchSource<T> {
59
+ abstract class BaseSearchSourceBase<T> implements ISearchSource<T> {
69
60
  state: StateStore<SearchSourceState<T>>;
70
- protected pageSize: number;
61
+ pageSize: number;
71
62
  protected allowEmptySearchString: boolean;
63
+ protected resetOnNewSearchQuery: boolean;
72
64
  abstract readonly type: SearchSourceType;
73
- protected searchDebounced!: DebouncedExecQueryFunction;
74
65
 
75
66
  protected constructor(options?: SearchSourceOptions) {
76
- const { debounceMs, pageSize, allowEmptySearchString } = {
77
- ...DEFAULT_SEARCH_SOURCE_OPTIONS,
78
- ...options,
79
- };
67
+ const { pageSize, allowEmptySearchString, resetOnNewSearchQuery } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
80
68
  this.pageSize = pageSize;
81
69
  this.allowEmptySearchString = allowEmptySearchString;
70
+ this.resetOnNewSearchQuery = resetOnNewSearchQuery;
82
71
  this.state = new StateStore<SearchSourceState<T>>(this.initialState);
83
- this.setDebounceOptions({ debounceMs });
84
72
  }
85
73
 
86
74
  get lastQueryError() {
@@ -132,14 +120,6 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
132
120
  return this.state.getLatestValue().searchQuery;
133
121
  }
134
122
 
135
- protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
136
-
137
- protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
138
-
139
- setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
140
- this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
141
- };
142
-
143
123
  activate = () => {
144
124
  if (this.isActive) return;
145
125
  this.state.partialNext({ isActive: true });
@@ -161,13 +141,16 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
161
141
  );
162
142
  };
163
143
 
164
- protected getStateBeforeFirstQuery(
165
- newSearchString: string,
166
- ): SearchSourceState<T> {
144
+ protected getStateBeforeFirstQuery(newSearchString: string): SearchSourceState<T> {
145
+ const initialState = this.initialState;
146
+ const oldItems = this.items;
147
+
148
+ const items = this.resetOnNewSearchQuery ? initialState.items : oldItems;
167
149
  return {
168
150
  ...this.initialState,
151
+ items,
169
152
  isActive: this.isActive,
170
- isLoading: true,
153
+ isLoading: this.resetOnNewSearchQuery ? true : !oldItems,
171
154
  searchQuery: newSearchString,
172
155
  };
173
156
  }
@@ -184,12 +167,11 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
184
167
  isLoading: false,
185
168
  items: isFirstPage
186
169
  ? stateUpdate.items
187
- : [...(this.items ?? []), ...(stateUpdate.items ?? [])],
170
+ : [...(this.items ?? []), ...(stateUpdate.items || [])],
188
171
  };
189
172
  }
190
173
 
191
- async executeQuery(newSearchString?: string) {
192
- if (!this.canExecuteQuery(newSearchString)) return;
174
+ protected prepareStateForQuery(newSearchString?: string) {
193
175
  const hasNewSearchQuery = typeof newSearchString !== 'undefined';
194
176
  const searchString = newSearchString ?? this.searchQuery;
195
177
 
@@ -199,20 +181,67 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
199
181
  this.state.partialNext({ isLoading: true });
200
182
  }
201
183
 
184
+ return { searchString, hasNewSearchQuery };
185
+ }
186
+
187
+ protected updatePaginationStateFromQuery(result: QueryReturnValue<T>) {
188
+ const { items, next } = result;
189
+
202
190
  const stateUpdate: Partial<SearchSourceState<T>> = {};
191
+ if (next || next === null) {
192
+ stateUpdate.next = next;
193
+ stateUpdate.hasNext = !!next;
194
+ } else {
195
+ stateUpdate.offset = (this.offset ?? 0) + items.length;
196
+ stateUpdate.hasNext = items.length === this.pageSize;
197
+ }
198
+
199
+ return stateUpdate;
200
+ }
201
+
202
+ resetState() {
203
+ this.state.next(this.initialState);
204
+ }
205
+
206
+ resetStateAndActivate() {
207
+ this.resetState();
208
+ this.activate();
209
+ }
210
+ }
211
+
212
+ export abstract class BaseSearchSource<T>
213
+ extends BaseSearchSourceBase<T>
214
+ implements SearchSource<T>
215
+ {
216
+ protected searchDebounced!: DebouncedExecQueryFunction;
217
+
218
+ constructor(options?: SearchSourceOptions) {
219
+ const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
220
+ super(options);
221
+ this.setDebounceOptions({ debounceMs });
222
+ }
223
+
224
+ protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
225
+
226
+ protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
227
+
228
+ setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
229
+ this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
230
+ };
231
+
232
+ async executeQuery(newSearchString?: string) {
233
+ if (!this.canExecuteQuery(newSearchString)) return;
234
+
235
+ const { hasNewSearchQuery, searchString } =
236
+ this.prepareStateForQuery(newSearchString);
237
+
238
+ let stateUpdate: Partial<SearchSourceState<T>> = {};
203
239
  try {
204
240
  const results = await this.query(searchString);
205
241
  if (!results) return;
206
- const { items, next } = results;
207
-
208
- if (typeof next === 'string' || next === null) {
209
- stateUpdate.next = next;
210
- stateUpdate.hasNext = !!next;
211
- } else {
212
- stateUpdate.offset = (this.offset ?? 0) + items.length;
213
- stateUpdate.hasNext = items.length === this.pageSize;
214
- }
215
242
 
243
+ const { items } = results;
244
+ stateUpdate = this.updatePaginationStateFromQuery(results);
216
245
  stateUpdate.items = await this.filterQueryResults(items);
217
246
  } catch (e) {
218
247
  stateUpdate.lastQueryError = e as Error;
@@ -226,13 +255,52 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
226
255
  cancelScheduledQuery() {
227
256
  this.searchDebounced.cancel();
228
257
  }
258
+ }
229
259
 
230
- resetState() {
231
- this.state.next(this.initialState);
260
+ export abstract class BaseSearchSourceSync<T>
261
+ extends BaseSearchSourceBase<T>
262
+ implements SearchSourceSync<T>
263
+ {
264
+ protected searchDebounced!: DebouncedExecQueryFunction;
265
+
266
+ constructor(options?: SearchSourceOptions) {
267
+ const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
268
+ super(options);
269
+ this.setDebounceOptions({ debounceMs });
232
270
  }
233
271
 
234
- resetStateAndActivate() {
235
- this.resetState();
236
- this.activate();
272
+ protected abstract query(searchQuery: string): QueryReturnValue<T>;
273
+
274
+ protected abstract filterQueryResults(items: T[]): T[];
275
+
276
+ setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
277
+ this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
278
+ };
279
+
280
+ executeQuery(newSearchString?: string) {
281
+ if (!this.canExecuteQuery(newSearchString)) return;
282
+
283
+ const { hasNewSearchQuery, searchString } =
284
+ this.prepareStateForQuery(newSearchString);
285
+
286
+ let stateUpdate: Partial<SearchSourceState<T>> = {};
287
+ try {
288
+ const results = this.query(searchString);
289
+ if (!results) return;
290
+
291
+ const { items } = results;
292
+ stateUpdate = this.updatePaginationStateFromQuery(results);
293
+ stateUpdate.items = this.filterQueryResults(items);
294
+ } catch (e) {
295
+ stateUpdate.lastQueryError = e as Error;
296
+ } finally {
297
+ this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
298
+ }
299
+ }
300
+
301
+ search = (searchQuery?: string) => this.searchDebounced(searchQuery);
302
+
303
+ cancelScheduledQuery() {
304
+ this.searchDebounced.cancel();
237
305
  }
238
306
  }
@@ -1,8 +1,8 @@
1
1
  import { BaseSearchSource } from './BaseSearchSource';
2
- import type { SearchSourceOptions } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './types';
3
3
 
4
- import { FeedsClient } from '../feeds-client';
5
- import { Feed } from '../feed';
4
+ import { FeedsClient } from '../../feeds-client';
5
+ import { Feed } from '../../feed';
6
6
 
7
7
  export type FeedSearchSourceOptions = SearchSourceOptions & {
8
8
  groupId?: string;
@@ -1,4 +1,4 @@
1
- import { StateStore } from './StateStore';
1
+ import { StateStore } from '../StateStore';
2
2
  import type { SearchSource } from './BaseSearchSource';
3
3
 
4
4
  export type SearchControllerState = {
@@ -23,8 +23,6 @@ export class SearchController {
23
23
  /**
24
24
  * Not intended for direct use by integrators, might be removed without notice resulting in
25
25
  * broken integrations.
26
- *
27
- * @internal
28
26
  */
29
27
  _internalState: StateStore<InternalSearchControllerState>;
30
28
  state: StateStore<SearchControllerState>;
@@ -39,7 +37,6 @@ export class SearchController {
39
37
  this._internalState = new StateStore<InternalSearchControllerState>({});
40
38
  this.config = { keepSingleActiveSource: true, ...config };
41
39
  }
42
-
43
40
  get hasNext() {
44
41
  return this.sources.some((source) => source.hasNext);
45
42
  }
@@ -117,9 +114,7 @@ export class SearchController {
117
114
  this.state.partialNext({
118
115
  searchQuery,
119
116
  });
120
- await Promise.all(
121
- searchedSources.map((source) => source.search(searchQuery)),
122
- );
117
+ await Promise.all(searchedSources.map((source) => source.search(searchQuery)));
123
118
  };
124
119
 
125
120
  cancelSearchQueries = () => {
@@ -1,8 +1,8 @@
1
1
  import { BaseSearchSource } from './BaseSearchSource';
2
- import type { SearchSourceOptions } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './types';
3
3
 
4
- import { FeedsClient } from '../feeds-client';
5
- import { UserResponse } from '../gen/models';
4
+ import { FeedsClient } from '../../feeds-client';
5
+ import { UserResponse } from '../../gen/models';
6
6
 
7
7
  export class UserSearchSource extends BaseSearchSource<UserResponse> {
8
8
  readonly type = 'user' as const;
@@ -0,0 +1,6 @@
1
+ export * from './SearchController';
2
+ export * from './BaseSearchSource';
3
+ export * from './ActivitySearchSource';
4
+ export * from './FeedSearchSource';
5
+ export * from './UserSearchSource';
6
+ export * from './types';
@@ -0,0 +1,21 @@
1
+ export type SearchSourceState<T = any> = {
2
+ hasNext: boolean;
3
+ isActive: boolean;
4
+ isLoading: boolean;
5
+ items: T[] | undefined;
6
+ searchQuery: string;
7
+ lastQueryError?: Error;
8
+ next?: string | null;
9
+ offset?: number;
10
+ };
11
+
12
+ export type SearchSourceOptions = {
13
+ /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
14
+ debounceMs?: number;
15
+ pageSize?: number;
16
+ allowEmptySearchString?: boolean;
17
+ resetOnNewSearchQuery?: boolean;
18
+ };
19
+
20
+ export type SearchSourceType = 'activity' | 'user' | 'feed' | (string & {});
21
+ export type QueryReturnValue<T> = { items: T[]; next?: string | null };
@@ -1,3 +1,5 @@
1
+ export * from './search/types';
2
+
1
3
  export type FeedsClientOptions = {
2
4
  base_url?: string;
3
5
  timeout?: number;
@@ -0,0 +1,208 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ ActivityMarkEvent,
4
+ NotificationStatusResponse,
5
+ } from '../../../gen/models';
6
+ import { updateNotificationStatusFromActivityMarked } from './handle-activity-marked';
7
+
8
+ const createMockActivityMarkEvent = (
9
+ overrides: Partial<ActivityMarkEvent> = {},
10
+ ): ActivityMarkEvent => ({
11
+ created_at: new Date(),
12
+ fid: 'user:notification',
13
+ custom: {},
14
+ type: 'feeds.activity.marked',
15
+ ...overrides,
16
+ });
17
+
18
+ const createMockNotificationStatus = (
19
+ overrides: Partial<NotificationStatusResponse> = {},
20
+ ): NotificationStatusResponse => ({
21
+ unread: 0,
22
+ unseen: 0,
23
+ ...overrides,
24
+ });
25
+
26
+ const createMockAggregatedActivity = (group: string) => ({ group });
27
+
28
+ describe('activity-marked-utils', () => {
29
+ describe('updateNotificationStatusFromActivityMarked', () => {
30
+ it('should return unchanged if notification_status is undefined', () => {
31
+ const event = createMockActivityMarkEvent({ mark_all_read: true });
32
+ const currentStatus = undefined;
33
+
34
+ const result = updateNotificationStatusFromActivityMarked(
35
+ event,
36
+ currentStatus,
37
+ );
38
+
39
+ expect(result.changed).toBe(false);
40
+ });
41
+
42
+ it('should handle mark_all_read by adding all aggregated activity groups', () => {
43
+ const event = createMockActivityMarkEvent({ mark_all_read: true });
44
+ const currentStatus = createMockNotificationStatus({
45
+ read_activities: ['existing1'],
46
+ });
47
+ const aggregatedActivities = [
48
+ createMockAggregatedActivity('group1'),
49
+ createMockAggregatedActivity('group2'),
50
+ createMockAggregatedActivity('group3'),
51
+ ];
52
+
53
+ const result = updateNotificationStatusFromActivityMarked(
54
+ event,
55
+ currentStatus,
56
+ aggregatedActivities,
57
+ );
58
+
59
+ expect(result.changed).toBe(true);
60
+ expect(result.data?.notification_status.read_activities).toEqual([
61
+ 'existing1',
62
+ 'group1',
63
+ 'group2',
64
+ 'group3',
65
+ ]);
66
+ });
67
+
68
+ it('should handle mark_read by adding specific activity IDs', () => {
69
+ const event = createMockActivityMarkEvent({
70
+ mark_read: ['activity1', 'activity2'],
71
+ });
72
+ const currentStatus = createMockNotificationStatus({
73
+ read_activities: ['existing1'],
74
+ });
75
+
76
+ const result = updateNotificationStatusFromActivityMarked(
77
+ event,
78
+ currentStatus,
79
+ );
80
+
81
+ expect(result.changed).toBe(true);
82
+ expect(result.data?.notification_status.read_activities).toEqual([
83
+ 'existing1',
84
+ 'activity1',
85
+ 'activity2',
86
+ ]);
87
+ });
88
+
89
+ it('should handle mark_all_seen by setting last_seen_at to current date', () => {
90
+ const event = createMockActivityMarkEvent({ mark_all_seen: true });
91
+ const currentStatus = createMockNotificationStatus({
92
+ last_seen_at: new Date('2023-01-01'),
93
+ });
94
+
95
+ const result = updateNotificationStatusFromActivityMarked(
96
+ event,
97
+ currentStatus,
98
+ );
99
+
100
+ expect(result.changed).toBe(true);
101
+ expect(result.data?.notification_status.last_seen_at).toBeInstanceOf(
102
+ Date,
103
+ );
104
+ expect(
105
+ result.data?.notification_status.last_seen_at!.getTime(),
106
+ ).toBeGreaterThan(new Date('2023-01-01').getTime());
107
+ });
108
+
109
+ it('should handle multiple mark flags simultaneously', () => {
110
+ const event = createMockActivityMarkEvent({
111
+ mark_all_read: true,
112
+ mark_all_seen: true,
113
+ });
114
+ const currentStatus = createMockNotificationStatus({
115
+ read_activities: ['existing1'],
116
+ last_seen_at: new Date('2023-01-01'),
117
+ });
118
+ const aggregatedActivities = [
119
+ createMockAggregatedActivity('group1'),
120
+ createMockAggregatedActivity('group2'),
121
+ ];
122
+
123
+ const result = updateNotificationStatusFromActivityMarked(
124
+ event,
125
+ currentStatus,
126
+ aggregatedActivities,
127
+ );
128
+
129
+ expect(result.changed).toBe(true);
130
+ expect(result.data?.notification_status.read_activities).toEqual([
131
+ 'existing1',
132
+ 'group1',
133
+ 'group2',
134
+ ]);
135
+ expect(result.data?.notification_status.last_seen_at).toBeInstanceOf(
136
+ Date,
137
+ );
138
+ });
139
+
140
+ it('should deduplicate read activities when adding new ones', () => {
141
+ const event = createMockActivityMarkEvent({
142
+ mark_read: ['activity1', 'activity1', 'activity2'],
143
+ });
144
+ const currentStatus = createMockNotificationStatus({
145
+ read_activities: ['existing1', 'activity1'],
146
+ });
147
+
148
+ const result = updateNotificationStatusFromActivityMarked(
149
+ event,
150
+ currentStatus,
151
+ );
152
+
153
+ expect(result.changed).toBe(true);
154
+ expect(result.data?.notification_status.read_activities).toEqual([
155
+ 'existing1',
156
+ 'activity1',
157
+ 'activity2',
158
+ ]);
159
+ });
160
+
161
+ it('should preserve existing notification status fields', () => {
162
+ const event = createMockActivityMarkEvent({ mark_all_seen: true });
163
+ const currentStatus = createMockNotificationStatus({
164
+ unread: 5,
165
+ unseen: 3,
166
+ read_activities: ['existing1'],
167
+ });
168
+
169
+ const result = updateNotificationStatusFromActivityMarked(
170
+ event,
171
+ currentStatus,
172
+ );
173
+
174
+ expect(result.changed).toBe(true);
175
+ expect(result.data?.notification_status.unread).toBe(5);
176
+ expect(result.data?.notification_status.unseen).toBe(3);
177
+ expect(result.data?.notification_status.read_activities).toEqual([
178
+ 'existing1',
179
+ ]);
180
+ expect(result.data?.notification_status.last_seen_at).toBeInstanceOf(
181
+ Date,
182
+ );
183
+ });
184
+
185
+ it('should handle mark_all_read with no existing read_activities', () => {
186
+ const event = createMockActivityMarkEvent({ mark_all_read: true });
187
+ const currentStatus = createMockNotificationStatus({
188
+ read_activities: undefined,
189
+ });
190
+ const aggregatedActivities = [
191
+ createMockAggregatedActivity('group1'),
192
+ createMockAggregatedActivity('group2'),
193
+ ];
194
+
195
+ const result = updateNotificationStatusFromActivityMarked(
196
+ event,
197
+ currentStatus,
198
+ aggregatedActivities,
199
+ );
200
+
201
+ expect(result.changed).toBe(true);
202
+ expect(result.data?.notification_status.read_activities).toEqual([
203
+ 'group1',
204
+ 'group2',
205
+ ]);
206
+ });
207
+ });
208
+ });
@@ -191,7 +191,7 @@ describe('activity-utils', () => {
191
191
 
192
192
  expect(result.changed).toBe(true);
193
193
  expect(result.activities).toHaveLength(1);
194
- expect(result.activities[0].id).toBe('activity2');
194
+ expect(result.activities![0].id).toBe('activity2');
195
195
  // Make sure we create a new array
196
196
  expect(activities === result.activities).toBe(false);
197
197
  });
@@ -205,7 +205,7 @@ describe('activity-utils', () => {
205
205
 
206
206
  expect(result.changed).toBe(false);
207
207
  expect(result.activities).toHaveLength(1);
208
- expect(result.activities[0].id).toBe('activity1');
208
+ expect(result.activities![0].id).toBe('activity1');
209
209
  });
210
210
 
211
211
  it('should handle empty activities array', () => {