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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/browser.js +1 -1
  2. package/dist/cjs/rtk-avatar_24.cjs.entry.js +240 -213
  3. package/dist/cjs/rtk-chat-toggle.cjs.entry.js +1 -1
  4. package/dist/cjs/rtk-notifications.cjs.entry.js +4 -1
  5. package/dist/collection/components/rtk-chat/rtk-chat.js +16 -2
  6. package/dist/collection/components/rtk-chat-messages-ui-paginated/rtk-chat-messages-ui-paginated.js +36 -16
  7. package/dist/collection/components/rtk-chat-toggle/rtk-chat-toggle.js +1 -1
  8. package/dist/collection/components/rtk-notifications/rtk-notifications.js +4 -1
  9. package/dist/collection/components/rtk-paginated-list/rtk-paginated-list.js +213 -200
  10. package/dist/components/{p-85872241.js → p-7e90e964.js} +18 -4
  11. package/dist/components/{p-b6781e91.js → p-9213c3fc.js} +17 -17
  12. package/dist/components/{p-e7e2156a.js → p-ad8282dc.js} +208 -195
  13. package/dist/components/rtk-chat-messages-ui-paginated.js +1 -1
  14. package/dist/components/rtk-chat-search-results.js +1 -1
  15. package/dist/components/rtk-chat-toggle.js +1 -1
  16. package/dist/components/rtk-chat.js +1 -1
  17. package/dist/components/rtk-meeting.js +3 -3
  18. package/dist/components/rtk-notifications.js +4 -1
  19. package/dist/components/rtk-paginated-list.js +1 -1
  20. package/dist/docs/docs-components.json +29 -9
  21. package/dist/esm/loader.js +244 -214
  22. package/dist/esm/rtk-avatar_24.entry.js +240 -213
  23. package/dist/esm/rtk-chat-toggle.entry.js +1 -1
  24. package/dist/esm/rtk-notifications.entry.js +4 -1
  25. package/dist/realtimekit-ui/p-342b4926.entry.js +1 -0
  26. package/dist/realtimekit-ui/p-8f4f3160.entry.js +1 -0
  27. package/dist/realtimekit-ui/{p-421e4c6f.entry.js → p-ec5ed8a4.entry.js} +1 -1
  28. package/dist/realtimekit-ui/realtimekit-ui.esm.js +1 -1
  29. package/dist/types/components/rtk-chat/rtk-chat.d.ts +1 -0
  30. package/dist/types/components/rtk-chat-messages-ui-paginated/rtk-chat-messages-ui-paginated.d.ts +2 -0
  31. package/dist/types/components/rtk-paginated-list/rtk-paginated-list.d.ts +35 -48
  32. package/dist/types/components.d.ts +8 -3
  33. package/package.json +1 -1
  34. package/dist/realtimekit-ui/p-19587963.entry.js +0 -1
  35. package/dist/realtimekit-ui/p-a859d883.entry.js +0 -1
@@ -11045,6 +11045,13 @@ const RtkChat = class {
11045
11045
  const message = event.detail;
11046
11046
  this.meeting.chat.deleteMessage(message.id);
11047
11047
  };
11048
+ this.onMessageEdit = (event) => {
11049
+ const message = event.detail;
11050
+ if (message.type !== 'text')
11051
+ return;
11052
+ this.replyMessage = null;
11053
+ this.editingMessage = message;
11054
+ };
11048
11055
  this.getPrivateChatRecipients = () => {
11049
11056
  const participants = this.getFilteredParticipants().map((participant) => {
11050
11057
  const key = generateChatGroupKey([participant.userId, this.meeting.self.userId]);
@@ -11229,14 +11236,21 @@ const RtkChat = class {
11229
11236
  const uiProps = { iconPack: this.iconPack, t: this.t, size: this.size };
11230
11237
  const message = this.editingMessage ? this.editingMessage.message : '';
11231
11238
  const quotedMessage = this.replyMessage ? this.replyMessage.message : '';
11232
- return (h("rtk-chat-composer-view", Object.assign({ message: message, storageKey: (_a = this.selectedChannelId) !== null && _a !== void 0 ? _a : `draft-${this.selectedChannelId}`, quotedMessage: quotedMessage, isEditing: !!this.editingMessage, canSendTextMessage: this.isTextMessagingAllowed(), canSendFiles: this.isFileMessagingAllowed(), disableEmojiPicker: this.overrides.disableEmojiPicker, maxLength: this.meeting.chat.maxTextLimit, rateLimits: this.meeting.chat.rateLimits, inputTextPlaceholder: this.t('chat.message_placeholder'), onNewMessage: this.onNewMessageHandler, onEditMessage: this.onEditMessageHandler, onEditCancel: this.onEditCancel, onQuotedMessageDismiss: this.onQuotedMessageDismiss }, uiProps), h("slot", { name: "chat-addon", slot: "chat-addon" })));
11239
+ const draftStorageKey = this.selectedChannelId
11240
+ ? `rtk-chat-draft-${this.selectedChannelId}`
11241
+ : 'rtk-chat-draft';
11242
+ const editStorageKey = this.editingMessage
11243
+ ? `rtk-chat-edit-${(_a = this.selectedChannelId) !== null && _a !== void 0 ? _a : 'no-channel'}-${this.editingMessage.id}`
11244
+ : 'rtk-chat-edit';
11245
+ const storageKey = this.editingMessage ? editStorageKey : draftStorageKey;
11246
+ return (h("rtk-chat-composer-view", Object.assign({ message: message, storageKey: storageKey, quotedMessage: quotedMessage, isEditing: !!this.editingMessage, canSendTextMessage: this.isTextMessagingAllowed(), canSendFiles: this.isFileMessagingAllowed(), disableEmojiPicker: this.overrides.disableEmojiPicker, maxLength: this.meeting.chat.maxTextLimit, rateLimits: this.meeting.chat.rateLimits, inputTextPlaceholder: this.t('chat.message_placeholder'), onNewMessage: this.onNewMessageHandler, onEditMessage: this.onEditMessageHandler, onEditCancel: this.onEditCancel, onQuotedMessageDismiss: this.onQuotedMessageDismiss }, uiProps), h("slot", { name: "chat-addon", slot: "chat-addon" })));
11233
11247
  }
11234
11248
  render() {
11235
11249
  var _a;
11236
11250
  if (!this.meeting) {
11237
11251
  return null;
11238
11252
  }
11239
- return (h(Host, null, h("div", { class: "chat-container" }, h("div", { class: "chat" }, this.isFileMessagingAllowed() && (h("div", { id: "dropzone", class: { active: this.dropzoneActivated }, part: "dropzone" }, h("rtk-icon", { icon: this.iconPack.attach }), h("p", null, this.t('chat.send_attachment')))), this.renderPinnedMessagesHeader(), this.isPrivateChatSupported() && (h("rtk-channel-selector-view", { channels: this.getPrivateChatRecipients(), selectedChannelId: ((_a = this.selectedParticipant) === null || _a === void 0 ? void 0 : _a.userId) || 'everyone', onChannelChange: this.updateRecipients, t: this.t, viewAs: "dropdown" })), h("rtk-chat-messages-ui-paginated", { meeting: this.meeting, onPinMessage: this.onPinMessage, onDeleteMessage: this.onDeleteMessage, size: this.size, iconPack: this.iconPack, t: this.t }), this.renderComposerUI()))));
11253
+ return (h(Host, null, h("div", { class: "chat-container" }, h("div", { class: "chat" }, this.isFileMessagingAllowed() && (h("div", { id: "dropzone", class: { active: this.dropzoneActivated }, part: "dropzone" }, h("rtk-icon", { icon: this.iconPack.attach }), h("p", null, this.t('chat.send_attachment')))), this.renderPinnedMessagesHeader(), this.isPrivateChatSupported() && (h("rtk-channel-selector-view", { channels: this.getPrivateChatRecipients(), selectedChannelId: ((_a = this.selectedParticipant) === null || _a === void 0 ? void 0 : _a.userId) || 'everyone', onChannelChange: this.updateRecipients, t: this.t, viewAs: "dropdown" })), h("rtk-chat-messages-ui-paginated", { meeting: this.meeting, onPinMessage: this.onPinMessage, onEditMessage: this.onMessageEdit, onDeleteMessage: this.onDeleteMessage, size: this.size, iconPack: this.iconPack, t: this.t }), this.renderComposerUI()))));
11240
11254
  }
11241
11255
  get host() { return getElement(this); }
11242
11256
  static get watchers() { return {
@@ -11479,6 +11493,7 @@ const RtkChatMessagesUiPaginated = class {
11479
11493
  registerInstance(this, hostRef);
11480
11494
  this.editMessageInit = createEvent(this, "editMessageInit", 7);
11481
11495
  this.onPinMessage = createEvent(this, "pinMessage", 7);
11496
+ this.onEditMessage = createEvent(this, "editMessage", 7);
11482
11497
  this.onDeleteMessage = createEvent(this, "deleteMessage", 7);
11483
11498
  this.stateUpdate = createEvent(this, "rtkStateUpdate", 7);
11484
11499
  /** Icon pack */
@@ -11529,22 +11544,18 @@ const RtkChatMessagesUiPaginated = class {
11529
11544
  };
11530
11545
  this.getMessageActions = (message) => {
11531
11546
  const actions = [];
11532
- // const isSelf = this.meeting.self.userId === message.userId;
11533
- // const chatMessagePermissions = this.meeting.self.permissions?.chatMessage;
11534
- // const canEdit =
11535
- // chatMessagePermissions === undefined
11536
- // ? isSelf
11537
- // : chatMessagePermissions.canEdit === 'ALL' ||
11538
- // (chatMessagePermissions.canEdit === 'SELF' && isSelf);
11539
- const canDelete = message.userId === this.meeting.self.userId;
11540
- if (this.meeting.self.permissions.pinParticipant) {
11547
+ const messageBelongsToSelf = message.userId === this.meeting.self.userId;
11548
+ actions.push({
11549
+ id: 'pin_message',
11550
+ label: message.pinned ? this.t('unpin') : this.t('pin'),
11551
+ icon: this.iconPack.pin,
11552
+ });
11553
+ if (messageBelongsToSelf) {
11541
11554
  actions.push({
11542
- id: 'pin_message',
11543
- label: message.pinned ? this.t('unpin') : this.t('pin'),
11544
- icon: this.iconPack.pin,
11555
+ id: 'edit_message',
11556
+ label: this.t('chat.edit_msg'),
11557
+ icon: this.iconPack.edit,
11545
11558
  });
11546
- }
11547
- if (canDelete) {
11548
11559
  actions.push({
11549
11560
  id: 'delete_message',
11550
11561
  label: this.t('chat.delete_msg'),
@@ -11558,6 +11569,9 @@ const RtkChatMessagesUiPaginated = class {
11558
11569
  case 'pin_message':
11559
11570
  this.onPinMessage.emit(message);
11560
11571
  break;
11572
+ case 'edit_message':
11573
+ this.onEditMessage.emit(message);
11574
+ break;
11561
11575
  case 'delete_message':
11562
11576
  this.onDeleteMessage.emit(message);
11563
11577
  break;
@@ -11586,7 +11600,7 @@ const RtkChatMessagesUiPaginated = class {
11586
11600
  }
11587
11601
  const isSelf = message.userId === this.meeting.self.userId;
11588
11602
  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: () => {
11603
+ 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
11604
  this.stateUpdate.emit({ image: message });
11591
11605
  } }))))))));
11592
11606
  };
@@ -11634,7 +11648,7 @@ const RtkChatMessagesUiPaginated = class {
11634
11648
  this.lastReadMessageIndex = -1;
11635
11649
  }
11636
11650
  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' }))));
11651
+ return (h(Host, { key: '55b594d1fad2c164a70b71e297f63273ece0bc4f' }, h("rtk-paginated-list", { key: '1554ee896643feec72a02672f156f95c988813ce', 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: '03aeb2069b87cb217b9759cabd3835c9bac81f11' }))));
11638
11652
  }
11639
11653
  get host() { return getElement(this); }
11640
11654
  static get watchers() { return {
@@ -12590,32 +12604,30 @@ const rtkPaginatedListCss = ".scrollbar{scrollbar-width:thin;scrollbar-color:var
12590
12604
  const RtkPaginatedListStyle0 = rtkPaginatedListCss;
12591
12605
 
12592
12606
  /**
12593
- * HOW INFINITE SCROLL WORKS:
12594
- *
12595
- * We use intersectionObserver to scroll up.
12596
- * We use scrollEnd listener to scroll down.
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.
12607
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
12601
12608
  *
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,
12609
+ * Uses scrollend listener for 2way scrolling.
12610
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
12604
12611
  *
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).
12612
+ * UPWARD SCROLLING:
12613
+ * - Fetch top anchor (element currently visible to the user near top)
12614
+ * - Fetch older messages, push to end of 2D array
12615
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
12609
12616
  *
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.
12617
+ * DOWNWARD SCROLLING:
12618
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
12619
+ * - Fetch new page, insert at the start
12620
+ * - Update timestamps & firstEmptyIndex, then rerender
12621
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
12613
12622
  *
12614
- * If we have exceeded our page allowance we delete old pages.
12623
+ * ADDING NEW NODES:
12624
+ * - If no pages exist, load old page
12625
+ * - If on 1st page, append messages till page size is full and then load new page
12615
12626
  *
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.
12627
+ * DELETE NODE:
12628
+ * - If deleting the only available node, reset to initial state
12629
+ * - If page is empty, delete it
12630
+ * - Update timestamp curors
12619
12631
  */
12620
12632
  var __decorate$2$8 = function (decorators, target, key, desc) {
12621
12633
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -12630,43 +12642,27 @@ var __decorate$2$8 = function (decorators, target, key, desc) {
12630
12642
  const RtkPaginatedList = class {
12631
12643
  constructor(hostRef) {
12632
12644
  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
12645
  // the length of pages will always be pageSize + 2
12641
12646
  this.pages = [];
12647
+ // Controls whether to keep auto-scrolling when a new page load.
12648
+ this.shouldScrollToBottom = false;
12649
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
12650
+ this.showNewMessagesCTR = false;
12642
12651
  /** label to show when empty */
12643
12652
  this.emptyListLabel = null;
12644
- this.rerenderBoolean = false;
12645
- this.showEmptyListLabel = false;
12646
12653
  /** Icon pack */
12647
12654
  this.iconPack = defaultIconPack;
12648
12655
  /** Language */
12649
12656
  this.t = useLanguage();
12657
+ this.rerenderBoolean = false;
12658
+ this.showEmptyListLabel = false;
12650
12659
  this.isLoading = false;
12651
12660
  this.isLoadingTop = false;
12652
12661
  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();
12662
+ // Tells us if we need to scroll to a specific anchor after a rerender
12663
+ this.pendingScrollAnchor = null;
12664
+ this.isInView = (el) => {
12665
+ const rect = el.getBoundingClientRect();
12670
12666
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
12671
12667
  };
12672
12668
  }
@@ -12675,224 +12671,255 @@ const RtkPaginatedList = class {
12675
12671
  * @param {DataNode} node - The data node to add to the beginning of the list
12676
12672
  */
12677
12673
  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) {
12682
- // if there are no pages, load the first page
12683
- if (this.pages.length < 1) {
12684
- // update old timer to 1ms ahead of the latest message as we subtract this value to avoid loading duplicate messages when scrolling
12685
- this.oldTS = node.timeMs + 1;
12686
- this.loadPrevPage();
12674
+ // if there are no pages, load the first page
12675
+ if (this.pages.length < 1) {
12676
+ this.oldTS = node.timeMs + 1;
12677
+ this.loadPrevPage();
12678
+ }
12679
+ else if (this.maxTS === this.newTS) {
12680
+ this.maxTS = node.timeMs;
12681
+ // append messages to the page if page has not reached full capacity
12682
+ if (this.pages[0].length < this.pageSize) {
12683
+ this.pages[0].unshift(node);
12684
+ this.newTS = node.timeMs;
12685
+ this.rerender();
12687
12686
  }
12688
12687
  else {
12689
- // append messages to the page if page has not reached full capacity
12690
- if (this.pages[0].length < this.pageSize) {
12691
- this.pages[0].unshift(node);
12692
- this.newTS = node.timeMs;
12693
- this.rerender();
12694
- }
12695
- else {
12696
- // if page is at full capacity, load next page
12697
- this.loadNextPage();
12698
- }
12688
+ // if page is at full capacity, load next page
12689
+ this.loadNextPage();
12699
12690
  }
12700
12691
  }
12701
- // If autoscroll is enabled, this method will scroll to the bottom
12692
+ // If autoscroll is enabled, scroll to the bottom
12702
12693
  if (this.autoScroll) {
12703
12694
  this.shouldScrollToBottom = true;
12704
12695
  this.scrollToBottom();
12705
12696
  }
12706
- else {
12707
- this.showNewMessagesCTR = true;
12708
- }
12709
- }
12710
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
12711
- scrollToBottom() {
12712
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
12713
12697
  }
12714
12698
  /**
12715
12699
  * Deletes a node anywhere from the list
12716
12700
  * @param {string} id - The id of the node to delete
12717
12701
  * */
12718
12702
  async onNodeDelete(id) {
12719
- // Iterate only over pages that have content (not empty)
12720
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
12703
+ var _a, _b;
12704
+ let didDelete = false;
12705
+ for (let i = this.pages.length - 1; i >= 0; i--) {
12721
12706
  const index = this.pages[i].findIndex((node) => node.id === id);
12722
- // message in view
12723
- if (index !== -1) {
12724
- // delete message
12725
- this.pages[i].splice(index, 1);
12726
- // if we are on the first page and it's now empty, we need to go back to initial state
12727
- if (i === 0 && this.pages[i].length === 0) {
12728
- this.pages.shift();
12729
- this.firstEmptyIndex = -1;
12730
- }
12731
- else if (i === this.firstEmptyIndex + 1) {
12732
- // if newest page is empty, update first empty index
12733
- if (this.pages[i].length === 0)
12734
- this.firstEmptyIndex++;
12735
- // update timestamp, first empty index could be -1, so we need to cap it at 0
12736
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
12737
- }
12738
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
12739
- // if oldest page is empty, remove it
12740
- if (this.pages[i].length === 0)
12741
- this.pages.pop();
12742
- // update timestamp
12743
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
12744
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
12745
- }
12746
- this.rerender();
12747
- }
12707
+ // if message not found, move on
12708
+ if (index === -1)
12709
+ continue;
12710
+ // delete message
12711
+ this.pages[i].splice(index, 1);
12712
+ // if page is empty, delete it
12713
+ if (this.pages[i].length === 0)
12714
+ this.pages.splice(i, 1);
12715
+ didDelete = true;
12716
+ break;
12748
12717
  }
12718
+ if (!didDelete)
12719
+ return;
12720
+ // update timestamps
12721
+ const firstPage = this.pages[0];
12722
+ const lastPage = this.pages[this.pages.length - 1];
12723
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
12724
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
12725
+ this.rerender();
12749
12726
  }
12750
12727
  /**
12751
12728
  * Updates a new node anywhere in the list
12752
- * @param {string} _id - The id of the node to update
12753
- * @param {DataNode} _node - The updated data node
12729
+ * @param {string} id - The id of the node to update
12730
+ * @param {DataNode} node - The updated data node
12754
12731
  * */
12755
- async onNodeUpdate(_id, _node) { }
12756
- rerender() {
12757
- this.rerenderBoolean = !this.rerenderBoolean;
12732
+ async onNodeUpdate(id, node) {
12733
+ for (let i = this.pages.length - 1; i >= 0; i--) {
12734
+ const index = this.pages[i].findIndex((node) => node.id === id);
12735
+ // if message not found, move on
12736
+ if (index === -1)
12737
+ continue;
12738
+ // edit message
12739
+ this.pages[i][index] = node;
12740
+ this.rerender();
12741
+ break;
12742
+ }
12758
12743
  }
12759
12744
  connectedCallback() {
12760
12745
  this.rerender = debounce$1(this.rerender.bind(this), 50, { maxWait: 200 });
12761
- this.intersectionObserver = new IntersectionObserver((entries) => {
12762
- writeTask(async () => {
12763
- for (const entry of entries) {
12764
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
12765
- this.isLoadingTop = true;
12766
- await this.loadPrevPage();
12767
- this.isLoadingTop = false;
12768
- }
12769
- }
12770
- });
12771
- });
12772
12746
  }
12773
12747
  componentDidLoad() {
12774
- this.observe(this.$topRef);
12748
+ // initial load
12749
+ this.loadPrevPage();
12775
12750
  if (this.$containerRef) {
12776
12751
  this.$containerRef.onscrollend = async () => {
12777
- /**
12778
- * Load new page if:
12779
- * if there are nodes to load at the bottom (maxTS > newTS)
12780
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
12781
- */
12782
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
12783
- this.isLoadingBottom = true;
12752
+ if (this.isInView(this.$bottomRef)) {
12784
12753
  await this.loadNextPage();
12785
- this.isLoadingBottom = false;
12786
- if (this.shouldScrollToBottom)
12787
- this.scrollToBottom();
12754
+ }
12755
+ else if (this.isInView(this.$topRef)) {
12756
+ this.showNewMessagesCTR = true;
12757
+ await this.loadPrevPage();
12788
12758
  }
12789
12759
  };
12790
12760
  }
12791
12761
  }
12762
+ componentDidRender() {
12763
+ if (!this.pendingScrollAnchor)
12764
+ return;
12765
+ const anchor = this.pendingScrollAnchor;
12766
+ this.pendingScrollAnchor = null;
12767
+ this.restoreScrollToAnchor(anchor);
12768
+ }
12792
12769
  async loadPrevPage() {
12793
12770
  if (this.isLoading)
12794
12771
  return;
12795
- /**
12796
- * NOTE(ikabra): this case also runs on initial load
12797
- * if scrolling up ->
12798
- * fetch older messages and push to the end of the array
12799
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
12800
- */
12772
+ const scrollAnchor = this.getScrollAnchor('top');
12801
12773
  // if no old timestamp, it means we are at initial state
12802
12774
  if (!this.oldTS)
12803
12775
  this.oldTS = new Date().getTime();
12804
12776
  // load data
12805
12777
  this.isLoading = true;
12778
+ this.isLoadingTop = true;
12806
12779
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
12807
12780
  this.isLoading = false;
12781
+ this.isLoadingTop = false;
12808
12782
  // no more old messages to show, we are at the top of the page
12809
12783
  if (!data.length)
12810
12784
  return;
12811
12785
  // add old data to the end of the array
12812
12786
  this.pages.push(data);
12813
12787
  // clear old pages when we reach the limit
12814
- if (this.pages.length > this.pagesAllowed) {
12815
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
12816
- /**
12817
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
12818
- * we are doing this because any of the middle pages in the currently rendered pages
12819
- * could be empty as we allow deleting messages.
12820
- * This helps us set the first empty index correctly.
12821
- */
12822
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
12823
- if (this.pages[i].length > 0)
12824
- break;
12825
- this.firstEmptyIndex = i;
12826
- }
12827
- }
12828
- // update the old timestamp
12788
+ if (this.pages.length > this.pagesAllowed)
12789
+ this.pages.shift();
12790
+ // update timestamps
12829
12791
  const lastPage = this.pages[this.pages.length - 1];
12830
12792
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
12831
- // update the new timestamp
12832
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
12793
+ this.newTS = this.pages[0][0].timeMs;
12794
+ if (!this.maxTS)
12795
+ this.maxTS = this.newTS;
12833
12796
  this.rerender();
12797
+ // fix scroll position
12798
+ if (scrollAnchor)
12799
+ this.pendingScrollAnchor = scrollAnchor;
12834
12800
  }
12835
12801
  async loadNextPage() {
12836
12802
  if (this.isLoading)
12837
12803
  return;
12838
- // new timestamp needs to be assigned by loadPrevPage method
12804
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
12839
12805
  if (!this.newTS) {
12840
12806
  this.showNewMessagesCTR = false;
12841
12807
  this.shouldScrollToBottom = false;
12842
12808
  return;
12843
12809
  }
12844
- // load data
12810
+ // for autoscroll or scroll to bottom button
12811
+ const maxAutoLoads = 200;
12812
+ let loads = 0;
12813
+ let prevNewTS = this.newTS;
12845
12814
  this.isLoading = true;
12846
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
12847
- this.isLoading = false;
12848
- // no more new messages to load
12849
- if (!data.length) {
12850
- this.showNewMessagesCTR = false;
12851
- this.shouldScrollToBottom = false;
12852
- // remove extra pages from the start if any (could be due to users deleting messages)
12853
- this.pages = this.pages.filter((page) => page.length > 0);
12854
- this.firstEmptyIndex = -1;
12855
- return;
12856
- }
12857
- // when filling empty pages
12858
- if (this.firstEmptyIndex > -1) {
12859
- this.pages[this.firstEmptyIndex] = data.reverse();
12860
- }
12861
- else {
12862
- // when already at the bottom and loading messages in realtime
12815
+ this.isLoadingBottom = true;
12816
+ while (loads < maxAutoLoads) {
12817
+ const scrollAnchor = this.getScrollAnchor('bottom');
12818
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
12819
+ this.isLoading = false;
12820
+ this.isLoadingBottom = false;
12821
+ // no more new messages to load
12822
+ if (!data.length) {
12823
+ this.maxTS = this.newTS;
12824
+ this.showNewMessagesCTR = false;
12825
+ this.shouldScrollToBottom = false;
12826
+ break;
12827
+ }
12828
+ // load new messages and append to the start
12863
12829
  this.pages.unshift(data.reverse());
12830
+ // remove pages if out of bounds
12831
+ if (this.pages.length > this.pagesAllowed)
12832
+ this.pages.pop();
12833
+ // update timestamps
12834
+ const lastPage = this.pages[this.pages.length - 1];
12835
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
12836
+ this.newTS = this.pages[0][0].timeMs;
12837
+ this.rerender();
12838
+ this.pendingScrollAnchor = scrollAnchor;
12839
+ if (!this.shouldScrollToBottom)
12840
+ break;
12841
+ // if should scroll to bottom then retrigger
12842
+ await this.waitForNextFrame();
12843
+ this.scrollToBottom();
12844
+ await this.waitForNextFrame();
12845
+ // if no new messages, break
12846
+ if (this.newTS === prevNewTS)
12847
+ break;
12848
+ prevNewTS = this.newTS;
12849
+ loads++;
12864
12850
  }
12865
- if (this.pages.length > this.pagesAllowed) {
12866
- this.pages.pop();
12851
+ }
12852
+ // Find the element that is closest to the top/bottom of the container
12853
+ getScrollAnchor(edge = 'top') {
12854
+ if (!this.$containerRef)
12855
+ return null;
12856
+ const containerRect = this.$containerRef.getBoundingClientRect();
12857
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
12858
+ let best = null;
12859
+ for (const el of candidates) {
12860
+ const rect = el.getBoundingClientRect();
12861
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
12862
+ if (!isVisibleInContainer)
12863
+ continue;
12864
+ if (edge === 'top') {
12865
+ const offsetTop = rect.top - containerRect.top;
12866
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
12867
+ best = { id: el.id, edge: 'top', offsetTop };
12868
+ }
12869
+ }
12870
+ else {
12871
+ const offsetBottom = containerRect.bottom - rect.bottom;
12872
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
12873
+ best = { id: el.id, edge: 'bottom', offsetBottom };
12874
+ }
12875
+ }
12867
12876
  }
12868
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
12869
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
12870
- // remove all empty pages from the end
12871
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
12872
- if (this.pages[i].length > 0)
12873
- break;
12874
- // if page is empty, remove it
12875
- this.pages.pop();
12877
+ return best;
12878
+ }
12879
+ //instant scroll to anchor to make sure we are at the same position after a rerender
12880
+ restoreScrollToAnchor(anchor) {
12881
+ if (!this.$containerRef)
12882
+ return;
12883
+ // make element id safe to use inside a CSS selector
12884
+ const escapeId = (id) => {
12885
+ var _a;
12886
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
12887
+ return typeof cssEscape === 'function'
12888
+ ? cssEscape(id)
12889
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
12890
+ };
12891
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
12892
+ if (!el)
12893
+ return;
12894
+ const containerRect = this.$containerRef.getBoundingClientRect();
12895
+ const rect = el.getBoundingClientRect();
12896
+ if (anchor.edge === 'top') {
12897
+ const newOffsetTop = rect.top - containerRect.top;
12898
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
12876
12899
  }
12877
- // update the old timestamp
12878
- const lastPage = this.pages[this.pages.length - 1];
12879
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
12880
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
12881
- if (this.$containerRef.scrollTop === 0)
12882
- this.$containerRef.scrollTop = -60;
12883
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
12884
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
12885
- this.rerender();
12900
+ else {
12901
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
12902
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
12903
+ }
12904
+ }
12905
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
12906
+ scrollToBottom() {
12907
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
12908
+ }
12909
+ waitForNextFrame() {
12910
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
12911
+ }
12912
+ rerender() {
12913
+ this.rerenderBoolean = !this.rerenderBoolean;
12886
12914
  }
12887
12915
  render() {
12888
12916
  /**
12889
- * div.container is flex=column-reverse
12890
- * which is why div#bottom-scroll comes before div#top-scroll
12917
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
12891
12918
  */
12892
- return (h(Host, { key: '9bf695bccf42f2dd43b04725a855b6b77a4062fd' }, h("div", { key: '7c6805ed8b5e831ea6d932cfb5dc3ecf3d312775', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, h("div", { key: 'eb6c68f97385b5a14a94ab2dc0abff4836f016f3', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, h("rtk-button", { key: 'd1f04db290a9e4128ca58c9bd20b2146a7fc8f12', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
12919
+ return (h(Host, { key: '5f036ac16ace127734d5ee172d537c64baeab415' }, h("div", { key: 'b6d8cf3019a72350f7a3a5b4d020b6ab39793f53', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, h("div", { key: '5c63462ffd995a3e266652bba4e3377636c5f9ca', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, h("rtk-button", { key: 'c1fc4f2759d5be662047245b0dae3eb6f65a9b50', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
12893
12920
  this.shouldScrollToBottom = true;
12894
12921
  this.scrollToBottom();
12895
- } }, h("rtk-icon", { key: '71d8a85911513bb4069578a63ae45f21ac53554c', icon: this.iconPack.chevron_down }))), h("div", { key: 'b1aa46b8163431df6060163d7c5a87eb1ebcffa3', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && h("rtk-spinner", { key: 'a3de0e7b8ca1ac505a596d86c0b8ecafee5fabba', size: "sm" }), this.isLoading && this.pages.length < 1 && h("rtk-spinner", { key: '21fa3a4091d3f1311f5d575c277859602718338c', size: "lg" }), !this.isLoading && this.pages.flat().length === 0 ? (h("div", { class: "empty-list" }, this.t('list.empty'))) : (h("div", { class: "page-wrapper" }, this.pages.map((page, pageIndex) => (h("div", { class: "page", "data-page-index": pageIndex }, this.createNodes([...page].reverse())))))), this.isLoadingTop && this.pages.length > 0 && h("rtk-spinner", { key: '7e018af534e7cd0be3345ca9ad9c8f0a7ab7cc3f', size: "sm" }), h("div", { key: '6c877368fc03c884338402bbcd878929cd4034fb', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
12922
+ } }, h("rtk-icon", { key: '96b19395a2ca8e87ca5004f675cf79f8d58f036c', icon: this.iconPack.chevron_down }))), h("div", { key: '84789a3d0fa4645be711a87cda1e109e4f7d0db2', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && h("rtk-spinner", { key: 'd17f28cc01695220ed6e705d528e8f555b77e8ea', size: "sm" }), this.isLoading && this.pages.length < 1 && h("rtk-spinner", { key: '4ee308d335cdc1f86e2bfdcc20611f50a51ef816', size: "lg" }), !this.isLoading && this.pages.flat().length === 0 ? (h("div", { class: "empty-list" }, this.t('list.empty'))) : (h("div", { class: "page-wrapper" }, this.pages.map((page, pageIndex) => (h("div", { class: "page", "data-page-index": pageIndex }, this.createNodes([...page].reverse())))))), this.isLoadingTop && this.pages.length > 0 && h("rtk-spinner", { key: 'c07c4dd4c4ed0adf01d2fc224b7196a0f86243fd', size: "sm" }), h("div", { key: '52161ac30062c2262f1cdbcded50f2716f6ed20e', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
12896
12923
  }
12897
12924
  };
12898
12925
  __decorate$2$8([
@@ -15418,7 +15445,7 @@ const RtkChatToggle = class {
15418
15445
  const newMessages = messages.filter((m) => m.timeMs > meetingStartedTimeMs);
15419
15446
  if (newMessages.length === this.pageSize && newMessages.length > 0) {
15420
15447
  // all messages are new, so we can't know the exact count, but we know there are at least pageSize - 1 new messages
15421
- this.unreadMessageCount = this.pageSize - 1;
15448
+ this.unreadMessageCount = newMessages.length;
15422
15449
  }
15423
15450
  else {
15424
15451
  this.unreadMessageCount = newMessages.length;
@@ -18708,7 +18735,10 @@ const RtkNotifications = class {
18708
18735
  this.waitlistedParticipantLeftListener = (participant) => {
18709
18736
  this.remove(`${participant.id}-joined-waitlist`);
18710
18737
  };
18711
- this.chatUpdateListener = ({ message }) => {
18738
+ this.chatUpdateListener = ({ message, action }) => {
18739
+ // NOTE(ikabra): we only want notifications for new messages
18740
+ if (action !== 'add')
18741
+ return;
18712
18742
  const parsedMessage = parseMessageForTarget(message);
18713
18743
  if (parsedMessage != null) {
18714
18744
  if (parsedMessage.userId === meeting.self.userId) {