@knocklabs/client 0.8.17 → 0.8.19

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