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