diff --git a/index.html b/index.html
index b468365..68c0d77 100644
--- a/index.html
+++ b/index.html
@@ -543,6 +543,7 @@
Explore Cha
+
diff --git a/src/js/ui/ChatAreaUI.js b/src/js/ui/ChatAreaUI.js
index 3f63ff0..c3c998e 100644
--- a/src/js/ui/ChatAreaUI.js
+++ b/src/js/ui/ChatAreaUI.js
@@ -336,6 +336,15 @@ class ChatAreaUI {
}
}
+ /** The lines about the history, fixed above the messages; empty hides the strip. */
+ _setHistoryStrip(html) {
+ const strip = document.getElementById('history-status-strip');
+ if (!strip) return;
+ strip.innerHTML = html;
+ strip.classList.toggle('hidden', !html);
+ strip.parentElement?.style.setProperty('--history-strip-height', `${strip.offsetHeight}px`);
+ }
+
/**
* Empty-state copy for a history read the storage node refused.
* @param {{status: number, signed: boolean}} error
@@ -633,6 +642,7 @@ class ChatAreaUI {
}
if (messagesForRender.length === 0) {
+ this._setHistoryStrip('');
// "No messages yet" requires every loading signal to be quiescent;
// otherwise we keep the spinner. Avoids the prior timer-based
// fallback that raced slow resend iterators.
@@ -843,7 +853,8 @@ class ChatAreaUI {
messagesHtml += messageRenderer.buildMessageGroupCloseHTML();
}
- this.messagesArea.innerHTML = historyErrorBanner + overridesBanner + historyStartIndicator + messagesHtml;
+ this._setHistoryStrip(historyErrorBanner + overridesBanner);
+ this.messagesArea.innerHTML = historyStartIndicator + messagesHtml;
if (!this.isLoadingMore) {
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
diff --git a/src/styles/components.css b/src/styles/components.css
index fc3e8fc..8aeb2b6 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -1278,6 +1278,15 @@ body.cannot-write .reply-trigger {
top: calc(1.125rem + 36px);
}
+/* The history strip is in the same flow; its height is set by ChatAreaUI,
+ since a line can wrap. */
+.messages-wrapper:has(#history-status-strip:not(.hidden)) .pinned-banner {
+ top: calc(0.625rem + var(--history-strip-height, 44px));
+}
+.messages-wrapper:has(.subscription-banner:not(.hidden)):has(#history-status-strip:not(.hidden)) .pinned-banner {
+ top: calc(1.125rem + 36px + var(--history-strip-height, 44px));
+}
+
@media (min-width: 768px) {
.subscription-banner,
.pinned-banner-name,
diff --git a/tests/unit/ChatAreaUI.historyStart.test.js b/tests/unit/ChatAreaUI.historyStart.test.js
index 989a380..c97163c 100644
--- a/tests/unit/ChatAreaUI.historyStart.test.js
+++ b/tests/unit/ChatAreaUI.historyStart.test.js
@@ -61,11 +61,12 @@ import { subscriptionBannerUI } from '../../src/js/ui/SubscriptionBannerUI.js';
const MESSAGES = [{ id: 'm1', text: 'hello', sender: '0xabc', timestamp: 1_789_000_000_000 }];
-function render({ hasMoreHistory, historyError, historyReadFailed = false, historyRetrying = false, overridesOwed = false }) {
+/** The strip fixed above the messages, then the scrolled list, as one string. */
+function render({ hasMoreHistory, historyError, historyReadFailed = false, historyRetrying = false, overridesOwed = false, messages = MESSAGES }) {
const channel = {
streamId: '0xowner/chan-1',
name: 'Chan',
- messages: MESSAGES,
+ messages,
hasMoreHistory,
historyError,
historyReadFailed,
@@ -77,10 +78,17 @@ function render({ hasMoreHistory, historyError, historyReadFailed = false, histo
channelManager: { getCurrentChannel: () => channel },
authManager: { getAddress: () => '0xabc' }
});
- chatAreaUI.renderMessages(MESSAGES);
- return document.getElementById('messages-area').innerHTML;
+ chatAreaUI.renderMessages(messages);
+ return document.getElementById('history-status-strip').innerHTML
+ + document.getElementById('messages-area').innerHTML;
}
+const PAGE = `
+
+
+
+`;
+
const claimsTheStart = (html) => html.includes('beginning of conversation');
describe('the start-of-history line', () => {
@@ -90,10 +98,7 @@ describe('the start-of-history line', () => {
chatAreaUI.isLoadingMore = false;
chatAreaUI._channelSwitching = false;
chatAreaUI._loadOp = null;
- document.body.innerHTML = `
-
-
- `;
+ document.body.innerHTML = PAGE;
chatAreaUI.init({
messagesArea: document.getElementById('messages-area'),
messageInput: document.getElementById('message-input')
@@ -140,7 +145,7 @@ describe('the line for edits and deletions that did not come back', () => {
beforeEach(() => {
vi.clearAllMocks();
subscriptionBannerUI.stateOf.mockReturnValue('active');
- document.body.innerHTML = '';
+ document.body.innerHTML = PAGE;
chatAreaUI.init({
messagesArea: document.getElementById('messages-area'),
messageInput: document.getElementById('message-input')
@@ -161,6 +166,24 @@ describe('the line for edits and deletions that did not come back', () => {
it('is absent when they came back', () => {
expect(render({ hasMoreHistory: true, historyError: null })).not.toContain('overrides-owed-banner');
});
+
+ it('sits in the strip above the messages, not in the list that scrolls', () => {
+ render({ hasMoreHistory: true, historyError: null, overridesOwed: true, historyRetrying: true });
+ const strip = document.getElementById('history-status-strip');
+ expect(strip.classList.contains('hidden')).toBe(false);
+ expect(strip.innerHTML).toContain('overrides-owed-banner');
+ expect(document.getElementById('messages-area').innerHTML).not.toContain('overrides-owed-banner');
+ // The floating pinned pill clears the strip by this height.
+ expect(strip.parentElement.style.getPropertyValue('--history-strip-height')).toMatch(/^\d+px$/);
+ });
+
+ it('leaves an empty channel to its empty state, with the strip hidden', () => {
+ render({ hasMoreHistory: true, historyError: null, overridesOwed: true, historyRetrying: true });
+ render({ hasMoreHistory: false, historyError: null, overridesOwed: true, historyReadFailed: true, messages: [] });
+ const strip = document.getElementById('history-status-strip');
+ expect(strip.classList.contains('hidden')).toBe(true);
+ expect(strip.innerHTML).toBe('');
+ });
});
describe('the refusal banner over the messages', () => {
@@ -168,7 +191,7 @@ describe('the refusal banner over the messages', () => {
beforeEach(() => {
vi.clearAllMocks();
- document.body.innerHTML = '';
+ document.body.innerHTML = PAGE;
chatAreaUI.init({
messagesArea: document.getElementById('messages-area'),
messageInput: document.getElementById('message-input')