@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
@@ -11586,7 +11586,7 @@ const RtkChatMessagesUiPaginated = class {
11586
11586
  }
11587
11587
  const isSelf = message.userId === this.meeting.self.userId;
11588
11588
  const viewType = isSelf ? 'outgoing' : 'incoming';
11589
- 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: () => {
11589
+ 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: () => {
11590
11590
  this.stateUpdate.emit({ image: message });
11591
11591
  } }))))))));
11592
11592
  };
@@ -11634,7 +11634,7 @@ const RtkChatMessagesUiPaginated = class {
11634
11634
  this.lastReadMessageIndex = -1;
11635
11635
  }
11636
11636
  render() {
11637
- 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' }))));
11637
+ 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' }))));
11638
11638
  }
11639
11639
  get host() { return getElement(this); }
11640
11640
  static get watchers() { return {
@@ -12590,32 +12590,30 @@ const rtkPaginatedListCss = ".scrollbar{scrollbar-width:thin;scrollbar-color:var
12590
12590
  const RtkPaginatedListStyle0 = rtkPaginatedListCss;
12591
12591
 
12592
12592
  /**
12593
- * HOW INFINITE SCROLL WORKS:
12593
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
12594
12594
  *
12595
- * We use intersectionObserver to scroll up.
12596
- * We use scrollEnd listener to scroll down.
12595
+ * Uses scrollend listener for 2way scrolling.
12596
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
12597
12597
  *
12598
- * Why?
12599
- * intersectionObserver doesn't work reliably for 2 way scrolling but has great ux,
12600
- * so we use it to smoothly scroll up.
12598
+ * UPWARD SCROLLING:
12599
+ * - Fetch top anchor (element currently visible to the user near top)
12600
+ * - Fetch older messages, push to end of 2D array
12601
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
12601
12602
  *
12602
- * We have empty divs at the top and bottom ($topRef, $bottomRef)
12603
- * which act as triggers to tell that we have reached the top or end of our messages and need to fetch new messages,
12603
+ * DOWNWARD SCROLLING:
12604
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
12605
+ * - Fetch new page, insert at the start
12606
+ * - Update timestamps & firstEmptyIndex, then rerender
12607
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
12604
12608
  *
12605
- * When scrolling up, we can't remove pages as intersectionObserver relies on
12606
- * the index of dom elements to work properly.
12607
- * So instead, we fetch older messages and push them to the end of the 2d array
12608
- * if length exceeds pagesAllowed, we free up the pages and keep the first empty index in memory (firstEmptyIndex).
12609
+ * ADDING NEW NODES:
12610
+ * - If no pages exist, load old page
12611
+ * - If on 1st page, append messages till page size is full and then load new page
12609
12612
  *
12610
- * For scrolling down, when scroll ends we see if the bottomRef is in view.
12611
- * If yes, we fetch the new page and insert it at the firstEmptyIndex.
12612
- * We update timestamps & firstEmptyIndex, then we rerender.
12613
- *
12614
- * If we have exceeded our page allowance we delete old pages.
12615
- *
12616
- * In this case deleting pages is okay as we are not relying on the index of dom elements to detect page end.
12617
- *
12618
- * This also simplifies the code because when a user scrolls up we do not need to manage a lastEmptyIndex.
12613
+ * DELETE NODE:
12614
+ * - If deleting the only available node, reset to initial state
12615
+ * - If page is empty, delete it
12616
+ * - Update timestamp curors
12619
12617
  */
12620
12618
  var __decorate$2$8 = function (decorators, target, key, desc) {
12621
12619
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -12630,43 +12628,27 @@ var __decorate$2$8 = function (decorators, target, key, desc) {
12630
12628
  const RtkPaginatedList = class {
12631
12629
  constructor(hostRef) {
12632
12630
  registerInstance(this, hostRef);
12633
- /**
12634
- * when scrolling up, we can't remove pages as intersectionObserver relies on
12635
- * the index of dom elements to stay stable.
12636
- * So, instead we free up the pages and keep the last empty index in memory.
12637
- */
12638
- this.firstEmptyIndex = -1;
12639
- this.maxTS = 0;
12640
12631
  // the length of pages will always be pageSize + 2
12641
12632
  this.pages = [];
12633
+ // Controls whether to keep auto-scrolling when a new page load.
12634
+ this.shouldScrollToBottom = false;
12635
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
12636
+ this.showNewMessagesCTR = false;
12642
12637
  /** label to show when empty */
12643
12638
  this.emptyListLabel = null;
12644
- this.rerenderBoolean = false;
12645
- this.showEmptyListLabel = false;
12646
12639
  /** Icon pack */
12647
12640
  this.iconPack = defaultIconPack;
12648
12641
  /** Language */
12649
12642
  this.t = useLanguage();
12643
+ this.rerenderBoolean = false;
12644
+ this.showEmptyListLabel = false;
12650
12645
  this.isLoading = false;
12651
12646
  this.isLoadingTop = false;
12652
12647
  this.isLoadingBottom = false;
12653
- /**
12654
- * Even when auto scroll is enabled, we only want to scroll if a new realtime message has arrived.
12655
- * This variable tells us if we should respect auto scroll after a new page has been loaded.
12656
- * It is also used by the scroll to bottom button.
12657
- * */
12658
- this.shouldScrollToBottom = false;
12659
- /** UI Indicator for the "scroll to bottom" button.
12660
- * Toggles on when a new node is added and autoscroll is disabled.
12661
- * Toggles off when all nodes are loaded */
12662
- this.showNewMessagesCTR = false;
12663
- this.observe = (el) => {
12664
- if (!el)
12665
- return;
12666
- this.intersectionObserver.observe(el);
12667
- };
12668
- this.isAtBottom = () => {
12669
- const rect = this.$bottomRef.getBoundingClientRect();
12648
+ // Tells us if we need to scroll to a specific anchor after a rerender
12649
+ this.pendingScrollAnchor = null;
12650
+ this.isInView = (el) => {
12651
+ const rect = el.getBoundingClientRect();
12670
12652
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
12671
12653
  };
12672
12654
  }
@@ -12675,10 +12657,12 @@ const RtkPaginatedList = class {
12675
12657
  * @param {DataNode} node - The data node to add to the beginning of the list
12676
12658
  */
12677
12659
  async onNewNode(node) {
12678
- // Always update the maxTS. New messages will load on scroll till the end cursor (newTS) reaches this value.
12679
- this.maxTS = Math.max(this.maxTS, node.timeMs);
12680
- // if we are at the bottom of the page
12681
- if (this.firstEmptyIndex === -1) {
12660
+ // if there are no pages, load the first page
12661
+ if (this.pages.length < 1) {
12662
+ this.oldTS = node.timeMs + 1;
12663
+ this.loadPrevPage();
12664
+ }
12665
+ else {
12682
12666
  // append messages to the page if page has not reached full capacity
12683
12667
  if (this.pages[0].length < this.pageSize) {
12684
12668
  this.pages[0].unshift(node);
@@ -12690,49 +12674,40 @@ const RtkPaginatedList = class {
12690
12674
  this.loadNextPage();
12691
12675
  }
12692
12676
  }
12693
- // If autoscroll is enabled, this method will scroll to the bottom
12677
+ // If autoscroll is enabled, scroll to the bottom
12694
12678
  if (this.autoScroll) {
12695
12679
  this.shouldScrollToBottom = true;
12696
12680
  this.scrollToBottom();
12697
12681
  }
12698
- else {
12699
- this.showNewMessagesCTR = true;
12700
- }
12701
- }
12702
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
12703
- scrollToBottom() {
12704
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
12705
12682
  }
12706
12683
  /**
12707
12684
  * Deletes a node anywhere from the list
12708
12685
  * @param {string} id - The id of the node to delete
12709
12686
  * */
12710
12687
  async onNodeDelete(id) {
12711
- // Iterate only over pages that have content (not empty)
12712
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
12688
+ var _a, _b;
12689
+ let didDelete = false;
12690
+ for (let i = this.pages.length - 1; i >= 0; i--) {
12713
12691
  const index = this.pages[i].findIndex((node) => node.id === id);
12714
- // message in view
12715
- if (index !== -1) {
12716
- // delete message
12717
- this.pages[i].splice(index, 1);
12718
- if (i === this.firstEmptyIndex + 1) {
12719
- // if newest page is empty, update first empty index
12720
- if (this.pages[i].length === 0)
12721
- this.firstEmptyIndex++;
12722
- // update timestamp, first empty index could be -1, so we need to cap it at 0
12723
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
12724
- }
12725
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
12726
- // if oldest page is empty, remove it
12727
- if (this.pages[i].length === 0)
12728
- this.pages.pop();
12729
- // update timestamp
12730
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
12731
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
12732
- }
12733
- this.rerender();
12734
- }
12692
+ // if message not found, move on
12693
+ if (index === -1)
12694
+ continue;
12695
+ // delete message
12696
+ this.pages[i].splice(index, 1);
12697
+ // if page is empty, delete it
12698
+ if (this.pages[i].length === 0)
12699
+ this.pages.splice(i, 1);
12700
+ didDelete = true;
12701
+ break;
12735
12702
  }
12703
+ if (!didDelete)
12704
+ return;
12705
+ // update timestamps
12706
+ const firstPage = this.pages[0];
12707
+ const lastPage = this.pages[this.pages.length - 1];
12708
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
12709
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
12710
+ this.rerender();
12736
12711
  }
12737
12712
  /**
12738
12713
  * Updates a new node anywhere in the list
@@ -12740,146 +12715,182 @@ const RtkPaginatedList = class {
12740
12715
  * @param {DataNode} _node - The updated data node
12741
12716
  * */
12742
12717
  async onNodeUpdate(_id, _node) { }
12743
- rerender() {
12744
- this.rerenderBoolean = !this.rerenderBoolean;
12745
- }
12746
12718
  connectedCallback() {
12747
12719
  this.rerender = debounce$1(this.rerender.bind(this), 50, { maxWait: 200 });
12748
- this.intersectionObserver = new IntersectionObserver((entries) => {
12749
- writeTask(async () => {
12750
- for (const entry of entries) {
12751
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
12752
- this.isLoadingTop = true;
12753
- await this.loadPrevPage();
12754
- this.isLoadingTop = false;
12755
- }
12756
- }
12757
- });
12758
- });
12759
12720
  }
12760
12721
  componentDidLoad() {
12761
- this.observe(this.$topRef);
12722
+ // initial load
12723
+ this.loadPrevPage();
12762
12724
  if (this.$containerRef) {
12763
12725
  this.$containerRef.onscrollend = async () => {
12764
- /**
12765
- * Load new page if:
12766
- * if there are nodes to load at the bottom (maxTS > newTS)
12767
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
12768
- */
12769
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
12770
- this.isLoadingBottom = true;
12726
+ if (this.isInView(this.$bottomRef)) {
12771
12727
  await this.loadNextPage();
12772
- this.isLoadingBottom = false;
12773
- if (this.shouldScrollToBottom)
12774
- this.scrollToBottom();
12728
+ }
12729
+ else if (this.isInView(this.$topRef)) {
12730
+ this.showNewMessagesCTR = true;
12731
+ await this.loadPrevPage();
12775
12732
  }
12776
12733
  };
12777
12734
  }
12778
12735
  }
12736
+ componentDidRender() {
12737
+ if (!this.pendingScrollAnchor)
12738
+ return;
12739
+ const anchor = this.pendingScrollAnchor;
12740
+ this.pendingScrollAnchor = null;
12741
+ this.restoreScrollToAnchor(anchor);
12742
+ }
12779
12743
  async loadPrevPage() {
12780
12744
  if (this.isLoading)
12781
12745
  return;
12782
- /**
12783
- * NOTE(ikabra): this case also runs on initial load
12784
- * if scrolling up ->
12785
- * fetch older messages and push to the end of the array
12786
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
12787
- */
12746
+ const scrollAnchor = this.getScrollAnchor('top');
12788
12747
  // if no old timestamp, it means we are at initial state
12789
12748
  if (!this.oldTS)
12790
12749
  this.oldTS = new Date().getTime();
12791
12750
  // load data
12792
12751
  this.isLoading = true;
12752
+ this.isLoadingTop = true;
12793
12753
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
12794
12754
  this.isLoading = false;
12755
+ this.isLoadingTop = false;
12795
12756
  // no more old messages to show, we are at the top of the page
12796
12757
  if (!data.length)
12797
12758
  return;
12798
12759
  // add old data to the end of the array
12799
12760
  this.pages.push(data);
12800
12761
  // clear old pages when we reach the limit
12801
- if (this.pages.length > this.pagesAllowed) {
12802
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
12803
- /**
12804
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
12805
- * we are doing this because any of the middle pages in the currently rendered pages
12806
- * could be empty as we allow deleting messages.
12807
- * This helps us set the first empty index correctly.
12808
- */
12809
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
12810
- if (this.pages[i].length > 0)
12811
- break;
12812
- this.firstEmptyIndex = i;
12813
- }
12814
- }
12815
- // update the old timestamp
12762
+ if (this.pages.length > this.pagesAllowed)
12763
+ this.pages.shift();
12764
+ // update timestamps
12816
12765
  const lastPage = this.pages[this.pages.length - 1];
12817
12766
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
12818
- // update the new timestamp
12819
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
12767
+ this.newTS = this.pages[0][0].timeMs;
12820
12768
  this.rerender();
12769
+ // fix scroll position
12770
+ if (scrollAnchor)
12771
+ this.pendingScrollAnchor = scrollAnchor;
12821
12772
  }
12822
12773
  async loadNextPage() {
12823
12774
  if (this.isLoading)
12824
12775
  return;
12825
- // new timestamp needs to be assigned by loadPrevPage method
12776
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
12826
12777
  if (!this.newTS) {
12827
12778
  this.showNewMessagesCTR = false;
12828
12779
  this.shouldScrollToBottom = false;
12829
12780
  return;
12830
12781
  }
12831
- // load data
12782
+ // for autoscroll or scroll to bottom button
12783
+ const maxAutoLoads = 200;
12784
+ let loads = 0;
12785
+ let prevNewTS = this.newTS;
12832
12786
  this.isLoading = true;
12833
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
12834
- this.isLoading = false;
12835
- // no more new messages to load
12836
- if (!data.length) {
12837
- this.showNewMessagesCTR = false;
12838
- this.shouldScrollToBottom = false;
12839
- // remove extra pages from the start if any (could be due to users deleting messages)
12840
- this.pages = this.pages.filter((page) => page.length > 0);
12841
- this.firstEmptyIndex = -1;
12842
- return;
12843
- }
12844
- // when filling empty pages
12845
- if (this.firstEmptyIndex > -1) {
12846
- this.pages[this.firstEmptyIndex] = data.reverse();
12847
- }
12848
- else {
12849
- // when already at the bottom and loading messages in realtime
12787
+ this.isLoadingBottom = true;
12788
+ while (loads < maxAutoLoads) {
12789
+ const scrollAnchor = this.getScrollAnchor('bottom');
12790
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
12791
+ this.isLoading = false;
12792
+ this.isLoadingBottom = false;
12793
+ // no more new messages to load
12794
+ if (!data.length) {
12795
+ this.showNewMessagesCTR = false;
12796
+ this.shouldScrollToBottom = false;
12797
+ break;
12798
+ }
12799
+ // load new messages and append to the start
12850
12800
  this.pages.unshift(data.reverse());
12801
+ // remove pages if out of bounds
12802
+ if (this.pages.length > this.pagesAllowed)
12803
+ this.pages.pop();
12804
+ // update timestamps
12805
+ const lastPage = this.pages[this.pages.length - 1];
12806
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
12807
+ this.newTS = this.pages[0][0].timeMs;
12808
+ this.rerender();
12809
+ this.pendingScrollAnchor = scrollAnchor;
12810
+ if (!this.shouldScrollToBottom)
12811
+ break;
12812
+ // if should scroll to bottom then retrigger
12813
+ await this.waitForNextFrame();
12814
+ this.scrollToBottom();
12815
+ await this.waitForNextFrame();
12816
+ // if no new messages, break
12817
+ if (this.newTS === prevNewTS)
12818
+ break;
12819
+ prevNewTS = this.newTS;
12820
+ loads++;
12851
12821
  }
12852
- if (this.pages.length > this.pagesAllowed) {
12853
- this.pages.pop();
12822
+ }
12823
+ // Find the element that is closest to the top/bottom of the container
12824
+ getScrollAnchor(edge = 'top') {
12825
+ if (!this.$containerRef)
12826
+ return null;
12827
+ const containerRect = this.$containerRef.getBoundingClientRect();
12828
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
12829
+ let best = null;
12830
+ for (const el of candidates) {
12831
+ const rect = el.getBoundingClientRect();
12832
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
12833
+ if (!isVisibleInContainer)
12834
+ continue;
12835
+ if (edge === 'top') {
12836
+ const offsetTop = rect.top - containerRect.top;
12837
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
12838
+ best = { id: el.id, edge: 'top', offsetTop };
12839
+ }
12840
+ }
12841
+ else {
12842
+ const offsetBottom = containerRect.bottom - rect.bottom;
12843
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
12844
+ best = { id: el.id, edge: 'bottom', offsetBottom };
12845
+ }
12846
+ }
12854
12847
  }
12855
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
12856
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
12857
- // remove all empty pages from the end
12858
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
12859
- if (this.pages[i].length > 0)
12860
- break;
12861
- // if page is empty, remove it
12862
- this.pages.pop();
12848
+ return best;
12849
+ }
12850
+ //instant scroll to anchor to make sure we are at the same position after a rerender
12851
+ restoreScrollToAnchor(anchor) {
12852
+ if (!this.$containerRef)
12853
+ return;
12854
+ // make element id safe to use inside a CSS selector
12855
+ const escapeId = (id) => {
12856
+ var _a;
12857
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
12858
+ return typeof cssEscape === 'function'
12859
+ ? cssEscape(id)
12860
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
12861
+ };
12862
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
12863
+ if (!el)
12864
+ return;
12865
+ const containerRect = this.$containerRef.getBoundingClientRect();
12866
+ const rect = el.getBoundingClientRect();
12867
+ if (anchor.edge === 'top') {
12868
+ const newOffsetTop = rect.top - containerRect.top;
12869
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
12863
12870
  }
12864
- // update the old timestamp
12865
- const lastPage = this.pages[this.pages.length - 1];
12866
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
12867
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
12868
- if (this.$containerRef.scrollTop === 0)
12869
- this.$containerRef.scrollTop = -60;
12870
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
12871
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
12872
- this.rerender();
12871
+ else {
12872
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
12873
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
12874
+ }
12875
+ }
12876
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
12877
+ scrollToBottom() {
12878
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
12879
+ }
12880
+ waitForNextFrame() {
12881
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
12882
+ }
12883
+ rerender() {
12884
+ this.rerenderBoolean = !this.rerenderBoolean;
12873
12885
  }
12874
12886
  render() {
12875
12887
  /**
12876
- * div.container is flex=column-reverse
12877
- * which is why div#bottom-scroll comes before div#top-scroll
12888
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
12878
12889
  */
12879
- 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: () => {
12890
+ 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: () => {
12880
12891
  this.shouldScrollToBottom = true;
12881
12892
  this.scrollToBottom();
12882
- } }, 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) }))));
12893
+ } }, 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) }))));
12883
12894
  }
12884
12895
  };
12885
12896
  __decorate$2$8([
@@ -15403,9 +15414,9 @@ const RtkChatToggle = class {
15403
15414
  const { messages } = await chat.getMessages(new Date().getTime(), this.pageSize, true);
15404
15415
  const meetingStartedTimeMs = (_b = (_a = this.meeting.meta) === null || _a === void 0 ? void 0 : _a.meetingStartedTimestamp.getTime()) !== null && _b !== void 0 ? _b : 0;
15405
15416
  const newMessages = messages.filter((m) => m.timeMs > meetingStartedTimeMs);
15406
- if (newMessages.length === messages.length && messages.length > 0) {
15417
+ if (newMessages.length === this.pageSize && newMessages.length > 0) {
15407
15418
  // all messages are new, so we can't know the exact count, but we know there are at least pageSize - 1 new messages
15408
- this.unreadMessageCount = this.pageSize - 1;
15419
+ this.unreadMessageCount = newMessages.length;
15409
15420
  }
15410
15421
  else {
15411
15422
  this.unreadMessageCount = newMessages.length;
@@ -18695,7 +18706,10 @@ const RtkNotifications = class {
18695
18706
  this.waitlistedParticipantLeftListener = (participant) => {
18696
18707
  this.remove(`${participant.id}-joined-waitlist`);
18697
18708
  };
18698
- this.chatUpdateListener = ({ message }) => {
18709
+ this.chatUpdateListener = ({ message, action }) => {
18710
+ // NOTE(ikabra): we only want notifications for new messages
18711
+ if (action !== 'add')
18712
+ return;
18699
18713
  const parsedMessage = parseMessageForTarget(message);
18700
18714
  if (parsedMessage != null) {
18701
18715
  if (parsedMessage.userId === meeting.self.userId) {