chat: peer-switch strip, back button moved to flyout header
Switching conversations used to require bouncing back to the list view and picking from there. Now the thread view has a horizontal peer strip at the top: one click hops to another conversation, active tab is highlighted and auto-scrolls into view, unread peers show a dot. Scrollbar is hidden — vertical wheel delta is redirected to horizontal scroll so the user can slide the strip with the mouse wheel when it overflows. In-thread header is gone; the back button moved next to "chat" in the flyout header and only shows while a thread is open. Peer name stays visible via the active tab. Bumps to 1.9.3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bfd5420981
commit
81d9ce0e87
5 files changed
+111
-14
No files matched your search
@@ -142,6 +142,7 @@ function showListView() {
|
||||
$('chat-thread-view').classList.add('hidden');
|
||||
$('chat-pick-view').classList.add('hidden');
|
||||
$('chat-flyout-title').textContent = 'chat';
|
||||
$('chat-header-back')?.classList.add('hidden');
|
||||
currentPeer = null;
|
||||
}
|
||||
|
||||
@@ -150,14 +151,17 @@ function showThreadView(peer) {
|
||||
$('chat-thread-view').classList.remove('hidden');
|
||||
$('chat-pick-view').classList.add('hidden');
|
||||
$('chat-flyout-title').textContent = 'chat';
|
||||
$('chat-thread-peer').textContent = peer;
|
||||
$('chat-header-back')?.classList.remove('hidden');
|
||||
currentPeer = peer;
|
||||
// Re-render tabs so the active highlight follows the thread switch.
|
||||
renderPeerTabs();
|
||||
}
|
||||
|
||||
function showPickView() {
|
||||
$('chat-list-view').classList.add('hidden');
|
||||
$('chat-thread-view').classList.add('hidden');
|
||||
$('chat-pick-view').classList.remove('hidden');
|
||||
$('chat-header-back')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
// ── Conversation list ─────────────────────────────────────────────
|
||||
@@ -174,6 +178,7 @@ function renderConversations() {
|
||||
const list = $('chat-list');
|
||||
const empty = $('chat-empty');
|
||||
if (!list || !empty) return;
|
||||
renderPeerTabs();
|
||||
if (!conversations.length) {
|
||||
list.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
@@ -205,6 +210,56 @@ function renderConversations() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Peer tabs (thread-view quick-switch strip) ────────────────────
|
||||
// Horizontal row above the thread so the user can hop between
|
||||
// conversations without going back to the list. Hidden scrollbar;
|
||||
// vertical wheel delta is redirected to horizontal scroll.
|
||||
|
||||
function renderPeerTabs() {
|
||||
const strip = $('chat-thread-peers');
|
||||
if (!strip) return;
|
||||
// Fold the current peer in even if no messages exist yet (fresh
|
||||
// pick-view selection), so switching back to it via tab is always
|
||||
// possible.
|
||||
const peers = conversations.slice();
|
||||
if (currentPeer && !peers.some(c => c.peer === currentPeer)) {
|
||||
peers.unshift({ peer: currentPeer, unread: 0 });
|
||||
}
|
||||
if (!peers.length) { strip.innerHTML = ''; return; }
|
||||
strip.innerHTML = peers.map(c => {
|
||||
const active = c.peer === currentPeer ? ' active' : '';
|
||||
const dot = parseInt(c.unread) > 0 && c.peer !== currentPeer
|
||||
? '<span class="chat-peer-dot"></span>' : '';
|
||||
return `<button class="chat-peer-tab${active}" data-peer="${escapeHtml(c.peer)}" title="${escapeHtml(c.peer)}">${dot}<span>${escapeHtml(c.peer)}</span></button>`;
|
||||
}).join('');
|
||||
strip.querySelectorAll('.chat-peer-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const peer = tab.dataset.peer;
|
||||
if (peer && peer !== currentPeer) openConversation(peer);
|
||||
});
|
||||
});
|
||||
// Keep the active tab in view after a switch so the user can see
|
||||
// what's selected when the list overflows.
|
||||
const active = strip.querySelector('.chat-peer-tab.active');
|
||||
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
|
||||
// Redirect vertical wheel scrolls to horizontal scroll on the peer
|
||||
// strip. Attached once on init — preventDefault only when we actually
|
||||
// consume the delta, so a trackpad user's native horizontal gesture
|
||||
// still works.
|
||||
function wirePeerStripWheel() {
|
||||
const strip = $('chat-thread-peers');
|
||||
if (!strip || strip.dataset.wheelWired) return;
|
||||
strip.dataset.wheelWired = '1';
|
||||
strip.addEventListener('wheel', (e) => {
|
||||
if (e.deltaY === 0) return;
|
||||
if (strip.scrollWidth <= strip.clientWidth) return;
|
||||
e.preventDefault();
|
||||
strip.scrollLeft += e.deltaY;
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// ── Thread ────────────────────────────────────────────────────────
|
||||
|
||||
async function openConversation(peer) {
|
||||
@@ -380,9 +435,10 @@ function init() {
|
||||
$('chat-refresh-btn').addEventListener('click', () => pollOnce().then(loadConversations));
|
||||
$('chat-new-btn').addEventListener('click', openPickView);
|
||||
$('chat-empty-start')?.addEventListener('click', openPickView);
|
||||
$('chat-thread-back').addEventListener('click', () => { showListView(); loadConversations(); });
|
||||
$('chat-header-back').addEventListener('click', () => { showListView(); loadConversations(); });
|
||||
$('chat-pick-back').addEventListener('click', showListView);
|
||||
$('chat-thread-form').addEventListener('submit', sendMessage);
|
||||
wirePeerStripWheel();
|
||||
|
||||
// Esc closes flyout when focus is inside it
|
||||
$('chat-flyout').addEventListener('keydown', e => {
|
||||
|
||||
Reference in new issue
Block a user