From f2356df10d32bdc8804c0a299a639733527becbb Mon Sep 17 00:00:00 2001 From: Ocnrb Date: Sun, 27 Sep 2026 18:46:16 +0100 Subject: [PATCH] Keep the history lines fixed above the messages, where a long channel still shows them Co-authored-by: Claude Opus 5.5 --- index.html | 1 + src/js/ui/ChatAreaUI.js | 13 ++++++- src/styles/components.css | 9 +++++ tests/unit/ChatAreaUI.historyStart.test.js | 43 +++++++++++++++++----- 4 files changed, 55 insertions(+), 11 deletions(-) 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')