@cloudflare/realtimekit-ui 1.1.0-staging.6 → 1.1.0-staging.8

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 (35) hide show
  1. package/dist/browser.js +1 -1
  2. package/dist/cjs/rtk-avatar_24.cjs.entry.js +240 -213
  3. package/dist/cjs/rtk-chat-toggle.cjs.entry.js +1 -1
  4. package/dist/cjs/rtk-notifications.cjs.entry.js +4 -1
  5. package/dist/collection/components/rtk-chat/rtk-chat.js +16 -2
  6. package/dist/collection/components/rtk-chat-messages-ui-paginated/rtk-chat-messages-ui-paginated.js +36 -16
  7. package/dist/collection/components/rtk-chat-toggle/rtk-chat-toggle.js +1 -1
  8. package/dist/collection/components/rtk-notifications/rtk-notifications.js +4 -1
  9. package/dist/collection/components/rtk-paginated-list/rtk-paginated-list.js +213 -200
  10. package/dist/components/{p-85872241.js → p-7e90e964.js} +18 -4
  11. package/dist/components/{p-b6781e91.js → p-9213c3fc.js} +17 -17
  12. package/dist/components/{p-e7e2156a.js → p-ad8282dc.js} +208 -195
  13. package/dist/components/rtk-chat-messages-ui-paginated.js +1 -1
  14. package/dist/components/rtk-chat-search-results.js +1 -1
  15. package/dist/components/rtk-chat-toggle.js +1 -1
  16. package/dist/components/rtk-chat.js +1 -1
  17. package/dist/components/rtk-meeting.js +3 -3
  18. package/dist/components/rtk-notifications.js +4 -1
  19. package/dist/components/rtk-paginated-list.js +1 -1
  20. package/dist/docs/docs-components.json +29 -9
  21. package/dist/esm/loader.js +244 -214
  22. package/dist/esm/rtk-avatar_24.entry.js +240 -213
  23. package/dist/esm/rtk-chat-toggle.entry.js +1 -1
  24. package/dist/esm/rtk-notifications.entry.js +4 -1
  25. package/dist/realtimekit-ui/p-342b4926.entry.js +1 -0
  26. package/dist/realtimekit-ui/p-8f4f3160.entry.js +1 -0
  27. package/dist/realtimekit-ui/{p-421e4c6f.entry.js → p-ec5ed8a4.entry.js} +1 -1
  28. package/dist/realtimekit-ui/realtimekit-ui.esm.js +1 -1
  29. package/dist/types/components/rtk-chat/rtk-chat.d.ts +1 -0
  30. package/dist/types/components/rtk-chat-messages-ui-paginated/rtk-chat-messages-ui-paginated.d.ts +2 -0
  31. package/dist/types/components/rtk-paginated-list/rtk-paginated-list.d.ts +35 -48
  32. package/dist/types/components.d.ts +8 -3
  33. package/package.json +1 -1
  34. package/dist/realtimekit-ui/p-19587963.entry.js +0 -1
  35. package/dist/realtimekit-ui/p-a859d883.entry.js +0 -1
@@ -1,30 +1,28 @@
1
1
  /**
2
- * HOW INFINITE SCROLL WORKS:
2
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
3
3
  *
4
- * We use intersectionObserver to scroll up.
5
- * We use scrollEnd listener to scroll down.
4
+ * Uses scrollend listener for 2way scrolling.
5
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
6
6
  *
7
- * Why?
8
- * intersectionObserver doesn't work reliably for 2 way scrolling but has great ux,
9
- * so we use it to smoothly scroll up.
7
+ * UPWARD SCROLLING:
8
+ * - Fetch top anchor (element currently visible to the user near top)
9
+ * - Fetch older messages, push to end of 2D array
10
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
10
11
  *
11
- * We have empty divs at the top and bottom ($topRef, $bottomRef)
12
- * which act as triggers to tell that we have reached the top or end of our messages and need to fetch new messages,
12
+ * DOWNWARD SCROLLING:
13
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
14
+ * - Fetch new page, insert at the start
15
+ * - Update timestamps & firstEmptyIndex, then rerender
16
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
13
17
  *
14
- * When scrolling up, we can't remove pages as intersectionObserver relies on
15
- * the index of dom elements to work properly.
16
- * So instead, we fetch older messages and push them to the end of the 2d array
17
- * if length exceeds pagesAllowed, we free up the pages and keep the first empty index in memory (firstEmptyIndex).
18
+ * ADDING NEW NODES:
19
+ * - If no pages exist, load old page
20
+ * - If on 1st page, append messages till page size is full and then load new page
18
21
  *
19
- * For scrolling down, when scroll ends we see if the bottomRef is in view.
20
- * If yes, we fetch the new page and insert it at the firstEmptyIndex.
21
- * We update timestamps & firstEmptyIndex, then we rerender.
22
- *
23
- * If we have exceeded our page allowance we delete old pages.
24
- *
25
- * In this case deleting pages is okay as we are not relying on the index of dom elements to detect page end.
26
- *
27
- * This also simplifies the code because when a user scrolls up we do not need to manage a lastEmptyIndex.
22
+ * DELETE NODE:
23
+ * - If deleting the only available node, reset to initial state
24
+ * - If page is empty, delete it
25
+ * - Update timestamp curors
28
26
  */
29
27
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
30
28
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -36,50 +34,34 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
36
34
  r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
37
35
  return c > 3 && r && Object.defineProperty(target, key, r), r;
38
36
  };
39
- import { Host, h, writeTask } from "@stencil/core";
37
+ import { Host, h } from "@stencil/core";
40
38
  import { defaultIconPack } from "../../lib/icons";
41
39
  import { useLanguage } from "../../lib/lang";
42
40
  import { SyncWithStore } from "../../utils/sync-with-store";
43
41
  import { debounce } from "lodash-es";
44
42
  export class RtkPaginatedList {
45
43
  constructor() {
46
- /**
47
- * when scrolling up, we can't remove pages as intersectionObserver relies on
48
- * the index of dom elements to stay stable.
49
- * So, instead we free up the pages and keep the last empty index in memory.
50
- */
51
- this.firstEmptyIndex = -1;
52
- this.maxTS = 0;
53
44
  // the length of pages will always be pageSize + 2
54
45
  this.pages = [];
46
+ // Controls whether to keep auto-scrolling when a new page load.
47
+ this.shouldScrollToBottom = false;
48
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
49
+ this.showNewMessagesCTR = false;
55
50
  /** label to show when empty */
56
51
  this.emptyListLabel = null;
57
- this.rerenderBoolean = false;
58
- this.showEmptyListLabel = false;
59
52
  /** Icon pack */
60
53
  this.iconPack = defaultIconPack;
61
54
  /** Language */
62
55
  this.t = useLanguage();
56
+ this.rerenderBoolean = false;
57
+ this.showEmptyListLabel = false;
63
58
  this.isLoading = false;
64
59
  this.isLoadingTop = false;
65
60
  this.isLoadingBottom = false;
66
- /**
67
- * Even when auto scroll is enabled, we only want to scroll if a new realtime message has arrived.
68
- * This variable tells us if we should respect auto scroll after a new page has been loaded.
69
- * It is also used by the scroll to bottom button.
70
- * */
71
- this.shouldScrollToBottom = false;
72
- /** UI Indicator for the "scroll to bottom" button.
73
- * Toggles on when a new node is added and autoscroll is disabled.
74
- * Toggles off when all nodes are loaded */
75
- this.showNewMessagesCTR = false;
76
- this.observe = (el) => {
77
- if (!el)
78
- return;
79
- this.intersectionObserver.observe(el);
80
- };
81
- this.isAtBottom = () => {
82
- const rect = this.$bottomRef.getBoundingClientRect();
61
+ // Tells us if we need to scroll to a specific anchor after a rerender
62
+ this.pendingScrollAnchor = null;
63
+ this.isInView = (el) => {
64
+ const rect = el.getBoundingClientRect();
83
65
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
84
66
  };
85
67
  }
@@ -88,224 +70,255 @@ export class RtkPaginatedList {
88
70
  * @param {DataNode} node - The data node to add to the beginning of the list
89
71
  */
90
72
  async onNewNode(node) {
91
- // Always update the maxTS. New messages will load on scroll till the end cursor (newTS) reaches this value.
92
- this.maxTS = Math.max(this.maxTS, node.timeMs);
93
- // if we are at the bottom of the page
94
- if (this.firstEmptyIndex === -1) {
95
- // if there are no pages, load the first page
96
- if (this.pages.length < 1) {
97
- // update old timer to 1ms ahead of the latest message as we subtract this value to avoid loading duplicate messages when scrolling
98
- this.oldTS = node.timeMs + 1;
99
- this.loadPrevPage();
73
+ // if there are no pages, load the first page
74
+ if (this.pages.length < 1) {
75
+ this.oldTS = node.timeMs + 1;
76
+ this.loadPrevPage();
77
+ }
78
+ else if (this.maxTS === this.newTS) {
79
+ this.maxTS = node.timeMs;
80
+ // append messages to the page if page has not reached full capacity
81
+ if (this.pages[0].length < this.pageSize) {
82
+ this.pages[0].unshift(node);
83
+ this.newTS = node.timeMs;
84
+ this.rerender();
100
85
  }
101
86
  else {
102
- // append messages to the page if page has not reached full capacity
103
- if (this.pages[0].length < this.pageSize) {
104
- this.pages[0].unshift(node);
105
- this.newTS = node.timeMs;
106
- this.rerender();
107
- }
108
- else {
109
- // if page is at full capacity, load next page
110
- this.loadNextPage();
111
- }
87
+ // if page is at full capacity, load next page
88
+ this.loadNextPage();
112
89
  }
113
90
  }
114
- // If autoscroll is enabled, this method will scroll to the bottom
91
+ // If autoscroll is enabled, scroll to the bottom
115
92
  if (this.autoScroll) {
116
93
  this.shouldScrollToBottom = true;
117
94
  this.scrollToBottom();
118
95
  }
119
- else {
120
- this.showNewMessagesCTR = true;
121
- }
122
- }
123
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
124
- scrollToBottom() {
125
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
126
96
  }
127
97
  /**
128
98
  * Deletes a node anywhere from the list
129
99
  * @param {string} id - The id of the node to delete
130
100
  * */
131
101
  async onNodeDelete(id) {
132
- // Iterate only over pages that have content (not empty)
133
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
102
+ var _a, _b;
103
+ let didDelete = false;
104
+ for (let i = this.pages.length - 1; i >= 0; i--) {
134
105
  const index = this.pages[i].findIndex((node) => node.id === id);
135
- // message in view
136
- if (index !== -1) {
137
- // delete message
138
- this.pages[i].splice(index, 1);
139
- // if we are on the first page and it's now empty, we need to go back to initial state
140
- if (i === 0 && this.pages[i].length === 0) {
141
- this.pages.shift();
142
- this.firstEmptyIndex = -1;
143
- }
144
- else if (i === this.firstEmptyIndex + 1) {
145
- // if newest page is empty, update first empty index
146
- if (this.pages[i].length === 0)
147
- this.firstEmptyIndex++;
148
- // update timestamp, first empty index could be -1, so we need to cap it at 0
149
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
150
- }
151
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
152
- // if oldest page is empty, remove it
153
- if (this.pages[i].length === 0)
154
- this.pages.pop();
155
- // update timestamp
156
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
157
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
158
- }
159
- this.rerender();
160
- }
106
+ // if message not found, move on
107
+ if (index === -1)
108
+ continue;
109
+ // delete message
110
+ this.pages[i].splice(index, 1);
111
+ // if page is empty, delete it
112
+ if (this.pages[i].length === 0)
113
+ this.pages.splice(i, 1);
114
+ didDelete = true;
115
+ break;
161
116
  }
117
+ if (!didDelete)
118
+ return;
119
+ // update timestamps
120
+ const firstPage = this.pages[0];
121
+ const lastPage = this.pages[this.pages.length - 1];
122
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
123
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
124
+ this.rerender();
162
125
  }
163
126
  /**
164
127
  * Updates a new node anywhere in the list
165
- * @param {string} _id - The id of the node to update
166
- * @param {DataNode} _node - The updated data node
128
+ * @param {string} id - The id of the node to update
129
+ * @param {DataNode} node - The updated data node
167
130
  * */
168
- async onNodeUpdate(_id, _node) { }
169
- rerender() {
170
- this.rerenderBoolean = !this.rerenderBoolean;
131
+ async onNodeUpdate(id, node) {
132
+ for (let i = this.pages.length - 1; i >= 0; i--) {
133
+ const index = this.pages[i].findIndex((node) => node.id === id);
134
+ // if message not found, move on
135
+ if (index === -1)
136
+ continue;
137
+ // edit message
138
+ this.pages[i][index] = node;
139
+ this.rerender();
140
+ break;
141
+ }
171
142
  }
172
143
  connectedCallback() {
173
144
  this.rerender = debounce(this.rerender.bind(this), 50, { maxWait: 200 });
174
- this.intersectionObserver = new IntersectionObserver((entries) => {
175
- writeTask(async () => {
176
- for (const entry of entries) {
177
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
178
- this.isLoadingTop = true;
179
- await this.loadPrevPage();
180
- this.isLoadingTop = false;
181
- }
182
- }
183
- });
184
- });
185
145
  }
186
146
  componentDidLoad() {
187
- this.observe(this.$topRef);
147
+ // initial load
148
+ this.loadPrevPage();
188
149
  if (this.$containerRef) {
189
150
  this.$containerRef.onscrollend = async () => {
190
- /**
191
- * Load new page if:
192
- * if there are nodes to load at the bottom (maxTS > newTS)
193
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
194
- */
195
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
196
- this.isLoadingBottom = true;
151
+ if (this.isInView(this.$bottomRef)) {
197
152
  await this.loadNextPage();
198
- this.isLoadingBottom = false;
199
- if (this.shouldScrollToBottom)
200
- this.scrollToBottom();
153
+ }
154
+ else if (this.isInView(this.$topRef)) {
155
+ this.showNewMessagesCTR = true;
156
+ await this.loadPrevPage();
201
157
  }
202
158
  };
203
159
  }
204
160
  }
161
+ componentDidRender() {
162
+ if (!this.pendingScrollAnchor)
163
+ return;
164
+ const anchor = this.pendingScrollAnchor;
165
+ this.pendingScrollAnchor = null;
166
+ this.restoreScrollToAnchor(anchor);
167
+ }
205
168
  async loadPrevPage() {
206
169
  if (this.isLoading)
207
170
  return;
208
- /**
209
- * NOTE(ikabra): this case also runs on initial load
210
- * if scrolling up ->
211
- * fetch older messages and push to the end of the array
212
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
213
- */
171
+ const scrollAnchor = this.getScrollAnchor('top');
214
172
  // if no old timestamp, it means we are at initial state
215
173
  if (!this.oldTS)
216
174
  this.oldTS = new Date().getTime();
217
175
  // load data
218
176
  this.isLoading = true;
177
+ this.isLoadingTop = true;
219
178
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
220
179
  this.isLoading = false;
180
+ this.isLoadingTop = false;
221
181
  // no more old messages to show, we are at the top of the page
222
182
  if (!data.length)
223
183
  return;
224
184
  // add old data to the end of the array
225
185
  this.pages.push(data);
226
186
  // clear old pages when we reach the limit
227
- if (this.pages.length > this.pagesAllowed) {
228
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
229
- /**
230
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
231
- * we are doing this because any of the middle pages in the currently rendered pages
232
- * could be empty as we allow deleting messages.
233
- * This helps us set the first empty index correctly.
234
- */
235
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
236
- if (this.pages[i].length > 0)
237
- break;
238
- this.firstEmptyIndex = i;
239
- }
240
- }
241
- // update the old timestamp
187
+ if (this.pages.length > this.pagesAllowed)
188
+ this.pages.shift();
189
+ // update timestamps
242
190
  const lastPage = this.pages[this.pages.length - 1];
243
191
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
244
- // update the new timestamp
245
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
192
+ this.newTS = this.pages[0][0].timeMs;
193
+ if (!this.maxTS)
194
+ this.maxTS = this.newTS;
246
195
  this.rerender();
196
+ // fix scroll position
197
+ if (scrollAnchor)
198
+ this.pendingScrollAnchor = scrollAnchor;
247
199
  }
248
200
  async loadNextPage() {
249
201
  if (this.isLoading)
250
202
  return;
251
- // new timestamp needs to be assigned by loadPrevPage method
203
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
252
204
  if (!this.newTS) {
253
205
  this.showNewMessagesCTR = false;
254
206
  this.shouldScrollToBottom = false;
255
207
  return;
256
208
  }
257
- // load data
209
+ // for autoscroll or scroll to bottom button
210
+ const maxAutoLoads = 200;
211
+ let loads = 0;
212
+ let prevNewTS = this.newTS;
258
213
  this.isLoading = true;
259
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
260
- this.isLoading = false;
261
- // no more new messages to load
262
- if (!data.length) {
263
- this.showNewMessagesCTR = false;
264
- this.shouldScrollToBottom = false;
265
- // remove extra pages from the start if any (could be due to users deleting messages)
266
- this.pages = this.pages.filter((page) => page.length > 0);
267
- this.firstEmptyIndex = -1;
268
- return;
269
- }
270
- // when filling empty pages
271
- if (this.firstEmptyIndex > -1) {
272
- this.pages[this.firstEmptyIndex] = data.reverse();
273
- }
274
- else {
275
- // when already at the bottom and loading messages in realtime
214
+ this.isLoadingBottom = true;
215
+ while (loads < maxAutoLoads) {
216
+ const scrollAnchor = this.getScrollAnchor('bottom');
217
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
218
+ this.isLoading = false;
219
+ this.isLoadingBottom = false;
220
+ // no more new messages to load
221
+ if (!data.length) {
222
+ this.maxTS = this.newTS;
223
+ this.showNewMessagesCTR = false;
224
+ this.shouldScrollToBottom = false;
225
+ break;
226
+ }
227
+ // load new messages and append to the start
276
228
  this.pages.unshift(data.reverse());
229
+ // remove pages if out of bounds
230
+ if (this.pages.length > this.pagesAllowed)
231
+ this.pages.pop();
232
+ // update timestamps
233
+ const lastPage = this.pages[this.pages.length - 1];
234
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
235
+ this.newTS = this.pages[0][0].timeMs;
236
+ this.rerender();
237
+ this.pendingScrollAnchor = scrollAnchor;
238
+ if (!this.shouldScrollToBottom)
239
+ break;
240
+ // if should scroll to bottom then retrigger
241
+ await this.waitForNextFrame();
242
+ this.scrollToBottom();
243
+ await this.waitForNextFrame();
244
+ // if no new messages, break
245
+ if (this.newTS === prevNewTS)
246
+ break;
247
+ prevNewTS = this.newTS;
248
+ loads++;
249
+ }
250
+ }
251
+ // Find the element that is closest to the top/bottom of the container
252
+ getScrollAnchor(edge = 'top') {
253
+ if (!this.$containerRef)
254
+ return null;
255
+ const containerRect = this.$containerRef.getBoundingClientRect();
256
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
257
+ let best = null;
258
+ for (const el of candidates) {
259
+ const rect = el.getBoundingClientRect();
260
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
261
+ if (!isVisibleInContainer)
262
+ continue;
263
+ if (edge === 'top') {
264
+ const offsetTop = rect.top - containerRect.top;
265
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
266
+ best = { id: el.id, edge: 'top', offsetTop };
267
+ }
268
+ }
269
+ else {
270
+ const offsetBottom = containerRect.bottom - rect.bottom;
271
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
272
+ best = { id: el.id, edge: 'bottom', offsetBottom };
273
+ }
274
+ }
277
275
  }
278
- if (this.pages.length > this.pagesAllowed) {
279
- this.pages.pop();
276
+ return best;
277
+ }
278
+ //instant scroll to anchor to make sure we are at the same position after a rerender
279
+ restoreScrollToAnchor(anchor) {
280
+ if (!this.$containerRef)
281
+ return;
282
+ // make element id safe to use inside a CSS selector
283
+ const escapeId = (id) => {
284
+ var _a;
285
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
286
+ return typeof cssEscape === 'function'
287
+ ? cssEscape(id)
288
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
289
+ };
290
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
291
+ if (!el)
292
+ return;
293
+ const containerRect = this.$containerRef.getBoundingClientRect();
294
+ const rect = el.getBoundingClientRect();
295
+ if (anchor.edge === 'top') {
296
+ const newOffsetTop = rect.top - containerRect.top;
297
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
280
298
  }
281
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
282
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
283
- // remove all empty pages from the end
284
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
285
- if (this.pages[i].length > 0)
286
- break;
287
- // if page is empty, remove it
288
- this.pages.pop();
299
+ else {
300
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
301
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
289
302
  }
290
- // update the old timestamp
291
- const lastPage = this.pages[this.pages.length - 1];
292
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
293
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
294
- if (this.$containerRef.scrollTop === 0)
295
- this.$containerRef.scrollTop = -60;
296
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
297
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
298
- this.rerender();
303
+ }
304
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
305
+ scrollToBottom() {
306
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
307
+ }
308
+ waitForNextFrame() {
309
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
310
+ }
311
+ rerender() {
312
+ this.rerenderBoolean = !this.rerenderBoolean;
299
313
  }
300
314
  render() {
301
315
  /**
302
- * div.container is flex=column-reverse
303
- * which is why div#bottom-scroll comes before div#top-scroll
316
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
304
317
  */
305
- return (h(Host, { key: '9bf695bccf42f2dd43b04725a855b6b77a4062fd' }, h("div", { key: '7c6805ed8b5e831ea6d932cfb5dc3ecf3d312775', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, h("div", { key: 'eb6c68f97385b5a14a94ab2dc0abff4836f016f3', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, h("rtk-button", { key: 'd1f04db290a9e4128ca58c9bd20b2146a7fc8f12', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
318
+ return (h(Host, { key: '5f036ac16ace127734d5ee172d537c64baeab415' }, h("div", { key: 'b6d8cf3019a72350f7a3a5b4d020b6ab39793f53', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, h("div", { key: '5c63462ffd995a3e266652bba4e3377636c5f9ca', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, h("rtk-button", { key: 'c1fc4f2759d5be662047245b0dae3eb6f65a9b50', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
306
319
  this.shouldScrollToBottom = true;
307
320
  this.scrollToBottom();
308
- } }, h("rtk-icon", { key: '71d8a85911513bb4069578a63ae45f21ac53554c', icon: this.iconPack.chevron_down }))), h("div", { key: 'b1aa46b8163431df6060163d7c5a87eb1ebcffa3', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && h("rtk-spinner", { key: 'a3de0e7b8ca1ac505a596d86c0b8ecafee5fabba', size: "sm" }), this.isLoading && this.pages.length < 1 && h("rtk-spinner", { key: '21fa3a4091d3f1311f5d575c277859602718338c', size: "lg" }), !this.isLoading && this.pages.flat().length === 0 ? (h("div", { class: "empty-list" }, this.t('list.empty'))) : (h("div", { class: "page-wrapper" }, this.pages.map((page, pageIndex) => (h("div", { class: "page", "data-page-index": pageIndex }, this.createNodes([...page].reverse())))))), this.isLoadingTop && this.pages.length > 0 && h("rtk-spinner", { key: '7e018af534e7cd0be3345ca9ad9c8f0a7ab7cc3f', size: "sm" }), h("div", { key: '6c877368fc03c884338402bbcd878929cd4034fb', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
321
+ } }, h("rtk-icon", { key: '96b19395a2ca8e87ca5004f675cf79f8d58f036c', icon: this.iconPack.chevron_down }))), h("div", { key: '84789a3d0fa4645be711a87cda1e109e4f7d0db2', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && h("rtk-spinner", { key: 'd17f28cc01695220ed6e705d528e8f555b77e8ea', size: "sm" }), this.isLoading && this.pages.length < 1 && h("rtk-spinner", { key: '4ee308d335cdc1f86e2bfdcc20611f50a51ef816', size: "lg" }), !this.isLoading && this.pages.flat().length === 0 ? (h("div", { class: "empty-list" }, this.t('list.empty'))) : (h("div", { class: "page-wrapper" }, this.pages.map((page, pageIndex) => (h("div", { class: "page", "data-page-index": pageIndex }, this.createNodes([...page].reverse())))))), this.isLoadingTop && this.pages.length > 0 && h("rtk-spinner", { key: 'c07c4dd4c4ed0adf01d2fc224b7196a0f86243fd', size: "sm" }), h("div", { key: '52161ac30062c2262f1cdbcded50f2716f6ed20e', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
309
322
  }
310
323
  static get is() { return "rtk-paginated-list"; }
311
324
  static get encapsulation() { return "shadow"; }
@@ -578,13 +591,13 @@ export class RtkPaginatedList {
578
591
  },
579
592
  "onNodeUpdate": {
580
593
  "complexType": {
581
- "signature": "(_id: string, _node: DataNode) => Promise<void>",
594
+ "signature": "(id: string, node: DataNode) => Promise<void>",
582
595
  "parameters": [{
583
- "name": "_id",
596
+ "name": "id",
584
597
  "type": "string",
585
598
  "docs": "- The id of the node to update"
586
599
  }, {
587
- "name": "_node",
600
+ "name": "node",
588
601
  "type": "DataNode",
589
602
  "docs": "- The updated data node"
590
603
  }],
@@ -605,10 +618,10 @@ export class RtkPaginatedList {
605
618
  "text": "Updates a new node anywhere in the list",
606
619
  "tags": [{
607
620
  "name": "param",
608
- "text": "_id - The id of the node to update"
621
+ "text": "id - The id of the node to update"
609
622
  }, {
610
623
  "name": "param",
611
- "text": "_node - The updated data node"
624
+ "text": "node - The updated data node"
612
625
  }]
613
626
  }
614
627
  }
@@ -7,7 +7,7 @@ import { d as defineCustomElement$m } from './p-241a8245.js';
7
7
  import { d as defineCustomElement$l } from './p-1391bef0.js';
8
8
  import { d as defineCustomElement$k } from './p-a73665b4.js';
9
9
  import { d as defineCustomElement$j } from './p-28170a8d.js';
10
- import { d as defineCustomElement$i } from './p-b6781e91.js';
10
+ import { d as defineCustomElement$i } from './p-9213c3fc.js';
11
11
  import { d as defineCustomElement$h } from './p-1f5a4682.js';
12
12
  import { d as defineCustomElement$g } from './p-598dc3f2.js';
13
13
  import { d as defineCustomElement$f } from './p-0e5cc539.js';
@@ -20,7 +20,7 @@ import { d as defineCustomElement$9 } from './p-2447a26f.js';
20
20
  import { d as defineCustomElement$8 } from './p-819cb785.js';
21
21
  import { d as defineCustomElement$7 } from './p-7148ec6a.js';
22
22
  import { d as defineCustomElement$6 } from './p-0f2de0f8.js';
23
- import { d as defineCustomElement$5 } from './p-e7e2156a.js';
23
+ import { d as defineCustomElement$5 } from './p-ad8282dc.js';
24
24
  import { d as defineCustomElement$4 } from './p-4ebf9684.js';
25
25
  import { d as defineCustomElement$3 } from './p-4902c5cf.js';
26
26
  import { d as defineCustomElement$2 } from './p-6739a399.js';
@@ -306,6 +306,13 @@ const RtkChat = /*@__PURE__*/ proxyCustomElement(class RtkChat extends H {
306
306
  const message = event.detail;
307
307
  this.meeting.chat.deleteMessage(message.id);
308
308
  };
309
+ this.onMessageEdit = (event) => {
310
+ const message = event.detail;
311
+ if (message.type !== 'text')
312
+ return;
313
+ this.replyMessage = null;
314
+ this.editingMessage = message;
315
+ };
309
316
  this.getPrivateChatRecipients = () => {
310
317
  const participants = this.getFilteredParticipants().map((participant) => {
311
318
  const key = generateChatGroupKey([participant.userId, this.meeting.self.userId]);
@@ -490,14 +497,21 @@ const RtkChat = /*@__PURE__*/ proxyCustomElement(class RtkChat extends H {
490
497
  const uiProps = { iconPack: this.iconPack, t: this.t, size: this.size };
491
498
  const message = this.editingMessage ? this.editingMessage.message : '';
492
499
  const quotedMessage = this.replyMessage ? this.replyMessage.message : '';
493
- return (h("rtk-chat-composer-view", Object.assign({ message: message, storageKey: (_a = this.selectedChannelId) !== null && _a !== void 0 ? _a : `draft-${this.selectedChannelId}`, quotedMessage: quotedMessage, isEditing: !!this.editingMessage, canSendTextMessage: this.isTextMessagingAllowed(), canSendFiles: this.isFileMessagingAllowed(), disableEmojiPicker: this.overrides.disableEmojiPicker, maxLength: this.meeting.chat.maxTextLimit, rateLimits: this.meeting.chat.rateLimits, inputTextPlaceholder: this.t('chat.message_placeholder'), onNewMessage: this.onNewMessageHandler, onEditMessage: this.onEditMessageHandler, onEditCancel: this.onEditCancel, onQuotedMessageDismiss: this.onQuotedMessageDismiss }, uiProps), h("slot", { name: "chat-addon", slot: "chat-addon" })));
500
+ const draftStorageKey = this.selectedChannelId
501
+ ? `rtk-chat-draft-${this.selectedChannelId}`
502
+ : 'rtk-chat-draft';
503
+ const editStorageKey = this.editingMessage
504
+ ? `rtk-chat-edit-${(_a = this.selectedChannelId) !== null && _a !== void 0 ? _a : 'no-channel'}-${this.editingMessage.id}`
505
+ : 'rtk-chat-edit';
506
+ const storageKey = this.editingMessage ? editStorageKey : draftStorageKey;
507
+ return (h("rtk-chat-composer-view", Object.assign({ message: message, storageKey: storageKey, quotedMessage: quotedMessage, isEditing: !!this.editingMessage, canSendTextMessage: this.isTextMessagingAllowed(), canSendFiles: this.isFileMessagingAllowed(), disableEmojiPicker: this.overrides.disableEmojiPicker, maxLength: this.meeting.chat.maxTextLimit, rateLimits: this.meeting.chat.rateLimits, inputTextPlaceholder: this.t('chat.message_placeholder'), onNewMessage: this.onNewMessageHandler, onEditMessage: this.onEditMessageHandler, onEditCancel: this.onEditCancel, onQuotedMessageDismiss: this.onQuotedMessageDismiss }, uiProps), h("slot", { name: "chat-addon", slot: "chat-addon" })));
494
508
  }
495
509
  render() {
496
510
  var _a;
497
511
  if (!this.meeting) {
498
512
  return null;
499
513
  }
500
- return (h(Host, null, h("div", { class: "chat-container" }, h("div", { class: "chat" }, this.isFileMessagingAllowed() && (h("div", { id: "dropzone", class: { active: this.dropzoneActivated }, part: "dropzone" }, h("rtk-icon", { icon: this.iconPack.attach }), h("p", null, this.t('chat.send_attachment')))), this.renderPinnedMessagesHeader(), this.isPrivateChatSupported() && (h("rtk-channel-selector-view", { channels: this.getPrivateChatRecipients(), selectedChannelId: ((_a = this.selectedParticipant) === null || _a === void 0 ? void 0 : _a.userId) || 'everyone', onChannelChange: this.updateRecipients, t: this.t, viewAs: "dropdown" })), h("rtk-chat-messages-ui-paginated", { meeting: this.meeting, onPinMessage: this.onPinMessage, onDeleteMessage: this.onDeleteMessage, size: this.size, iconPack: this.iconPack, t: this.t }), this.renderComposerUI()))));
514
+ return (h(Host, null, h("div", { class: "chat-container" }, h("div", { class: "chat" }, this.isFileMessagingAllowed() && (h("div", { id: "dropzone", class: { active: this.dropzoneActivated }, part: "dropzone" }, h("rtk-icon", { icon: this.iconPack.attach }), h("p", null, this.t('chat.send_attachment')))), this.renderPinnedMessagesHeader(), this.isPrivateChatSupported() && (h("rtk-channel-selector-view", { channels: this.getPrivateChatRecipients(), selectedChannelId: ((_a = this.selectedParticipant) === null || _a === void 0 ? void 0 : _a.userId) || 'everyone', onChannelChange: this.updateRecipients, t: this.t, viewAs: "dropdown" })), h("rtk-chat-messages-ui-paginated", { meeting: this.meeting, onPinMessage: this.onPinMessage, onEditMessage: this.onMessageEdit, onDeleteMessage: this.onDeleteMessage, size: this.size, iconPack: this.iconPack, t: this.t }), this.renderComposerUI()))));
501
515
  }
502
516
  get host() { return this; }
503
517
  static get watchers() { return {