@cloudflare/realtimekit-ui 1.1.0-staging.5 → 1.1.0-staging.7

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 (31) hide show
  1. package/dist/browser.js +1 -1
  2. package/dist/cjs/rtk-avatar_24.cjs.entry.js +186 -175
  3. package/dist/cjs/rtk-chat-toggle.cjs.entry.js +2 -2
  4. package/dist/cjs/rtk-notifications.cjs.entry.js +4 -1
  5. package/dist/collection/components/rtk-chat-messages-ui-paginated/rtk-chat-messages-ui-paginated.js +2 -2
  6. package/dist/collection/components/rtk-chat-toggle/rtk-chat-toggle.js +2 -2
  7. package/dist/collection/components/rtk-notifications/rtk-notifications.js +4 -1
  8. package/dist/collection/components/rtk-paginated-list/rtk-paginated-list.js +184 -173
  9. package/dist/components/{p-32c6e86d.js → p-1d16490e.js} +2 -2
  10. package/dist/components/{p-ae376177.js → p-7be71567.js} +3 -3
  11. package/dist/components/{p-0d472019.js → p-7f8d9afc.js} +184 -173
  12. package/dist/components/rtk-chat-messages-ui-paginated.js +1 -1
  13. package/dist/components/rtk-chat-search-results.js +1 -1
  14. package/dist/components/rtk-chat-toggle.js +2 -2
  15. package/dist/components/rtk-chat.js +1 -1
  16. package/dist/components/rtk-meeting.js +3 -3
  17. package/dist/components/rtk-notifications.js +4 -1
  18. package/dist/components/rtk-paginated-list.js +1 -1
  19. package/dist/docs/docs-components.json +1 -1
  20. package/dist/esm/loader.js +191 -177
  21. package/dist/esm/rtk-avatar_24.entry.js +186 -175
  22. package/dist/esm/rtk-chat-toggle.entry.js +2 -2
  23. package/dist/esm/rtk-notifications.entry.js +4 -1
  24. package/dist/realtimekit-ui/p-25c13ff8.entry.js +1 -0
  25. package/dist/realtimekit-ui/p-342b4926.entry.js +1 -0
  26. package/dist/realtimekit-ui/{p-f457ae6f.entry.js → p-ec5ed8a4.entry.js} +1 -1
  27. package/dist/realtimekit-ui/realtimekit-ui.esm.js +1 -1
  28. package/dist/types/components/rtk-paginated-list/rtk-paginated-list.d.ts +32 -46
  29. package/package.json +1 -1
  30. package/dist/realtimekit-ui/p-83db4de1.entry.js +0 -1
  31. package/dist/realtimekit-ui/p-a859d883.entry.js +0 -1
@@ -1044,7 +1044,7 @@ const RtkChatMessagesUiPaginated = class {
1044
1044
  }
1045
1045
  const isSelf = message.userId === this.meeting.self.userId;
1046
1046
  const viewType = isSelf ? 'outgoing' : 'incoming';
1047
- return (h("div", null, h("div", { class: "message-wrapper" }, h("rtk-message-view", { pinned: message.pinned, time: message.time, actions: this.getMessageActions(message), authorName: message.displayName, isSelf: isSelf, avatarUrl: displayPicture, hideAuthorName: isContinued, viewType: viewType, variant: "bubble", onAction: (event) => this.onMessageActionHandler(event.detail, message) }, h("div", null, h("div", { class: "body" }, message.type === 'text' && (h("rtk-text-message-view", { text: message.message, isMarkdown: true })), message.type === 'file' && (h("rtk-file-message-view", { name: message.name, url: message.link, size: message.size })), message.type === 'image' && (h("rtk-image-message-view", { url: message.link, onPreview: () => {
1047
+ return (h("div", null, h("div", { class: "message-wrapper", id: message.id }, h("rtk-message-view", { pinned: message.pinned, time: message.time, actions: this.getMessageActions(message), authorName: message.displayName, isSelf: isSelf, avatarUrl: displayPicture, hideAuthorName: isContinued, viewType: viewType, variant: "bubble", onAction: (event) => this.onMessageActionHandler(event.detail, message) }, h("div", null, h("div", { class: "body" }, message.type === 'text' && (h("rtk-text-message-view", { text: message.message, isMarkdown: true })), message.type === 'file' && (h("rtk-file-message-view", { name: message.name, url: message.link, size: message.size })), message.type === 'image' && (h("rtk-image-message-view", { url: message.link, onPreview: () => {
1048
1048
  this.stateUpdate.emit({ image: message });
1049
1049
  } }))))))));
1050
1050
  };
@@ -1092,7 +1092,7 @@ const RtkChatMessagesUiPaginated = class {
1092
1092
  this.lastReadMessageIndex = -1;
1093
1093
  }
1094
1094
  render() {
1095
- return (h(Host, { key: '012b7189dfbdccfd8017cc9023263e6a7e9afd44' }, h("rtk-paginated-list", { key: '0ea37ee880fda0acdd7460b6da5f03e11ac304bf', ref: (el) => (this.$paginatedListRef = el), pageSize: this.pageSize, pagesAllowed: 3, fetchData: this.getChatMessages, createNodes: this.createChatNodes, selectedItemId: this.selectedChannelId, emptyListLabel: this.t('chat.empty_channel') }, h("slot", { key: '53cb197b6d9319f470e87fe73d7ca0d158778e3f' }))));
1095
+ return (h(Host, { key: 'c710da6e2fda420146905a2ed75d3444dd6d2c0b' }, h("rtk-paginated-list", { key: '51a36437e38e9c0242cca34bfda39f6d8309bee3', ref: (el) => (this.$paginatedListRef = el), pageSize: this.pageSize, pagesAllowed: 3, fetchData: this.getChatMessages, createNodes: this.createChatNodes, selectedItemId: this.selectedChannelId, emptyListLabel: this.t('chat.empty_channel') }, h("slot", { key: '69b54a41263510b425ce3e39af055321c4e2deb8' }))));
1096
1096
  }
1097
1097
  get host() { return getElement(this); }
1098
1098
  static get watchers() { return {
@@ -2054,32 +2054,30 @@ const rtkPaginatedListCss = ".scrollbar{scrollbar-width:thin;scrollbar-color:var
2054
2054
  const RtkPaginatedListStyle0 = rtkPaginatedListCss;
2055
2055
 
2056
2056
  /**
2057
- * HOW INFINITE SCROLL WORKS:
2057
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
2058
2058
  *
2059
- * We use intersectionObserver to scroll up.
2060
- * We use scrollEnd listener to scroll down.
2059
+ * Uses scrollend listener for 2way scrolling.
2060
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
2061
2061
  *
2062
- * Why?
2063
- * intersectionObserver doesn't work reliably for 2 way scrolling but has great ux,
2064
- * so we use it to smoothly scroll up.
2062
+ * UPWARD SCROLLING:
2063
+ * - Fetch top anchor (element currently visible to the user near top)
2064
+ * - Fetch older messages, push to end of 2D array
2065
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
2065
2066
  *
2066
- * We have empty divs at the top and bottom ($topRef, $bottomRef)
2067
- * which act as triggers to tell that we have reached the top or end of our messages and need to fetch new messages,
2067
+ * DOWNWARD SCROLLING:
2068
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
2069
+ * - Fetch new page, insert at the start
2070
+ * - Update timestamps & firstEmptyIndex, then rerender
2071
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
2068
2072
  *
2069
- * When scrolling up, we can't remove pages as intersectionObserver relies on
2070
- * the index of dom elements to work properly.
2071
- * So instead, we fetch older messages and push them to the end of the 2d array
2072
- * if length exceeds pagesAllowed, we free up the pages and keep the first empty index in memory (firstEmptyIndex).
2073
+ * ADDING NEW NODES:
2074
+ * - If no pages exist, load old page
2075
+ * - If on 1st page, append messages till page size is full and then load new page
2073
2076
  *
2074
- * For scrolling down, when scroll ends we see if the bottomRef is in view.
2075
- * If yes, we fetch the new page and insert it at the firstEmptyIndex.
2076
- * We update timestamps & firstEmptyIndex, then we rerender.
2077
- *
2078
- * If we have exceeded our page allowance we delete old pages.
2079
- *
2080
- * In this case deleting pages is okay as we are not relying on the index of dom elements to detect page end.
2081
- *
2082
- * This also simplifies the code because when a user scrolls up we do not need to manage a lastEmptyIndex.
2077
+ * DELETE NODE:
2078
+ * - If deleting the only available node, reset to initial state
2079
+ * - If page is empty, delete it
2080
+ * - Update timestamp curors
2083
2081
  */
2084
2082
  var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
2085
2083
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2094,43 +2092,27 @@ var __decorate$2 = (undefined && undefined.__decorate) || function (decorators,
2094
2092
  const RtkPaginatedList = class {
2095
2093
  constructor(hostRef) {
2096
2094
  registerInstance(this, hostRef);
2097
- /**
2098
- * when scrolling up, we can't remove pages as intersectionObserver relies on
2099
- * the index of dom elements to stay stable.
2100
- * So, instead we free up the pages and keep the last empty index in memory.
2101
- */
2102
- this.firstEmptyIndex = -1;
2103
- this.maxTS = 0;
2104
2095
  // the length of pages will always be pageSize + 2
2105
2096
  this.pages = [];
2097
+ // Controls whether to keep auto-scrolling when a new page load.
2098
+ this.shouldScrollToBottom = false;
2099
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
2100
+ this.showNewMessagesCTR = false;
2106
2101
  /** label to show when empty */
2107
2102
  this.emptyListLabel = null;
2108
- this.rerenderBoolean = false;
2109
- this.showEmptyListLabel = false;
2110
2103
  /** Icon pack */
2111
2104
  this.iconPack = defaultIconPack;
2112
2105
  /** Language */
2113
2106
  this.t = useLanguage();
2107
+ this.rerenderBoolean = false;
2108
+ this.showEmptyListLabel = false;
2114
2109
  this.isLoading = false;
2115
2110
  this.isLoadingTop = false;
2116
2111
  this.isLoadingBottom = false;
2117
- /**
2118
- * Even when auto scroll is enabled, we only want to scroll if a new realtime message has arrived.
2119
- * This variable tells us if we should respect auto scroll after a new page has been loaded.
2120
- * It is also used by the scroll to bottom button.
2121
- * */
2122
- this.shouldScrollToBottom = false;
2123
- /** UI Indicator for the "scroll to bottom" button.
2124
- * Toggles on when a new node is added and autoscroll is disabled.
2125
- * Toggles off when all nodes are loaded */
2126
- this.showNewMessagesCTR = false;
2127
- this.observe = (el) => {
2128
- if (!el)
2129
- return;
2130
- this.intersectionObserver.observe(el);
2131
- };
2132
- this.isAtBottom = () => {
2133
- const rect = this.$bottomRef.getBoundingClientRect();
2112
+ // Tells us if we need to scroll to a specific anchor after a rerender
2113
+ this.pendingScrollAnchor = null;
2114
+ this.isInView = (el) => {
2115
+ const rect = el.getBoundingClientRect();
2134
2116
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
2135
2117
  };
2136
2118
  }
@@ -2139,10 +2121,12 @@ const RtkPaginatedList = class {
2139
2121
  * @param {DataNode} node - The data node to add to the beginning of the list
2140
2122
  */
2141
2123
  async onNewNode(node) {
2142
- // Always update the maxTS. New messages will load on scroll till the end cursor (newTS) reaches this value.
2143
- this.maxTS = Math.max(this.maxTS, node.timeMs);
2144
- // if we are at the bottom of the page
2145
- if (this.firstEmptyIndex === -1) {
2124
+ // if there are no pages, load the first page
2125
+ if (this.pages.length < 1) {
2126
+ this.oldTS = node.timeMs + 1;
2127
+ this.loadPrevPage();
2128
+ }
2129
+ else {
2146
2130
  // append messages to the page if page has not reached full capacity
2147
2131
  if (this.pages[0].length < this.pageSize) {
2148
2132
  this.pages[0].unshift(node);
@@ -2154,49 +2138,40 @@ const RtkPaginatedList = class {
2154
2138
  this.loadNextPage();
2155
2139
  }
2156
2140
  }
2157
- // If autoscroll is enabled, this method will scroll to the bottom
2141
+ // If autoscroll is enabled, scroll to the bottom
2158
2142
  if (this.autoScroll) {
2159
2143
  this.shouldScrollToBottom = true;
2160
2144
  this.scrollToBottom();
2161
2145
  }
2162
- else {
2163
- this.showNewMessagesCTR = true;
2164
- }
2165
- }
2166
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
2167
- scrollToBottom() {
2168
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
2169
2146
  }
2170
2147
  /**
2171
2148
  * Deletes a node anywhere from the list
2172
2149
  * @param {string} id - The id of the node to delete
2173
2150
  * */
2174
2151
  async onNodeDelete(id) {
2175
- // Iterate only over pages that have content (not empty)
2176
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
2152
+ var _a, _b;
2153
+ let didDelete = false;
2154
+ for (let i = this.pages.length - 1; i >= 0; i--) {
2177
2155
  const index = this.pages[i].findIndex((node) => node.id === id);
2178
- // message in view
2179
- if (index !== -1) {
2180
- // delete message
2181
- this.pages[i].splice(index, 1);
2182
- if (i === this.firstEmptyIndex + 1) {
2183
- // if newest page is empty, update first empty index
2184
- if (this.pages[i].length === 0)
2185
- this.firstEmptyIndex++;
2186
- // update timestamp, first empty index could be -1, so we need to cap it at 0
2187
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
2188
- }
2189
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
2190
- // if oldest page is empty, remove it
2191
- if (this.pages[i].length === 0)
2192
- this.pages.pop();
2193
- // update timestamp
2194
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
2195
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
2196
- }
2197
- this.rerender();
2198
- }
2199
- }
2156
+ // if message not found, move on
2157
+ if (index === -1)
2158
+ continue;
2159
+ // delete message
2160
+ this.pages[i].splice(index, 1);
2161
+ // if page is empty, delete it
2162
+ if (this.pages[i].length === 0)
2163
+ this.pages.splice(i, 1);
2164
+ didDelete = true;
2165
+ break;
2166
+ }
2167
+ if (!didDelete)
2168
+ return;
2169
+ // update timestamps
2170
+ const firstPage = this.pages[0];
2171
+ const lastPage = this.pages[this.pages.length - 1];
2172
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
2173
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
2174
+ this.rerender();
2200
2175
  }
2201
2176
  /**
2202
2177
  * Updates a new node anywhere in the list
@@ -2204,146 +2179,182 @@ const RtkPaginatedList = class {
2204
2179
  * @param {DataNode} _node - The updated data node
2205
2180
  * */
2206
2181
  async onNodeUpdate(_id, _node) { }
2207
- rerender() {
2208
- this.rerenderBoolean = !this.rerenderBoolean;
2209
- }
2210
2182
  connectedCallback() {
2211
2183
  this.rerender = debounce(this.rerender.bind(this), 50, { maxWait: 200 });
2212
- this.intersectionObserver = new IntersectionObserver((entries) => {
2213
- writeTask(async () => {
2214
- for (const entry of entries) {
2215
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
2216
- this.isLoadingTop = true;
2217
- await this.loadPrevPage();
2218
- this.isLoadingTop = false;
2219
- }
2220
- }
2221
- });
2222
- });
2223
2184
  }
2224
2185
  componentDidLoad() {
2225
- this.observe(this.$topRef);
2186
+ // initial load
2187
+ this.loadPrevPage();
2226
2188
  if (this.$containerRef) {
2227
2189
  this.$containerRef.onscrollend = async () => {
2228
- /**
2229
- * Load new page if:
2230
- * if there are nodes to load at the bottom (maxTS > newTS)
2231
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
2232
- */
2233
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
2234
- this.isLoadingBottom = true;
2190
+ if (this.isInView(this.$bottomRef)) {
2235
2191
  await this.loadNextPage();
2236
- this.isLoadingBottom = false;
2237
- if (this.shouldScrollToBottom)
2238
- this.scrollToBottom();
2192
+ }
2193
+ else if (this.isInView(this.$topRef)) {
2194
+ this.showNewMessagesCTR = true;
2195
+ await this.loadPrevPage();
2239
2196
  }
2240
2197
  };
2241
2198
  }
2242
2199
  }
2200
+ componentDidRender() {
2201
+ if (!this.pendingScrollAnchor)
2202
+ return;
2203
+ const anchor = this.pendingScrollAnchor;
2204
+ this.pendingScrollAnchor = null;
2205
+ this.restoreScrollToAnchor(anchor);
2206
+ }
2243
2207
  async loadPrevPage() {
2244
2208
  if (this.isLoading)
2245
2209
  return;
2246
- /**
2247
- * NOTE(ikabra): this case also runs on initial load
2248
- * if scrolling up ->
2249
- * fetch older messages and push to the end of the array
2250
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
2251
- */
2210
+ const scrollAnchor = this.getScrollAnchor('top');
2252
2211
  // if no old timestamp, it means we are at initial state
2253
2212
  if (!this.oldTS)
2254
2213
  this.oldTS = new Date().getTime();
2255
2214
  // load data
2256
2215
  this.isLoading = true;
2216
+ this.isLoadingTop = true;
2257
2217
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
2258
2218
  this.isLoading = false;
2219
+ this.isLoadingTop = false;
2259
2220
  // no more old messages to show, we are at the top of the page
2260
2221
  if (!data.length)
2261
2222
  return;
2262
2223
  // add old data to the end of the array
2263
2224
  this.pages.push(data);
2264
2225
  // clear old pages when we reach the limit
2265
- if (this.pages.length > this.pagesAllowed) {
2266
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
2267
- /**
2268
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
2269
- * we are doing this because any of the middle pages in the currently rendered pages
2270
- * could be empty as we allow deleting messages.
2271
- * This helps us set the first empty index correctly.
2272
- */
2273
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
2274
- if (this.pages[i].length > 0)
2275
- break;
2276
- this.firstEmptyIndex = i;
2277
- }
2278
- }
2279
- // update the old timestamp
2226
+ if (this.pages.length > this.pagesAllowed)
2227
+ this.pages.shift();
2228
+ // update timestamps
2280
2229
  const lastPage = this.pages[this.pages.length - 1];
2281
2230
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
2282
- // update the new timestamp
2283
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
2231
+ this.newTS = this.pages[0][0].timeMs;
2284
2232
  this.rerender();
2233
+ // fix scroll position
2234
+ if (scrollAnchor)
2235
+ this.pendingScrollAnchor = scrollAnchor;
2285
2236
  }
2286
2237
  async loadNextPage() {
2287
2238
  if (this.isLoading)
2288
2239
  return;
2289
- // new timestamp needs to be assigned by loadPrevPage method
2240
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
2290
2241
  if (!this.newTS) {
2291
2242
  this.showNewMessagesCTR = false;
2292
2243
  this.shouldScrollToBottom = false;
2293
2244
  return;
2294
2245
  }
2295
- // load data
2246
+ // for autoscroll or scroll to bottom button
2247
+ const maxAutoLoads = 200;
2248
+ let loads = 0;
2249
+ let prevNewTS = this.newTS;
2296
2250
  this.isLoading = true;
2297
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
2298
- this.isLoading = false;
2299
- // no more new messages to load
2300
- if (!data.length) {
2301
- this.showNewMessagesCTR = false;
2302
- this.shouldScrollToBottom = false;
2303
- // remove extra pages from the start if any (could be due to users deleting messages)
2304
- this.pages = this.pages.filter((page) => page.length > 0);
2305
- this.firstEmptyIndex = -1;
2306
- return;
2307
- }
2308
- // when filling empty pages
2309
- if (this.firstEmptyIndex > -1) {
2310
- this.pages[this.firstEmptyIndex] = data.reverse();
2311
- }
2312
- else {
2313
- // when already at the bottom and loading messages in realtime
2251
+ this.isLoadingBottom = true;
2252
+ while (loads < maxAutoLoads) {
2253
+ const scrollAnchor = this.getScrollAnchor('bottom');
2254
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
2255
+ this.isLoading = false;
2256
+ this.isLoadingBottom = false;
2257
+ // no more new messages to load
2258
+ if (!data.length) {
2259
+ this.showNewMessagesCTR = false;
2260
+ this.shouldScrollToBottom = false;
2261
+ break;
2262
+ }
2263
+ // load new messages and append to the start
2314
2264
  this.pages.unshift(data.reverse());
2265
+ // remove pages if out of bounds
2266
+ if (this.pages.length > this.pagesAllowed)
2267
+ this.pages.pop();
2268
+ // update timestamps
2269
+ const lastPage = this.pages[this.pages.length - 1];
2270
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
2271
+ this.newTS = this.pages[0][0].timeMs;
2272
+ this.rerender();
2273
+ this.pendingScrollAnchor = scrollAnchor;
2274
+ if (!this.shouldScrollToBottom)
2275
+ break;
2276
+ // if should scroll to bottom then retrigger
2277
+ await this.waitForNextFrame();
2278
+ this.scrollToBottom();
2279
+ await this.waitForNextFrame();
2280
+ // if no new messages, break
2281
+ if (this.newTS === prevNewTS)
2282
+ break;
2283
+ prevNewTS = this.newTS;
2284
+ loads++;
2285
+ }
2286
+ }
2287
+ // Find the element that is closest to the top/bottom of the container
2288
+ getScrollAnchor(edge = 'top') {
2289
+ if (!this.$containerRef)
2290
+ return null;
2291
+ const containerRect = this.$containerRef.getBoundingClientRect();
2292
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
2293
+ let best = null;
2294
+ for (const el of candidates) {
2295
+ const rect = el.getBoundingClientRect();
2296
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
2297
+ if (!isVisibleInContainer)
2298
+ continue;
2299
+ if (edge === 'top') {
2300
+ const offsetTop = rect.top - containerRect.top;
2301
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
2302
+ best = { id: el.id, edge: 'top', offsetTop };
2303
+ }
2304
+ }
2305
+ else {
2306
+ const offsetBottom = containerRect.bottom - rect.bottom;
2307
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
2308
+ best = { id: el.id, edge: 'bottom', offsetBottom };
2309
+ }
2310
+ }
2315
2311
  }
2316
- if (this.pages.length > this.pagesAllowed) {
2317
- this.pages.pop();
2312
+ return best;
2313
+ }
2314
+ //instant scroll to anchor to make sure we are at the same position after a rerender
2315
+ restoreScrollToAnchor(anchor) {
2316
+ if (!this.$containerRef)
2317
+ return;
2318
+ // make element id safe to use inside a CSS selector
2319
+ const escapeId = (id) => {
2320
+ var _a;
2321
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
2322
+ return typeof cssEscape === 'function'
2323
+ ? cssEscape(id)
2324
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
2325
+ };
2326
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
2327
+ if (!el)
2328
+ return;
2329
+ const containerRect = this.$containerRef.getBoundingClientRect();
2330
+ const rect = el.getBoundingClientRect();
2331
+ if (anchor.edge === 'top') {
2332
+ const newOffsetTop = rect.top - containerRect.top;
2333
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
2318
2334
  }
2319
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
2320
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
2321
- // remove all empty pages from the end
2322
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
2323
- if (this.pages[i].length > 0)
2324
- break;
2325
- // if page is empty, remove it
2326
- this.pages.pop();
2335
+ else {
2336
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
2337
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
2327
2338
  }
2328
- // update the old timestamp
2329
- const lastPage = this.pages[this.pages.length - 1];
2330
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
2331
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
2332
- if (this.$containerRef.scrollTop === 0)
2333
- this.$containerRef.scrollTop = -60;
2334
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
2335
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
2336
- this.rerender();
2339
+ }
2340
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
2341
+ scrollToBottom() {
2342
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
2343
+ }
2344
+ waitForNextFrame() {
2345
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
2346
+ }
2347
+ rerender() {
2348
+ this.rerenderBoolean = !this.rerenderBoolean;
2337
2349
  }
2338
2350
  render() {
2339
2351
  /**
2340
- * div.container is flex=column-reverse
2341
- * which is why div#bottom-scroll comes before div#top-scroll
2352
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
2342
2353
  */
2343
- return (h(Host, { key: '91ac7d0ca3fb720259945ffaa97f465b34c694fa' }, h("div", { key: '33896c19ecc4359ae163c65b5c71b9f17673e765', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, h("div", { key: 'e26a5ef3979ec132277b9598afc17ea65683f6c8', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, h("rtk-button", { key: 'e769a8f54a298af456552733dc9de27d059e5138', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
2354
+ return (h(Host, { key: 'e0f806cccdcba162d0c834476863b34630cb1a1e' }, h("div", { key: '6d54d50ed703a59df8d26399499533e3cb0d70fe', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, h("div", { key: '4e9fcbed725fb55d9fbb67eca94b0e770662d51b', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, h("rtk-button", { key: '7db0236d35db3fb9856fea7a4f62c1fbd421829e', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
2344
2355
  this.shouldScrollToBottom = true;
2345
2356
  this.scrollToBottom();
2346
- } }, h("rtk-icon", { key: '6fb4cbc2247eb971004a94926b95ebd0f90ab0fd', icon: this.iconPack.chevron_down }))), h("div", { key: 'e91dd8f25012e4509e0ff3cb4d6b65aa9467d427', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && h("rtk-spinner", { key: '199c0ccffd57716bd5a05dcef8610113d3c58d3d', size: "sm" }), this.isLoading && this.pages.length < 1 && h("rtk-spinner", { key: 'b8a3e08a25b2bc1d50b5a9b1b2deda802ae5eb28', 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: '2cb56b4f70d37548fd9aa71b961559b43c54a922', size: "sm" }), h("div", { key: '4b183c49bfe60fd63af40e02b9b46215c08bb484', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
2357
+ } }, h("rtk-icon", { key: '6e247e86029560601080e0b4d6dcfccbd90fcdd6', icon: this.iconPack.chevron_down }))), h("div", { key: '01d1ba7eacc67ece70b805fb2dfb493cdf1c9d23', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && h("rtk-spinner", { key: '24391bfc34914675cbfd0c287332bfdf3f5f5000', size: "sm" }), this.isLoading && this.pages.length < 1 && h("rtk-spinner", { key: 'e6c2cb44fce52ca54c9f1e543d91a29648745408', 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: 'd90a15494bcd7ec6c9c7ffc5e9a55054252a4258', size: "sm" }), h("div", { key: '2a65f98c3c4e6f2510c6cf1b4e2bcf1e607a7552', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
2347
2358
  }
2348
2359
  };
2349
2360
  __decorate$2([
@@ -90,9 +90,9 @@ const RtkChatToggle = class {
90
90
  const { messages } = await chat.getMessages(new Date().getTime(), this.pageSize, true);
91
91
  const meetingStartedTimeMs = (_b = (_a = this.meeting.meta) === null || _a === void 0 ? void 0 : _a.meetingStartedTimestamp.getTime()) !== null && _b !== void 0 ? _b : 0;
92
92
  const newMessages = messages.filter((m) => m.timeMs > meetingStartedTimeMs);
93
- if (newMessages.length === messages.length && messages.length > 0) {
93
+ if (newMessages.length === this.pageSize && newMessages.length > 0) {
94
94
  // all messages are new, so we can't know the exact count, but we know there are at least pageSize - 1 new messages
95
- this.unreadMessageCount = this.pageSize - 1;
95
+ this.unreadMessageCount = newMessages.length;
96
96
  }
97
97
  else {
98
98
  this.unreadMessageCount = newMessages.length;
@@ -228,7 +228,10 @@ const RtkNotifications = class {
228
228
  this.waitlistedParticipantLeftListener = (participant) => {
229
229
  this.remove(`${participant.id}-joined-waitlist`);
230
230
  };
231
- this.chatUpdateListener = ({ message }) => {
231
+ this.chatUpdateListener = ({ message, action }) => {
232
+ // NOTE(ikabra): we only want notifications for new messages
233
+ if (action !== 'add')
234
+ return;
232
235
  const parsedMessage = parseMessageForTarget(message);
233
236
  if (parsedMessage != null) {
234
237
  if (parsedMessage.userId === meeting.self.userId) {