@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
@@ -503,6 +503,13 @@ const RtkChat = class {
503
503
  const message = event.detail;
504
504
  this.meeting.chat.deleteMessage(message.id);
505
505
  };
506
+ this.onMessageEdit = (event) => {
507
+ const message = event.detail;
508
+ if (message.type !== 'text')
509
+ return;
510
+ this.replyMessage = null;
511
+ this.editingMessage = message;
512
+ };
506
513
  this.getPrivateChatRecipients = () => {
507
514
  const participants = this.getFilteredParticipants().map((participant) => {
508
515
  const key = generateChatGroupKey([participant.userId, this.meeting.self.userId]);
@@ -687,14 +694,21 @@ const RtkChat = class {
687
694
  const uiProps = { iconPack: this.iconPack, t: this.t, size: this.size };
688
695
  const message = this.editingMessage ? this.editingMessage.message : '';
689
696
  const quotedMessage = this.replyMessage ? this.replyMessage.message : '';
690
- 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" })));
697
+ const draftStorageKey = this.selectedChannelId
698
+ ? `rtk-chat-draft-${this.selectedChannelId}`
699
+ : 'rtk-chat-draft';
700
+ const editStorageKey = this.editingMessage
701
+ ? `rtk-chat-edit-${(_a = this.selectedChannelId) !== null && _a !== void 0 ? _a : 'no-channel'}-${this.editingMessage.id}`
702
+ : 'rtk-chat-edit';
703
+ const storageKey = this.editingMessage ? editStorageKey : draftStorageKey;
704
+ 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" })));
691
705
  }
692
706
  render() {
693
707
  var _a;
694
708
  if (!this.meeting) {
695
709
  return null;
696
710
  }
697
- 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()))));
711
+ 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()))));
698
712
  }
699
713
  get host() { return getElement(this); }
700
714
  static get watchers() { return {
@@ -937,6 +951,7 @@ const RtkChatMessagesUiPaginated = class {
937
951
  registerInstance(this, hostRef);
938
952
  this.editMessageInit = createEvent(this, "editMessageInit", 7);
939
953
  this.onPinMessage = createEvent(this, "pinMessage", 7);
954
+ this.onEditMessage = createEvent(this, "editMessage", 7);
940
955
  this.onDeleteMessage = createEvent(this, "deleteMessage", 7);
941
956
  this.stateUpdate = createEvent(this, "rtkStateUpdate", 7);
942
957
  /** Icon pack */
@@ -987,22 +1002,18 @@ const RtkChatMessagesUiPaginated = class {
987
1002
  };
988
1003
  this.getMessageActions = (message) => {
989
1004
  const actions = [];
990
- // const isSelf = this.meeting.self.userId === message.userId;
991
- // const chatMessagePermissions = this.meeting.self.permissions?.chatMessage;
992
- // const canEdit =
993
- // chatMessagePermissions === undefined
994
- // ? isSelf
995
- // : chatMessagePermissions.canEdit === 'ALL' ||
996
- // (chatMessagePermissions.canEdit === 'SELF' && isSelf);
997
- const canDelete = message.userId === this.meeting.self.userId;
998
- if (this.meeting.self.permissions.pinParticipant) {
1005
+ const messageBelongsToSelf = message.userId === this.meeting.self.userId;
1006
+ actions.push({
1007
+ id: 'pin_message',
1008
+ label: message.pinned ? this.t('unpin') : this.t('pin'),
1009
+ icon: this.iconPack.pin,
1010
+ });
1011
+ if (messageBelongsToSelf) {
999
1012
  actions.push({
1000
- id: 'pin_message',
1001
- label: message.pinned ? this.t('unpin') : this.t('pin'),
1002
- icon: this.iconPack.pin,
1013
+ id: 'edit_message',
1014
+ label: this.t('chat.edit_msg'),
1015
+ icon: this.iconPack.edit,
1003
1016
  });
1004
- }
1005
- if (canDelete) {
1006
1017
  actions.push({
1007
1018
  id: 'delete_message',
1008
1019
  label: this.t('chat.delete_msg'),
@@ -1016,6 +1027,9 @@ const RtkChatMessagesUiPaginated = class {
1016
1027
  case 'pin_message':
1017
1028
  this.onPinMessage.emit(message);
1018
1029
  break;
1030
+ case 'edit_message':
1031
+ this.onEditMessage.emit(message);
1032
+ break;
1019
1033
  case 'delete_message':
1020
1034
  this.onDeleteMessage.emit(message);
1021
1035
  break;
@@ -1044,7 +1058,7 @@ const RtkChatMessagesUiPaginated = class {
1044
1058
  }
1045
1059
  const isSelf = message.userId === this.meeting.self.userId;
1046
1060
  const viewType = isSelf ? 'outgoing' : 'incoming';
1047
- return (h("div", null, h("div", { class: "message-wrapper" }, h("rtk-message-view", { pinned: message.pinned, time: message.time, actions: this.getMessageActions(message), authorName: message.displayName, isSelf: isSelf, avatarUrl: displayPicture, hideAuthorName: isContinued, viewType: viewType, variant: "bubble", onAction: (event) => this.onMessageActionHandler(event.detail, message) }, h("div", null, h("div", { class: "body" }, message.type === 'text' && (h("rtk-text-message-view", { text: message.message, isMarkdown: true })), message.type === 'file' && (h("rtk-file-message-view", { name: message.name, url: message.link, size: message.size })), message.type === 'image' && (h("rtk-image-message-view", { url: message.link, onPreview: () => {
1061
+ return (h("div", null, h("div", { class: "message-wrapper", id: message.id }, h("rtk-message-view", { pinned: message.pinned, time: message.time, actions: this.getMessageActions(message), authorName: message.displayName, isSelf: isSelf, avatarUrl: displayPicture, hideAuthorName: isContinued, viewType: viewType, variant: "bubble", onAction: (event) => this.onMessageActionHandler(event.detail, message) }, h("div", null, h("div", { class: "body" }, message.type === 'text' && (h("rtk-text-message-view", { text: message.message, isMarkdown: true })), message.type === 'file' && (h("rtk-file-message-view", { name: message.name, url: message.link, size: message.size })), message.type === 'image' && (h("rtk-image-message-view", { url: message.link, onPreview: () => {
1048
1062
  this.stateUpdate.emit({ image: message });
1049
1063
  } }))))))));
1050
1064
  };
@@ -1092,7 +1106,7 @@ const RtkChatMessagesUiPaginated = class {
1092
1106
  this.lastReadMessageIndex = -1;
1093
1107
  }
1094
1108
  render() {
1095
- return (h(Host, { key: '012b7189dfbdccfd8017cc9023263e6a7e9afd44' }, h("rtk-paginated-list", { key: '0ea37ee880fda0acdd7460b6da5f03e11ac304bf', ref: (el) => (this.$paginatedListRef = el), pageSize: this.pageSize, pagesAllowed: 3, fetchData: this.getChatMessages, createNodes: this.createChatNodes, selectedItemId: this.selectedChannelId, emptyListLabel: this.t('chat.empty_channel') }, h("slot", { key: '53cb197b6d9319f470e87fe73d7ca0d158778e3f' }))));
1109
+ 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' }))));
1096
1110
  }
1097
1111
  get host() { return getElement(this); }
1098
1112
  static get watchers() { return {
@@ -2054,32 +2068,30 @@ const rtkPaginatedListCss = ".scrollbar{scrollbar-width:thin;scrollbar-color:var
2054
2068
  const RtkPaginatedListStyle0 = rtkPaginatedListCss;
2055
2069
 
2056
2070
  /**
2057
- * HOW INFINITE SCROLL WORKS:
2058
- *
2059
- * We use intersectionObserver to scroll up.
2060
- * We use scrollEnd listener to scroll down.
2061
- *
2062
- * Why?
2063
- * intersectionObserver doesn't work reliably for 2 way scrolling but has great ux,
2064
- * so we use it to smoothly scroll up.
2071
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
2065
2072
  *
2066
- * We have empty divs at the top and bottom ($topRef, $bottomRef)
2067
- * which act as triggers to tell that we have reached the top or end of our messages and need to fetch new messages,
2073
+ * Uses scrollend listener for 2way scrolling.
2074
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
2068
2075
  *
2069
- * When scrolling up, we can't remove pages as intersectionObserver relies on
2070
- * the index of dom elements to work properly.
2071
- * So instead, we fetch older messages and push them to the end of the 2d array
2072
- * if length exceeds pagesAllowed, we free up the pages and keep the first empty index in memory (firstEmptyIndex).
2076
+ * UPWARD SCROLLING:
2077
+ * - Fetch top anchor (element currently visible to the user near top)
2078
+ * - Fetch older messages, push to end of 2D array
2079
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
2073
2080
  *
2074
- * For scrolling down, when scroll ends we see if the bottomRef is in view.
2075
- * If yes, we fetch the new page and insert it at the firstEmptyIndex.
2076
- * We update timestamps & firstEmptyIndex, then we rerender.
2081
+ * DOWNWARD SCROLLING:
2082
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
2083
+ * - Fetch new page, insert at the start
2084
+ * - Update timestamps & firstEmptyIndex, then rerender
2085
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
2077
2086
  *
2078
- * If we have exceeded our page allowance we delete old pages.
2087
+ * ADDING NEW NODES:
2088
+ * - If no pages exist, load old page
2089
+ * - If on 1st page, append messages till page size is full and then load new page
2079
2090
  *
2080
- * In this case deleting pages is okay as we are not relying on the index of dom elements to detect page end.
2081
- *
2082
- * This also simplifies the code because when a user scrolls up we do not need to manage a lastEmptyIndex.
2091
+ * DELETE NODE:
2092
+ * - If deleting the only available node, reset to initial state
2093
+ * - If page is empty, delete it
2094
+ * - Update timestamp curors
2083
2095
  */
2084
2096
  var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
2085
2097
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2094,43 +2106,27 @@ var __decorate$2 = (undefined && undefined.__decorate) || function (decorators,
2094
2106
  const RtkPaginatedList = class {
2095
2107
  constructor(hostRef) {
2096
2108
  registerInstance(this, hostRef);
2097
- /**
2098
- * when scrolling up, we can't remove pages as intersectionObserver relies on
2099
- * the index of dom elements to stay stable.
2100
- * So, instead we free up the pages and keep the last empty index in memory.
2101
- */
2102
- this.firstEmptyIndex = -1;
2103
- this.maxTS = 0;
2104
2109
  // the length of pages will always be pageSize + 2
2105
2110
  this.pages = [];
2111
+ // Controls whether to keep auto-scrolling when a new page load.
2112
+ this.shouldScrollToBottom = false;
2113
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
2114
+ this.showNewMessagesCTR = false;
2106
2115
  /** label to show when empty */
2107
2116
  this.emptyListLabel = null;
2108
- this.rerenderBoolean = false;
2109
- this.showEmptyListLabel = false;
2110
2117
  /** Icon pack */
2111
2118
  this.iconPack = defaultIconPack;
2112
2119
  /** Language */
2113
2120
  this.t = useLanguage();
2121
+ this.rerenderBoolean = false;
2122
+ this.showEmptyListLabel = false;
2114
2123
  this.isLoading = false;
2115
2124
  this.isLoadingTop = false;
2116
2125
  this.isLoadingBottom = false;
2117
- /**
2118
- * Even when auto scroll is enabled, we only want to scroll if a new realtime message has arrived.
2119
- * This variable tells us if we should respect auto scroll after a new page has been loaded.
2120
- * It is also used by the scroll to bottom button.
2121
- * */
2122
- this.shouldScrollToBottom = false;
2123
- /** UI Indicator for the "scroll to bottom" button.
2124
- * Toggles on when a new node is added and autoscroll is disabled.
2125
- * Toggles off when all nodes are loaded */
2126
- this.showNewMessagesCTR = false;
2127
- this.observe = (el) => {
2128
- if (!el)
2129
- return;
2130
- this.intersectionObserver.observe(el);
2131
- };
2132
- this.isAtBottom = () => {
2133
- const rect = this.$bottomRef.getBoundingClientRect();
2126
+ // Tells us if we need to scroll to a specific anchor after a rerender
2127
+ this.pendingScrollAnchor = null;
2128
+ this.isInView = (el) => {
2129
+ const rect = el.getBoundingClientRect();
2134
2130
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
2135
2131
  };
2136
2132
  }
@@ -2139,224 +2135,255 @@ const RtkPaginatedList = class {
2139
2135
  * @param {DataNode} node - The data node to add to the beginning of the list
2140
2136
  */
2141
2137
  async onNewNode(node) {
2142
- // Always update the maxTS. New messages will load on scroll till the end cursor (newTS) reaches this value.
2143
- this.maxTS = Math.max(this.maxTS, node.timeMs);
2144
- // if we are at the bottom of the page
2145
- if (this.firstEmptyIndex === -1) {
2146
- // if there are no pages, load the first page
2147
- if (this.pages.length < 1) {
2148
- // update old timer to 1ms ahead of the latest message as we subtract this value to avoid loading duplicate messages when scrolling
2149
- this.oldTS = node.timeMs + 1;
2150
- this.loadPrevPage();
2138
+ // if there are no pages, load the first page
2139
+ if (this.pages.length < 1) {
2140
+ this.oldTS = node.timeMs + 1;
2141
+ this.loadPrevPage();
2142
+ }
2143
+ else if (this.maxTS === this.newTS) {
2144
+ this.maxTS = node.timeMs;
2145
+ // append messages to the page if page has not reached full capacity
2146
+ if (this.pages[0].length < this.pageSize) {
2147
+ this.pages[0].unshift(node);
2148
+ this.newTS = node.timeMs;
2149
+ this.rerender();
2151
2150
  }
2152
2151
  else {
2153
- // append messages to the page if page has not reached full capacity
2154
- if (this.pages[0].length < this.pageSize) {
2155
- this.pages[0].unshift(node);
2156
- this.newTS = node.timeMs;
2157
- this.rerender();
2158
- }
2159
- else {
2160
- // if page is at full capacity, load next page
2161
- this.loadNextPage();
2162
- }
2152
+ // if page is at full capacity, load next page
2153
+ this.loadNextPage();
2163
2154
  }
2164
2155
  }
2165
- // If autoscroll is enabled, this method will scroll to the bottom
2156
+ // If autoscroll is enabled, scroll to the bottom
2166
2157
  if (this.autoScroll) {
2167
2158
  this.shouldScrollToBottom = true;
2168
2159
  this.scrollToBottom();
2169
2160
  }
2170
- else {
2171
- this.showNewMessagesCTR = true;
2172
- }
2173
- }
2174
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
2175
- scrollToBottom() {
2176
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
2177
2161
  }
2178
2162
  /**
2179
2163
  * Deletes a node anywhere from the list
2180
2164
  * @param {string} id - The id of the node to delete
2181
2165
  * */
2182
2166
  async onNodeDelete(id) {
2183
- // Iterate only over pages that have content (not empty)
2184
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
2167
+ var _a, _b;
2168
+ let didDelete = false;
2169
+ for (let i = this.pages.length - 1; i >= 0; i--) {
2185
2170
  const index = this.pages[i].findIndex((node) => node.id === id);
2186
- // message in view
2187
- if (index !== -1) {
2188
- // delete message
2189
- this.pages[i].splice(index, 1);
2190
- // if we are on the first page and it's now empty, we need to go back to initial state
2191
- if (i === 0 && this.pages[i].length === 0) {
2192
- this.pages.shift();
2193
- this.firstEmptyIndex = -1;
2194
- }
2195
- else if (i === this.firstEmptyIndex + 1) {
2196
- // if newest page is empty, update first empty index
2197
- if (this.pages[i].length === 0)
2198
- this.firstEmptyIndex++;
2199
- // update timestamp, first empty index could be -1, so we need to cap it at 0
2200
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
2201
- }
2202
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
2203
- // if oldest page is empty, remove it
2204
- if (this.pages[i].length === 0)
2205
- this.pages.pop();
2206
- // update timestamp
2207
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
2208
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
2209
- }
2210
- this.rerender();
2211
- }
2212
- }
2171
+ // if message not found, move on
2172
+ if (index === -1)
2173
+ continue;
2174
+ // delete message
2175
+ this.pages[i].splice(index, 1);
2176
+ // if page is empty, delete it
2177
+ if (this.pages[i].length === 0)
2178
+ this.pages.splice(i, 1);
2179
+ didDelete = true;
2180
+ break;
2181
+ }
2182
+ if (!didDelete)
2183
+ return;
2184
+ // update timestamps
2185
+ const firstPage = this.pages[0];
2186
+ const lastPage = this.pages[this.pages.length - 1];
2187
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
2188
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
2189
+ this.rerender();
2213
2190
  }
2214
2191
  /**
2215
2192
  * Updates a new node anywhere in the list
2216
- * @param {string} _id - The id of the node to update
2217
- * @param {DataNode} _node - The updated data node
2193
+ * @param {string} id - The id of the node to update
2194
+ * @param {DataNode} node - The updated data node
2218
2195
  * */
2219
- async onNodeUpdate(_id, _node) { }
2220
- rerender() {
2221
- this.rerenderBoolean = !this.rerenderBoolean;
2196
+ async onNodeUpdate(id, node) {
2197
+ for (let i = this.pages.length - 1; i >= 0; i--) {
2198
+ const index = this.pages[i].findIndex((node) => node.id === id);
2199
+ // if message not found, move on
2200
+ if (index === -1)
2201
+ continue;
2202
+ // edit message
2203
+ this.pages[i][index] = node;
2204
+ this.rerender();
2205
+ break;
2206
+ }
2222
2207
  }
2223
2208
  connectedCallback() {
2224
2209
  this.rerender = debounce(this.rerender.bind(this), 50, { maxWait: 200 });
2225
- this.intersectionObserver = new IntersectionObserver((entries) => {
2226
- writeTask(async () => {
2227
- for (const entry of entries) {
2228
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
2229
- this.isLoadingTop = true;
2230
- await this.loadPrevPage();
2231
- this.isLoadingTop = false;
2232
- }
2233
- }
2234
- });
2235
- });
2236
2210
  }
2237
2211
  componentDidLoad() {
2238
- this.observe(this.$topRef);
2212
+ // initial load
2213
+ this.loadPrevPage();
2239
2214
  if (this.$containerRef) {
2240
2215
  this.$containerRef.onscrollend = async () => {
2241
- /**
2242
- * Load new page if:
2243
- * if there are nodes to load at the bottom (maxTS > newTS)
2244
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
2245
- */
2246
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
2247
- this.isLoadingBottom = true;
2216
+ if (this.isInView(this.$bottomRef)) {
2248
2217
  await this.loadNextPage();
2249
- this.isLoadingBottom = false;
2250
- if (this.shouldScrollToBottom)
2251
- this.scrollToBottom();
2218
+ }
2219
+ else if (this.isInView(this.$topRef)) {
2220
+ this.showNewMessagesCTR = true;
2221
+ await this.loadPrevPage();
2252
2222
  }
2253
2223
  };
2254
2224
  }
2255
2225
  }
2226
+ componentDidRender() {
2227
+ if (!this.pendingScrollAnchor)
2228
+ return;
2229
+ const anchor = this.pendingScrollAnchor;
2230
+ this.pendingScrollAnchor = null;
2231
+ this.restoreScrollToAnchor(anchor);
2232
+ }
2256
2233
  async loadPrevPage() {
2257
2234
  if (this.isLoading)
2258
2235
  return;
2259
- /**
2260
- * NOTE(ikabra): this case also runs on initial load
2261
- * if scrolling up ->
2262
- * fetch older messages and push to the end of the array
2263
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
2264
- */
2236
+ const scrollAnchor = this.getScrollAnchor('top');
2265
2237
  // if no old timestamp, it means we are at initial state
2266
2238
  if (!this.oldTS)
2267
2239
  this.oldTS = new Date().getTime();
2268
2240
  // load data
2269
2241
  this.isLoading = true;
2242
+ this.isLoadingTop = true;
2270
2243
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
2271
2244
  this.isLoading = false;
2245
+ this.isLoadingTop = false;
2272
2246
  // no more old messages to show, we are at the top of the page
2273
2247
  if (!data.length)
2274
2248
  return;
2275
2249
  // add old data to the end of the array
2276
2250
  this.pages.push(data);
2277
2251
  // clear old pages when we reach the limit
2278
- if (this.pages.length > this.pagesAllowed) {
2279
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
2280
- /**
2281
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
2282
- * we are doing this because any of the middle pages in the currently rendered pages
2283
- * could be empty as we allow deleting messages.
2284
- * This helps us set the first empty index correctly.
2285
- */
2286
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
2287
- if (this.pages[i].length > 0)
2288
- break;
2289
- this.firstEmptyIndex = i;
2290
- }
2291
- }
2292
- // update the old timestamp
2252
+ if (this.pages.length > this.pagesAllowed)
2253
+ this.pages.shift();
2254
+ // update timestamps
2293
2255
  const lastPage = this.pages[this.pages.length - 1];
2294
2256
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
2295
- // update the new timestamp
2296
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
2257
+ this.newTS = this.pages[0][0].timeMs;
2258
+ if (!this.maxTS)
2259
+ this.maxTS = this.newTS;
2297
2260
  this.rerender();
2261
+ // fix scroll position
2262
+ if (scrollAnchor)
2263
+ this.pendingScrollAnchor = scrollAnchor;
2298
2264
  }
2299
2265
  async loadNextPage() {
2300
2266
  if (this.isLoading)
2301
2267
  return;
2302
- // new timestamp needs to be assigned by loadPrevPage method
2268
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
2303
2269
  if (!this.newTS) {
2304
2270
  this.showNewMessagesCTR = false;
2305
2271
  this.shouldScrollToBottom = false;
2306
2272
  return;
2307
2273
  }
2308
- // load data
2274
+ // for autoscroll or scroll to bottom button
2275
+ const maxAutoLoads = 200;
2276
+ let loads = 0;
2277
+ let prevNewTS = this.newTS;
2309
2278
  this.isLoading = true;
2310
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
2311
- this.isLoading = false;
2312
- // no more new messages to load
2313
- if (!data.length) {
2314
- this.showNewMessagesCTR = false;
2315
- this.shouldScrollToBottom = false;
2316
- // remove extra pages from the start if any (could be due to users deleting messages)
2317
- this.pages = this.pages.filter((page) => page.length > 0);
2318
- this.firstEmptyIndex = -1;
2319
- return;
2320
- }
2321
- // when filling empty pages
2322
- if (this.firstEmptyIndex > -1) {
2323
- this.pages[this.firstEmptyIndex] = data.reverse();
2324
- }
2325
- else {
2326
- // when already at the bottom and loading messages in realtime
2279
+ this.isLoadingBottom = true;
2280
+ while (loads < maxAutoLoads) {
2281
+ const scrollAnchor = this.getScrollAnchor('bottom');
2282
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
2283
+ this.isLoading = false;
2284
+ this.isLoadingBottom = false;
2285
+ // no more new messages to load
2286
+ if (!data.length) {
2287
+ this.maxTS = this.newTS;
2288
+ this.showNewMessagesCTR = false;
2289
+ this.shouldScrollToBottom = false;
2290
+ break;
2291
+ }
2292
+ // load new messages and append to the start
2327
2293
  this.pages.unshift(data.reverse());
2294
+ // remove pages if out of bounds
2295
+ if (this.pages.length > this.pagesAllowed)
2296
+ this.pages.pop();
2297
+ // update timestamps
2298
+ const lastPage = this.pages[this.pages.length - 1];
2299
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
2300
+ this.newTS = this.pages[0][0].timeMs;
2301
+ this.rerender();
2302
+ this.pendingScrollAnchor = scrollAnchor;
2303
+ if (!this.shouldScrollToBottom)
2304
+ break;
2305
+ // if should scroll to bottom then retrigger
2306
+ await this.waitForNextFrame();
2307
+ this.scrollToBottom();
2308
+ await this.waitForNextFrame();
2309
+ // if no new messages, break
2310
+ if (this.newTS === prevNewTS)
2311
+ break;
2312
+ prevNewTS = this.newTS;
2313
+ loads++;
2328
2314
  }
2329
- if (this.pages.length > this.pagesAllowed) {
2330
- this.pages.pop();
2315
+ }
2316
+ // Find the element that is closest to the top/bottom of the container
2317
+ getScrollAnchor(edge = 'top') {
2318
+ if (!this.$containerRef)
2319
+ return null;
2320
+ const containerRect = this.$containerRef.getBoundingClientRect();
2321
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
2322
+ let best = null;
2323
+ for (const el of candidates) {
2324
+ const rect = el.getBoundingClientRect();
2325
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
2326
+ if (!isVisibleInContainer)
2327
+ continue;
2328
+ if (edge === 'top') {
2329
+ const offsetTop = rect.top - containerRect.top;
2330
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
2331
+ best = { id: el.id, edge: 'top', offsetTop };
2332
+ }
2333
+ }
2334
+ else {
2335
+ const offsetBottom = containerRect.bottom - rect.bottom;
2336
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
2337
+ best = { id: el.id, edge: 'bottom', offsetBottom };
2338
+ }
2339
+ }
2331
2340
  }
2332
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
2333
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
2334
- // remove all empty pages from the end
2335
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
2336
- if (this.pages[i].length > 0)
2337
- break;
2338
- // if page is empty, remove it
2339
- this.pages.pop();
2341
+ return best;
2342
+ }
2343
+ //instant scroll to anchor to make sure we are at the same position after a rerender
2344
+ restoreScrollToAnchor(anchor) {
2345
+ if (!this.$containerRef)
2346
+ return;
2347
+ // make element id safe to use inside a CSS selector
2348
+ const escapeId = (id) => {
2349
+ var _a;
2350
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
2351
+ return typeof cssEscape === 'function'
2352
+ ? cssEscape(id)
2353
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
2354
+ };
2355
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
2356
+ if (!el)
2357
+ return;
2358
+ const containerRect = this.$containerRef.getBoundingClientRect();
2359
+ const rect = el.getBoundingClientRect();
2360
+ if (anchor.edge === 'top') {
2361
+ const newOffsetTop = rect.top - containerRect.top;
2362
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
2340
2363
  }
2341
- // update the old timestamp
2342
- const lastPage = this.pages[this.pages.length - 1];
2343
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
2344
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
2345
- if (this.$containerRef.scrollTop === 0)
2346
- this.$containerRef.scrollTop = -60;
2347
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
2348
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
2349
- this.rerender();
2364
+ else {
2365
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
2366
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
2367
+ }
2368
+ }
2369
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
2370
+ scrollToBottom() {
2371
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
2372
+ }
2373
+ waitForNextFrame() {
2374
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
2375
+ }
2376
+ rerender() {
2377
+ this.rerenderBoolean = !this.rerenderBoolean;
2350
2378
  }
2351
2379
  render() {
2352
2380
  /**
2353
- * div.container is flex=column-reverse
2354
- * which is why div#bottom-scroll comes before div#top-scroll
2381
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
2355
2382
  */
2356
- 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: () => {
2383
+ 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: () => {
2357
2384
  this.shouldScrollToBottom = true;
2358
2385
  this.scrollToBottom();
2359
- } }, 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) }))));
2386
+ } }, 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) }))));
2360
2387
  }
2361
2388
  };
2362
2389
  __decorate$2([
@@ -92,7 +92,7 @@ const RtkChatToggle = class {
92
92
  const newMessages = messages.filter((m) => m.timeMs > meetingStartedTimeMs);
93
93
  if (newMessages.length === this.pageSize && newMessages.length > 0) {
94
94
  // all messages are new, so we can't know the exact count, but we know there are at least pageSize - 1 new messages
95
- this.unreadMessageCount = this.pageSize - 1;
95
+ this.unreadMessageCount = newMessages.length;
96
96
  }
97
97
  else {
98
98
  this.unreadMessageCount = newMessages.length;