@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
@@ -11,7 +11,7 @@ import { d as defineCustomElement$7 } from './p-2447a26f.js';
11
11
  import { d as defineCustomElement$6 } from './p-819cb785.js';
12
12
  import { d as defineCustomElement$5 } from './p-7148ec6a.js';
13
13
  import { d as defineCustomElement$4 } from './p-0f2de0f8.js';
14
- import { d as defineCustomElement$3 } from './p-e7e2156a.js';
14
+ import { d as defineCustomElement$3 } from './p-ad8282dc.js';
15
15
  import { d as defineCustomElement$2 } from './p-4ebf9684.js';
16
16
  import { d as defineCustomElement$1 } from './p-6739a399.js';
17
17
 
@@ -35,6 +35,7 @@ const RtkChatMessagesUiPaginated = /*@__PURE__*/ proxyCustomElement(class RtkCha
35
35
  this.__attachShadow();
36
36
  this.editMessageInit = createEvent(this, "editMessageInit", 7);
37
37
  this.onPinMessage = createEvent(this, "pinMessage", 7);
38
+ this.onEditMessage = createEvent(this, "editMessage", 7);
38
39
  this.onDeleteMessage = createEvent(this, "deleteMessage", 7);
39
40
  this.stateUpdate = createEvent(this, "rtkStateUpdate", 7);
40
41
  /** Icon pack */
@@ -85,22 +86,18 @@ const RtkChatMessagesUiPaginated = /*@__PURE__*/ proxyCustomElement(class RtkCha
85
86
  };
86
87
  this.getMessageActions = (message) => {
87
88
  const actions = [];
88
- // const isSelf = this.meeting.self.userId === message.userId;
89
- // const chatMessagePermissions = this.meeting.self.permissions?.chatMessage;
90
- // const canEdit =
91
- // chatMessagePermissions === undefined
92
- // ? isSelf
93
- // : chatMessagePermissions.canEdit === 'ALL' ||
94
- // (chatMessagePermissions.canEdit === 'SELF' && isSelf);
95
- const canDelete = message.userId === this.meeting.self.userId;
96
- if (this.meeting.self.permissions.pinParticipant) {
89
+ const messageBelongsToSelf = message.userId === this.meeting.self.userId;
90
+ actions.push({
91
+ id: 'pin_message',
92
+ label: message.pinned ? this.t('unpin') : this.t('pin'),
93
+ icon: this.iconPack.pin,
94
+ });
95
+ if (messageBelongsToSelf) {
97
96
  actions.push({
98
- id: 'pin_message',
99
- label: message.pinned ? this.t('unpin') : this.t('pin'),
100
- icon: this.iconPack.pin,
97
+ id: 'edit_message',
98
+ label: this.t('chat.edit_msg'),
99
+ icon: this.iconPack.edit,
101
100
  });
102
- }
103
- if (canDelete) {
104
101
  actions.push({
105
102
  id: 'delete_message',
106
103
  label: this.t('chat.delete_msg'),
@@ -114,6 +111,9 @@ const RtkChatMessagesUiPaginated = /*@__PURE__*/ proxyCustomElement(class RtkCha
114
111
  case 'pin_message':
115
112
  this.onPinMessage.emit(message);
116
113
  break;
114
+ case 'edit_message':
115
+ this.onEditMessage.emit(message);
116
+ break;
117
117
  case 'delete_message':
118
118
  this.onDeleteMessage.emit(message);
119
119
  break;
@@ -142,7 +142,7 @@ const RtkChatMessagesUiPaginated = /*@__PURE__*/ proxyCustomElement(class RtkCha
142
142
  }
143
143
  const isSelf = message.userId === this.meeting.self.userId;
144
144
  const viewType = isSelf ? 'outgoing' : 'incoming';
145
- 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: () => {
145
+ 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: () => {
146
146
  this.stateUpdate.emit({ image: message });
147
147
  } }))))))));
148
148
  };
@@ -190,7 +190,7 @@ const RtkChatMessagesUiPaginated = /*@__PURE__*/ proxyCustomElement(class RtkCha
190
190
  this.lastReadMessageIndex = -1;
191
191
  }
192
192
  render() {
193
- 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' }))));
193
+ 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' }))));
194
194
  }
195
195
  get host() { return this; }
196
196
  static get watchers() { return {
@@ -1,4 +1,4 @@
1
- import { p as proxyCustomElement, H, w as writeTask, h, e as Host } from './p-c3592601.js';
1
+ import { p as proxyCustomElement, H, h, e as Host } from './p-c3592601.js';
2
2
  import { e as defaultIconPack, i as useLanguage } from './p-e847fee9.js';
3
3
  import { S as SyncWithStore } from './p-6f7c46d2.js';
4
4
  import { d as defineCustomElement$3 } from './p-1391bef0.js';
@@ -10,32 +10,30 @@ const rtkPaginatedListCss = ".scrollbar{scrollbar-width:thin;scrollbar-color:var
10
10
  const RtkPaginatedListStyle0 = rtkPaginatedListCss;
11
11
 
12
12
  /**
13
- * HOW INFINITE SCROLL WORKS:
13
+ * NOTE(ikabra): INFINITE SCROLL IMPLEMENTATION:
14
14
  *
15
- * We use intersectionObserver to scroll up.
16
- * We use scrollEnd listener to scroll down.
15
+ * Uses scrollend listener for 2way scrolling.
16
+ * Empty divs ($topRef, $bottomRef) act as scroll triggers to fetch new messages.
17
17
  *
18
- * Why?
19
- * intersectionObserver doesn't work reliably for 2 way scrolling but has great ux,
20
- * so we use it to smoothly scroll up.
18
+ * UPWARD SCROLLING:
19
+ * - Fetch top anchor (element currently visible to the user near top)
20
+ * - Fetch older messages, push to end of 2D array
21
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
21
22
  *
22
- * We have empty divs at the top and bottom ($topRef, $bottomRef)
23
- * which act as triggers to tell that we have reached the top or end of our messages and need to fetch new messages,
23
+ * DOWNWARD SCROLLING:
24
+ * - Fetch bottom anchor (element currently visible to the user near bottom)
25
+ * - Fetch new page, insert at the start
26
+ * - Update timestamps & firstEmptyIndex, then rerender
27
+ * - When exceeding pagesAllowed, delete pages and scroll back to anchor
24
28
  *
25
- * When scrolling up, we can't remove pages as intersectionObserver relies on
26
- * the index of dom elements to work properly.
27
- * So instead, we fetch older messages and push them to the end of the 2d array
28
- * if length exceeds pagesAllowed, we free up the pages and keep the first empty index in memory (firstEmptyIndex).
29
+ * ADDING NEW NODES:
30
+ * - If no pages exist, load old page
31
+ * - If on 1st page, append messages till page size is full and then load new page
29
32
  *
30
- * For scrolling down, when scroll ends we see if the bottomRef is in view.
31
- * If yes, we fetch the new page and insert it at the firstEmptyIndex.
32
- * We update timestamps & firstEmptyIndex, then we rerender.
33
- *
34
- * If we have exceeded our page allowance we delete old pages.
35
- *
36
- * In this case deleting pages is okay as we are not relying on the index of dom elements to detect page end.
37
- *
38
- * This also simplifies the code because when a user scrolls up we do not need to manage a lastEmptyIndex.
33
+ * DELETE NODE:
34
+ * - If deleting the only available node, reset to initial state
35
+ * - If page is empty, delete it
36
+ * - Update timestamp curors
39
37
  */
40
38
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
41
39
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -52,43 +50,27 @@ const RtkPaginatedList = /*@__PURE__*/ proxyCustomElement(class RtkPaginatedList
52
50
  super();
53
51
  this.__registerHost();
54
52
  this.__attachShadow();
55
- /**
56
- * when scrolling up, we can't remove pages as intersectionObserver relies on
57
- * the index of dom elements to stay stable.
58
- * So, instead we free up the pages and keep the last empty index in memory.
59
- */
60
- this.firstEmptyIndex = -1;
61
- this.maxTS = 0;
62
53
  // the length of pages will always be pageSize + 2
63
54
  this.pages = [];
55
+ // Controls whether to keep auto-scrolling when a new page load.
56
+ this.shouldScrollToBottom = false;
57
+ // Shows "scroll to bottom" button when new nodes arrive and autoscroll is off.
58
+ this.showNewMessagesCTR = false;
64
59
  /** label to show when empty */
65
60
  this.emptyListLabel = null;
66
- this.rerenderBoolean = false;
67
- this.showEmptyListLabel = false;
68
61
  /** Icon pack */
69
62
  this.iconPack = defaultIconPack;
70
63
  /** Language */
71
64
  this.t = useLanguage();
65
+ this.rerenderBoolean = false;
66
+ this.showEmptyListLabel = false;
72
67
  this.isLoading = false;
73
68
  this.isLoadingTop = false;
74
69
  this.isLoadingBottom = false;
75
- /**
76
- * Even when auto scroll is enabled, we only want to scroll if a new realtime message has arrived.
77
- * This variable tells us if we should respect auto scroll after a new page has been loaded.
78
- * It is also used by the scroll to bottom button.
79
- * */
80
- this.shouldScrollToBottom = false;
81
- /** UI Indicator for the "scroll to bottom" button.
82
- * Toggles on when a new node is added and autoscroll is disabled.
83
- * Toggles off when all nodes are loaded */
84
- this.showNewMessagesCTR = false;
85
- this.observe = (el) => {
86
- if (!el)
87
- return;
88
- this.intersectionObserver.observe(el);
89
- };
90
- this.isAtBottom = () => {
91
- const rect = this.$bottomRef.getBoundingClientRect();
70
+ // Tells us if we need to scroll to a specific anchor after a rerender
71
+ this.pendingScrollAnchor = null;
72
+ this.isInView = (el) => {
73
+ const rect = el.getBoundingClientRect();
92
74
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
93
75
  };
94
76
  }
@@ -97,224 +79,255 @@ const RtkPaginatedList = /*@__PURE__*/ proxyCustomElement(class RtkPaginatedList
97
79
  * @param {DataNode} node - The data node to add to the beginning of the list
98
80
  */
99
81
  async onNewNode(node) {
100
- // Always update the maxTS. New messages will load on scroll till the end cursor (newTS) reaches this value.
101
- this.maxTS = Math.max(this.maxTS, node.timeMs);
102
- // if we are at the bottom of the page
103
- if (this.firstEmptyIndex === -1) {
104
- // if there are no pages, load the first page
105
- if (this.pages.length < 1) {
106
- // update old timer to 1ms ahead of the latest message as we subtract this value to avoid loading duplicate messages when scrolling
107
- this.oldTS = node.timeMs + 1;
108
- this.loadPrevPage();
82
+ // if there are no pages, load the first page
83
+ if (this.pages.length < 1) {
84
+ this.oldTS = node.timeMs + 1;
85
+ this.loadPrevPage();
86
+ }
87
+ else if (this.maxTS === this.newTS) {
88
+ this.maxTS = node.timeMs;
89
+ // append messages to the page if page has not reached full capacity
90
+ if (this.pages[0].length < this.pageSize) {
91
+ this.pages[0].unshift(node);
92
+ this.newTS = node.timeMs;
93
+ this.rerender();
109
94
  }
110
95
  else {
111
- // append messages to the page if page has not reached full capacity
112
- if (this.pages[0].length < this.pageSize) {
113
- this.pages[0].unshift(node);
114
- this.newTS = node.timeMs;
115
- this.rerender();
116
- }
117
- else {
118
- // if page is at full capacity, load next page
119
- this.loadNextPage();
120
- }
96
+ // if page is at full capacity, load next page
97
+ this.loadNextPage();
121
98
  }
122
99
  }
123
- // If autoscroll is enabled, this method will scroll to the bottom
100
+ // If autoscroll is enabled, scroll to the bottom
124
101
  if (this.autoScroll) {
125
102
  this.shouldScrollToBottom = true;
126
103
  this.scrollToBottom();
127
104
  }
128
- else {
129
- this.showNewMessagesCTR = true;
130
- }
131
- }
132
- // this method is called recursively based on shouldScrollToBottom (see scrollEnd listener)
133
- scrollToBottom() {
134
- this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
135
105
  }
136
106
  /**
137
107
  * Deletes a node anywhere from the list
138
108
  * @param {string} id - The id of the node to delete
139
109
  * */
140
110
  async onNodeDelete(id) {
141
- // Iterate only over pages that have content (not empty)
142
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
111
+ var _a, _b;
112
+ let didDelete = false;
113
+ for (let i = this.pages.length - 1; i >= 0; i--) {
143
114
  const index = this.pages[i].findIndex((node) => node.id === id);
144
- // message in view
145
- if (index !== -1) {
146
- // delete message
147
- this.pages[i].splice(index, 1);
148
- // if we are on the first page and it's now empty, we need to go back to initial state
149
- if (i === 0 && this.pages[i].length === 0) {
150
- this.pages.shift();
151
- this.firstEmptyIndex = -1;
152
- }
153
- else if (i === this.firstEmptyIndex + 1) {
154
- // if newest page is empty, update first empty index
155
- if (this.pages[i].length === 0)
156
- this.firstEmptyIndex++;
157
- // update timestamp, first empty index could be -1, so we need to cap it at 0
158
- this.newTS = this.pages[Math.max(this.firstEmptyIndex, 0)][0].timeMs;
159
- }
160
- else if (i === this.firstEmptyIndex + this.pagesAllowed) {
161
- // if oldest page is empty, remove it
162
- if (this.pages[i].length === 0)
163
- this.pages.pop();
164
- // update timestamp
165
- const lastPage = this.pages[this.firstEmptyIndex + this.pagesAllowed];
166
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
167
- }
168
- this.rerender();
169
- }
115
+ // if message not found, move on
116
+ if (index === -1)
117
+ continue;
118
+ // delete message
119
+ this.pages[i].splice(index, 1);
120
+ // if page is empty, delete it
121
+ if (this.pages[i].length === 0)
122
+ this.pages.splice(i, 1);
123
+ didDelete = true;
124
+ break;
170
125
  }
126
+ if (!didDelete)
127
+ return;
128
+ // update timestamps
129
+ const firstPage = this.pages[0];
130
+ const lastPage = this.pages[this.pages.length - 1];
131
+ this.newTS = (_a = firstPage === null || firstPage === void 0 ? void 0 : firstPage[0]) === null || _a === void 0 ? void 0 : _a.timeMs;
132
+ this.oldTS = (_b = lastPage === null || lastPage === void 0 ? void 0 : lastPage[lastPage.length - 1]) === null || _b === void 0 ? void 0 : _b.timeMs;
133
+ this.rerender();
171
134
  }
172
135
  /**
173
136
  * Updates a new node anywhere in the list
174
- * @param {string} _id - The id of the node to update
175
- * @param {DataNode} _node - The updated data node
137
+ * @param {string} id - The id of the node to update
138
+ * @param {DataNode} node - The updated data node
176
139
  * */
177
- async onNodeUpdate(_id, _node) { }
178
- rerender() {
179
- this.rerenderBoolean = !this.rerenderBoolean;
140
+ async onNodeUpdate(id, node) {
141
+ for (let i = this.pages.length - 1; i >= 0; i--) {
142
+ const index = this.pages[i].findIndex((node) => node.id === id);
143
+ // if message not found, move on
144
+ if (index === -1)
145
+ continue;
146
+ // edit message
147
+ this.pages[i][index] = node;
148
+ this.rerender();
149
+ break;
150
+ }
180
151
  }
181
152
  connectedCallback() {
182
153
  this.rerender = debounce(this.rerender.bind(this), 50, { maxWait: 200 });
183
- this.intersectionObserver = new IntersectionObserver((entries) => {
184
- writeTask(async () => {
185
- for (const entry of entries) {
186
- if (entry.target.id === 'top-scroll' && entry.isIntersecting) {
187
- this.isLoadingTop = true;
188
- await this.loadPrevPage();
189
- this.isLoadingTop = false;
190
- }
191
- }
192
- });
193
- });
194
154
  }
195
155
  componentDidLoad() {
196
- this.observe(this.$topRef);
156
+ // initial load
157
+ this.loadPrevPage();
197
158
  if (this.$containerRef) {
198
159
  this.$containerRef.onscrollend = async () => {
199
- /**
200
- * Load new page if:
201
- * if there are nodes to load at the bottom (maxTS > newTS)
202
- * or if there are pages to fill at the bottom (firstEmptyIndex > -1)
203
- */
204
- if (this.isAtBottom() && (this.maxTS > this.newTS || this.firstEmptyIndex > -1)) {
205
- this.isLoadingBottom = true;
160
+ if (this.isInView(this.$bottomRef)) {
206
161
  await this.loadNextPage();
207
- this.isLoadingBottom = false;
208
- if (this.shouldScrollToBottom)
209
- this.scrollToBottom();
162
+ }
163
+ else if (this.isInView(this.$topRef)) {
164
+ this.showNewMessagesCTR = true;
165
+ await this.loadPrevPage();
210
166
  }
211
167
  };
212
168
  }
213
169
  }
170
+ componentDidRender() {
171
+ if (!this.pendingScrollAnchor)
172
+ return;
173
+ const anchor = this.pendingScrollAnchor;
174
+ this.pendingScrollAnchor = null;
175
+ this.restoreScrollToAnchor(anchor);
176
+ }
214
177
  async loadPrevPage() {
215
178
  if (this.isLoading)
216
179
  return;
217
- /**
218
- * NOTE(ikabra): this case also runs on initial load
219
- * if scrolling up ->
220
- * fetch older messages and push to the end of the array
221
- * cleanup 1st non empty page from the array if length exceeds pagesAllowed
222
- */
180
+ const scrollAnchor = this.getScrollAnchor('top');
223
181
  // if no old timestamp, it means we are at initial state
224
182
  if (!this.oldTS)
225
183
  this.oldTS = new Date().getTime();
226
184
  // load data
227
185
  this.isLoading = true;
186
+ this.isLoadingTop = true;
228
187
  const data = await this.fetchData(this.oldTS - 1, this.pageSize, true);
229
188
  this.isLoading = false;
189
+ this.isLoadingTop = false;
230
190
  // no more old messages to show, we are at the top of the page
231
191
  if (!data.length)
232
192
  return;
233
193
  // add old data to the end of the array
234
194
  this.pages.push(data);
235
195
  // clear old pages when we reach the limit
236
- if (this.pages.length > this.pagesAllowed) {
237
- this.pages[this.pages.length - this.pagesAllowed - 1] = [];
238
- /**
239
- * find last non empty page in range (this.pages.length, this.firstEmptyIndex)
240
- * we are doing this because any of the middle pages in the currently rendered pages
241
- * could be empty as we allow deleting messages.
242
- * This helps us set the first empty index correctly.
243
- */
244
- for (let i = this.firstEmptyIndex + 1; i < this.pages.length; i++) {
245
- if (this.pages[i].length > 0)
246
- break;
247
- this.firstEmptyIndex = i;
248
- }
249
- }
250
- // update the old timestamp
196
+ if (this.pages.length > this.pagesAllowed)
197
+ this.pages.shift();
198
+ // update timestamps
251
199
  const lastPage = this.pages[this.pages.length - 1];
252
200
  this.oldTS = lastPage[lastPage.length - 1].timeMs;
253
- // update the new timestamp
254
- this.newTS = this.pages[this.firstEmptyIndex + 1][0].timeMs;
201
+ this.newTS = this.pages[0][0].timeMs;
202
+ if (!this.maxTS)
203
+ this.maxTS = this.newTS;
255
204
  this.rerender();
205
+ // fix scroll position
206
+ if (scrollAnchor)
207
+ this.pendingScrollAnchor = scrollAnchor;
256
208
  }
257
209
  async loadNextPage() {
258
210
  if (this.isLoading)
259
211
  return;
260
- // new timestamp needs to be assigned by loadPrevPage method
212
+ // Do nothing. New timestamp needs to be assigned by loadPrevPage method
261
213
  if (!this.newTS) {
262
214
  this.showNewMessagesCTR = false;
263
215
  this.shouldScrollToBottom = false;
264
216
  return;
265
217
  }
266
- // load data
218
+ // for autoscroll or scroll to bottom button
219
+ const maxAutoLoads = 200;
220
+ let loads = 0;
221
+ let prevNewTS = this.newTS;
267
222
  this.isLoading = true;
268
- const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
269
- this.isLoading = false;
270
- // no more new messages to load
271
- if (!data.length) {
272
- this.showNewMessagesCTR = false;
273
- this.shouldScrollToBottom = false;
274
- // remove extra pages from the start if any (could be due to users deleting messages)
275
- this.pages = this.pages.filter((page) => page.length > 0);
276
- this.firstEmptyIndex = -1;
277
- return;
278
- }
279
- // when filling empty pages
280
- if (this.firstEmptyIndex > -1) {
281
- this.pages[this.firstEmptyIndex] = data.reverse();
282
- }
283
- else {
284
- // when already at the bottom and loading messages in realtime
223
+ this.isLoadingBottom = true;
224
+ while (loads < maxAutoLoads) {
225
+ const scrollAnchor = this.getScrollAnchor('bottom');
226
+ const data = await this.fetchData(this.newTS + 1, this.pageSize, false);
227
+ this.isLoading = false;
228
+ this.isLoadingBottom = false;
229
+ // no more new messages to load
230
+ if (!data.length) {
231
+ this.maxTS = this.newTS;
232
+ this.showNewMessagesCTR = false;
233
+ this.shouldScrollToBottom = false;
234
+ break;
235
+ }
236
+ // load new messages and append to the start
285
237
  this.pages.unshift(data.reverse());
238
+ // remove pages if out of bounds
239
+ if (this.pages.length > this.pagesAllowed)
240
+ this.pages.pop();
241
+ // update timestamps
242
+ const lastPage = this.pages[this.pages.length - 1];
243
+ this.oldTS = lastPage[lastPage.length - 1].timeMs;
244
+ this.newTS = this.pages[0][0].timeMs;
245
+ this.rerender();
246
+ this.pendingScrollAnchor = scrollAnchor;
247
+ if (!this.shouldScrollToBottom)
248
+ break;
249
+ // if should scroll to bottom then retrigger
250
+ await this.waitForNextFrame();
251
+ this.scrollToBottom();
252
+ await this.waitForNextFrame();
253
+ // if no new messages, break
254
+ if (this.newTS === prevNewTS)
255
+ break;
256
+ prevNewTS = this.newTS;
257
+ loads++;
286
258
  }
287
- if (this.pages.length > this.pagesAllowed) {
288
- this.pages.pop();
259
+ }
260
+ // Find the element that is closest to the top/bottom of the container
261
+ getScrollAnchor(edge = 'top') {
262
+ if (!this.$containerRef)
263
+ return null;
264
+ const containerRect = this.$containerRef.getBoundingClientRect();
265
+ const candidates = Array.from(this.$containerRef.querySelectorAll('[id]')).filter((el) => el.id !== 'top-scroll' && el.id !== 'bottom-scroll');
266
+ let best = null;
267
+ for (const el of candidates) {
268
+ const rect = el.getBoundingClientRect();
269
+ const isVisibleInContainer = rect.bottom > containerRect.top && rect.top < containerRect.bottom;
270
+ if (!isVisibleInContainer)
271
+ continue;
272
+ if (edge === 'top') {
273
+ const offsetTop = rect.top - containerRect.top;
274
+ if (best == null || (best.edge === 'top' && offsetTop < best.offsetTop)) {
275
+ best = { id: el.id, edge: 'top', offsetTop };
276
+ }
277
+ }
278
+ else {
279
+ const offsetBottom = containerRect.bottom - rect.bottom;
280
+ if (best == null || (best.edge === 'bottom' && offsetBottom < best.offsetBottom)) {
281
+ best = { id: el.id, edge: 'bottom', offsetBottom };
282
+ }
283
+ }
289
284
  }
290
- // smallest value for firstEmptyIndex can be -1, so we cap the index at 0
291
- this.newTS = this.pages[Math.max(0, this.firstEmptyIndex)][0].timeMs;
292
- // remove all empty pages from the end
293
- for (let i = this.pages.length - 1; i > this.firstEmptyIndex; i--) {
294
- if (this.pages[i].length > 0)
295
- break;
296
- // if page is empty, remove it
297
- this.pages.pop();
285
+ return best;
286
+ }
287
+ //instant scroll to anchor to make sure we are at the same position after a rerender
288
+ restoreScrollToAnchor(anchor) {
289
+ if (!this.$containerRef)
290
+ return;
291
+ // make element id safe to use inside a CSS selector
292
+ const escapeId = (id) => {
293
+ var _a;
294
+ const cssEscape = (_a = globalThis.CSS) === null || _a === void 0 ? void 0 : _a.escape;
295
+ return typeof cssEscape === 'function'
296
+ ? cssEscape(id)
297
+ : id.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
298
+ };
299
+ const el = this.$containerRef.querySelector(`#${escapeId(anchor.id)}`);
300
+ if (!el)
301
+ return;
302
+ const containerRect = this.$containerRef.getBoundingClientRect();
303
+ const rect = el.getBoundingClientRect();
304
+ if (anchor.edge === 'top') {
305
+ const newOffsetTop = rect.top - containerRect.top;
306
+ this.$containerRef.scrollTop += newOffsetTop - anchor.offsetTop;
298
307
  }
299
- // update the old timestamp
300
- const lastPage = this.pages[this.pages.length - 1];
301
- this.oldTS = lastPage[lastPage.length - 1].timeMs;
302
- // when scrolling too fast scroll a bit to the top to be able to load new messages when you scroll down
303
- if (this.$containerRef.scrollTop === 0)
304
- this.$containerRef.scrollTop = -60;
305
- // smallest value for this index can be -1 (indicates we are at the bottom of the page).
306
- this.firstEmptyIndex = Math.max(-1, this.firstEmptyIndex - 1);
307
- this.rerender();
308
+ else {
309
+ const newOffsetBottom = containerRect.bottom - rect.bottom;
310
+ this.$containerRef.scrollTop += anchor.offsetBottom - newOffsetBottom;
311
+ }
312
+ }
313
+ // this method is called recursively based on shouldScrollToBottom (see loadNextPage)
314
+ scrollToBottom() {
315
+ this.$bottomRef.scrollIntoView({ behavior: 'smooth' });
316
+ }
317
+ waitForNextFrame() {
318
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
319
+ }
320
+ rerender() {
321
+ this.rerenderBoolean = !this.rerenderBoolean;
308
322
  }
309
323
  render() {
310
324
  /**
311
- * div.container is flex=column-reverse
312
- * which is why div#bottom-scroll comes before div#top-scroll
325
+ * div.container is flex=column-reversewhich is why div#bottom-scroll comes before div#top-scroll
313
326
  */
314
- 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: () => {
327
+ 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: () => {
315
328
  this.shouldScrollToBottom = true;
316
329
  this.scrollToBottom();
317
- } }, 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) }))));
330
+ } }, 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) }))));
318
331
  }
319
332
  static get style() { return RtkPaginatedListStyle0; }
320
333
  }, [1, "rtk-paginated-list", {
@@ -1,4 +1,4 @@
1
- import { R as RtkChatMessagesUiPaginated$1, d as defineCustomElement$1 } from './p-b6781e91.js';
1
+ import { R as RtkChatMessagesUiPaginated$1, d as defineCustomElement$1 } from './p-9213c3fc.js';
2
2
 
3
3
  const RtkChatMessagesUiPaginated = RtkChatMessagesUiPaginated$1;
4
4
  const defineCustomElement = defineCustomElement$1;
@@ -11,7 +11,7 @@ import { d as defineCustomElement$8 } from './p-fa476519.js';
11
11
  import { d as defineCustomElement$7 } from './p-2447a26f.js';
12
12
  import { d as defineCustomElement$6 } from './p-819cb785.js';
13
13
  import { d as defineCustomElement$5 } from './p-7148ec6a.js';
14
- import { d as defineCustomElement$4 } from './p-e7e2156a.js';
14
+ import { d as defineCustomElement$4 } from './p-ad8282dc.js';
15
15
  import { d as defineCustomElement$3 } from './p-4ebf9684.js';
16
16
  import { d as defineCustomElement$2 } from './p-46d99dd9.js';
17
17
 
@@ -97,7 +97,7 @@ const RtkChatToggle$1 = /*@__PURE__*/ proxyCustomElement(class RtkChatToggle ext
97
97
  const newMessages = messages.filter((m) => m.timeMs > meetingStartedTimeMs);
98
98
  if (newMessages.length === this.pageSize && newMessages.length > 0) {
99
99
  // all messages are new, so we can't know the exact count, but we know there are at least pageSize - 1 new messages
100
- this.unreadMessageCount = this.pageSize - 1;
100
+ this.unreadMessageCount = newMessages.length;
101
101
  }
102
102
  else {
103
103
  this.unreadMessageCount = newMessages.length;
@@ -1,4 +1,4 @@
1
- import { R as RtkChat$1, d as defineCustomElement$1 } from './p-85872241.js';
1
+ import { R as RtkChat$1, d as defineCustomElement$1 } from './p-7e90e964.js';
2
2
 
3
3
  const RtkChat = RtkChat$1;
4
4
  const defineCustomElement = defineCustomElement$1;