@knocklabs/client 0.8.20 → 0.8.21

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 (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cjs/api.js +1 -1
  3. package/dist/cjs/api.js.map +1 -1
  4. package/dist/cjs/clients/feed/feed.js +1 -1
  5. package/dist/cjs/clients/feed/feed.js.map +1 -1
  6. package/dist/cjs/clients/feed/store.js +1 -1
  7. package/dist/cjs/clients/feed/store.js.map +1 -1
  8. package/dist/esm/{api.js → api.mjs} +1 -1
  9. package/dist/esm/api.mjs.map +1 -0
  10. package/dist/esm/clients/feed/{feed.js → feed.mjs} +4 -4
  11. package/dist/esm/clients/feed/feed.mjs.map +1 -0
  12. package/dist/esm/clients/feed/{index.js → index.mjs} +2 -2
  13. package/dist/esm/clients/feed/index.mjs.map +1 -0
  14. package/dist/esm/clients/feed/{store.js → store.mjs} +3 -3
  15. package/dist/esm/clients/feed/store.mjs.map +1 -0
  16. package/dist/esm/clients/feed/{utils.js → utils.mjs} +1 -1
  17. package/dist/esm/clients/feed/utils.mjs.map +1 -0
  18. package/dist/esm/clients/preferences/{index.js → index.mjs} +1 -1
  19. package/dist/esm/clients/preferences/index.mjs.map +1 -0
  20. package/dist/esm/clients/users/{index.js → index.mjs} +1 -1
  21. package/dist/esm/clients/users/index.mjs.map +1 -0
  22. package/dist/esm/index.mjs +12 -0
  23. package/dist/esm/index.mjs.map +1 -0
  24. package/dist/esm/{knock.js → knock.mjs} +5 -5
  25. package/dist/esm/knock.mjs.map +1 -0
  26. package/dist/esm/{networkStatus.js → networkStatus.mjs} +1 -1
  27. package/dist/esm/networkStatus.mjs.map +1 -0
  28. package/package.json +6 -5
  29. package/dist/api.d.ts +0 -25
  30. package/dist/api.d.ts.map +0 -1
  31. package/dist/api.js +0 -84
  32. package/dist/clients/feed/feed.d.ts +0 -64
  33. package/dist/clients/feed/feed.d.ts.map +0 -1
  34. package/dist/clients/feed/feed.js +0 -572
  35. package/dist/clients/feed/index.d.ts +0 -15
  36. package/dist/clients/feed/index.d.ts.map +0 -1
  37. package/dist/clients/feed/index.js +0 -34
  38. package/dist/clients/feed/interfaces.d.ts +0 -60
  39. package/dist/clients/feed/interfaces.d.ts.map +0 -1
  40. package/dist/clients/feed/interfaces.js +0 -2
  41. package/dist/clients/feed/store.d.ts +0 -3
  42. package/dist/clients/feed/store.d.ts.map +0 -1
  43. package/dist/clients/feed/store.js +0 -72
  44. package/dist/clients/feed/types.d.ts +0 -34
  45. package/dist/clients/feed/types.d.ts.map +0 -1
  46. package/dist/clients/feed/types.js +0 -2
  47. package/dist/clients/feed/utils.d.ts +0 -4
  48. package/dist/clients/feed/utils.d.ts.map +0 -1
  49. package/dist/clients/feed/utils.js +0 -21
  50. package/dist/clients/preferences/index.d.ts +0 -46
  51. package/dist/clients/preferences/index.d.ts.map +0 -1
  52. package/dist/clients/preferences/index.js +0 -129
  53. package/dist/clients/preferences/interfaces.d.ts +0 -26
  54. package/dist/clients/preferences/interfaces.d.ts.map +0 -1
  55. package/dist/clients/preferences/interfaces.js +0 -2
  56. package/dist/clients/users/index.d.ts +0 -16
  57. package/dist/clients/users/index.d.ts.map +0 -1
  58. package/dist/clients/users/index.js +0 -56
  59. package/dist/clients/users/interfaces.d.ts +0 -8
  60. package/dist/clients/users/interfaces.d.ts.map +0 -1
  61. package/dist/clients/users/interfaces.js +0 -2
  62. package/dist/esm/api.js.map +0 -1
  63. package/dist/esm/clients/feed/feed.js.map +0 -1
  64. package/dist/esm/clients/feed/index.js.map +0 -1
  65. package/dist/esm/clients/feed/store.js.map +0 -1
  66. package/dist/esm/clients/feed/utils.js.map +0 -1
  67. package/dist/esm/clients/preferences/index.js.map +0 -1
  68. package/dist/esm/clients/users/index.js.map +0 -1
  69. package/dist/esm/index.js +0 -12
  70. package/dist/esm/index.js.map +0 -1
  71. package/dist/esm/knock.js.map +0 -1
  72. package/dist/esm/networkStatus.js.map +0 -1
  73. package/dist/index.d.ts +0 -11
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/index.js +0 -43
  76. package/dist/interfaces.d.ts +0 -41
  77. package/dist/interfaces.d.ts.map +0 -1
  78. package/dist/interfaces.js +0 -2
  79. package/dist/knock.d.ts +0 -30
  80. package/dist/knock.d.ts.map +0 -1
  81. package/dist/knock.js +0 -135
  82. package/dist/networkStatus.d.ts +0 -8
  83. package/dist/networkStatus.d.ts.map +0 -1
  84. package/dist/networkStatus.js +0 -18
@@ -1,572 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const eventemitter2_1 = require("eventemitter2");
7
- const networkStatus_1 = require("../../networkStatus");
8
- const store_1 = __importDefault(require("./store"));
9
- // Default options to apply
10
- const feedClientDefaults = {
11
- archived: "exclude",
12
- };
13
- const DEFAULT_DISCONNECT_DELAY = 2000;
14
- class Feed {
15
- knock;
16
- feedId;
17
- userFeedId;
18
- channel;
19
- broadcaster;
20
- defaultOptions;
21
- broadcastChannel;
22
- disconnectTimer = null;
23
- hasSubscribedToRealTimeUpdates = false;
24
- // The raw store instance, used for binding in React and other environments
25
- store;
26
- constructor(knock, feedId, options) {
27
- this.knock = knock;
28
- this.feedId = feedId;
29
- this.feedId = feedId;
30
- this.userFeedId = this.buildUserFeedId();
31
- this.store = (0, store_1.default)();
32
- this.broadcaster = new eventemitter2_1.EventEmitter2({ wildcard: true, delimiter: "." });
33
- this.defaultOptions = { ...feedClientDefaults, ...options };
34
- this.knock.log(`[Feed] Initialized a feed on channel ${feedId}`);
35
- // Attempt to setup a realtime connection (does not join)
36
- this.initializeRealtimeConnection();
37
- this.setupBroadcastChannel();
38
- }
39
- /**
40
- * Used to reinitialize a current feed instance, which is useful when reauthenticating users
41
- */
42
- reinitialize() {
43
- // Reinitialize the user feed id incase the userId changed
44
- this.userFeedId = this.buildUserFeedId();
45
- // Reinitialize the real-time connection
46
- this.initializeRealtimeConnection();
47
- // Reinitialize our broadcast channel
48
- this.setupBroadcastChannel();
49
- }
50
- /**
51
- * Cleans up a feed instance by destroying the store and disconnecting
52
- * an open socket connection.
53
- */
54
- teardown() {
55
- this.knock.log("[Feed] Tearing down feed instance");
56
- if (this.channel) {
57
- this.channel.leave();
58
- this.channel.off("new-message");
59
- }
60
- if (this.disconnectTimer) {
61
- clearTimeout(this.disconnectTimer);
62
- this.disconnectTimer = null;
63
- }
64
- if (this.broadcastChannel) {
65
- this.broadcastChannel.close();
66
- }
67
- }
68
- /** Tears down an instance and removes it entirely from the feed manager */
69
- dispose() {
70
- this.knock.log("[Feed] Disposing of feed instance");
71
- this.teardown();
72
- this.broadcaster.removeAllListeners();
73
- this.knock.feeds.removeInstance(this);
74
- }
75
- /*
76
- Initializes a real-time connection to Knock, connecting the websocket for the
77
- current ApiClient instance if the socket is not already connected.
78
- */
79
- listenForUpdates() {
80
- this.knock.log("[Feed] Connecting to real-time service");
81
- this.hasSubscribedToRealTimeUpdates = true;
82
- const maybeSocket = this.knock.client().socket;
83
- // Connect the socket only if we don't already have a connection
84
- if (maybeSocket && !maybeSocket.isConnected()) {
85
- maybeSocket.connect();
86
- }
87
- // Only join the channel if we're not already in a joining state
88
- if (this.channel && ["closed", "errored"].includes(this.channel.state)) {
89
- this.channel.join();
90
- }
91
- }
92
- /* Binds a handler to be invoked when event occurs */
93
- on(eventName, callback) {
94
- this.broadcaster.on(eventName, callback);
95
- }
96
- off(eventName, callback) {
97
- this.broadcaster.off(eventName, callback);
98
- }
99
- getState() {
100
- return this.store.getState();
101
- }
102
- async markAsSeen(itemOrItems) {
103
- const now = new Date().toISOString();
104
- this.optimisticallyPerformStatusUpdate(itemOrItems, "seen", { seen_at: now }, "unseen_count");
105
- return this.makeStatusUpdate(itemOrItems, "seen");
106
- }
107
- async markAllAsSeen() {
108
- // To mark all of the messages as seen we:
109
- // 1. Optimistically update *everything* we have in the store
110
- // 2. We decrement the `unseen_count` to zero optimistically
111
- // 3. We issue the API call to the endpoint
112
- //
113
- // Note: there is the potential for a race condition here because the bulk
114
- // update is an async method, so if a new message comes in during this window before
115
- // the update has been processed we'll effectively reset the `unseen_count` to be what it was.
116
- //
117
- // Note: here we optimistically handle the case whereby the feed is scoped to show only `unseen`
118
- // items by removing everything from view.
119
- const { getState, setState } = this.store;
120
- const { metadata, items } = getState();
121
- const isViewingOnlyUnseen = this.defaultOptions.status === "unseen";
122
- // If we're looking at the unseen view, then we want to remove all of the items optimistically
123
- // from the store given that nothing should be visible. We do this by resetting the store state
124
- // and setting the current metadata counts to 0
125
- if (isViewingOnlyUnseen) {
126
- setState((store) => store.resetStore({
127
- ...metadata,
128
- total_count: 0,
129
- unseen_count: 0,
130
- }));
131
- }
132
- else {
133
- // Otherwise we want to update the metadata and mark all of the items in the store as seen
134
- setState((store) => store.setMetadata({ ...metadata, unseen_count: 0 }));
135
- const attrs = { seen_at: new Date().toISOString() };
136
- const itemIds = items.map((item) => item.id);
137
- setState((store) => store.setItemAttrs(itemIds, attrs));
138
- }
139
- // Issue the API request to the bulk status change API
140
- const result = await this.makeBulkStatusUpdate("seen");
141
- this.broadcaster.emit(`items:all_seen`, { items });
142
- this.broadcastOverChannel(`items:all_seen`, { items });
143
- return result;
144
- }
145
- async markAsUnseen(itemOrItems) {
146
- this.optimisticallyPerformStatusUpdate(itemOrItems, "unseen", { seen_at: null }, "unseen_count");
147
- return this.makeStatusUpdate(itemOrItems, "unseen");
148
- }
149
- async markAsRead(itemOrItems) {
150
- const now = new Date().toISOString();
151
- this.optimisticallyPerformStatusUpdate(itemOrItems, "read", { read_at: now }, "unread_count");
152
- return this.makeStatusUpdate(itemOrItems, "read");
153
- }
154
- async markAllAsRead() {
155
- // To mark all of the messages as read we:
156
- // 1. Optimistically update *everything* we have in the store
157
- // 2. We decrement the `unread_count` to zero optimistically
158
- // 3. We issue the API call to the endpoint
159
- //
160
- // Note: there is the potential for a race condition here because the bulk
161
- // update is an async method, so if a new message comes in during this window before
162
- // the update has been processed we'll effectively reset the `unread_count` to be what it was.
163
- //
164
- // Note: here we optimistically handle the case whereby the feed is scoped to show only `unread`
165
- // items by removing everything from view.
166
- const { getState, setState } = this.store;
167
- const { metadata, items } = getState();
168
- const isViewingOnlyUnread = this.defaultOptions.status === "unread";
169
- // If we're looking at the unread view, then we want to remove all of the items optimistically
170
- // from the store given that nothing should be visible. We do this by resetting the store state
171
- // and setting the current metadata counts to 0
172
- if (isViewingOnlyUnread) {
173
- setState((store) => store.resetStore({
174
- ...metadata,
175
- total_count: 0,
176
- unread_count: 0,
177
- }));
178
- }
179
- else {
180
- // Otherwise we want to update the metadata and mark all of the items in the store as seen
181
- setState((store) => store.setMetadata({ ...metadata, unread_count: 0 }));
182
- const attrs = { read_at: new Date().toISOString() };
183
- const itemIds = items.map((item) => item.id);
184
- setState((store) => store.setItemAttrs(itemIds, attrs));
185
- }
186
- // Issue the API request to the bulk status change API
187
- const result = await this.makeBulkStatusUpdate("read");
188
- this.broadcaster.emit(`items:all_read`, { items });
189
- this.broadcastOverChannel(`items:all_read`, { items });
190
- return result;
191
- }
192
- async markAsUnread(itemOrItems) {
193
- this.optimisticallyPerformStatusUpdate(itemOrItems, "unread", { read_at: null }, "unread_count");
194
- return this.makeStatusUpdate(itemOrItems, "unread");
195
- }
196
- async markAsInteracted(itemOrItems) {
197
- const now = new Date().toISOString();
198
- this.optimisticallyPerformStatusUpdate(itemOrItems, "interacted", {
199
- read_at: now,
200
- interacted_at: now,
201
- }, "unread_count");
202
- return this.makeStatusUpdate(itemOrItems, "interacted");
203
- }
204
- /*
205
- Marking one or more items as archived should:
206
-
207
- - Decrement the badge count for any unread / unseen items
208
- - Remove the item from the feed list when the `archived` flag is "exclude" (default)
209
-
210
- TODO: how do we handle rollbacks?
211
- */
212
- async markAsArchived(itemOrItems) {
213
- const { getState, setState } = this.store;
214
- const state = getState();
215
- const shouldOptimisticallyRemoveItems = this.defaultOptions.archived === "exclude";
216
- const normalizedItems = Array.isArray(itemOrItems)
217
- ? itemOrItems
218
- : [itemOrItems];
219
- const itemIds = normalizedItems.map((item) => item.id);
220
- /*
221
- In the code here we want to optimistically update counts and items
222
- that are persisted such that we can display updates immediately on the feed
223
- without needing to make a network request.
224
-
225
- Note: right now this does *not* take into account offline handling or any extensive retry
226
- logic, so rollbacks aren't considered. That probably needs to be a future consideration for
227
- this library.
228
-
229
- Scenarios to consider:
230
-
231
- ## Feed scope to archived *only*
232
-
233
- - Counts should not be decremented
234
- - Items should not be removed
235
-
236
- ## Feed scoped to exclude archived items (the default)
237
-
238
- - Counts should be decremented
239
- - Items should be removed
240
-
241
- ## Feed scoped to include archived items as well
242
-
243
- - Counts should not be decremented
244
- - Items should not be removed
245
- */
246
- if (shouldOptimisticallyRemoveItems) {
247
- // If any of the items are unseen or unread, then capture as we'll want to decrement
248
- // the counts for these in the metadata we have
249
- const unseenCount = normalizedItems.filter((i) => !i.seen_at).length;
250
- const unreadCount = normalizedItems.filter((i) => !i.read_at).length;
251
- // Build the new metadata
252
- const updatedMetadata = {
253
- ...state.metadata,
254
- total_count: state.metadata.total_count - normalizedItems.length,
255
- unseen_count: state.metadata.unseen_count - unseenCount,
256
- unread_count: state.metadata.unread_count - unreadCount,
257
- };
258
- // Remove the archiving entries
259
- const entriesToSet = state.items.filter((item) => !itemIds.includes(item.id));
260
- setState((state) => state.setResult({
261
- entries: entriesToSet,
262
- meta: updatedMetadata,
263
- page_info: state.pageInfo,
264
- }));
265
- }
266
- else {
267
- // Mark all the entries being updated as archived either way so the state is correct
268
- state.setItemAttrs(itemIds, { archived_at: new Date().toISOString() });
269
- }
270
- return this.makeStatusUpdate(itemOrItems, "archived");
271
- }
272
- async markAllAsArchived() {
273
- // Note: there is the potential for a race condition here because the bulk
274
- // update is an async method, so if a new message comes in during this window before
275
- // the update has been processed we'll effectively reset the `unseen_count` to be what it was.
276
- const { setState, getState } = this.store;
277
- const { items } = getState();
278
- // Here if we're looking at a feed that excludes all of the archived items by default then we
279
- // will want to optimistically remove all of the items from the feed as they are now all excluded
280
- const shouldOptimisticallyRemoveItems = this.defaultOptions.archived === "exclude";
281
- if (shouldOptimisticallyRemoveItems) {
282
- // Reset the store to clear out all of items and reset the badge count
283
- setState((store) => store.resetStore());
284
- }
285
- else {
286
- // Mark all the entries being updated as archived either way so the state is correct
287
- setState((store) => {
288
- const itemIds = items.map((i) => i.id);
289
- store.setItemAttrs(itemIds, { archived_at: new Date().toISOString() });
290
- });
291
- }
292
- // Issue the API request to the bulk status change API
293
- const result = await this.makeBulkStatusUpdate("archive");
294
- this.broadcaster.emit(`items:all_archived`, { items });
295
- this.broadcastOverChannel(`items:all_archived`, { items });
296
- return result;
297
- }
298
- async markAsUnarchived(itemOrItems) {
299
- this.optimisticallyPerformStatusUpdate(itemOrItems, "unarchived", {
300
- archived_at: null,
301
- });
302
- return this.makeStatusUpdate(itemOrItems, "unarchived");
303
- }
304
- /* Fetches the feed content, appending it to the store */
305
- async fetch(options = {}) {
306
- const { setState, getState } = this.store;
307
- const { networkStatus } = getState();
308
- // If there's an existing request in flight, then do nothing
309
- if ((0, networkStatus_1.isRequestInFlight)(networkStatus)) {
310
- return;
311
- }
312
- // Set the loading type based on the request type it is
313
- setState((store) => store.setNetworkStatus(options.__loadingType ?? networkStatus_1.NetworkStatus.loading));
314
- // Always include the default params, if they have been set
315
- const queryParams = {
316
- ...this.defaultOptions,
317
- ...options,
318
- // Unset options that should not be sent to the API
319
- __loadingType: undefined,
320
- __fetchSource: undefined,
321
- __experimentalCrossBrowserUpdates: undefined,
322
- auto_manage_socket_connection: undefined,
323
- auto_manage_socket_connection_delay: undefined,
324
- };
325
- const result = await this.knock.client().makeRequest({
326
- method: "GET",
327
- url: `/v1/users/${this.knock.userId}/feeds/${this.feedId}`,
328
- params: queryParams,
329
- });
330
- if (result.statusCode === "error" || !result.body) {
331
- setState((store) => store.setNetworkStatus(networkStatus_1.NetworkStatus.error));
332
- return {
333
- status: result.statusCode,
334
- data: result.error || result.body,
335
- };
336
- }
337
- const response = {
338
- entries: result.body.entries,
339
- meta: result.body.meta,
340
- page_info: result.body.page_info,
341
- };
342
- if (options.before) {
343
- const opts = { shouldSetPage: false, shouldAppend: true };
344
- setState((state) => state.setResult(response, opts));
345
- }
346
- else if (options.after) {
347
- const opts = { shouldSetPage: true, shouldAppend: true };
348
- setState((state) => state.setResult(response, opts));
349
- }
350
- else {
351
- setState((state) => state.setResult(response));
352
- }
353
- // Legacy `messages.new` event, should be removed in a future version
354
- this.broadcast("messages.new", response);
355
- // Broadcast the appropriate event type depending on the fetch source
356
- const feedEventType = options.__fetchSource === "socket"
357
- ? "items.received.realtime"
358
- : "items.received.page";
359
- const eventPayload = {
360
- items: response.entries,
361
- metadata: response.meta,
362
- event: feedEventType,
363
- };
364
- this.broadcast(eventPayload.event, eventPayload);
365
- return { data: response, status: result.statusCode };
366
- }
367
- async fetchNextPage() {
368
- // Attempts to fetch the next page of results (if we have any)
369
- const { getState } = this.store;
370
- const { pageInfo } = getState();
371
- if (!pageInfo.after) {
372
- // Nothing more to fetch
373
- return;
374
- }
375
- this.fetch({
376
- after: pageInfo.after,
377
- __loadingType: networkStatus_1.NetworkStatus.fetchMore,
378
- });
379
- }
380
- broadcast(eventName, data) {
381
- this.broadcaster.emit(eventName, data);
382
- }
383
- // Invoked when a new real-time message comes in from the socket
384
- async onNewMessageReceived({ metadata, }) {
385
- this.knock.log("[Feed] Received new real-time message");
386
- // Handle the new message coming in
387
- const { getState, setState } = this.store;
388
- const { items } = getState();
389
- const currentHead = items[0];
390
- // Optimistically set the badge counts
391
- setState((state) => state.setMetadata(metadata));
392
- // Fetch the items before the current head (if it exists)
393
- this.fetch({ before: currentHead?.__cursor, __fetchSource: "socket" });
394
- }
395
- buildUserFeedId() {
396
- return `${this.feedId}:${this.knock.userId}`;
397
- }
398
- optimisticallyPerformStatusUpdate(itemOrItems, type, attrs, badgeCountAttr) {
399
- const { getState, setState } = this.store;
400
- const normalizedItems = Array.isArray(itemOrItems)
401
- ? itemOrItems
402
- : [itemOrItems];
403
- const itemIds = normalizedItems.map((item) => item.id);
404
- if (badgeCountAttr) {
405
- const { metadata } = getState();
406
- // We only want to update the counts of items that have not already been counted towards the
407
- // badge count total to avoid updating the badge count unnecessarily.
408
- const itemsToUpdate = normalizedItems.filter((item) => {
409
- switch (type) {
410
- case "seen":
411
- return item.seen_at === null;
412
- case "unseen":
413
- return item.seen_at !== null;
414
- case "read":
415
- case "interacted":
416
- return item.read_at === null;
417
- case "unread":
418
- return item.read_at !== null;
419
- default:
420
- return true;
421
- }
422
- });
423
- // Tnis is a hack to determine the direction of whether we're
424
- // adding or removing from the badge count
425
- const direction = type.startsWith("un")
426
- ? itemsToUpdate.length
427
- : -itemsToUpdate.length;
428
- setState((store) => store.setMetadata({
429
- ...metadata,
430
- [badgeCountAttr]: Math.max(0, metadata[badgeCountAttr] + direction),
431
- }));
432
- }
433
- // Update the items with the given attributes
434
- setState((store) => store.setItemAttrs(itemIds, attrs));
435
- }
436
- async makeStatusUpdate(itemOrItems, type) {
437
- // Always treat items as a batch to use the corresponding batch endpoint
438
- const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
439
- const itemIds = items.map((item) => item.id);
440
- const result = await this.knock.client().makeRequest({
441
- method: "POST",
442
- url: `/v1/messages/batch/${type}`,
443
- data: { message_ids: itemIds },
444
- });
445
- // Emit the event that these items had their statuses changed
446
- // Note: we do this after the update to ensure that the server event actually completed
447
- this.broadcaster.emit(`items:${type}`, { items });
448
- this.broadcastOverChannel(`items:${type}`, { items });
449
- return result;
450
- }
451
- async makeBulkStatusUpdate(type) {
452
- // The base scope for the call should take into account all of the options currently
453
- // set on the feed, as well as being scoped for the current user. We do this so that
454
- // we ONLY make changes to the messages that are currently in view on this feed, and not
455
- // all messages that exist.
456
- const options = {
457
- user_ids: [this.knock.userId],
458
- engagement_status: this.defaultOptions.status !== "all"
459
- ? this.defaultOptions.status
460
- : undefined,
461
- archived: this.defaultOptions.archived,
462
- has_tenant: this.defaultOptions.has_tenant,
463
- tenants: this.defaultOptions.tenant
464
- ? [this.defaultOptions.tenant]
465
- : undefined,
466
- };
467
- return await this.knock.client().makeRequest({
468
- method: "POST",
469
- url: `/v1/channels/${this.feedId}/messages/bulk/${type}`,
470
- data: options,
471
- });
472
- }
473
- setupBroadcastChannel() {
474
- // Attempt to bind to listen to other events from this feed in different tabs
475
- // Note: here we ensure `self` is available (it's not in server rendered envs)
476
- this.broadcastChannel =
477
- typeof self !== "undefined" && "BroadcastChannel" in self
478
- ? new BroadcastChannel(`knock:feed:${this.userFeedId}`)
479
- : null;
480
- // Opt into receiving updates from _other tabs for the same user / feed_ via the broadcast
481
- // channel (iff it's enabled and exists)
482
- if (this.broadcastChannel &&
483
- this.defaultOptions.__experimentalCrossBrowserUpdates === true) {
484
- this.broadcastChannel.onmessage = (e) => {
485
- switch (e.data.type) {
486
- case "items:archived":
487
- case "items:unarchived":
488
- case "items:seen":
489
- case "items:unseen":
490
- case "items:read":
491
- case "items:unread":
492
- case "items:all_read":
493
- case "items:all_seen":
494
- case "items:all_archived":
495
- // When items are updated in any other tab, simply refetch to get the latest state
496
- // to make sure that the state gets updated accordingly. In the future here we could
497
- // maybe do this optimistically without the fetch.
498
- return this.fetch();
499
- default:
500
- return null;
501
- }
502
- };
503
- }
504
- }
505
- broadcastOverChannel(type, payload) {
506
- // The broadcastChannel may not be available in non-browser environments
507
- if (!this.broadcastChannel) {
508
- return;
509
- }
510
- // Here we stringify our payload and try and send as JSON such that we
511
- // don't get any `An object could not be cloned` errors when trying to broadcast
512
- try {
513
- const stringifiedPayload = JSON.parse(JSON.stringify(payload));
514
- this.broadcastChannel.postMessage({
515
- type,
516
- payload: stringifiedPayload,
517
- });
518
- }
519
- catch (e) {
520
- console.warn(`Could not broadcast ${type}, got error: ${e}`);
521
- }
522
- }
523
- initializeRealtimeConnection() {
524
- const { socket: maybeSocket } = this.knock.client();
525
- // In server environments we might not have a socket connection
526
- if (!maybeSocket)
527
- return;
528
- // Reinitialize channel connections incase the socket changed
529
- this.channel = maybeSocket.channel(`feeds:${this.userFeedId}`, this.defaultOptions);
530
- this.channel.on("new-message", (resp) => this.onNewMessageReceived(resp));
531
- if (this.defaultOptions.auto_manage_socket_connection) {
532
- this.setupAutoSocketManager(this.defaultOptions.auto_manage_socket_connection_delay);
533
- }
534
- // If we're initializing but they have previously opted to listen to real-time updates
535
- // then we will automatically reconnect on their behalf
536
- if (this.hasSubscribedToRealTimeUpdates) {
537
- if (!maybeSocket.isConnected())
538
- maybeSocket.connect();
539
- this.channel.join();
540
- }
541
- }
542
- /**
543
- * Listen for changes to document visibility and automatically disconnect
544
- * or reconnect the socket after a delay
545
- */
546
- setupAutoSocketManager(autoManageSocketConnectionDelay) {
547
- const disconnectDelay = autoManageSocketConnectionDelay ?? DEFAULT_DISCONNECT_DELAY;
548
- document.addEventListener("visibilitychange", () => {
549
- const client = this.knock.client();
550
- if (document.visibilityState === "hidden") {
551
- // When the tab is hidden, clean up the socket connection after a delay
552
- this.disconnectTimer = setTimeout(() => {
553
- client.socket?.disconnect();
554
- this.disconnectTimer = null;
555
- }, disconnectDelay);
556
- }
557
- else if (document.visibilityState === "visible") {
558
- // When the tab is visible, clear the disconnect timer if active to cancel disconnecting
559
- // This handles cases where the tab is only briefly hidden to avoid unnecessary disconnects
560
- if (this.disconnectTimer) {
561
- clearTimeout(this.disconnectTimer);
562
- this.disconnectTimer = null;
563
- }
564
- // If the socket is not connected, try to reconnect
565
- if (!client.socket?.isConnected()) {
566
- this.initializeRealtimeConnection();
567
- }
568
- }
569
- });
570
- }
571
- }
572
- exports.default = Feed;
@@ -1,15 +0,0 @@
1
- import Knock from "../../knock";
2
- import Feed from "./feed";
3
- import { FeedClientOptions } from "./interfaces";
4
- declare class FeedClient {
5
- private instance;
6
- private feedInstances;
7
- constructor(instance: Knock);
8
- initialize(feedChannelId: string, options?: FeedClientOptions): Feed;
9
- removeInstance(feed: Feed): void;
10
- teardownInstances(): void;
11
- reinitializeInstances(): void;
12
- }
13
- export { Feed };
14
- export default FeedClient;
15
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/clients/feed/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,aAAa,CAAC;AAEhC,OAAO,IAAI,MAAM,QAAQ,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,cAAM,UAAU;IACd,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,aAAa,CAAc;gBAEvB,QAAQ,EAAE,KAAK;IAI3B,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB;IAOjE,cAAc,CAAC,IAAI,EAAE,IAAI;IAIzB,iBAAiB;IAMjB,qBAAqB;CAKtB;AAED,OAAO,EAAE,IAAI,EAAE,CAAC;AAChB,eAAe,UAAU,CAAC"}
@@ -1,34 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Feed = void 0;
7
- const feed_1 = __importDefault(require("./feed"));
8
- exports.Feed = feed_1.default;
9
- class FeedClient {
10
- instance;
11
- feedInstances = [];
12
- constructor(instance) {
13
- this.instance = instance;
14
- }
15
- initialize(feedChannelId, options = {}) {
16
- const feedInstance = new feed_1.default(this.instance, feedChannelId, options);
17
- this.feedInstances.push(feedInstance);
18
- return feedInstance;
19
- }
20
- removeInstance(feed) {
21
- this.feedInstances = this.feedInstances.filter((f) => f !== feed);
22
- }
23
- teardownInstances() {
24
- for (const feed of this.feedInstances) {
25
- feed.teardown();
26
- }
27
- }
28
- reinitializeInstances() {
29
- for (const feed of this.feedInstances) {
30
- feed.reinitialize();
31
- }
32
- }
33
- }
34
- exports.default = FeedClient;
@@ -1,60 +0,0 @@
1
- import { GenericData, PageInfo } from "@knocklabs/types";
2
- import { Activity, Recipient } from "../../interfaces";
3
- import { NetworkStatus } from "../../networkStatus";
4
- export interface FeedClientOptions {
5
- before?: string;
6
- after?: string;
7
- page_size?: number;
8
- status?: "unread" | "read" | "unseen" | "seen" | "all";
9
- source?: string;
10
- tenant?: string;
11
- has_tenant?: boolean;
12
- workflow_categories?: string[];
13
- archived?: "include" | "exclude" | "only";
14
- trigger_data?: GenericData;
15
- __experimentalCrossBrowserUpdates?: boolean;
16
- auto_manage_socket_connection?: boolean;
17
- auto_manage_socket_connection_delay?: number;
18
- }
19
- export type FetchFeedOptions = {
20
- __loadingType?: NetworkStatus.loading | NetworkStatus.fetchMore;
21
- __fetchSource?: "socket" | "http";
22
- } & Omit<FeedClientOptions, "__experimentalCrossBrowserUpdates">;
23
- export interface ContentBlock {
24
- content: string;
25
- rendered: string;
26
- type: "markdown" | "text";
27
- name: string;
28
- }
29
- export interface NotificationSource {
30
- key: string;
31
- version_id: string;
32
- }
33
- export interface FeedItem<T = GenericData> {
34
- __cursor: string;
35
- id: string;
36
- activities: Activity<T>[];
37
- actors: Recipient[];
38
- blocks: ContentBlock[];
39
- inserted_at: string;
40
- updated_at: string;
41
- read_at: string | null;
42
- seen_at: string | null;
43
- archived_at: string | null;
44
- total_activities: number;
45
- total_actors: number;
46
- data: T | null;
47
- source: NotificationSource;
48
- tenant: string | null;
49
- }
50
- export interface FeedMetadata {
51
- total_count: number;
52
- unread_count: number;
53
- unseen_count: number;
54
- }
55
- export interface FeedResponse {
56
- entries: FeedItem[];
57
- meta: FeedMetadata;
58
- page_info: PageInfo;
59
- }
60
- //# sourceMappingURL=interfaces.d.ts.map