@bbki.ng/bb-msg-history 0.14.1 → 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/component.d.ts +6 -18
- package/dist/component.js +43 -275
- 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/authors.js +1 -1
- 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/message-processor.d.ts +56 -0
- package/dist/core/message-processor.js +85 -0
- package/dist/core/renderer.d.ts +87 -0
- package/dist/core/renderer.js +196 -0
- package/dist/core/scroll-manager.d.ts +54 -0
- package/dist/core/scroll-manager.js +119 -0
- 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/event-tracker.d.ts +23 -0
- package/dist/utils/event-tracker.js +33 -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/component.ts +56 -338
- package/src/const/authors.ts +3 -2
- package/src/const/styles.ts +0 -33
- package/src/core/message-processor.ts +120 -0
- package/src/core/renderer.ts +276 -0
- package/src/core/scroll-manager.ts +148 -0
- package/src/utils/event-tracker.ts +38 -0
- package/src/utils/message-builder.ts +0 -15
- package/src/utils/tooltip.ts +0 -16
|
@@ -0,0 +1,59 @@
|
|
|
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, unsafeCSS } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
import { THEME } from '../const/theme.js';
|
|
10
|
+
/**
|
|
11
|
+
* Timestamp component - displays message timestamp
|
|
12
|
+
*/
|
|
13
|
+
let BBTimestamp = class BBTimestamp extends LitElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.value = '';
|
|
17
|
+
this.side = 'left';
|
|
18
|
+
}
|
|
19
|
+
static { this.styles = css `
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.timestamp {
|
|
25
|
+
font-size: 11px;
|
|
26
|
+
color: ${unsafeCSS(THEME.gray[400])};
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
line-height: 1;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:host([side='left']) .timestamp {
|
|
33
|
+
text-align: left;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:host([side='right']) .timestamp {
|
|
37
|
+
text-align: right;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@media (prefers-color-scheme: dark) {
|
|
41
|
+
.timestamp {
|
|
42
|
+
color: ${unsafeCSS(THEME.gray[500])};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`; }
|
|
46
|
+
render() {
|
|
47
|
+
return html `<div class="timestamp">${this.value}</div>`;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
__decorate([
|
|
51
|
+
property()
|
|
52
|
+
], BBTimestamp.prototype, "value", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
property({ reflect: true })
|
|
55
|
+
], BBTimestamp.prototype, "side", void 0);
|
|
56
|
+
BBTimestamp = __decorate([
|
|
57
|
+
customElement('bb-timestamp')
|
|
58
|
+
], BBTimestamp);
|
|
59
|
+
export { BBTimestamp };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BBLetterAvatar } from './bb-letter-avatar.js';
|
|
2
|
+
export { BBCustomAvatar } from './bb-custom-avatar.js';
|
|
3
|
+
export { BBMessageBubble } from './bb-message-bubble.js';
|
|
4
|
+
export { BBTimestamp } from './bb-timestamp.js';
|
|
5
|
+
export { BBMessage } from './bb-message.js';
|
|
6
|
+
export { BBScrollButton } from './bb-scroll-button.js';
|
|
7
|
+
export { BBLoadingOverlay } from './bb-loading-overlay.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BBLetterAvatar } from './bb-letter-avatar.js';
|
|
2
|
+
export { BBCustomAvatar } from './bb-custom-avatar.js';
|
|
3
|
+
export { BBMessageBubble } from './bb-message-bubble.js';
|
|
4
|
+
export { BBTimestamp } from './bb-timestamp.js';
|
|
5
|
+
export { BBMessage } from './bb-message.js';
|
|
6
|
+
export { BBScrollButton } from './bb-scroll-button.js';
|
|
7
|
+
export { BBLoadingOverlay } from './bb-loading-overlay.js';
|
package/dist/const/authors.js
CHANGED
|
@@ -22,7 +22,7 @@ export const AUTHOR_CONFIG = {
|
|
|
22
22
|
textColor: THEME.gray[900],
|
|
23
23
|
side: 'left',
|
|
24
24
|
},
|
|
25
|
-
|
|
25
|
+
GitHub: {
|
|
26
26
|
avatar: '<div style="width: 60%; height: 60%; margin: auto"><svg data-testid="geist-icon" height="16" stroke-linejoin="round" viewBox="0 0 16 16" width="16" style="color: currentcolor;"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.46252C4.40875 1.46252 1.5 4.37029 1.5 7.96032C1.5 10.8356 3.36062 13.2642 5.94438 14.1251C6.26937 14.182 6.39125 13.987 6.39125 13.8165C6.39125 13.6621 6.38313 13.1504 6.38313 12.6063C4.75 12.9068 4.3275 12.2083 4.1975 11.8428C4.12437 11.6559 3.8075 11.0793 3.53125 10.9249C3.30375 10.8031 2.97875 10.5026 3.52312 10.4945C4.035 10.4863 4.40062 10.9656 4.5225 11.1605C5.1075 12.1433 6.04188 11.8671 6.41563 11.6966C6.4725 11.2742 6.64313 10.9899 6.83 10.8275C5.38375 10.665 3.8725 10.1046 3.8725 7.61919C3.8725 6.91255 4.12438 6.32775 4.53875 5.87291C4.47375 5.71046 4.24625 5.04444 4.60375 4.15099C4.60375 4.15099 5.14812 3.98042 6.39125 4.81701C6.91125 4.67081 7.46375 4.59771 8.01625 4.59771C8.56875 4.59771 9.12125 4.67081 9.64125 4.81701C10.8844 3.9723 11.4288 4.15099 11.4288 4.15099C11.7863 5.04444 11.5588 5.71046 11.4938 5.87291C11.9081 6.32775 12.16 6.90443 12.16 7.61919C12.16 10.1127 10.6406 10.665 9.19438 10.8275C9.43 11.0305 9.63313 11.4204 9.63313 12.0296C9.63313 12.8987 9.625 13.5972 9.625 13.8165C9.625 13.987 9.74687 14.1901 10.0719 14.1251C11.3622 13.6896 12.4835 12.8606 13.2779 11.7547C14.0722 10.6488 14.4997 9.32178 14.5 7.96032C14.5 4.37029 11.5913 1.46252 8 1.46252Z" fill="currentColor"></path></svg></div>',
|
|
27
27
|
side: 'left',
|
|
28
28
|
bubbleColor: '#ecf4ec',
|
package/dist/const/styles.js
CHANGED
|
@@ -159,7 +159,6 @@ export const MAIN_STYLES = `
|
|
|
159
159
|
background: #ffffff;
|
|
160
160
|
border-radius: 50%;
|
|
161
161
|
overflow: hidden;
|
|
162
|
-
cursor: help;
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
.avatar-wrapper--hidden {
|
|
@@ -182,38 +181,6 @@ export const MAIN_STYLES = `
|
|
|
182
181
|
height: 100%;
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
/* Hover tooltip */
|
|
186
|
-
.avatar-tooltip {
|
|
187
|
-
position: fixed;
|
|
188
|
-
padding: 0.25rem 0.5rem;
|
|
189
|
-
background: ${THEME.gray[800]};
|
|
190
|
-
color: ${THEME.gray[50]};
|
|
191
|
-
font-size: 0.75rem;
|
|
192
|
-
border-radius: 0.25rem;
|
|
193
|
-
white-space: nowrap;
|
|
194
|
-
opacity: 0;
|
|
195
|
-
visibility: hidden;
|
|
196
|
-
pointer-events: none;
|
|
197
|
-
z-index: 10;
|
|
198
|
-
font-weight: 500;
|
|
199
|
-
letter-spacing: 0.02em;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.avatar-tooltip::after {
|
|
203
|
-
content: '';
|
|
204
|
-
position: absolute;
|
|
205
|
-
top: calc(100% - 1px);
|
|
206
|
-
left: 50%;
|
|
207
|
-
transform: translateX(-50%);
|
|
208
|
-
border: 4px solid transparent;
|
|
209
|
-
border-top-color: ${THEME.gray[800]};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.avatar-wrapper:hover .avatar-tooltip {
|
|
213
|
-
opacity: 1;
|
|
214
|
-
visibility: visible;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
184
|
/* Message content area */
|
|
218
185
|
.msg-content {
|
|
219
186
|
display: flex;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthorOptions } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lit Context for author configuration sharing
|
|
4
|
+
* Allows sub-components to access author configs without prop drilling
|
|
5
|
+
*/
|
|
6
|
+
export declare const authorContext: {
|
|
7
|
+
__context__: Map<string, AuthorOptions>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactiveController } from 'lit';
|
|
2
|
+
import type { LitElement } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* ScrollController - Reactive controller for scroll behavior
|
|
5
|
+
*
|
|
6
|
+
* Encapsulates scroll-related logic:
|
|
7
|
+
* - Scroll to bottom with smooth animation
|
|
8
|
+
* - Detect when user is near/at bottom of content
|
|
9
|
+
* - Control scroll button visibility based on scroll position
|
|
10
|
+
*/
|
|
11
|
+
export declare class ScrollController implements ReactiveController {
|
|
12
|
+
private host;
|
|
13
|
+
private _container?;
|
|
14
|
+
private _intersectionObserver?;
|
|
15
|
+
private _resizeObserver?;
|
|
16
|
+
private _isVisible;
|
|
17
|
+
private readonly BOTTOM_THRESHOLD;
|
|
18
|
+
constructor(host: LitElement);
|
|
19
|
+
hostConnected(): void;
|
|
20
|
+
hostDisconnected(): void;
|
|
21
|
+
hostUpdated(): void;
|
|
22
|
+
private _init;
|
|
23
|
+
private _cleanup;
|
|
24
|
+
private _initIntersectionObserver;
|
|
25
|
+
private _initResizeObserver;
|
|
26
|
+
/**
|
|
27
|
+
* Check current scroll position and update visibility state
|
|
28
|
+
*/
|
|
29
|
+
checkPosition(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Scroll the container to the bottom
|
|
32
|
+
* @param behavior - Scroll behavior: 'smooth' or 'auto'
|
|
33
|
+
*/
|
|
34
|
+
scrollToBottom(behavior?: ScrollBehavior): void;
|
|
35
|
+
/**
|
|
36
|
+
* Check if the container is currently at or near the bottom
|
|
37
|
+
* @returns true if within threshold pixels of bottom
|
|
38
|
+
*/
|
|
39
|
+
isAtBottom(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Get the current button visibility state
|
|
42
|
+
*/
|
|
43
|
+
get isVisible(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Get the scrollable container element
|
|
46
|
+
*/
|
|
47
|
+
get container(): HTMLElement | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Update the observed last message (call when messages change)
|
|
50
|
+
*/
|
|
51
|
+
updateObservedMessage(): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollController - Reactive controller for scroll behavior
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates scroll-related logic:
|
|
5
|
+
* - Scroll to bottom with smooth animation
|
|
6
|
+
* - Detect when user is near/at bottom of content
|
|
7
|
+
* - Control scroll button visibility based on scroll position
|
|
8
|
+
*/
|
|
9
|
+
export class ScrollController {
|
|
10
|
+
constructor(host) {
|
|
11
|
+
this._isVisible = false;
|
|
12
|
+
// Threshold in pixels from bottom to consider "at bottom"
|
|
13
|
+
this.BOTTOM_THRESHOLD = 50;
|
|
14
|
+
this.host = host;
|
|
15
|
+
host.addController(this);
|
|
16
|
+
}
|
|
17
|
+
hostConnected() {
|
|
18
|
+
// Wait for render to complete before querying DOM
|
|
19
|
+
this.host.updateComplete.then(() => {
|
|
20
|
+
this._init();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
hostDisconnected() {
|
|
24
|
+
this._cleanup();
|
|
25
|
+
}
|
|
26
|
+
hostUpdated() {
|
|
27
|
+
// Re-initialize if container becomes available
|
|
28
|
+
if (!this._container) {
|
|
29
|
+
this._init();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
_init() {
|
|
33
|
+
const host = this.host;
|
|
34
|
+
this._container = host.renderRoot?.querySelector('.history-container');
|
|
35
|
+
if (this._container) {
|
|
36
|
+
this._initIntersectionObserver();
|
|
37
|
+
this._initResizeObserver();
|
|
38
|
+
this.checkPosition();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
_cleanup() {
|
|
42
|
+
this._intersectionObserver?.disconnect();
|
|
43
|
+
this._resizeObserver?.disconnect();
|
|
44
|
+
this._container = undefined;
|
|
45
|
+
}
|
|
46
|
+
_initIntersectionObserver() {
|
|
47
|
+
if (!this._container)
|
|
48
|
+
return;
|
|
49
|
+
// Observe the last message to detect if we're at bottom
|
|
50
|
+
const lastMsg = this._container.lastElementChild;
|
|
51
|
+
if (!lastMsg)
|
|
52
|
+
return;
|
|
53
|
+
this._intersectionObserver = new IntersectionObserver(entries => {
|
|
54
|
+
entries.forEach(entry => {
|
|
55
|
+
const wasVisible = this._isVisible;
|
|
56
|
+
this._isVisible = !entry.isIntersecting;
|
|
57
|
+
if (wasVisible !== this._isVisible) {
|
|
58
|
+
this.host.requestUpdate();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}, {
|
|
62
|
+
root: this._container,
|
|
63
|
+
threshold: 0.1,
|
|
64
|
+
rootMargin: '0px 0px 50px 0px',
|
|
65
|
+
});
|
|
66
|
+
this._intersectionObserver.observe(lastMsg);
|
|
67
|
+
}
|
|
68
|
+
_initResizeObserver() {
|
|
69
|
+
if (!this._container)
|
|
70
|
+
return;
|
|
71
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
72
|
+
this.checkPosition();
|
|
73
|
+
});
|
|
74
|
+
this._resizeObserver.observe(this._container);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check current scroll position and update visibility state
|
|
78
|
+
*/
|
|
79
|
+
checkPosition() {
|
|
80
|
+
if (!this._container)
|
|
81
|
+
return;
|
|
82
|
+
const isAtBottom = this.isAtBottom();
|
|
83
|
+
const hasOverflow = this._container.scrollHeight > this._container.clientHeight;
|
|
84
|
+
// Show button when not at bottom and content has overflow
|
|
85
|
+
const shouldShow = !isAtBottom && hasOverflow;
|
|
86
|
+
if (shouldShow !== this._isVisible) {
|
|
87
|
+
this._isVisible = shouldShow;
|
|
88
|
+
this.host.requestUpdate();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Scroll the container to the bottom
|
|
93
|
+
* @param behavior - Scroll behavior: 'smooth' or 'auto'
|
|
94
|
+
*/
|
|
95
|
+
scrollToBottom(behavior = 'smooth') {
|
|
96
|
+
if (!this._container)
|
|
97
|
+
return;
|
|
98
|
+
this._container.scrollTo({
|
|
99
|
+
top: this._container.scrollHeight,
|
|
100
|
+
behavior,
|
|
101
|
+
});
|
|
102
|
+
// Hide button since we're scrolling to bottom
|
|
103
|
+
if (this._isVisible) {
|
|
104
|
+
this._isVisible = false;
|
|
105
|
+
this.host.requestUpdate();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if the container is currently at or near the bottom
|
|
110
|
+
* @returns true if within threshold pixels of bottom
|
|
111
|
+
*/
|
|
112
|
+
isAtBottom() {
|
|
113
|
+
if (!this._container)
|
|
114
|
+
return true;
|
|
115
|
+
const distanceFromBottom = this._container.scrollHeight - this._container.scrollTop - this._container.clientHeight;
|
|
116
|
+
return distanceFromBottom < this.BOTTOM_THRESHOLD;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the current button visibility state
|
|
120
|
+
*/
|
|
121
|
+
get isVisible() {
|
|
122
|
+
return this._isVisible;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the scrollable container element
|
|
126
|
+
*/
|
|
127
|
+
get container() {
|
|
128
|
+
return this._container;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Update the observed last message (call when messages change)
|
|
132
|
+
*/
|
|
133
|
+
updateObservedMessage() {
|
|
134
|
+
this._intersectionObserver?.disconnect();
|
|
135
|
+
this._initIntersectionObserver();
|
|
136
|
+
this.checkPosition();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Message } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extended message with grouping metadata
|
|
4
|
+
*/
|
|
5
|
+
export interface ProcessedMessage extends Message {
|
|
6
|
+
/** Whether this is the first message from this author in the current group */
|
|
7
|
+
isFirstFromAuthor: boolean;
|
|
8
|
+
/** Whether this is the last message in the current group */
|
|
9
|
+
isLastInGroup: boolean;
|
|
10
|
+
/** The timestamp to display for this group (if any) */
|
|
11
|
+
groupTimestamp?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Result of processing messages, including state for incremental updates
|
|
15
|
+
*/
|
|
16
|
+
export interface ProcessResult {
|
|
17
|
+
/** Processed messages with grouping metadata */
|
|
18
|
+
processed: ProcessedMessage[];
|
|
19
|
+
/** The author of the last message */
|
|
20
|
+
lastAuthor: string;
|
|
21
|
+
/** The timestamp of the current group */
|
|
22
|
+
lastGroupTimestamp?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* MessageProcessor - Handles message grouping and metadata calculation
|
|
26
|
+
*
|
|
27
|
+
* Encapsulates the grouping algorithm to determine:
|
|
28
|
+
* - Which messages are first from an author (show avatar)
|
|
29
|
+
* - Which messages are last in a group (show timestamp)
|
|
30
|
+
* - Group timestamps for display
|
|
31
|
+
*
|
|
32
|
+
* Optimized to process messages in a single pass instead of multiple traversals.
|
|
33
|
+
*/
|
|
34
|
+
export declare class MessageProcessor {
|
|
35
|
+
/**
|
|
36
|
+
* Check if two messages can be grouped together
|
|
37
|
+
* Messages can be grouped if they have the same author and compatible timestamps
|
|
38
|
+
*
|
|
39
|
+
* @param prev - Previous message (null if this is the first)
|
|
40
|
+
* @param curr - Current message
|
|
41
|
+
* @returns true if messages can be grouped
|
|
42
|
+
*/
|
|
43
|
+
private canGroup;
|
|
44
|
+
/**
|
|
45
|
+
* Process messages to add grouping metadata
|
|
46
|
+
*
|
|
47
|
+
* This method performs a single-pass algorithm that:
|
|
48
|
+
* 1. Determines first/last status for each message in its group
|
|
49
|
+
* 2. Assigns group timestamps consistently
|
|
50
|
+
* 3. Tracks state for incremental updates
|
|
51
|
+
*
|
|
52
|
+
* @param messages - Raw messages from parser
|
|
53
|
+
* @returns Processed messages with grouping metadata and state
|
|
54
|
+
*/
|
|
55
|
+
process(messages: Message[]): ProcessResult;
|
|
56
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageProcessor - Handles message grouping and metadata calculation
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the grouping algorithm to determine:
|
|
5
|
+
* - Which messages are first from an author (show avatar)
|
|
6
|
+
* - Which messages are last in a group (show timestamp)
|
|
7
|
+
* - Group timestamps for display
|
|
8
|
+
*
|
|
9
|
+
* Optimized to process messages in a single pass instead of multiple traversals.
|
|
10
|
+
*/
|
|
11
|
+
export class MessageProcessor {
|
|
12
|
+
/**
|
|
13
|
+
* Check if two messages can be grouped together
|
|
14
|
+
* Messages can be grouped if they have the same author and compatible timestamps
|
|
15
|
+
*
|
|
16
|
+
* @param prev - Previous message (null if this is the first)
|
|
17
|
+
* @param curr - Current message
|
|
18
|
+
* @returns true if messages can be grouped
|
|
19
|
+
*/
|
|
20
|
+
canGroup(prev, curr) {
|
|
21
|
+
if (!prev)
|
|
22
|
+
return false;
|
|
23
|
+
if (prev.author !== curr.author)
|
|
24
|
+
return false;
|
|
25
|
+
// Different timestamps = break group
|
|
26
|
+
if (prev.timestamp && curr.timestamp && prev.timestamp !== curr.timestamp) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Process messages to add grouping metadata
|
|
33
|
+
*
|
|
34
|
+
* This method performs a single-pass algorithm that:
|
|
35
|
+
* 1. Determines first/last status for each message in its group
|
|
36
|
+
* 2. Assigns group timestamps consistently
|
|
37
|
+
* 3. Tracks state for incremental updates
|
|
38
|
+
*
|
|
39
|
+
* @param messages - Raw messages from parser
|
|
40
|
+
* @returns Processed messages with grouping metadata and state
|
|
41
|
+
*/
|
|
42
|
+
process(messages) {
|
|
43
|
+
if (messages.length === 0) {
|
|
44
|
+
return { processed: [], lastAuthor: '' };
|
|
45
|
+
}
|
|
46
|
+
const processed = [];
|
|
47
|
+
let lastAuthor = '';
|
|
48
|
+
let lastGroupTimestamp;
|
|
49
|
+
// Track group state
|
|
50
|
+
let currentGroupTimestamp;
|
|
51
|
+
for (let i = 0; i < messages.length; i++) {
|
|
52
|
+
const msg = messages[i];
|
|
53
|
+
const prev = i > 0 ? messages[i - 1] : null;
|
|
54
|
+
const next = i < messages.length - 1 ? messages[i + 1] : null;
|
|
55
|
+
// Determine if this is first from author
|
|
56
|
+
const isFirstFromAuthor = !this.canGroup(prev, msg);
|
|
57
|
+
// Start of new group - initialize group timestamp
|
|
58
|
+
if (isFirstFromAuthor) {
|
|
59
|
+
currentGroupTimestamp = msg.timestamp;
|
|
60
|
+
}
|
|
61
|
+
else if (!currentGroupTimestamp && msg.timestamp) {
|
|
62
|
+
// If no timestamp yet and current msg has one, use it
|
|
63
|
+
currentGroupTimestamp = msg.timestamp;
|
|
64
|
+
}
|
|
65
|
+
// Determine if this is last in group
|
|
66
|
+
const isLastInGroup = !next || !this.canGroup(msg, next);
|
|
67
|
+
// Create processed message with metadata
|
|
68
|
+
const processedMsg = {
|
|
69
|
+
...msg,
|
|
70
|
+
isFirstFromAuthor,
|
|
71
|
+
isLastInGroup,
|
|
72
|
+
groupTimestamp: isLastInGroup ? currentGroupTimestamp : undefined,
|
|
73
|
+
};
|
|
74
|
+
processed.push(processedMsg);
|
|
75
|
+
// Update state tracking
|
|
76
|
+
lastAuthor = msg.author;
|
|
77
|
+
// If this is the last in group, reset group timestamp
|
|
78
|
+
if (isLastInGroup) {
|
|
79
|
+
lastGroupTimestamp = currentGroupTimestamp;
|
|
80
|
+
currentGroupTimestamp = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { processed, lastAuthor, lastGroupTimestamp };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AuthorOptions, Message } from '../types/index.js';
|
|
2
|
+
import type { ProcessedMessage } from './message-processor.js';
|
|
3
|
+
/**
|
|
4
|
+
* State for incremental message appending
|
|
5
|
+
*/
|
|
6
|
+
export interface LastState {
|
|
7
|
+
author: string;
|
|
8
|
+
groupTimestamp?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Result of a full render operation
|
|
12
|
+
*/
|
|
13
|
+
export interface RenderResult {
|
|
14
|
+
wasAtBottom: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Result of an incremental append operation
|
|
18
|
+
*/
|
|
19
|
+
export interface AppendResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
lastAuthor: string;
|
|
22
|
+
lastGroupTimestamp?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Renderer - Manages all DOM rendering operations
|
|
26
|
+
*
|
|
27
|
+
* Centralizes DOM manipulation logic:
|
|
28
|
+
* - Full render of all messages
|
|
29
|
+
* - Incremental update when appending single messages
|
|
30
|
+
* - Empty state rendering
|
|
31
|
+
* - Loading overlay management
|
|
32
|
+
*/
|
|
33
|
+
export declare class Renderer {
|
|
34
|
+
private shadowRoot;
|
|
35
|
+
constructor(shadowRoot: ShadowRoot);
|
|
36
|
+
/**
|
|
37
|
+
* Render the complete message history
|
|
38
|
+
*
|
|
39
|
+
* @param messages - Processed messages with grouping metadata
|
|
40
|
+
* @param authors - User-defined author configurations
|
|
41
|
+
* @param isLoading - Whether to show loading overlay
|
|
42
|
+
* @param hideScrollButton - Whether to hide the scroll-to-bottom button
|
|
43
|
+
* @returns Result indicating if we were at bottom before render
|
|
44
|
+
*/
|
|
45
|
+
render(messages: ProcessedMessage[], authors: Map<string, AuthorOptions>, isLoading: boolean, hideScrollButton: boolean): RenderResult;
|
|
46
|
+
/**
|
|
47
|
+
* Build HTML string for all messages
|
|
48
|
+
*/
|
|
49
|
+
private buildMessagesHtml;
|
|
50
|
+
/**
|
|
51
|
+
* Render full structure including styles, container, and scroll button
|
|
52
|
+
*/
|
|
53
|
+
private renderFullStructure;
|
|
54
|
+
/**
|
|
55
|
+
* Update content while preserving DOM structure
|
|
56
|
+
*/
|
|
57
|
+
private updateContent;
|
|
58
|
+
/**
|
|
59
|
+
* Append a single message without full re-render
|
|
60
|
+
*
|
|
61
|
+
* @param message - The message to append
|
|
62
|
+
* @param authors - User-defined author configurations
|
|
63
|
+
* @param lastState - Previous state for grouping logic
|
|
64
|
+
* @returns Result indicating success and updated state
|
|
65
|
+
*/
|
|
66
|
+
appendSingleMessage(message: Message, authors: Map<string, AuthorOptions>, lastState: LastState): AppendResult;
|
|
67
|
+
/**
|
|
68
|
+
* Check if two messages can be grouped
|
|
69
|
+
*/
|
|
70
|
+
private canGroupMessages;
|
|
71
|
+
/**
|
|
72
|
+
* Render empty state (no messages)
|
|
73
|
+
*/
|
|
74
|
+
renderEmpty(isLoading: boolean): void;
|
|
75
|
+
/**
|
|
76
|
+
* Update loading overlay visibility
|
|
77
|
+
*/
|
|
78
|
+
updateLoadingOverlay(shouldShow: boolean): void;
|
|
79
|
+
/**
|
|
80
|
+
* Get the history container element
|
|
81
|
+
*/
|
|
82
|
+
getHistoryContainer(): HTMLElement | null;
|
|
83
|
+
/**
|
|
84
|
+
* Get the scroll-to-bottom button element
|
|
85
|
+
*/
|
|
86
|
+
getScrollButton(): HTMLButtonElement | null;
|
|
87
|
+
}
|