@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
@@ -507,6 +507,13 @@ const RtkChat = class {
507
507
  const message = event.detail;
508
508
  this.meeting.chat.deleteMessage(message.id);
509
509
  };
510
+ this.onMessageEdit = (event) => {
511
+ const message = event.detail;
512
+ if (message.type !== 'text')
513
+ return;
514
+ this.replyMessage = null;
515
+ this.editingMessage = message;
516
+ };
510
517
  this.getPrivateChatRecipients = () => {
511
518
  const participants = this.getFilteredParticipants().map((participant) => {
512
519
  const key = chat.generateChatGroupKey([participant.userId, this.meeting.self.userId]);
@@ -691,14 +698,21 @@ const RtkChat = class {
691
698
  const uiProps = { iconPack: this.iconPack, t: this.t, size: this.size };
692
699
  const message = this.editingMessage ? this.editingMessage.message : '';
693
700
  const quotedMessage = this.replyMessage ? this.replyMessage.message : '';
694
- return (index$1.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), index$1.h("slot", { name: "chat-addon", slot: "chat-addon" })));
701
+ const draftStorageKey = this.selectedChannelId
702
+ ? `rtk-chat-draft-${this.selectedChannelId}`
703
+ : 'rtk-chat-draft';
704
+ const editStorageKey = this.editingMessage
705
+ ? `rtk-chat-edit-${(_a = this.selectedChannelId) !== null && _a !== void 0 ? _a : 'no-channel'}-${this.editingMessage.id}`
706
+ : 'rtk-chat-edit';
707
+ const storageKey = this.editingMessage ? editStorageKey : draftStorageKey;
708
+ return (index$1.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), index$1.h("slot", { name: "chat-addon", slot: "chat-addon" })));
695
709
  }
696
710
  render() {
697
711
  var _a;
698
712
  if (!this.meeting) {
699
713
  return null;
700
714
  }
701
- return (index$1.h(index$1.Host, null, index$1.h("div", { class: "chat-container" }, index$1.h("div", { class: "chat" }, this.isFileMessagingAllowed() && (index$1.h("div", { id: "dropzone", class: { active: this.dropzoneActivated }, part: "dropzone" }, index$1.h("rtk-icon", { icon: this.iconPack.attach }), index$1.h("p", null, this.t('chat.send_attachment')))), this.renderPinnedMessagesHeader(), this.isPrivateChatSupported() && (index$1.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" })), index$1.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()))));
715
+ return (index$1.h(index$1.Host, null, index$1.h("div", { class: "chat-container" }, index$1.h("div", { class: "chat" }, this.isFileMessagingAllowed() && (index$1.h("div", { id: "dropzone", class: { active: this.dropzoneActivated }, part: "dropzone" }, index$1.h("rtk-icon", { icon: this.iconPack.attach }), index$1.h("p", null, this.t('chat.send_attachment')))), this.renderPinnedMessagesHeader(), this.isPrivateChatSupported() && (index$1.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" })), index$1.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()))));
702
716
  }
703
717
  get host() { return index$1.getElement(this); }
704
718
  static get watchers() { return {
@@ -941,6 +955,7 @@ const RtkChatMessagesUiPaginated = class {
941
955
  index$1.registerInstance(this, hostRef);
942
956
  this.editMessageInit = index$1.createEvent(this, "editMessageInit", 7);
943
957
  this.onPinMessage = index$1.createEvent(this, "pinMessage", 7);
958
+ this.onEditMessage = index$1.createEvent(this, "editMessage", 7);
944
959
  this.onDeleteMessage = index$1.createEvent(this, "deleteMessage", 7);
945
960
  this.stateUpdate = index$1.createEvent(this, "rtkStateUpdate", 7);
946
961
  /** Icon pack */
@@ -991,22 +1006,18 @@ const RtkChatMessagesUiPaginated = class {
991
1006
  };
992
1007
  this.getMessageActions = (message) => {
993
1008
  const actions = [];
994
- // const isSelf = this.meeting.self.userId === message.userId;
995
- // const chatMessagePermissions = this.meeting.self.permissions?.chatMessage;
996
- // const canEdit =
997
- // chatMessagePermissions === undefined
998
- // ? isSelf
999
- // : chatMessagePermissions.canEdit === 'ALL' ||
1000
- // (chatMessagePermissions.canEdit === 'SELF' && isSelf);
1001
- const canDelete = message.userId === this.meeting.self.userId;
1002
- if (this.meeting.self.permissions.pinParticipant) {
1009
+ const messageBelongsToSelf = message.userId === this.meeting.self.userId;
1010
+ actions.push({
1011
+ id: 'pin_message',
1012
+ label: message.pinned ? this.t('unpin') : this.t('pin'),
1013
+ icon: this.iconPack.pin,
1014
+ });
1015
+ if (messageBelongsToSelf) {
1003
1016
  actions.push({
1004
- id: 'pin_message',
1005
- label: message.pinned ? this.t('unpin') : this.t('pin'),
1006
- icon: this.iconPack.pin,
1017
+ id: 'edit_message',
1018
+ label: this.t('chat.edit_msg'),
1019
+ icon: this.iconPack.edit,
1007
1020
  });
1008
- }
1009
- if (canDelete) {
1010
1021
  actions.push({
1011
1022
  id: 'delete_message',
1012
1023
  label: this.t('chat.delete_msg'),
@@ -1020,6 +1031,9 @@ const RtkChatMessagesUiPaginated = class {
1020
1031
  case 'pin_message':
1021
1032
  this.onPinMessage.emit(message);
1022
1033
  break;
1034
+ case 'edit_message':
1035
+ this.onEditMessage.emit(message);
1036
+ break;
1023
1037
  case 'delete_message':
1024
1038
  this.onDeleteMessage.emit(message);
1025
1039
  break;
@@ -1048,7 +1062,7 @@ const RtkChatMessagesUiPaginated = class {
1048
1062
  }
1049
1063
  const isSelf = message.userId === this.meeting.self.userId;
1050
1064
  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: () => {
1065
+ 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
1066
  this.stateUpdate.emit({ image: message });
1053
1067
  } }))))))));
1054
1068
  };
@@ -1096,7 +1110,7 @@ const RtkChatMessagesUiPaginated = class {
1096
1110
  this.lastReadMessageIndex = -1;
1097
1111
  }
1098
1112
  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' }))));
1113
+ return (index$1.h(index$1.Host, { key: '55b594d1fad2c164a70b71e297f63273ece0bc4f' }, index$1.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') }, index$1.h("slot", { key: '03aeb2069b87cb217b9759cabd3835c9bac81f11' }))));
1100
1114
  }
1101
1115
  get host() { return index$1.getElement(this); }
1102
1116
  static get watchers() { return {
@@ -2058,32 +2072,30 @@ const rtkPaginatedListCss = ".scrollbar{scrollbar-width:thin;scrollbar-color:var
2058
2072
  const RtkPaginatedListStyle0 = rtkPaginatedListCss;
2059
2073
 
2060
2074
  /**
2061
- * HOW INFINITE SCROLL WORKS:
2062
- *
2063
- * We use intersectionObserver to scroll up.
2064
- * We use scrollEnd listener to scroll down.
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.
2075
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
2069
2076
  *
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,
2077
+ * Uses scrollend listener for 2way scrolling.
2078
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
2072
2079
  *
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).
2080
+ * UPWARD SCROLLING:
2081
+ * - Fetch top anchor (element currently visible to the user near top)
2082
+ * - Fetch older messages, push to end of 2D array
2083
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
2077
2084
  *
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.
2085
+ * DOWNWARD SCROLLING:
2086
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
2087
+ * - Fetch new page, insert at the start
2088
+ * - Update timestamps & firstEmptyIndex, then rerender
2089
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
2081
2090
  *
2082
- * If we have exceeded our page allowance we delete old pages.
2091
+ * ADDING NEW NODES:
2092
+ * - If no pages exist, load old page
2093
+ * - If on 1st page, append messages till page size is full and then load new page
2083
2094
  *
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.
2095
+ * DELETE NODE:
2096
+ * - If deleting the only available node, reset to initial state
2097
+ * - If page is empty, delete it
2098
+ * - Update timestamp curors
2087
2099
  */
2088
2100
  var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
2089
2101
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2098,43 +2110,27 @@ var __decorate$2 = (undefined && undefined.__decorate) || function (decorators,
2098
2110
  const RtkPaginatedList = class {
2099
2111
  constructor(hostRef) {
2100
2112
  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
2113
  // the length of pages will always be pageSize + 2
2109
2114
  this.pages = [];
2115
+ // Controls whether to keep auto-scrolling when a new page load.
2116
+ this.shouldScrollToBottom = false;
2117
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
2118
+ this.showNewMessagesCTR = false;
2110
2119
  /** label to show when empty */
2111
2120
  this.emptyListLabel = null;
2112
- this.rerenderBoolean = false;
2113
- this.showEmptyListLabel = false;
2114
2121
  /** Icon pack */
2115
2122
  this.iconPack = uiStore.defaultIconPack;
2116
2123
  /** Language */
2117
2124
  this.t = uiStore.useLanguage();
2125
+ this.rerenderBoolean = false;
2126
+ this.showEmptyListLabel = false;
2118
2127
  this.isLoading = false;
2119
2128
  this.isLoadingTop = false;
2120
2129
  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();
2130
+ // Tells us if we need to scroll to a specific anchor after a rerender
2131
+ this.pendingScrollAnchor = null;
2132
+ this.isInView = (el) => {
2133
+ const rect = el.getBoundingClientRect();
2138
2134
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
2139
2135
  };
2140
2136
  }
@@ -2143,224 +2139,255 @@ const RtkPaginatedList = class {
2143
2139
  * @param {DataNode} node - The data node to add to the beginning of the list
2144
2140
  */
2145
2141
  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) {
2150
- // if there are no pages, load the first page
2151
- if (this.pages.length < 1) {
2152
- // update old timer to 1ms ahead of the latest message as we subtract this value to avoid loading duplicate messages when scrolling
2153
- this.oldTS = node.timeMs + 1;
2154
- this.loadPrevPage();
2142
+ // if there are no pages, load the first page
2143
+ if (this.pages.length < 1) {
2144
+ this.oldTS = node.timeMs + 1;
2145
+ this.loadPrevPage();
2146
+ }
2147
+ else if (this.maxTS === this.newTS) {
2148
+ this.maxTS = node.timeMs;
2149
+ // append messages to the page if page has not reached full capacity
2150
+ if (this.pages[0].length < this.pageSize) {
2151
+ this.pages[0].unshift(node);
2152
+ this.newTS = node.timeMs;
2153
+ this.rerender();
2155
2154
  }
2156
2155
  else {
2157
- // append messages to the page if page has not reached full capacity
2158
- if (this.pages[0].length < this.pageSize) {
2159
- this.pages[0].unshift(node);
2160
- this.newTS = node.timeMs;
2161
- this.rerender();
2162
- }
2163
- else {
2164
- // if page is at full capacity, load next page
2165
- this.loadNextPage();
2166
- }
2156
+ // if page is at full capacity, load next page
2157
+ this.loadNextPage();
2167
2158
  }
2168
2159
  }
2169
- // If autoscroll is enabled, this method will scroll to the bottom
2160
+ // If autoscroll is enabled, scroll to the bottom
2170
2161
  if (this.autoScroll) {
2171
2162
  this.shouldScrollToBottom = true;
2172
2163
  this.scrollToBottom();
2173
2164
  }
2174
- else {
2175
- this.showNewMessagesCTR = true;
2176
- }
2177
- }
2178
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
2179
- scrollToBottom() {
2180
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
2181
2165
  }
2182
2166
  /**
2183
2167
  * Deletes a node anywhere from the list
2184
2168
  * @param {string} id - The id of the node to delete
2185
2169
  * */
2186
2170
  async onNodeDelete(id) {
2187
- // Iterate only over pages that have content (not empty)
2188
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
2171
+ var _a, _b;
2172
+ let didDelete = false;
2173
+ for (let i = this.pages.length - 1; i >= 0; i--) {
2189
2174
  const index = this.pages[i].findIndex((node) => node.id === id);
2190
- // message in view
2191
- if (index !== -1) {
2192
- // delete message
2193
- this.pages[i].splice(index, 1);
2194
- // if we are on the first page and it's now empty, we need to go back to initial state
2195
- if (i === 0 && this.pages[i].length === 0) {
2196
- this.pages.shift();
2197
- this.firstEmptyIndex = -1;
2198
- }
2199
- else if (i === this.firstEmptyIndex + 1) {
2200
- // if newest page is empty, update first empty index
2201
- if (this.pages[i].length === 0)
2202
- this.firstEmptyIndex++;
2203
- // update timestamp, first empty index could be -1, so we need to cap it at 0
2204
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
2205
- }
2206
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
2207
- // if oldest page is empty, remove it
2208
- if (this.pages[i].length === 0)
2209
- this.pages.pop();
2210
- // update timestamp
2211
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
2212
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
2213
- }
2214
- this.rerender();
2215
- }
2216
- }
2175
+ // if message not found, move on
2176
+ if (index === -1)
2177
+ continue;
2178
+ // delete message
2179
+ this.pages[i].splice(index, 1);
2180
+ // if page is empty, delete it
2181
+ if (this.pages[i].length === 0)
2182
+ this.pages.splice(i, 1);
2183
+ didDelete = true;
2184
+ break;
2185
+ }
2186
+ if (!didDelete)
2187
+ return;
2188
+ // update timestamps
2189
+ const firstPage = this.pages[0];
2190
+ const lastPage = this.pages[this.pages.length - 1];
2191
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
2192
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
2193
+ this.rerender();
2217
2194
  }
2218
2195
  /**
2219
2196
  * Updates a new node anywhere in the list
2220
- * @param {string} _id - The id of the node to update
2221
- * @param {DataNode} _node - The updated data node
2197
+ * @param {string} id - The id of the node to update
2198
+ * @param {DataNode} node - The updated data node
2222
2199
  * */
2223
- async onNodeUpdate(_id, _node) { }
2224
- rerender() {
2225
- this.rerenderBoolean = !this.rerenderBoolean;
2200
+ async onNodeUpdate(id, node) {
2201
+ for (let i = this.pages.length - 1; i >= 0; i--) {
2202
+ const index = this.pages[i].findIndex((node) => node.id === id);
2203
+ // if message not found, move on
2204
+ if (index === -1)
2205
+ continue;
2206
+ // edit message
2207
+ this.pages[i][index] = node;
2208
+ this.rerender();
2209
+ break;
2210
+ }
2226
2211
  }
2227
2212
  connectedCallback() {
2228
2213
  this.rerender = debounce.debounce(this.rerender.bind(this), 50, { maxWait: 200 });
2229
- this.intersectionObserver = new IntersectionObserver((entries) => {
2230
- index$1.writeTask(async () => {
2231
- for (const entry of entries) {
2232
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
2233
- this.isLoadingTop = true;
2234
- await this.loadPrevPage();
2235
- this.isLoadingTop = false;
2236
- }
2237
- }
2238
- });
2239
- });
2240
2214
  }
2241
2215
  componentDidLoad() {
2242
- this.observe(this.$topRef);
2216
+ // initial load
2217
+ this.loadPrevPage();
2243
2218
  if (this.$containerRef) {
2244
2219
  this.$containerRef.onscrollend = async () => {
2245
- /**
2246
- * Load new page if:
2247
- * if there are nodes to load at the bottom (maxTS > newTS)
2248
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
2249
- */
2250
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
2251
- this.isLoadingBottom = true;
2220
+ if (this.isInView(this.$bottomRef)) {
2252
2221
  await this.loadNextPage();
2253
- this.isLoadingBottom = false;
2254
- if (this.shouldScrollToBottom)
2255
- this.scrollToBottom();
2222
+ }
2223
+ else if (this.isInView(this.$topRef)) {
2224
+ this.showNewMessagesCTR = true;
2225
+ await this.loadPrevPage();
2256
2226
  }
2257
2227
  };
2258
2228
  }
2259
2229
  }
2230
+ componentDidRender() {
2231
+ if (!this.pendingScrollAnchor)
2232
+ return;
2233
+ const anchor = this.pendingScrollAnchor;
2234
+ this.pendingScrollAnchor = null;
2235
+ this.restoreScrollToAnchor(anchor);
2236
+ }
2260
2237
  async loadPrevPage() {
2261
2238
  if (this.isLoading)
2262
2239
  return;
2263
- /**
2264
- * NOTE(ikabra): this case also runs on initial load
2265
- * if scrolling up ->
2266
- * fetch older messages and push to the end of the array
2267
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
2268
- */
2240
+ const scrollAnchor = this.getScrollAnchor('top');
2269
2241
  // if no old timestamp, it means we are at initial state
2270
2242
  if (!this.oldTS)
2271
2243
  this.oldTS = new Date().getTime();
2272
2244
  // load data
2273
2245
  this.isLoading = true;
2246
+ this.isLoadingTop = true;
2274
2247
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
2275
2248
  this.isLoading = false;
2249
+ this.isLoadingTop = false;
2276
2250
  // no more old messages to show, we are at the top of the page
2277
2251
  if (!data.length)
2278
2252
  return;
2279
2253
  // add old data to the end of the array
2280
2254
  this.pages.push(data);
2281
2255
  // clear old pages when we reach the limit
2282
- if (this.pages.length > this.pagesAllowed) {
2283
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
2284
- /**
2285
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
2286
- * we are doing this because any of the middle pages in the currently rendered pages
2287
- * could be empty as we allow deleting messages.
2288
- * This helps us set the first empty index correctly.
2289
- */
2290
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
2291
- if (this.pages[i].length > 0)
2292
- break;
2293
- this.firstEmptyIndex = i;
2294
- }
2295
- }
2296
- // update the old timestamp
2256
+ if (this.pages.length > this.pagesAllowed)
2257
+ this.pages.shift();
2258
+ // update timestamps
2297
2259
  const lastPage = this.pages[this.pages.length - 1];
2298
2260
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
2299
- // update the new timestamp
2300
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
2261
+ this.newTS = this.pages[0][0].timeMs;
2262
+ if (!this.maxTS)
2263
+ this.maxTS = this.newTS;
2301
2264
  this.rerender();
2265
+ // fix scroll position
2266
+ if (scrollAnchor)
2267
+ this.pendingScrollAnchor = scrollAnchor;
2302
2268
  }
2303
2269
  async loadNextPage() {
2304
2270
  if (this.isLoading)
2305
2271
  return;
2306
- // new timestamp needs to be assigned by loadPrevPage method
2272
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
2307
2273
  if (!this.newTS) {
2308
2274
  this.showNewMessagesCTR = false;
2309
2275
  this.shouldScrollToBottom = false;
2310
2276
  return;
2311
2277
  }
2312
- // load data
2278
+ // for autoscroll or scroll to bottom button
2279
+ const maxAutoLoads = 200;
2280
+ let loads = 0;
2281
+ let prevNewTS = this.newTS;
2313
2282
  this.isLoading = true;
2314
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
2315
- this.isLoading = false;
2316
- // no more new messages to load
2317
- if (!data.length) {
2318
- this.showNewMessagesCTR = false;
2319
- this.shouldScrollToBottom = false;
2320
- // remove extra pages from the start if any (could be due to users deleting messages)
2321
- this.pages = this.pages.filter((page) => page.length > 0);
2322
- this.firstEmptyIndex = -1;
2323
- return;
2324
- }
2325
- // when filling empty pages
2326
- if (this.firstEmptyIndex > -1) {
2327
- this.pages[this.firstEmptyIndex] = data.reverse();
2328
- }
2329
- else {
2330
- // when already at the bottom and loading messages in realtime
2283
+ this.isLoadingBottom = true;
2284
+ while (loads < maxAutoLoads) {
2285
+ const scrollAnchor = this.getScrollAnchor('bottom');
2286
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
2287
+ this.isLoading = false;
2288
+ this.isLoadingBottom = false;
2289
+ // no more new messages to load
2290
+ if (!data.length) {
2291
+ this.maxTS = this.newTS;
2292
+ this.showNewMessagesCTR = false;
2293
+ this.shouldScrollToBottom = false;
2294
+ break;
2295
+ }
2296
+ // load new messages and append to the start
2331
2297
  this.pages.unshift(data.reverse());
2298
+ // remove pages if out of bounds
2299
+ if (this.pages.length > this.pagesAllowed)
2300
+ this.pages.pop();
2301
+ // update timestamps
2302
+ const lastPage = this.pages[this.pages.length - 1];
2303
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
2304
+ this.newTS = this.pages[0][0].timeMs;
2305
+ this.rerender();
2306
+ this.pendingScrollAnchor = scrollAnchor;
2307
+ if (!this.shouldScrollToBottom)
2308
+ break;
2309
+ // if should scroll to bottom then retrigger
2310
+ await this.waitForNextFrame();
2311
+ this.scrollToBottom();
2312
+ await this.waitForNextFrame();
2313
+ // if no new messages, break
2314
+ if (this.newTS === prevNewTS)
2315
+ break;
2316
+ prevNewTS = this.newTS;
2317
+ loads++;
2332
2318
  }
2333
- if (this.pages.length > this.pagesAllowed) {
2334
- this.pages.pop();
2319
+ }
2320
+ // Find the element that is closest to the top/bottom of the container
2321
+ getScrollAnchor(edge = 'top') {
2322
+ if (!this.$containerRef)
2323
+ return null;
2324
+ const containerRect = this.$containerRef.getBoundingClientRect();
2325
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
2326
+ let best = null;
2327
+ for (const el of candidates) {
2328
+ const rect = el.getBoundingClientRect();
2329
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
2330
+ if (!isVisibleInContainer)
2331
+ continue;
2332
+ if (edge === 'top') {
2333
+ const offsetTop = rect.top - containerRect.top;
2334
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
2335
+ best = { id: el.id, edge: 'top', offsetTop };
2336
+ }
2337
+ }
2338
+ else {
2339
+ const offsetBottom = containerRect.bottom - rect.bottom;
2340
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
2341
+ best = { id: el.id, edge: 'bottom', offsetBottom };
2342
+ }
2343
+ }
2335
2344
  }
2336
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
2337
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
2338
- // remove all empty pages from the end
2339
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
2340
- if (this.pages[i].length > 0)
2341
- break;
2342
- // if page is empty, remove it
2343
- this.pages.pop();
2345
+ return best;
2346
+ }
2347
+ //instant scroll to anchor to make sure we are at the same position after a rerender
2348
+ restoreScrollToAnchor(anchor) {
2349
+ if (!this.$containerRef)
2350
+ return;
2351
+ // make element id safe to use inside a CSS selector
2352
+ const escapeId = (id) => {
2353
+ var _a;
2354
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
2355
+ return typeof cssEscape === 'function'
2356
+ ? cssEscape(id)
2357
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
2358
+ };
2359
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
2360
+ if (!el)
2361
+ return;
2362
+ const containerRect = this.$containerRef.getBoundingClientRect();
2363
+ const rect = el.getBoundingClientRect();
2364
+ if (anchor.edge === 'top') {
2365
+ const newOffsetTop = rect.top - containerRect.top;
2366
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
2344
2367
  }
2345
- // update the old timestamp
2346
- const lastPage = this.pages[this.pages.length - 1];
2347
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
2348
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
2349
- if (this.$containerRef.scrollTop === 0)
2350
- this.$containerRef.scrollTop = -60;
2351
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
2352
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
2353
- this.rerender();
2368
+ else {
2369
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
2370
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
2371
+ }
2372
+ }
2373
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
2374
+ scrollToBottom() {
2375
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
2376
+ }
2377
+ waitForNextFrame() {
2378
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
2379
+ }
2380
+ rerender() {
2381
+ this.rerenderBoolean = !this.rerenderBoolean;
2354
2382
  }
2355
2383
  render() {
2356
2384
  /**
2357
- * div.container is flex=column-reverse
2358
- * which is why div#bottom-scroll comes before div#top-scroll
2385
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
2359
2386
  */
2360
- return (index$1.h(index$1.Host, { key: '9bf695bccf42f2dd43b04725a855b6b77a4062fd' }, index$1.h("div", { key: '7c6805ed8b5e831ea6d932cfb5dc3ecf3d312775', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, index$1.h("div", { key: 'eb6c68f97385b5a14a94ab2dc0abff4836f016f3', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, index$1.h("rtk-button", { key: 'd1f04db290a9e4128ca58c9bd20b2146a7fc8f12', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
2387
+ return (index$1.h(index$1.Host, { key: '5f036ac16ace127734d5ee172d537c64baeab415' }, index$1.h("div", { key: 'b6d8cf3019a72350f7a3a5b4d020b6ab39793f53', class: "scrollbar container", part: "container", ref: (el) => (this.$containerRef = el) }, index$1.h("div", { key: '5c63462ffd995a3e266652bba4e3377636c5f9ca', class: { 'show-new-messages-ctr': true, active: this.showNewMessagesCTR } }, index$1.h("rtk-button", { key: 'c1fc4f2759d5be662047245b0dae3eb6f65a9b50', class: "show-new-messages", kind: "icon", variant: "secondary", part: "show-new-messages", onClick: () => {
2361
2388
  this.shouldScrollToBottom = true;
2362
2389
  this.scrollToBottom();
2363
- } }, index$1.h("rtk-icon", { key: '71d8a85911513bb4069578a63ae45f21ac53554c', icon: this.iconPack.chevron_down }))), index$1.h("div", { key: 'b1aa46b8163431df6060163d7c5a87eb1ebcffa3', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && index$1.h("rtk-spinner", { key: 'a3de0e7b8ca1ac505a596d86c0b8ecafee5fabba', size: "sm" }), this.isLoading && this.pages.length < 1 && index$1.h("rtk-spinner", { key: '21fa3a4091d3f1311f5d575c277859602718338c', 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: '7e018af534e7cd0be3345ca9ad9c8f0a7ab7cc3f', size: "sm" }), index$1.h("div", { key: '6c877368fc03c884338402bbcd878929cd4034fb', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
2390
+ } }, index$1.h("rtk-icon", { key: '96b19395a2ca8e87ca5004f675cf79f8d58f036c', icon: this.iconPack.chevron_down }))), index$1.h("div", { key: '84789a3d0fa4645be711a87cda1e109e4f7d0db2', class: "smallest-dom-element", id: "bottom-scroll", ref: (el) => (this.$bottomRef = el) }), this.isLoadingBottom && this.pages.length > 0 && index$1.h("rtk-spinner", { key: 'd17f28cc01695220ed6e705d528e8f555b77e8ea', size: "sm" }), this.isLoading && this.pages.length < 1 && index$1.h("rtk-spinner", { key: '4ee308d335cdc1f86e2bfdcc20611f50a51ef816', 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: 'c07c4dd4c4ed0adf01d2fc224b7196a0f86243fd', size: "sm" }), index$1.h("div", { key: '52161ac30062c2262f1cdbcded50f2716f6ed20e', class: "smallest-dom-element", id: "top-scroll", ref: (el) => (this.$topRef = el) }))));
2364
2391
  }
2365
2392
  };
2366
2393
  __decorate$2([
@@ -96,7 +96,7 @@ const RtkChatToggle = class {
96
96
  const newMessages = messages.filter((m) => m.timeMs > meetingStartedTimeMs);
97
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;