fix(mobile): make the chat layout responsive on phones; release v5.9.2
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

The chat did not lay out correctly on phones, iPhone worst of all: the header
would not stay at the top, and the layout shifted as you scrolled or opened the
keyboard.

The root cause was a floor, not a position. The app shell and column both carry
.minimal-bg, which sets min-height:100vh — and min-height always beats height. On
iOS 100vh is the URL-bar-retracted *large* viewport, so with the bar showing the
shell was held some 60-100px taller than the visible area. That surplus made the
document scrollable, and a scrolling document is how the header rode off the top.
It also left the visual-viewport tracking inert: forcing --sb-vh to 700px left the
shell at 844px. With the floor removed it follows to 700px and the document does
not scroll.

Also: the app shell is now sized from visualViewport rather than pinned, the
message list is the only scroller and can actually shrink, safe-area padding
applies (viewport-fit=cover) and collapses under the keyboard, and the
online/offline toast no longer sits on top of the header.

Adds ?preview=chat, which renders the chat layout with canned content and no
connection — presentational only, no peer manager, keys, or network.

Verified in an emulated iPhone viewport at rest and with the keyboard open:
header pinned at 0-64, composer flush to the keyboard line, document unscrollable.
This commit is contained in:
lockbitchat
2026-08-09 13:21:44 -04:00
parent 556727eb6f
commit 6a98e2eb00
14 changed files with 545 additions and 63 deletions
+1 -1
View File
@@ -21266,7 +21266,7 @@ var SecureMasterKeyManager = class {
var import_NotificationIntegration = __toESM(require_NotificationIntegration());
// package.json
var version = "5.9.1";
var version = "5.9.2";
// src/components/ui/Header.jsx
var APP_VERSION = `v${version}`;
+1 -1
View File
File diff suppressed because one or more lines are too long
Vendored
+111 -14
View File
@@ -2660,7 +2660,7 @@ var EnhancedChatInterface = ({
);
const composer = React.createElement(
"footer",
{ key: "composer", style: { flex: "none", padding: "12px 20px calc(18px + env(safe-area-inset-bottom, 0px))", background: "#0f0f11", borderTop: "1px solid rgba(255,255,255,0.05)" } },
{ key: "composer", style: { flex: "none", padding: "12px 20px calc(18px + var(--sb-safe-bottom, env(safe-area-inset-bottom, 0px)))", background: "#0f0f11", borderTop: "1px solid rgba(255,255,255,0.05)" } },
React.createElement(
"div",
{ style: { maxWidth: "1000px", margin: "0 auto" } },
@@ -2889,7 +2889,7 @@ var SessionsSidebar = ({ chats, collapsed, drawerOpen, onToggleCollapse, onSelec
const inner = collapsed ? collapsedInner : expandedInner;
return h(React.Fragment, null, [
// Responsive behaviour (inline styles can't express media queries).
h("style", { key: "css", dangerouslySetInnerHTML: { __html: "@media (max-width:1023px){.sb-rail{display:none !important;}.sb-burger{display:grid !important;}}@media (min-width:1024px){.sb-drawer-overlay{display:none !important;}}.sb-mobile-drawer .sb-collapse-btn{display:none !important;}html,body{background:#0f0f11 !important;overscroll-behavior:none;}.sb-app-shell{height:var(--sb-vh,100dvh) !important;}.sb-app-col{height:var(--sb-vh,100dvh) !important;}.chat-container{height:var(--sb-vh,100dvh) !important;}@media (max-width:768px){textarea,input,select{font-size:16px !important;}}@media (max-width:768px){.sb-rename-btn{display:none !important;}}" } }),
h("style", { key: "css", dangerouslySetInnerHTML: { __html: "@media (max-width:1023px){.sb-rail{display:none !important;}.sb-burger{display:grid !important;}}@media (min-width:1024px){.sb-drawer-overlay{display:none !important;}}.sb-mobile-drawer .sb-collapse-btn{display:none !important;}html,body{background:#0f0f11 !important;overscroll-behavior:none;}.sb-app-shell{height:var(--sb-vh,100dvh) !important;min-height:0 !important;overflow:hidden;}.sb-chat-header{position:sticky;top:0;z-index:20;}.sb-app-col{height:100% !important;min-height:0 !important;}.chat-container{height:100% !important;min-height:0 !important;}.sb-scroll{min-height:0 !important;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;}@media (max-width:768px){textarea,input,select{font-size:16px !important;}}@media (max-width:768px){.sb-rename-btn{display:none !important;}}" } }),
// Desktop rail
h("aside", { key: "rail", className: "sb-rail", style: railStyle }, inner),
// Mobile drawer overlay
@@ -3052,22 +3052,33 @@ var EnhancedSecureP2PChat = () => {
}, []);
React.useEffect(() => {
const vv = typeof window !== "undefined" ? window.visualViewport : null;
const root = document.documentElement;
let lastH = -1, lastInset = "";
const applyHeight = () => {
const h = Math.round(vv ? vv.height : window.innerHeight || 0);
if (h && h !== lastH) {
lastH = h;
root.style.setProperty("--sb-vh", h + "px");
}
};
const applyInset = () => {
const covered = !!vv && window.innerHeight - vv.height > 120;
const v = covered ? "0px" : "env(safe-area-inset-bottom, 0px)";
if (v !== lastInset) {
lastInset = v;
root.style.setProperty("--sb-safe-bottom", v);
}
};
const apply = () => {
const h = vv ? vv.height : window.innerHeight || 0;
if (h) document.documentElement.style.setProperty("--sb-vh", h + "px");
applyHeight();
applyInset();
};
apply();
if (vv) {
vv.addEventListener("resize", apply);
vv.addEventListener("scroll", apply);
}
if (vv) vv.addEventListener("resize", apply);
window.addEventListener("resize", apply);
window.addEventListener("orientationchange", apply);
return () => {
if (vv) {
vv.removeEventListener("resize", apply);
vv.removeEventListener("scroll", apply);
}
if (vv) vv.removeEventListener("resize", apply);
window.removeEventListener("resize", apply);
window.removeEventListener("orientationchange", apply);
};
@@ -5153,11 +5164,18 @@ var EnhancedSecureP2PChat = () => {
addMessageWithAutoScroll(" Secure connection successfully established and verified! You can now communicate safely with full protection against MITM attacks and Perfect Forward Secrecy..", "system");
}
}, [connectionStatus, isVerified]);
const previewMode = React.useMemo(() => {
try {
return new URLSearchParams(window.location.search).get("preview") === "chat";
} catch {
return false;
}
}, []);
const isConnectedAndVerified = (connectionStatus === "connected" || connectionStatus === "verified" || connectionStatus === "reconnecting") && isVerified;
React.useEffect(() => {
document.body.classList.toggle("sb-in-chat", isConnectedAndVerified);
document.body.classList.toggle("sb-in-chat", isConnectedAndVerified || previewMode);
return () => document.body.classList.remove("sb-in-chat");
}, [isConnectedAndVerified]);
}, [isConnectedAndVerified, previewMode]);
React.useEffect(() => {
if (isConnectedAndVerified && pendingSession && connectionStatus !== "failed") {
setPendingSession(null);
@@ -5253,6 +5271,85 @@ var EnhancedSecureP2PChat = () => {
const s = sessionsState.sessions[id];
return s && s.sas && s.sas.isVerified;
});
if (previewMode) {
const previewMessages = [
{ message: "Preview mode \u2014 no connection is open.", type: "system", id: 1, timestamp: Date.now() - 3e5 },
{ message: "This renders the real chat components so the layout can be checked without a handshake.", type: "received", id: 2, timestamp: Date.now() - 24e4 },
{ message: "Header pinned, list scrolls, composer sits above the keyboard.", type: "sent", id: 3, timestamp: Date.now() - 18e4 },
...Array.from({ length: 30 }, (_, i) => ({
message: "Filler message " + (i + 1) + " \u2014 enough content to make the list scroll.",
type: i % 2 ? "sent" : "received",
id: 10 + i,
timestamp: Date.now() - (30 - i) * 5e3
}))
];
const noop = () => {
};
return React.createElement("div", {
className: "minimal-bg sb-app-shell",
style: { display: "flex", flexDirection: "row", height: "100vh", width: "100%", overflow: "hidden" }
}, [
React.createElement(SessionsSidebar, {
key: "sidebar",
chats: [{ id: "preview", label: "Preview peer", unread: 0, active: true, verified: true }],
collapsed: sidebarCollapsed,
drawerOpen: sidebarDrawerOpen,
onToggleCollapse: () => setSidebarCollapsed((v) => !v),
onSelect: noop,
onNewChat: noop,
onRename: noop,
onCloseDrawer: () => setSidebarDrawerOpen(false),
myStatus,
onSetStatus: setMyStatus
}),
React.createElement("button", {
key: "burger",
className: "sb-burger",
onClick: () => setSidebarDrawerOpen(true),
style: { display: "none", position: "fixed", top: "13px", left: "13px", zIndex: 55, width: "38px", height: "38px", borderRadius: "10px", placeItems: "center", border: "1px solid rgba(255,255,255,0.1)", background: "rgba(18,18,20,0.9)", color: "#cfcfd4", cursor: "pointer" },
dangerouslySetInnerHTML: { __html: SB_SVG.burger }
}),
React.createElement("div", {
key: "col",
className: "minimal-bg sb-app-col",
style: { flex: 1, minWidth: 0, height: "100vh", overflow: "hidden", display: "flex", flexDirection: "column" }
}, React.createElement(
"main",
{ key: "main" },
React.createElement(EnhancedChatInterface, {
title: "Preview peer",
isOffline: false,
peerPresence: "available",
onRenameTitle: noop,
messages: previewMessages,
messageInput,
setMessageInput,
onSendMessage: noop,
onSendVoice: noop,
onDisconnect: noop,
keyFingerprint: "preview",
isVerified: true,
chatMessagesRef,
scrollToBottom: noop,
webrtcManager: null,
status: "connected",
pendingIncomingFiles: [],
onIncomingDecision: noop,
codeMode,
setCodeMode,
viewOnceMode,
setViewOnceMode,
viewOnceTtl,
setViewOnceTtl,
disappearTtl,
setDisappearTtl,
nowTick,
onUnsendMessage: noop,
onMessageExpire: noop
})
))
]);
}
return React.createElement("div", {
className: showSidebar ? "minimal-bg sb-app-shell" : "minimal-bg",
// With the rail visible the app is a fixed-height shell (rail + column
+2 -2
View File
File diff suppressed because one or more lines are too long