@bbki.ng/bb-msg-history 1.0.0 → 2.0.0
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.
- package/dist/components/bb-custom-avatar.d.ts +20 -0
- package/dist/components/bb-custom-avatar.js +145 -0
- package/dist/components/bb-letter-avatar.d.ts +14 -0
- package/dist/components/bb-letter-avatar.js +61 -0
- package/dist/components/bb-loading-overlay.d.ts +14 -0
- package/dist/components/bb-loading-overlay.js +89 -0
- package/dist/components/bb-message-bubble.d.ts +19 -0
- package/dist/components/bb-message-bubble.js +116 -0
- package/dist/components/bb-message.d.ts +27 -0
- package/dist/components/bb-message.js +174 -0
- package/dist/components/bb-msg-history.d.ts +111 -0
- package/dist/components/bb-msg-history.js +473 -0
- package/dist/components/bb-scroll-button.d.ts +16 -0
- package/dist/components/bb-scroll-button.js +161 -0
- package/dist/components/bb-timestamp.d.ts +15 -0
- package/dist/components/bb-timestamp.js +59 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +7 -0
- package/dist/const/styles.js +0 -33
- package/dist/contexts/author-context.d.ts +8 -0
- package/dist/contexts/author-context.js +6 -0
- package/dist/controllers/scroll-controller.d.ts +52 -0
- package/dist/controllers/scroll-controller.js +138 -0
- package/dist/core/renderer.js +1 -9
- package/dist/parsers/base.d.ts +21 -0
- package/dist/parsers/base.js +1 -0
- package/dist/parsers/default-parser.d.ts +10 -0
- package/dist/parsers/default-parser.js +40 -0
- package/dist/parsers/index.d.ts +2 -0
- package/dist/parsers/index.js +1 -0
- package/dist/utils/message-builder.d.ts +0 -4
- package/dist/utils/message-builder.js +0 -15
- package/dist/utils/tooltip.d.ts +11 -2
- package/dist/utils/tooltip.js +56 -13
- package/package.json +1 -1
- package/src/const/styles.ts +0 -33
- package/src/core/renderer.ts +1 -11
- package/src/utils/message-builder.ts +0 -15
- package/src/utils/tooltip.ts +0 -16
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import type { AuthorOptions } from '../types/index.js';
|
|
3
|
+
import type { MessageParser, MessageInput } from '../parsers/base.js';
|
|
4
|
+
import './bb-message.js';
|
|
5
|
+
import './bb-scroll-button.js';
|
|
6
|
+
import './bb-loading-overlay.js';
|
|
7
|
+
/**
|
|
8
|
+
* BBMsgHistory - A chat-style message history web component
|
|
9
|
+
*
|
|
10
|
+
* Uses Lit for reactive rendering with a compositional architecture.
|
|
11
|
+
* Preserves backward compatibility with the lightweight textContent mode.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```html
|
|
15
|
+
* <!-- Lightweight mode -->
|
|
16
|
+
* <bb-msg-history>
|
|
17
|
+
* alice: Hello!
|
|
18
|
+
* bob: Hi there!
|
|
19
|
+
* </bb-msg-history>
|
|
20
|
+
*
|
|
21
|
+
* <!-- With custom authors -->
|
|
22
|
+
* <bb-msg-history id="chat"></bb-msg-history>
|
|
23
|
+
* <script>
|
|
24
|
+
* document.getElementById('chat')
|
|
25
|
+
* .setAuthor('alice', { avatar: '🐱', side: 'right' });
|
|
26
|
+
* </script>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class BBMsgHistory extends LitElement {
|
|
30
|
+
static styles: import("lit").CSSResult;
|
|
31
|
+
loading: boolean;
|
|
32
|
+
hideScrollBar: boolean;
|
|
33
|
+
infinite: boolean;
|
|
34
|
+
hideScrollButton: boolean;
|
|
35
|
+
theme: 'light' | 'dark' | null;
|
|
36
|
+
private _messages;
|
|
37
|
+
private _processedMessages;
|
|
38
|
+
private _userAuthors;
|
|
39
|
+
private _mutationObserver?;
|
|
40
|
+
private _isParsing;
|
|
41
|
+
private _parser;
|
|
42
|
+
private _scrollController;
|
|
43
|
+
connectedCallback(): void;
|
|
44
|
+
disconnectedCallback(): void;
|
|
45
|
+
willUpdate(changedProps: PropertyValues<BBMsgHistory>): void;
|
|
46
|
+
updated(changedProps: PropertyValues<BBMsgHistory>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Configure an author's avatar, side, and colors.
|
|
49
|
+
* Call before or after rendering — the component re-renders automatically.
|
|
50
|
+
*/
|
|
51
|
+
setAuthor(name: string, options: AuthorOptions): this;
|
|
52
|
+
/**
|
|
53
|
+
* Remove a previously set author config.
|
|
54
|
+
*/
|
|
55
|
+
removeAuthor(name: string): this;
|
|
56
|
+
/**
|
|
57
|
+
* Show or hide the loading overlay.
|
|
58
|
+
*/
|
|
59
|
+
setLoading(isLoading: boolean): this;
|
|
60
|
+
/**
|
|
61
|
+
* Append a message to the history.
|
|
62
|
+
* Automatically scrolls to the new message with smooth animation.
|
|
63
|
+
*/
|
|
64
|
+
appendMessage(input: MessageInput): this;
|
|
65
|
+
/**
|
|
66
|
+
* Scroll to the bottom of the message history.
|
|
67
|
+
*/
|
|
68
|
+
scrollToBottom(): this;
|
|
69
|
+
/**
|
|
70
|
+
* Set a custom parser for message parsing
|
|
71
|
+
*/
|
|
72
|
+
setParser(parser: MessageParser): this;
|
|
73
|
+
/**
|
|
74
|
+
* Initialize MutationObserver for Light DOM observation
|
|
75
|
+
*/
|
|
76
|
+
private _initLightDOMObserver;
|
|
77
|
+
/**
|
|
78
|
+
* Parse Light DOM textContent into messages
|
|
79
|
+
*/
|
|
80
|
+
private _parseLightDOM;
|
|
81
|
+
/**
|
|
82
|
+
* Check if messages have changed
|
|
83
|
+
*/
|
|
84
|
+
private _messagesChanged;
|
|
85
|
+
/**
|
|
86
|
+
* Hide light DOM content visually while keeping it for accessibility/parsing
|
|
87
|
+
*/
|
|
88
|
+
private _hideLightDOMContent;
|
|
89
|
+
/**
|
|
90
|
+
* Sync current messages back to light DOM
|
|
91
|
+
*/
|
|
92
|
+
private _syncToLightDOM;
|
|
93
|
+
/**
|
|
94
|
+
* Compute message groups for rendering
|
|
95
|
+
*/
|
|
96
|
+
private _computeGroups;
|
|
97
|
+
/**
|
|
98
|
+
* Check if two messages can be grouped together
|
|
99
|
+
*/
|
|
100
|
+
private _canGroup;
|
|
101
|
+
/**
|
|
102
|
+
* Handle scroll button click
|
|
103
|
+
*/
|
|
104
|
+
private _onScrollButtonClick;
|
|
105
|
+
render(): import("lit").TemplateResult<1>;
|
|
106
|
+
}
|
|
107
|
+
declare global {
|
|
108
|
+
interface HTMLElementTagNameMap {
|
|
109
|
+
'bb-msg-history': BBMsgHistory;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, nothing, unsafeCSS } from 'lit';
|
|
8
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
9
|
+
import { provide } from '@lit/context';
|
|
10
|
+
import { authorContext } from '../contexts/author-context.js';
|
|
11
|
+
import { ScrollController } from '../controllers/scroll-controller.js';
|
|
12
|
+
import { DefaultMessageParser } from '../parsers/default-parser.js';
|
|
13
|
+
import { THEME } from '../const/theme.js';
|
|
14
|
+
import './bb-message.js';
|
|
15
|
+
import './bb-scroll-button.js';
|
|
16
|
+
import './bb-loading-overlay.js';
|
|
17
|
+
/**
|
|
18
|
+
* BBMsgHistory - A chat-style message history web component
|
|
19
|
+
*
|
|
20
|
+
* Uses Lit for reactive rendering with a compositional architecture.
|
|
21
|
+
* Preserves backward compatibility with the lightweight textContent mode.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```html
|
|
25
|
+
* <!-- Lightweight mode -->
|
|
26
|
+
* <bb-msg-history>
|
|
27
|
+
* alice: Hello!
|
|
28
|
+
* bob: Hi there!
|
|
29
|
+
* </bb-msg-history>
|
|
30
|
+
*
|
|
31
|
+
* <!-- With custom authors -->
|
|
32
|
+
* <bb-msg-history id="chat"></bb-msg-history>
|
|
33
|
+
* <script>
|
|
34
|
+
* document.getElementById('chat')
|
|
35
|
+
* .setAuthor('alice', { avatar: '🐱', side: 'right' });
|
|
36
|
+
* </script>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
let BBMsgHistory = class BBMsgHistory extends LitElement {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(...arguments);
|
|
42
|
+
// Public properties
|
|
43
|
+
this.loading = false;
|
|
44
|
+
this.hideScrollBar = false;
|
|
45
|
+
this.infinite = false;
|
|
46
|
+
this.hideScrollButton = false;
|
|
47
|
+
this.theme = null;
|
|
48
|
+
// Private state
|
|
49
|
+
this._messages = [];
|
|
50
|
+
this._processedMessages = [];
|
|
51
|
+
// Context provider for author configurations
|
|
52
|
+
this._userAuthors = new Map();
|
|
53
|
+
this._isParsing = false;
|
|
54
|
+
this._parser = new DefaultMessageParser();
|
|
55
|
+
// Scroll controller
|
|
56
|
+
this._scrollController = new ScrollController(this);
|
|
57
|
+
}
|
|
58
|
+
static { this.styles = css `
|
|
59
|
+
:host {
|
|
60
|
+
display: block;
|
|
61
|
+
position: relative;
|
|
62
|
+
font-family:
|
|
63
|
+
'PT Sans',
|
|
64
|
+
ui-sans-serif,
|
|
65
|
+
system-ui,
|
|
66
|
+
-apple-system,
|
|
67
|
+
BlinkMacSystemFont,
|
|
68
|
+
'Segoe UI',
|
|
69
|
+
Roboto,
|
|
70
|
+
'Helvetica Neue',
|
|
71
|
+
Arial,
|
|
72
|
+
'Noto Sans',
|
|
73
|
+
sans-serif,
|
|
74
|
+
'Apple Color Emoji',
|
|
75
|
+
'Segoe UI Emoji',
|
|
76
|
+
'Segoe UI Symbol',
|
|
77
|
+
'Noto Color Emoji';
|
|
78
|
+
--bb-bg-color: ${unsafeCSS(THEME.gray[50])};
|
|
79
|
+
--bb-max-height: 600px;
|
|
80
|
+
--bb-avatar-bg: #ffffff;
|
|
81
|
+
--bb-avatar-color: ${unsafeCSS(THEME.gray[600])};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:host([theme='dark']) {
|
|
85
|
+
--bb-bg-color: ${unsafeCSS(THEME.gray[900])};
|
|
86
|
+
--bb-avatar-bg: ${unsafeCSS(THEME.slate[600])};
|
|
87
|
+
--bb-avatar-color: ${unsafeCSS(THEME.slate[200])};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@media (prefers-color-scheme: dark) {
|
|
91
|
+
:host {
|
|
92
|
+
--bb-bg-color: ${unsafeCSS(THEME.gray[900])};
|
|
93
|
+
--bb-avatar-bg: ${unsafeCSS(THEME.slate[600])};
|
|
94
|
+
--bb-avatar-color: ${unsafeCSS(THEME.slate[200])};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.history-container {
|
|
99
|
+
margin: 0 auto;
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 0.25rem;
|
|
103
|
+
max-height: var(--bb-max-height, 600px);
|
|
104
|
+
overflow-y: auto;
|
|
105
|
+
scroll-behavior: smooth;
|
|
106
|
+
background-color: transparent;
|
|
107
|
+
border-radius: 0.5rem;
|
|
108
|
+
/* Firefox scrollbar */
|
|
109
|
+
scrollbar-width: thin;
|
|
110
|
+
scrollbar-color: ${unsafeCSS(THEME.gray[400])} transparent;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Custom scrollbar for webkit browsers */
|
|
114
|
+
.history-container::-webkit-scrollbar {
|
|
115
|
+
width: 6px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.history-container::-webkit-scrollbar-track {
|
|
119
|
+
background: transparent;
|
|
120
|
+
border-radius: 3px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.history-container::-webkit-scrollbar-thumb {
|
|
124
|
+
background: ${unsafeCSS(THEME.gray[400])};
|
|
125
|
+
border-radius: 3px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.history-container::-webkit-scrollbar-thumb:hover {
|
|
129
|
+
background: ${unsafeCSS(THEME.gray[500])};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Hide scrollbar */
|
|
133
|
+
:host([hide-scroll-bar]) .history-container {
|
|
134
|
+
scrollbar-width: none;
|
|
135
|
+
-ms-overflow-style: none;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
:host([hide-scroll-bar]) .history-container::-webkit-scrollbar {
|
|
139
|
+
display: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Infinite mode */
|
|
143
|
+
:host([infinite]) .history-container {
|
|
144
|
+
max-height: none;
|
|
145
|
+
overflow-y: visible;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Empty state */
|
|
149
|
+
.empty-state {
|
|
150
|
+
text-align: center;
|
|
151
|
+
padding: 2rem;
|
|
152
|
+
color: ${unsafeCSS(THEME.gray[400])};
|
|
153
|
+
font-size: 0.875rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (prefers-color-scheme: dark) {
|
|
157
|
+
.empty-state {
|
|
158
|
+
color: ${unsafeCSS(THEME.gray[500])};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.history-container {
|
|
162
|
+
scrollbar-color: ${unsafeCSS(THEME.gray[600])} transparent;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.history-container::-webkit-scrollbar-thumb {
|
|
166
|
+
background: ${unsafeCSS(THEME.gray[600])};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.history-container::-webkit-scrollbar-thumb:hover {
|
|
170
|
+
background: ${unsafeCSS(THEME.gray[500])};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (max-width: 480px) {
|
|
175
|
+
.history-container {
|
|
176
|
+
max-height: var(--bb-max-height, 70vh);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (prefers-reduced-motion: reduce) {
|
|
181
|
+
.history-container {
|
|
182
|
+
scroll-behavior: auto;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
`; }
|
|
186
|
+
connectedCallback() {
|
|
187
|
+
super.connectedCallback();
|
|
188
|
+
this._initLightDOMObserver();
|
|
189
|
+
// Initial parse if content exists (for SSR or static HTML)
|
|
190
|
+
if (this.textContent?.trim()) {
|
|
191
|
+
this._parseLightDOM();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
disconnectedCallback() {
|
|
195
|
+
super.disconnectedCallback();
|
|
196
|
+
this._mutationObserver?.disconnect();
|
|
197
|
+
}
|
|
198
|
+
willUpdate(changedProps) {
|
|
199
|
+
if (changedProps.has('_messages') ||
|
|
200
|
+
changedProps.has('_userAuthors')) {
|
|
201
|
+
this._processedMessages = this._computeGroups(this._messages);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
updated(changedProps) {
|
|
205
|
+
if (changedProps.has('_processedMessages')) {
|
|
206
|
+
// Update scroll observer when messages change
|
|
207
|
+
this._scrollController.updateObservedMessage();
|
|
208
|
+
// Auto-scroll if not in infinite mode
|
|
209
|
+
if (!this.infinite) {
|
|
210
|
+
this._scrollController.scrollToBottom('auto');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Configure an author's avatar, side, and colors.
|
|
216
|
+
* Call before or after rendering — the component re-renders automatically.
|
|
217
|
+
*/
|
|
218
|
+
setAuthor(name, options) {
|
|
219
|
+
this._userAuthors = new Map([...this._userAuthors, [name, options]]);
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Remove a previously set author config.
|
|
224
|
+
*/
|
|
225
|
+
removeAuthor(name) {
|
|
226
|
+
const newMap = new Map(this._userAuthors);
|
|
227
|
+
newMap.delete(name);
|
|
228
|
+
this._userAuthors = newMap;
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Show or hide the loading overlay.
|
|
233
|
+
*/
|
|
234
|
+
setLoading(isLoading) {
|
|
235
|
+
this.loading = isLoading;
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Append a message to the history.
|
|
240
|
+
* Automatically scrolls to the new message with smooth animation.
|
|
241
|
+
*/
|
|
242
|
+
appendMessage(input) {
|
|
243
|
+
const msg = {
|
|
244
|
+
author: input.author,
|
|
245
|
+
text: input.text,
|
|
246
|
+
timestamp: input.timestamp,
|
|
247
|
+
};
|
|
248
|
+
// Pause observation to avoid loop
|
|
249
|
+
this._mutationObserver?.disconnect();
|
|
250
|
+
// Update messages array
|
|
251
|
+
this._messages = [...this._messages, msg];
|
|
252
|
+
// Optionally sync back to light DOM (for consistency)
|
|
253
|
+
this._syncToLightDOM();
|
|
254
|
+
// Restore observation after current event loop
|
|
255
|
+
requestAnimationFrame(() => {
|
|
256
|
+
this._initLightDOMObserver();
|
|
257
|
+
});
|
|
258
|
+
// Scroll to bottom (skip in infinite mode)
|
|
259
|
+
if (!this.infinite) {
|
|
260
|
+
this._scrollController.scrollToBottom();
|
|
261
|
+
}
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Scroll to the bottom of the message history.
|
|
266
|
+
*/
|
|
267
|
+
scrollToBottom() {
|
|
268
|
+
this._scrollController.scrollToBottom();
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Set a custom parser for message parsing
|
|
273
|
+
*/
|
|
274
|
+
setParser(parser) {
|
|
275
|
+
this._parser = parser;
|
|
276
|
+
this._parseLightDOM();
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Initialize MutationObserver for Light DOM observation
|
|
281
|
+
*/
|
|
282
|
+
_initLightDOMObserver() {
|
|
283
|
+
if (this._mutationObserver) {
|
|
284
|
+
this._mutationObserver.disconnect();
|
|
285
|
+
}
|
|
286
|
+
this._mutationObserver = new MutationObserver(records => {
|
|
287
|
+
// Only react to light DOM changes, not shadow DOM
|
|
288
|
+
const hasLightDOMChange = records.some(r => {
|
|
289
|
+
const root = r.target.getRootNode();
|
|
290
|
+
return root === document || root === this;
|
|
291
|
+
});
|
|
292
|
+
if (hasLightDOMChange && !this._isParsing) {
|
|
293
|
+
this._parseLightDOM();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
this._mutationObserver.observe(this, {
|
|
297
|
+
childList: true,
|
|
298
|
+
subtree: true,
|
|
299
|
+
characterData: true,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Parse Light DOM textContent into messages
|
|
304
|
+
*/
|
|
305
|
+
_parseLightDOM() {
|
|
306
|
+
const rawText = this.textContent || '';
|
|
307
|
+
if (!rawText.trim()) {
|
|
308
|
+
this._messages = [];
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this._isParsing = true;
|
|
312
|
+
const parsed = this._parser.parse(rawText);
|
|
313
|
+
// Only update if messages actually changed (avoid unnecessary re-renders)
|
|
314
|
+
if (this._messagesChanged(parsed)) {
|
|
315
|
+
this._messages = parsed;
|
|
316
|
+
}
|
|
317
|
+
// Hide light DOM content visually but keep it accessible
|
|
318
|
+
this._hideLightDOMContent();
|
|
319
|
+
requestAnimationFrame(() => {
|
|
320
|
+
this._isParsing = false;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Check if messages have changed
|
|
325
|
+
*/
|
|
326
|
+
_messagesChanged(newMessages) {
|
|
327
|
+
if (newMessages.length !== this._messages.length)
|
|
328
|
+
return true;
|
|
329
|
+
return newMessages.some((msg, i) => {
|
|
330
|
+
const old = this._messages[i];
|
|
331
|
+
return (msg.author !== old?.author || msg.text !== old?.text || msg.timestamp !== old?.timestamp);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Hide light DOM content visually while keeping it for accessibility/parsing
|
|
336
|
+
*/
|
|
337
|
+
_hideLightDOMContent() {
|
|
338
|
+
// The content is parsed and rendered in shadow DOM,
|
|
339
|
+
// so we don't need to hide it explicitly - the shadow DOM overlays it.
|
|
340
|
+
// But we can ensure proper accessibility by marking it.
|
|
341
|
+
this.setAttribute('aria-live', 'polite');
|
|
342
|
+
this.setAttribute('role', 'log');
|
|
343
|
+
this.setAttribute('aria-label', 'Message history');
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Sync current messages back to light DOM
|
|
347
|
+
*/
|
|
348
|
+
_syncToLightDOM() {
|
|
349
|
+
const content = this._messages
|
|
350
|
+
.map(m => {
|
|
351
|
+
const timestamp = m.timestamp ? `[${m.timestamp}] ` : '';
|
|
352
|
+
return `${timestamp}${m.author}: ${m.text}`;
|
|
353
|
+
})
|
|
354
|
+
.join('\n');
|
|
355
|
+
// Temporarily disconnect observer to prevent loop
|
|
356
|
+
this._mutationObserver?.disconnect();
|
|
357
|
+
this.textContent = content;
|
|
358
|
+
this._initLightDOMObserver();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Compute message groups for rendering
|
|
362
|
+
*/
|
|
363
|
+
_computeGroups(messages) {
|
|
364
|
+
if (messages.length === 0)
|
|
365
|
+
return [];
|
|
366
|
+
const processed = [];
|
|
367
|
+
let currentGroupTimestamp;
|
|
368
|
+
for (let i = 0; i < messages.length; i++) {
|
|
369
|
+
const msg = messages[i];
|
|
370
|
+
const prev = i > 0 ? messages[i - 1] : null;
|
|
371
|
+
const next = i < messages.length - 1 ? messages[i + 1] : null;
|
|
372
|
+
// Determine if this is first from author
|
|
373
|
+
const isFirstFromAuthor = !this._canGroup(prev, msg);
|
|
374
|
+
// Start of new group - initialize group timestamp
|
|
375
|
+
if (isFirstFromAuthor) {
|
|
376
|
+
currentGroupTimestamp = msg.timestamp;
|
|
377
|
+
}
|
|
378
|
+
else if (!currentGroupTimestamp && msg.timestamp) {
|
|
379
|
+
currentGroupTimestamp = msg.timestamp;
|
|
380
|
+
}
|
|
381
|
+
// Determine if this is last in group
|
|
382
|
+
const isLastInGroup = !next || !this._canGroup(msg, next);
|
|
383
|
+
processed.push({
|
|
384
|
+
...msg,
|
|
385
|
+
isFirstFromAuthor,
|
|
386
|
+
isLastInGroup,
|
|
387
|
+
groupTimestamp: isLastInGroup ? currentGroupTimestamp : undefined,
|
|
388
|
+
});
|
|
389
|
+
// Reset group timestamp at end of group
|
|
390
|
+
if (isLastInGroup) {
|
|
391
|
+
currentGroupTimestamp = undefined;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return processed;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Check if two messages can be grouped together
|
|
398
|
+
*/
|
|
399
|
+
_canGroup(prev, curr) {
|
|
400
|
+
if (!prev)
|
|
401
|
+
return false;
|
|
402
|
+
if (prev.author !== curr.author)
|
|
403
|
+
return false;
|
|
404
|
+
if (prev.timestamp && curr.timestamp && prev.timestamp !== curr.timestamp) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Handle scroll button click
|
|
411
|
+
*/
|
|
412
|
+
_onScrollButtonClick() {
|
|
413
|
+
this._scrollController.scrollToBottom();
|
|
414
|
+
}
|
|
415
|
+
render() {
|
|
416
|
+
const hasMessages = this._processedMessages.length > 0;
|
|
417
|
+
return html `
|
|
418
|
+
<div class="history-container" role="log" aria-live="polite" aria-label="Message history">
|
|
419
|
+
${hasMessages
|
|
420
|
+
? this._processedMessages.map((msg, index) => html `
|
|
421
|
+
<bb-message
|
|
422
|
+
.author=${msg.author}
|
|
423
|
+
.text=${msg.text}
|
|
424
|
+
.timestamp=${msg.groupTimestamp || ''}
|
|
425
|
+
?subsequent=${!msg.isFirstFromAuthor}
|
|
426
|
+
?lastInGroup=${msg.isLastInGroup}
|
|
427
|
+
data-index="${index}"
|
|
428
|
+
></bb-message>
|
|
429
|
+
`)
|
|
430
|
+
: html ` <div class="empty-state">${this.loading ? '' : 'No messages'}</div> `}
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
${!this.hideScrollButton && !this.infinite
|
|
434
|
+
? html `
|
|
435
|
+
<bb-scroll-button
|
|
436
|
+
.visible=${this._scrollController.isVisible}
|
|
437
|
+
@bb-scroll-to-bottom=${this._onScrollButtonClick}
|
|
438
|
+
></bb-scroll-button>
|
|
439
|
+
`
|
|
440
|
+
: nothing}
|
|
441
|
+
${this.loading ? html `<bb-loading-overlay visible></bb-loading-overlay>` : nothing}
|
|
442
|
+
`;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
__decorate([
|
|
446
|
+
property({ type: Boolean, reflect: true })
|
|
447
|
+
], BBMsgHistory.prototype, "loading", void 0);
|
|
448
|
+
__decorate([
|
|
449
|
+
property({ type: Boolean, reflect: true, attribute: 'hide-scroll-bar' })
|
|
450
|
+
], BBMsgHistory.prototype, "hideScrollBar", void 0);
|
|
451
|
+
__decorate([
|
|
452
|
+
property({ type: Boolean, reflect: true })
|
|
453
|
+
], BBMsgHistory.prototype, "infinite", void 0);
|
|
454
|
+
__decorate([
|
|
455
|
+
property({ type: Boolean, reflect: true, attribute: 'hide-scroll-button' })
|
|
456
|
+
], BBMsgHistory.prototype, "hideScrollButton", void 0);
|
|
457
|
+
__decorate([
|
|
458
|
+
property({ reflect: true })
|
|
459
|
+
], BBMsgHistory.prototype, "theme", void 0);
|
|
460
|
+
__decorate([
|
|
461
|
+
state()
|
|
462
|
+
], BBMsgHistory.prototype, "_messages", void 0);
|
|
463
|
+
__decorate([
|
|
464
|
+
state()
|
|
465
|
+
], BBMsgHistory.prototype, "_processedMessages", void 0);
|
|
466
|
+
__decorate([
|
|
467
|
+
provide({ context: authorContext }),
|
|
468
|
+
state()
|
|
469
|
+
], BBMsgHistory.prototype, "_userAuthors", void 0);
|
|
470
|
+
BBMsgHistory = __decorate([
|
|
471
|
+
customElement('bb-msg-history')
|
|
472
|
+
], BBMsgHistory);
|
|
473
|
+
export { BBMsgHistory };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Scroll-to-bottom button component
|
|
4
|
+
* Emits bb-scroll-to-bottom event when clicked
|
|
5
|
+
*/
|
|
6
|
+
export declare class BBScrollButton extends LitElement {
|
|
7
|
+
static styles: import("lit").CSSResult;
|
|
8
|
+
visible: boolean;
|
|
9
|
+
private _handleClick;
|
|
10
|
+
render(): import("lit").TemplateResult<1>;
|
|
11
|
+
}
|
|
12
|
+
declare global {
|
|
13
|
+
interface HTMLElementTagNameMap {
|
|
14
|
+
'bb-scroll-button': BBScrollButton;
|
|
15
|
+
}
|
|
16
|
+
}
|