bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

commit f90614e6990592c48a5aa1cdb61e929e34a09466
parent b692a17ba440b0dd06ba4a7495fbf36bc621379f
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date:   Sat, 21 Feb 2026 17:32:08 -0700

UX fix -- chats just OPEN when you click on them, or they should anyway

Diffstat:
Mpublic/app.js | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 111 insertions(+), 5 deletions(-)

diff --git a/public/app.js b/public/app.js @@ -2627,6 +2627,100 @@ function nearestVisibleChatInstancePanelId(sourceEl) { return bestId; } +function panelIdFromSourceElement(sourceEl) { + const el = sourceEl instanceof HTMLElement ? sourceEl : null; + if (!el) return ""; + const panel = el.closest(".rackPanel"); + if (!(panel instanceof HTMLElement)) return ""; + return String(panel.dataset.panelId || "").trim(); +} + +function workspaceChatTargetCandidates() { + if (!rackLayoutEnabled) return []; + const left = ensureWorkspaceLeftRack(); + const right = ensureWorkspaceRightRack(); + if (!left || !right) return []; + const slots = [left, right]; + const out = []; + for (const slot of slots) { + const panel = slot.querySelector?.(":scope > .rackPanel:not(.hidden)"); + if (!(panel instanceof HTMLElement)) continue; + const panelId = String(panel.dataset.panelId || "").trim(); + if (!panelId) continue; + if (panelId === "chat") { + out.push({ kind: "main", panelId, element: panel }); + continue; + } + if (panelId.startsWith("chat:post:") && chatPanelInstances.has(panelId)) { + out.push({ kind: "instance", panelId, element: panel }); + } + } + return out; +} + +function chooseWorkspaceChatTarget(sourceEl) { + const candidates = workspaceChatTargetCandidates(); + if (!candidates.length) return null; + const anchor = sourceEl instanceof HTMLElement ? sourceEl : null; + if (!anchor) return candidates[0] || null; + const anchorRect = anchor.getBoundingClientRect(); + const ax = anchorRect.left + anchorRect.width / 2; + const ay = anchorRect.top + anchorRect.height / 2; + let best = null; + let bestDist = Number.POSITIVE_INFINITY; + for (const candidate of candidates) { + const rect = candidate.element.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + const dist = Math.hypot(cx - ax, cy - ay); + if (dist < bestDist) { + bestDist = dist; + best = candidate; + } + } + return best || candidates[0] || null; +} + +function chooseReferrerPanelForChatSplit(sourceEl) { + const sourcePanelId = panelIdFromSourceElement(sourceEl); + if (sourcePanelId && sourcePanelId !== "chat" && !sourcePanelId.startsWith("chat:")) return sourcePanelId; + + const left = ensureWorkspaceLeftRack(); + const right = ensureWorkspaceRightRack(); + const inWorkspace = []; + const leftPanel = left?.querySelector?.(":scope > .rackPanel:not(.hidden)"); + const rightPanel = right?.querySelector?.(":scope > .rackPanel:not(.hidden)"); + if (leftPanel instanceof HTMLElement) inWorkspace.push(String(leftPanel.dataset.panelId || "").trim()); + if (rightPanel instanceof HTMLElement) inWorkspace.push(String(rightPanel.dataset.panelId || "").trim()); + for (const id of inWorkspace) { + if (!id || id === "chat" || id.startsWith("chat:")) continue; + return id; + } + + const activePrimary = readWorkspaceActivePrimary(); + if (activePrimary && activePrimary !== "chat" && !activePrimary.startsWith("chat:")) return activePrimary; + + if (getPanelElement("hives")) return "hives"; + return ""; +} + +function ensureChatWorkspaceSplit(sourceEl) { + if (!rackLayoutEnabled || isMobileSwipeMode()) return false; + if (!chatPanelEl) return false; + const existingTarget = chooseWorkspaceChatTarget(sourceEl); + if (existingTarget) return true; + + const referrerPanelId = chooseReferrerPanelForChatSplit(sourceEl); + if (referrerPanelId) { + restorePanelToWorkspaceSlot(referrerPanelId, "workspaceLeftSlot"); + writeWorkspaceActivePrimary(referrerPanelId); + } + if (isDocked("chat")) undockPanel("chat"); + restorePanelToWorkspaceSlot("chat", "workspaceRightSlot"); + writeWorkspaceActivePrimary(referrerPanelId || "chat"); + return true; +} + function applyPluginPresetHint(panelDef) { if (!rackLayoutEnabled) return; const id = String(panelDef?.id || "").trim(); @@ -8277,19 +8371,31 @@ function openChat(postId, opts = null) { return; } - // Rack mode: switch the nearest visible chat panel when possible; otherwise use main chat. + // Rack mode: prefer an existing workspace chat panel; if none exists, create a split view + // (referrer on left, chat on right) so opening chat is deterministic. if (rackLayoutEnabled && !isStreamPost(post)) { - const nearestInstanceId = nearestVisibleChatInstancePanelId(sourceEl); - if (nearestInstanceId) { + const workspaceTarget = chooseWorkspaceChatTarget(sourceEl); + if (workspaceTarget?.kind === "instance" && workspaceTarget.panelId) { touchRecentHiveChat(postId); markRead(postId); renderFeed(); ws.send(JSON.stringify({ type: "getChat", postId })); - setChatInstancePanelPost(nearestInstanceId, postId, true); + setChatInstancePanelPost(workspaceTarget.panelId, postId, true); renderChatContextSelect(); return; } - if (chatPanelEl && typeof isDocked === "function" && !isDocked("chat")) { + if (workspaceTarget?.kind === "main") { + activeChatPostId = postId; + touchRecentHiveChat(postId); + markRead(postId); + renderFeed(); + ws.send(JSON.stringify({ type: "getChat", postId })); + renderChatPanel(true); + renderTypingIndicator(); + if (isMobileSwipeMode()) setMobilePanel("chat"); + return; + } + if (ensureChatWorkspaceSplit(sourceEl)) { activeChatPostId = postId; touchRecentHiveChat(postId); markRead(postId);