fix(mobile): make the chat layout responsive on phones; release v5.9.2
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:
1 parent
556727eb6f
commit
6a98e2eb00
14 files changed
+545
-63
No files matched your search
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## v5.9.2 — Responsive layout fixed for phones
|
||||
|
||||
The chat was not laying out correctly on phones, iPhone worst of all: the header
|
||||
would not stay at the top, and the layout shifted around as you scrolled or opened
|
||||
the keyboard. The responsive behaviour is fixed.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The header stops scrolling away on iPhone.** The shell and the app 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 area you can
|
||||
actually see. That surplus is what made the document scrollable, and a document
|
||||
that scrolls is how the header rode off the top. Removing the floor is also what
|
||||
makes everything below work at all: measured with `--sb-vh` forced to 700px, the
|
||||
shell stayed 844px before the fix and follows to 700px after.
|
||||
- **The online/offline toast no longer covers the peer name.** It is `fixed top-4`,
|
||||
which inside the chat landed on the 64px header; it now sits below it.
|
||||
- **The chat shell is sized from the visual viewport instead of guessing.** Its
|
||||
height follows `visualViewport`, with `100dvh` as the fallback — which is
|
||||
already correct on Android, where `interactive-widget=resizes-content` makes the
|
||||
layout viewport shrink for the keyboard. WebKit has not implemented
|
||||
`interactive-widget` at all, which is why the JS fallback exists.
|
||||
- **The layout no longer resizes while you scroll.** Height was recomputed on
|
||||
every `visualViewport` scroll event, which on iOS fires while the URL bar
|
||||
collapses and during rubber-banding, so the whole chat twitched under the
|
||||
finger. It now tracks resize only.
|
||||
- **Safe-area padding actually applies, and gets out of the way.** The composer
|
||||
already padded for the home indicator with `env(safe-area-inset-bottom)`, but
|
||||
the viewport meta lacked `viewport-fit=cover`, so the value was always 0 on
|
||||
notched iPhones. It now also collapses while the keyboard is up, where it would
|
||||
otherwise be dead space between the composer and the keyboard.
|
||||
- **The message list can shrink.** It is `flex: 1` in a column, which defaults to
|
||||
`min-height: auto`: it refused to shrink below its content and pushed the
|
||||
composer off the bottom of the screen in a long conversation. It is also the
|
||||
only scroller now, and no longer chains its scroll to the document.
|
||||
- **The chat header stays at the top**, via `position: sticky` on the header and
|
||||
nothing else. If an ancestor scrolls — which is what iOS does to lift a focused
|
||||
input above the keyboard — the header holds against the top instead of riding
|
||||
away with the page.
|
||||
- Nested containers no longer each claim a full viewport height, which counted
|
||||
the header twice.
|
||||
|
||||
### Added
|
||||
|
||||
- `?preview=chat` renders the chat layout with canned content and no connection,
|
||||
so it can be looked at without standing up a handshake first. Presentational
|
||||
only: no peer manager, no keys, no network, and the send handlers are no-ops.
|
||||
|
||||
The shell is sized, not pinned. An intermediate version of this fix used
|
||||
`position: fixed` with a JS-chased viewport offset and a body scroll lock; that
|
||||
makes the layout feel nailed down rather than laid out, and a fixed box is laid
|
||||
out against the layout viewport — the one iOS never shrinks for the keyboard — so
|
||||
it has to be chased forever. Driving height and leaving positioning alone is what
|
||||
the established iOS chat implementations do.
|
||||
|
||||
## v5.9.1 — Scanning a one-frame invitation
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
[](#forward-secrecy)
|
||||
|
||||
Vendored
+1
-1
@@ -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}`;
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../node_modules/dompurify/src/utils.ts", "../node_modules/dompurify/src/tags.ts", "../node_modules/dompurify/src/attrs.ts", "../node_modules/dompurify/src/regexp.ts", "../node_modules/dompurify/src/purify.ts", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/webrtc/config.js", "../src/network/webrtc/sdp.js", "../src/network/webrtc/audio.js", "../src/network/webrtc/video.js", "../src/network/webrtc/adaptation/metrics.js", "../src/network/webrtc/adaptation/controller.js", "../src/crypto/DoubleRatchet.js", "../src/network/descriptor/sbq2.js", "../src/network/descriptor/keyexchange.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../package.json", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/CommunityCTA.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.jsx", "../src/components/ui/CallUI.jsx"],
|
||||
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
|
||||
"sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise<boolean>} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n Line truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["text", "NotificationIntegration", "entries", "Object", "setPrototypeOf", "isFrozen", "getPrototypeOf", "getOwnPropertyDescriptor", "freeze", "seal", "create", "_ref", "Reflect", "apply", "construct", "x", "func", "thisArg", "_len", "arguments", "length", "args", "Array", "_key", "Func", "_len2", "_key2", "arrayForEach", "unapply", "prototype", "forEach", "arrayLastIndexOf", "lastIndexOf", "arrayPop", "pop", "arrayPush", "push", "arraySplice", "splice", "arrayIsArray", "isArray", "stringToLowerCase", "String", "toLowerCase", "stringToString", "toString", "stringMatch", "match", "stringReplace", "replace", "stringIndexOf", "indexOf", "stringTrim", "trim", "numberToString", "Number", "booleanToString", "Boolean", "bigintToString", "BigInt", "symbolToString", "Symbol", "objectHasOwnProperty", "hasOwnProperty", "objectToString", "regExpTest", "RegExp", "test", "typeErrorCreate", "unconstruct", "TypeError", "lastIndex", "_len3", "_key3", "_len4", "_key4", "addToSet", "set", "array", "transformCaseFunc", "l", "element", "lcElement", "cleanArray", "index", "isPropertyExist", "clone", "object", "newObject", "_ref2", "_ref3", "_slicedToArray", "property", "value", "constructor", "stringifyValue", "valueAsRecord", "valueToString", "lookupGetter", "stringified", "prop", "desc", "get", "fallbackValue", "isRegex", "_unused", "html", "svg", "svgFilters", "svgDisallowed", "mathMl", "mathMlDisallowed", "text", "xml", "MUSTACHE_EXPR", "ERB_EXPR", "TMPLIT_EXPR", "DATA_ATTR", "ARIA_ATTR", "IS_ALLOWED_URI", "IS_SCRIPT_OR_DATA", "ATTR_WHITESPACE", "DOCTYPE_NAME", "CUSTOM_ELEMENT", "ELEMENT_MARKUP_PROBE", "COMMENT_MARKUP_PROBE", "FALLBACK_TAG_CLOSE", "SELF_CLOSING_TAG", "NODE_TYPE", "attribute", "cdataSection", "entityReference", "entityNode", "processingInstruction", "comment", "document", "documentType", "documentFragment", "notation", "getGlobal", "window", "_createTrustedTypesPolicy", "trustedTypes", "purifyHostElement", "createPolicy", "suffix", "ATTR_NAME", "hasAttribute", "getAttribute", "policyName", "createHTML", "createScriptURL", "scriptUrl", "_", "console", "warn", "_createHooksMap", "afterSanitizeAttributes", "afterSanitizeElements", "afterSanitizeShadowDOM", "beforeSanitizeAttributes", "beforeSanitizeElements", "beforeSanitizeShadowDOM", "uponSanitizeAttribute", "uponSanitizeElement", "uponSanitizeShadowNode", "_resolveSetOption", "cfg", "key", "fallback", "options", "base", "transform", "createDOMPurify", "undefined", "DOMPurify", "root", "version", "VERSION", "removed", "nodeType", "Element", "isSupported", "originalDocument", "currentScript", "DocumentFragment", "HTMLTemplateElement", "Node", "NodeFilter", "_window$NamedNodeMap", "NamedNodeMap", "MozNamedAttrMap", "HTMLFormElement", "DOMParser", "ElementPrototype", "cloneNode", "remove", "getNextSibling", "getChildNodes", "getParentNode", "getShadowRoot", "getAttributes", "getNodeType", "getNodeName", "template", "createElement", "content", "ownerDocument", "trustedTypesPolicy", "emptyHTML", "defaultTrustedTypesPolicy", "defaultTrustedTypesPolicyResolved", "IN_TRUSTED_TYPES_POLICY", "_assertNotInTrustedTypesPolicy", "_createTrustedHTML", "_createTrustedScriptURL", "_getDefaultTrustedTypesPolicy", "_document", "implementation", "createNodeIterator", "createDocumentFragment", "getElementsByTagName", "importNode", "hooks", "createHTMLDocument", "EXPRESSIONS", "ALLOWED_TAGS", "DEFAULT_ALLOWED_TAGS", "TAGS", "ALLOWED_ATTR", "DEFAULT_ALLOWED_ATTR", "ATTRS", "CUSTOM_ELEMENT_HANDLING", "tagNameCheck", "writable", "configurable", "enumerable", "attributeNameCheck", "allowCustomizedBuiltInElements", "FORBID_TAGS", "FORBID_ATTR", "EXTRA_ELEMENT_HANDLING", "tagCheck", "attributeCheck", "ALLOW_ARIA_ATTR", "ALLOW_DATA_ATTR", "ALLOW_UNKNOWN_PROTOCOLS", "ALLOW_SELF_CLOSE_IN_ATTR", "SAFE_FOR_TEMPLATES", "SAFE_FOR_XML", "WHOLE_DOCUMENT", "SET_CONFIG", "FORCE_BODY", "RETURN_DOM", "RETURN_DOM_FRAGMENT", "RETURN_TRUSTED_TYPE", "SANITIZE_DOM", "SANITIZE_NAMED_PROPS", "SANITIZE_NAMED_PROPS_PREFIX", "KEEP_CONTENT", "IN_PLACE", "USE_PROFILES", "FORBID_CONTENTS", "DEFAULT_FORBID_CONTENTS", "DATA_URI_TAGS", "DEFAULT_DATA_URI_TAGS", "URI_SAFE_ATTRIBUTES", "DEFAULT_URI_SAFE_ATTRIBUTES", "MATHML_NAMESPACE", "SVG_NAMESPACE", "HTML_NAMESPACE", "NAMESPACE", "IS_EMPTY_INPUT", "ALLOWED_NAMESPACES", "DEFAULT_ALLOWED_NAMESPACES", "DEFAULT_MATHML_TEXT_INTEGRATION_POINTS", "MATHML_TEXT_INTEGRATION_POINTS", "DEFAULT_HTML_INTEGRATION_POINTS", "HTML_INTEGRATION_POINTS", "COMMON_SVG_AND_HTML_ELEMENTS", "PARSER_MEDIA_TYPE", "SUPPORTED_PARSER_MEDIA_TYPES", "DEFAULT_PARSER_MEDIA_TYPE", "CONFIG", "formElement", "isRegexOrFunction", "testValue", "Function", "_parseConfig", "ALLOWED_URI_REGEXP", "customElementHandling", "ADD_TAGS", "ADD_ATTR", "ADD_URI_SAFE_ATTR", "ADD_FORBID_CONTENTS", "table", "tbody", "TRUSTED_TYPES_POLICY", "previousTrustedTypesPolicy", "error", "ALL_SVG_TAGS", "ALL_MATHML_TAGS", "_checkSvgNamespace", "tagName", "parent", "parentTagName", "namespaceURI", "_checkMathMlNamespaceLine truncated
|
||||
}
|
||||
Vendored
+111
-14
@@ -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
|
||||
|
||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/utils/debugWindowHooks.js", "../src/network/iceSettingsStore.js", "../src/state/sessionsStore.js", "../src/app.jsx"],
|
||||
"sourcesContent": ["function isSecureBitDebugEnabled(targetWindow = globalThis.window) {\n return targetWindow?.SECUREBIT_DEBUG === true;\n}\n\nfunction installDebugWindowHooks({\n targetWindow = globalThis.window,\n webrtcManagerRef,\n onClearData,\n clearConsole = () => {\n if (typeof console.clear === 'function') {\n console.clear();\n }\n }\n}) {\n if (!isSecureBitDebugEnabled(targetWindow)) {\n return () => {};\n }\n\n targetWindow.forceCleanup = () => {\n onClearData();\n if (webrtcManagerRef.current) {\n webrtcManagerRef.current.disconnect();\n }\n };\n targetWindow.clearLogs = clearConsole;\n targetWindow.webrtcManagerRef = webrtcManagerRef;\n\n return () => {\n delete targetWindow.forceCleanup;\n delete targetWindow.clearLogs;\n delete targetWindow.webrtcManagerRef;\n };\n}\n\nexport { installDebugWindowHooks, isSecureBitDebugEnabled };\n", "// Persistent, at-rest-encrypted storage for the user's custom ICE (STUN/TURN)\n// configuration. Persistence is OPTIONAL: the UI only calls saveIceSettings when\n// the user explicitly opts in (\"Save on this device\"). Session-only use never\n// touches this store \u2014 the settings live in React state and vanish on reload.\n//\n// At-rest protection model:\n// - A non-extractable AES-GCM device key is generated once and kept in\n// IndexedDB. It can never be exported back into JS, so a copy of the\n// on-disk database is useless without executing code in this exact origin.\n// - The settings blob (which may contain TURN credentials) is encrypted with\n// that key before being written.\n// This protects against disk/profile inspection. It does NOT protect against a\n// live code-execution compromise of the page (consistent with the project's\n// stated threat model \u2014 see SECURITY.md). Credentials are never persisted in\n// plaintext, and the user can delete them at any time via clearIceSettings().\n\nconst DB_NAME = 'securebit-net';\nconst DB_VERSION = 1;\nconst STORE = 'kv';\nconst KEY_RECORD = 'ice-device-key';\nconst SETTINGS_RECORD = 'ice-settings';\nconst SETTINGS_VERSION = 1;\n\nfunction isSupported() {\n return typeof indexedDB !== 'undefined' &&\n typeof crypto !== 'undefined' &&\n !!crypto.subtle;\n}\n\nfunction openDb() {\n return new Promise((resolve, reject) => {\n let request;\n try {\n request = indexedDB.open(DB_NAME, DB_VERSION);\n } catch (error) {\n reject(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE);\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction idbGet(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly');\n const req = tx.objectStore(STORE).get(key);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nfunction idbPut(db, key, value) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nfunction idbDelete(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nasync function getOrCreateDeviceKey(db) {\n const existing = await idbGet(db, KEY_RECORD);\n if (existing instanceof CryptoKey) {\n return existing;\n }\n const key = await crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n false, // non-extractable\n ['encrypt', 'decrypt']\n );\n await idbPut(db, KEY_RECORD, key);\n return key;\n}\n\n/**\n * Persist custom ICE settings, encrypted at rest.\n * @param {{ servers: Array, privacyMode: string }} settings\n */\nexport async function saveIceSettings(settings) {\n if (!isSupported()) throw new Error('Persistent storage is not available in this browser');\n\n const db = await openDb();\n const key = await getOrCreateDeviceKey(db);\n\n const payload = JSON.stringify({\n version: SETTINGS_VERSION,\n servers: Array.isArray(settings?.servers) ? settings.servers : [],\n privacyMode: settings?.privacyMode === 'relay-only' ? 'relay-only' : 'standard'\n });\n\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n keLine truncated
|
||||
"mappings": "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 truncated
|
||||
"sourcesContent": ["function isSecureBitDebugEnabled(targetWindow = globalThis.window) {\n return targetWindow?.SECUREBIT_DEBUG === true;\n}\n\nfunction installDebugWindowHooks({\n targetWindow = globalThis.window,\n webrtcManagerRef,\n onClearData,\n clearConsole = () => {\n if (typeof console.clear === 'function') {\n console.clear();\n }\n }\n}) {\n if (!isSecureBitDebugEnabled(targetWindow)) {\n return () => {};\n }\n\n targetWindow.forceCleanup = () => {\n onClearData();\n if (webrtcManagerRef.current) {\n webrtcManagerRef.current.disconnect();\n }\n };\n targetWindow.clearLogs = clearConsole;\n targetWindow.webrtcManagerRef = webrtcManagerRef;\n\n return () => {\n delete targetWindow.forceCleanup;\n delete targetWindow.clearLogs;\n delete targetWindow.webrtcManagerRef;\n };\n}\n\nexport { installDebugWindowHooks, isSecureBitDebugEnabled };\n", "// Persistent, at-rest-encrypted storage for the user's custom ICE (STUN/TURN)\n// configuration. Persistence is OPTIONAL: the UI only calls saveIceSettings when\n// the user explicitly opts in (\"Save on this device\"). Session-only use never\n// touches this store \u2014 the settings live in React state and vanish on reload.\n//\n// At-rest protection model:\n// - A non-extractable AES-GCM device key is generated once and kept in\n// IndexedDB. It can never be exported back into JS, so a copy of the\n// on-disk database is useless without executing code in this exact origin.\n// - The settings blob (which may contain TURN credentials) is encrypted with\n// that key before being written.\n// This protects against disk/profile inspection. It does NOT protect against a\n// live code-execution compromise of the page (consistent with the project's\n// stated threat model \u2014 see SECURITY.md). Credentials are never persisted in\n// plaintext, and the user can delete them at any time via clearIceSettings().\n\nconst DB_NAME = 'securebit-net';\nconst DB_VERSION = 1;\nconst STORE = 'kv';\nconst KEY_RECORD = 'ice-device-key';\nconst SETTINGS_RECORD = 'ice-settings';\nconst SETTINGS_VERSION = 1;\n\nfunction isSupported() {\n return typeof indexedDB !== 'undefined' &&\n typeof crypto !== 'undefined' &&\n !!crypto.subtle;\n}\n\nfunction openDb() {\n return new Promise((resolve, reject) => {\n let request;\n try {\n request = indexedDB.open(DB_NAME, DB_VERSION);\n } catch (error) {\n reject(error);\n return;\n }\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE);\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction idbGet(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly');\n const req = tx.objectStore(STORE).get(key);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nfunction idbPut(db, key, value) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).put(value, key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nfunction idbDelete(db, key) {\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite');\n tx.objectStore(STORE).delete(key);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nasync function getOrCreateDeviceKey(db) {\n const existing = await idbGet(db, KEY_RECORD);\n if (existing instanceof CryptoKey) {\n return existing;\n }\n const key = await crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n false, // non-extractable\n ['encrypt', 'decrypt']\n );\n await idbPut(db, KEY_RECORD, key);\n return key;\n}\n\n/**\n * Persist custom ICE settings, encrypted at rest.\n * @param {{ servers: Array, privacyMode: string }} settings\n */\nexport async function saveIceSettings(settings) {\n if (!isSupported()) throw new Error('Persistent storage is not available in this browser');\n\n const db = await openDb();\n const key = await getOrCreateDeviceKey(db);\n\n const payload = JSON.stringify({\n version: SETTINGS_VERSION,\n servers: Array.isArray(settings?.servers) ? settings.servers : [],\n privacyMode: settings?.privacyMode === 'relay-only' ? 'relay-only' : 'standard'\n });\n\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n keLine truncated
|
||||
"mappings": "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 truncated
|
||||
"names": ["o", "setMessages", "addMessageWithAutoScroll", "updateMessageStatus", "patchMessageById", "patchMessageByFileId", "setConnectionStatus", "setKeyFingerprint", "setVerificationCode", "setIsVerified", "setLocalVerificationConfirmed", "setRemoteVerificationConfirmed", "setBothVerificationsConfirmed", "setShowVerification", "setShowOfferStep", "setShowAnswerStep", "setShowQRCode", "setQrCodeUrl", "setOfferData", "setAnswerData", "setOfferInput", "setAnswerInput", "setPendingIncomingFiles", "opts", "offerData", "payload", "UpdateCheckerWrapper"]
|
||||
}
|
||||
+2
-2
@@ -7,7 +7,7 @@ this document describes.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Release | v5.9.1 |
|
||||
| Release | v5.9.2 |
|
||||
| Protocol version | 4.1 |
|
||||
| Ratchet wire version | 1 |
|
||||
|
||||
@@ -238,5 +238,5 @@ worse than one that reports nothing.
|
||||
|
||||
## Scope
|
||||
|
||||
This describes the browser implementation as it stands in v5.9.1. It is not a
|
||||
This describes the browser implementation as it stands in v5.9.2. It is not a
|
||||
substitute for independent cryptographic review.
|
||||
+27
-23
@@ -2,7 +2,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
||||
<!-- viewport-fit=cover is what makes env(safe-area-inset-*) resolve to anything
|
||||
other than 0 on notched iPhones; the composer already pads for it.
|
||||
interactive-widget=resizes-content is honoured by Chrome/Android; iOS
|
||||
ignores it, which is why the shell also tracks visualViewport in JS. -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self';
|
||||
@@ -24,7 +28,7 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786072827999">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786294476636">
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -90,7 +94,7 @@
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786072827999">
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786294476636">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
|
||||
@@ -99,7 +103,7 @@
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786072827999">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786294476636">
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -183,7 +187,7 @@
|
||||
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
||||
both execute in document order after parsing, so React still runs before the
|
||||
app modules below, but the parser / first paint is no longer blocked. -->
|
||||
<script defer src="config/ice-servers.js?v=1786072827999"></script>
|
||||
<script defer src="config/ice-servers.js?v=1786294476636"></script>
|
||||
<script defer src="libs/react/react.production.min.js"></script>
|
||||
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
|
||||
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
|
||||
@@ -191,8 +195,8 @@
|
||||
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
|
||||
<script defer src="libs/prism/prism.js"></script>
|
||||
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1786072827999">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786072827999">
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1786294476636">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786294476636">
|
||||
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
|
||||
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
|
||||
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
|
||||
@@ -200,31 +204,31 @@
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1786072827999">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1786072827999">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1786072827999">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1786294476636">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1786294476636">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1786294476636">
|
||||
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
||||
<script defer src="src/scripts/load-async-css.js?v=1786072827999"></script>
|
||||
<script defer src="src/scripts/load-async-css.js?v=1786294476636"></script>
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="libs/prism/prism.css">
|
||||
</noscript>
|
||||
<script defer src="src/scripts/fa-check.js?v=1786072827999"></script>
|
||||
<script defer src="src/scripts/fa-check.js?v=1786294476636"></script>
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script defer src="src/utils/updateManager.js?v=1786072827999"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1786072827999"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1786072827999"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1786072827999"></script>
|
||||
<script defer src="src/utils/updateManager.js?v=1786294476636"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1786294476636"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1786294476636"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1786294476636"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1786072827999"></script>
|
||||
<script type="module" src="dist/app.js?v=1786072827999"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1786294476636"></script>
|
||||
<script type="module" src="dist/app.js?v=1786294476636"></script>
|
||||
|
||||
<script defer src="src/scripts/pwa-register.js?v=1786072827999"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1786072827999" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1786072827999" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1786072827999"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786072827999">
|
||||
<script defer src="src/scripts/pwa-register.js?v=1786294476636"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1786294476636" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1786294476636" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1786294476636"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786294476636">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1786072827999",
|
||||
"buildVersion": "1786072827999",
|
||||
"appVersion": "5.9.1",
|
||||
"buildTime": "2026-08-07T03:20:28.043Z",
|
||||
"buildId": "1786072827999-808fd99",
|
||||
"gitHash": "808fd99",
|
||||
"version": "1786294476636",
|
||||
"buildVersion": "1786294476636",
|
||||
"appVersion": "5.9.2",
|
||||
"buildTime": "2026-08-09T16:54:36.685Z",
|
||||
"buildId": "1786294476636-556727e",
|
||||
"gitHash": "556727e",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-08-07T03:20:28.044Z"
|
||||
"generatedAt": "2026-08-09T16:54:36.686Z"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "5.9.1",
|
||||
"version": "5.9.2",
|
||||
"description": "Secure P2P Communication Application with End-to-End Encryption",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": "npm run build && python -m http.server 8000",
|
||||
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
||||
"serve": "npx http-server -p 8000",
|
||||
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs && node tests/descriptor-sbq2.test.mjs && node tests/sbq2-key-exchange.test.mjs && node tests/qr-scan-single-frame.test.mjs"
|
||||
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs && node tests/descriptor-sbq2.test.mjs && node tests/sbq2-key-exchange.test.mjs && node tests/qr-scan-single-frame.test.mjs && node tests/mobile-chat-layout.test.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"p2p",
|
||||
|
||||
+154
-9
@@ -2432,7 +2432,7 @@ import {
|
||||
})
|
||||
);
|
||||
|
||||
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)' } },
|
||||
const composer = React.createElement('footer', { 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' } },
|
||||
isRecording
|
||||
? [recordingBar]
|
||||
@@ -2671,7 +2671,56 @@ import {
|
||||
// App-shell height tracks the *visual* viewport (--sb-vh, set from the
|
||||
// VisualViewport API) so the layout shrinks when the on-screen keyboard
|
||||
// opens — no grey gap under the composer. Falls back to 100dvh, then 100vh.
|
||||
'.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;}' +
|
||||
// The shell is sized by HEIGHT, not pinned by position.
|
||||
//
|
||||
// An earlier attempt used position:fixed with a JS-driven top
|
||||
// offset. That is the wrong instrument: a fixed element is laid
|
||||
// out against the layout viewport, which iOS does not shrink for
|
||||
// the keyboard, so it has to be chased with JS — and the result
|
||||
// feels nailed down rather than laid out. The reference iOS chat
|
||||
// implementations all drive HEIGHT from visualViewport and leave
|
||||
// positioning alone.
|
||||
//
|
||||
// --sb-vh is the visual viewport height; 100dvh is the fallback,
|
||||
// which is correct on its own everywhere the layout viewport does
|
||||
// resize for the keyboard (Chrome/Firefox on Android, via
|
||||
// interactive-widget=resizes-content). WebKit has not implemented
|
||||
// interactive-widget at all, which is exactly why the JS fallback
|
||||
// still exists.
|
||||
//
|
||||
// min-height:0 is what makes the height above mean anything.
|
||||
// The shell also carries .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 pinned ~60-100px taller than the area
|
||||
// you can actually see. That surplus is precisely what made the
|
||||
// document scrollable, and a document that scrolls is how the
|
||||
// header rode away off the top. Everything below — the visual
|
||||
// viewport tracking, the dvh fallback — was inert until this
|
||||
// floor was removed: --sb-vh could shrink all it liked and the
|
||||
// shell would not follow it below 100vh.
|
||||
'.sb-app-shell{height:var(--sb-vh,100dvh) !important;min-height:0 !important;overflow:hidden;}' +
|
||||
// Pin the header, and nothing else.
|
||||
//
|
||||
// Sticky is the whole fix: if an ancestor scrolls — which is what
|
||||
// iOS does to lift a focused input above the keyboard — the header
|
||||
// holds against the top instead of riding away with the page. An
|
||||
// earlier version answered this by taking the document's scroll
|
||||
// away altogether; that broke the connection screen and was a far
|
||||
// broader change than the problem called for.
|
||||
'.sb-chat-header{position:sticky;top:0;z-index:20;}' +
|
||||
// Children fill the shell rather than restating a viewport height,
|
||||
// which would count the header twice.
|
||||
// The column carries .minimal-bg too, so it needs the same floor
|
||||
// removed — !important rather than relying on this block winning
|
||||
// on source order, because it is load-bearing.
|
||||
'.sb-app-col{height:100% !important;min-height:0 !important;}' +
|
||||
'.chat-container{height:100% !important;min-height:0 !important;}' +
|
||||
// The message list is the only scroller. min-height:0 is not
|
||||
// cosmetic: a flex:1 item in a column defaults to min-height:auto,
|
||||
// refuses to shrink below its content, and pushes the composer off
|
||||
// the bottom once the conversation is long enough.
|
||||
'.sb-scroll{min-height:0 !important;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;}' +
|
||||
// iOS Safari zooms the page when a focused field has font-size < 16px.
|
||||
// Force 16px on mobile inputs to stop the zoom-and-reflow on tap.
|
||||
'@media (max-width:768px){textarea,input,select{font-size:16px !important;}}' +
|
||||
@@ -2879,16 +2928,35 @@ import {
|
||||
// var (--sb-vh) that the app shell uses, so the layout tracks the keyboard.
|
||||
React.useEffect(() => {
|
||||
const vv = (typeof window !== 'undefined') ? window.visualViewport : null;
|
||||
const apply = () => {
|
||||
const h = vv ? vv.height : (window.innerHeight || 0);
|
||||
if (h) document.documentElement.style.setProperty('--sb-vh', h + 'px');
|
||||
const root = document.documentElement;
|
||||
let lastH = -1, lastInset = '';
|
||||
|
||||
// HEIGHT changes only on resize. It deliberately does NOT follow
|
||||
// `scroll`: on iOS the visual viewport also moves while the URL bar
|
||||
// collapses and during rubber-banding, so resizing the shell on every
|
||||
// scroll event made the whole layout twitch under the finger — which
|
||||
// is most of what "the chat layout jumps" was.
|
||||
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'); }
|
||||
};
|
||||
// The home-indicator inset must collapse while the keyboard is up.
|
||||
// Once the shell has shrunk to the visible area, that padding is no
|
||||
// longer clearing the indicator — it is just dead space between the
|
||||
// composer and the keyboard.
|
||||
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 = () => { 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);
|
||||
};
|
||||
@@ -5425,14 +5493,21 @@ import {
|
||||
// conversation must stay on screen. Throwing the user back to the connect
|
||||
// screen for a two-second NAT rebind would defeat the recovery entirely —
|
||||
// the composer keeps working and queues (see the send path's offlineNow).
|
||||
// `?preview=chat` renders the chat layout with no connection behind it.
|
||||
// Declared here because the in-chat body class below depends on it.
|
||||
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;
|
||||
|
||||
// The PWA "Install app" pill is a landing-page affordance — hide it once
|
||||
// we're inside the chat (CSS: body.sb-in-chat #pwa-install-button).
|
||||
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(() => {
|
||||
// All security features are enabled by default - no session activation needed
|
||||
@@ -5554,6 +5629,76 @@ import {
|
||||
return s && s.sas && s.sas.isVerified;
|
||||
});
|
||||
|
||||
// Stop the document scrolling while the app shell is up, so the header
|
||||
// cannot ride off the top. Applied from JS rather than a :has() selector
|
||||
// because this is load-bearing, and removed again on the landing page,
|
||||
// which must keep scrolling normally.
|
||||
|
||||
|
||||
// ── LAYOUT PREVIEW ────────────────────────────────────────────
|
||||
// `?preview=chat` renders the real chat components inside the real
|
||||
// shell with canned content and no WebRTC, so the layout can be
|
||||
// looked at and iterated on without standing up a connection each
|
||||
// time. It is presentational only: no peer manager, no keys, no
|
||||
// network, nothing sendable — the send handlers are no-ops. It
|
||||
// cannot be reached by accident, and it is not a way around
|
||||
// verification, because there is no session behind it to reach.
|
||||
if (previewMode) {
|
||||
const previewMessages = [
|
||||
{ message: 'Preview mode — no connection is open.', type: 'system', id: 1, timestamp: Date.now() - 300000 },
|
||||
{ message: 'This renders the real chat components so the layout can be checked without a handshake.', type: 'received', id: 2, timestamp: Date.now() - 240000 },
|
||||
{ message: 'Header pinned, list scrolls, composer sits above the keyboard.', type: 'sent', id: 3, timestamp: Date.now() - 180000 },
|
||||
...Array.from({ length: 30 }, (_, i) => ({
|
||||
message: 'Filler message ' + (i + 1) + ' — enough content to make the list scroll.',
|
||||
type: i % 2 ? 'sent' : 'received',
|
||||
id: 10 + i,
|
||||
timestamp: Date.now() - (30 - i) * 5000
|
||||
}))
|
||||
];
|
||||
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: 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: messageInput, setMessageInput: setMessageInput,
|
||||
onSendMessage: noop, onSendVoice: noop, onDisconnect: noop,
|
||||
keyFingerprint: 'preview', isVerified: true,
|
||||
chatMessagesRef: chatMessagesRef, scrollToBottom: noop,
|
||||
webrtcManager: null, status: 'connected',
|
||||
pendingIncomingFiles: [], onIncomingDecision: noop,
|
||||
codeMode: codeMode, setCodeMode: setCodeMode,
|
||||
viewOnceMode: viewOnceMode, setViewOnceMode: setViewOnceMode,
|
||||
viewOnceTtl: viewOnceTtl, setViewOnceTtl: setViewOnceTtl,
|
||||
disappearTtl: disappearTtl, setDisappearTtl: setDisappearTtl,
|
||||
nowTick: 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
|
||||
|
||||
@@ -794,6 +794,13 @@ button i {
|
||||
@media (max-width: 560px) { .sb-start-left { padding: 30px 22px !important; } }
|
||||
/* PWA install pill belongs to the landing page only — hide it inside the chat. */
|
||||
body.sb-in-chat #pwa-install-button { display: none !important; }
|
||||
/* The online/offline toast is `fixed top-4 left-1/2`, which inside the chat puts it
|
||||
squarely on top of the 64px header — it covered the peer name. Drop it below the
|
||||
header there, and keep it clear of the notch. It stays where it is on the landing
|
||||
page, which has no header of its own to collide with. */
|
||||
body.sb-in-chat #pwa-connection-status {
|
||||
top: calc(64px + 12px + env(safe-area-inset-top, 0px)) !important;
|
||||
}
|
||||
/* The new design spaces icons with flex gap, not icon margins — neutralise the
|
||||
global `button i { margin-right: .5rem }` so icons stay centered in their tiles. */
|
||||
.sb-start button i, .sb-ice-overlay button i { margin-right: 0; vertical-align: baseline; }
|
||||
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
|
||||
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
|
||||
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
|
||||
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
|
||||
const SW_BUILD_VERSION = '1786072827999';
|
||||
const SW_BUILD_VERSION = '1786294476636';
|
||||
|
||||
// Load version from meta.json on install
|
||||
async function getAppVersion() {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Invariants that keep the chat from shifting on mobile, iOS in particular.
|
||||
//
|
||||
// Every assertion here corresponds to something that was actually wrong:
|
||||
//
|
||||
// - `env(safe-area-inset-*)` was already used by the composer but resolved to 0,
|
||||
// because the viewport meta lacked `viewport-fit=cover`.
|
||||
// - A first attempt pinned the shell with position:fixed and chased
|
||||
// visualViewport.offsetTop from JS. That is the wrong instrument: a fixed box
|
||||
// is laid out against the layout viewport, which iOS never shrinks for the
|
||||
// keyboard, so it must be chased forever — and it felt nailed down rather
|
||||
// than laid out. The shell is sized from the visual viewport instead.
|
||||
// - `--sb-vh` was rewritten on every `visualViewport` *scroll* event, so the
|
||||
// whole layout resized under the finger while the iOS URL bar collapsed.
|
||||
// - Children re-asserted a full viewport height, adding the header back.
|
||||
// - The message list is `flex: 1` in a column, which defaults to
|
||||
// `min-height: auto` — it refuses to shrink below its content and pushes the
|
||||
// composer off the bottom once the conversation is long enough.
|
||||
//
|
||||
// These are structural checks. The layout itself is measured against a real
|
||||
// connection in a real browser; see the notes in the pull request.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||
const app = readFileSync(new URL('../src/app.jsx', import.meta.url), 'utf8');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// safe areas
|
||||
// ---------------------------------------------------------------------------
|
||||
const meta = /<meta\s+name="viewport"\s+content="([^"]+)"/.exec(html);
|
||||
assert.ok(meta, 'a viewport meta tag must exist');
|
||||
assert.match(meta[1], /viewport-fit=cover/,
|
||||
'without viewport-fit=cover, env(safe-area-inset-*) is 0 on iOS and the ' +
|
||||
'composer padding that depends on it does nothing');
|
||||
assert.match(meta[1], /width=device-width/);
|
||||
assert.ok(app.includes('env(safe-area-inset-bottom'),
|
||||
'the composer must pad for the home indicator');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the shell is sized, not pinned
|
||||
//
|
||||
// An earlier fix pinned the shell with position:fixed and chased the visual
|
||||
// viewport offset from JS. That is the wrong instrument — the reference iOS chat
|
||||
// implementations drive HEIGHT from visualViewport and leave positioning alone —
|
||||
// and it made the layout feel nailed down instead of laid out.
|
||||
// ---------------------------------------------------------------------------
|
||||
assert.match(app, /\.sb-app-shell\{height:var\(--sb-vh,100dvh\) !important/,
|
||||
'the shell must be sized from the visual viewport, with a dvh fallback');
|
||||
|
||||
// The floor has to come off, or the height above is decorative.
|
||||
//
|
||||
// The shell and the column both carry .minimal-bg, which sets min-height:100vh
|
||||
// (src/styles/components.css, src/styles/main.css) — 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 ~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. Measured before the fix: with --sb-vh forced to 700px the shell stayed
|
||||
// 844px. After: 700px, and the document is not scrollable.
|
||||
assert.match(app, /\.sb-app-shell\{[^}]*min-height:0 !important/,
|
||||
'the shell must clear the min-height:100vh it inherits from .minimal-bg, or ' +
|
||||
'--sb-vh can never shrink it and the whole viewport-tracking path is inert');
|
||||
assert.match(app, /\.sb-app-col\{[^}]*min-height:0 !important/,
|
||||
'the column carries .minimal-bg too and needs the same floor removed');
|
||||
assert.ok(!/\.sb-app-shell\{position:fixed/.test(app),
|
||||
'the shell must not be position:fixed: a fixed box is laid out against the ' +
|
||||
'layout viewport, which iOS never shrinks for the keyboard, so it has to be ' +
|
||||
'chased with JS forever');
|
||||
assert.ok(!/--sb-vv-top/.test(app),
|
||||
'chasing visualViewport.offsetTop belongs to the pinned approach and should ' +
|
||||
'be gone with it');
|
||||
assert.ok(!/sb-scroll-locked/.test(app),
|
||||
'the body scroll lock existed only to stop rubber-banding behind a pinned ' +
|
||||
'shell; without the pin it just breaks normal scrolling');
|
||||
|
||||
// Children fill the shell; they must not restate a viewport height.
|
||||
for (const sel of ['.sb-app-col', '.chat-container']) {
|
||||
const re = new RegExp(`\\${sel}\\{height:100% !important`);
|
||||
assert.match(app, re, `${sel} must fill the shell, not re-assert a viewport height`);
|
||||
assert.ok(!new RegExp(`\\${sel}\\{height:var\\(--sb-vh`).test(app),
|
||||
`${sel} must not set a viewport height of its own — nested full-height ` +
|
||||
'boxes are how the header got counted twice');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the header stays at the top
|
||||
//
|
||||
// Sticky, and nothing more. An earlier attempt pinned the header by removing the
|
||||
// document's ability to scroll (html/body overflow:hidden). That is a far broader
|
||||
// change than the problem called for: it also killed scrolling on the connection
|
||||
// screen. The layout was already right at that point — the header was the only
|
||||
// outstanding item — and the lesson is to keep the fix the size of the problem.
|
||||
// ---------------------------------------------------------------------------
|
||||
assert.match(app, /\.sb-chat-header\{position:sticky;top:0/,
|
||||
'the header must stick to the top of whatever scrolls');
|
||||
assert.ok(!/sb-app-open/.test(app),
|
||||
'the document scroll lock must stay gone: it broke the connection screen and ' +
|
||||
'was never what the header needed');
|
||||
assert.ok(!/html[^{]*\{[^}]*overflow:hidden/.test(app),
|
||||
'nothing may take the document\'s scroll away');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the message list is the only scroller
|
||||
// ---------------------------------------------------------------------------
|
||||
assert.match(app, /\.sb-scroll\{min-height:0 !important/,
|
||||
'a flex:1 item in a column needs min-height:0 or it pushes the composer off ' +
|
||||
'the bottom of the screen');
|
||||
assert.match(app, /\.sb-scroll\{[^}]*overscroll-behavior:contain/,
|
||||
'the list must not chain its scroll to the document');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the home-indicator inset collapses while the keyboard is up
|
||||
// ---------------------------------------------------------------------------
|
||||
assert.match(app, /--sb-safe-bottom/,
|
||||
'the safe-area inset must be a variable so it can collapse with the keyboard');
|
||||
assert.match(app, /var\(--sb-safe-bottom, env\(safe-area-inset-bottom, 0px\)\)/,
|
||||
'the composer must use the toggled inset, falling back to the raw env()');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// viewport tracking: resize only, never scroll
|
||||
// ---------------------------------------------------------------------------
|
||||
{
|
||||
const i = app.indexOf('const applyHeight = ');
|
||||
assert.notEqual(i, -1, 'height tracking must exist');
|
||||
const block = app.slice(i, i + 2500);
|
||||
|
||||
assert.match(block, /vv\.addEventListener\('resize', apply\)/,
|
||||
'height must be recomputed on resize');
|
||||
assert.ok(!/vv\.addEventListener\('scroll'/.test(block),
|
||||
'nothing may be recomputed on visualViewport scroll: on iOS that fires ' +
|
||||
'while the URL bar collapses and during rubber-banding, and resizing the ' +
|
||||
'shell there is what made the layout twitch under the finger');
|
||||
|
||||
// Redundant writes cause a style recalculation on every event.
|
||||
assert.match(block, /h !== lastH/, 'skip no-op height writes');
|
||||
assert.match(block, /Math\.round/, 'sub-pixel churn must be rounded away');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// layout preview
|
||||
// ---------------------------------------------------------------------------
|
||||
assert.match(app, /get\('preview'\) === 'chat'/,
|
||||
'?preview=chat must render the chat layout without a connection');
|
||||
assert.match(app, /webrtcManager: null/,
|
||||
'the preview must not be given a peer manager');
|
||||
{
|
||||
const i = app.indexOf('if (previewMode) {');
|
||||
assert.notEqual(i, -1, 'the preview branch must exist');
|
||||
const branch = app.slice(i, app.indexOf('return React.createElement(\'div\', {\n className: showSidebar', i) + 1 || i + 6000);
|
||||
assert.ok(!/onSendMessage: handleSendMessage/.test(branch),
|
||||
'the preview must not wire real send handlers');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the connection toast must not sit on the header
|
||||
//
|
||||
// It is `fixed top-4 left-1/2` (src/pwa/pwa-manager.js), which inside the chat
|
||||
// lands on the 64px header and covers the peer name.
|
||||
// ---------------------------------------------------------------------------
|
||||
{
|
||||
const css = readFileSync(new URL('../src/styles/components.css', import.meta.url), 'utf8');
|
||||
assert.match(css, /body\.sb-in-chat #pwa-connection-status\s*\{[^}]*top:\s*calc\(64px/,
|
||||
'the online/offline toast must clear the chat header');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// iOS focus zoom
|
||||
// ---------------------------------------------------------------------------
|
||||
assert.match(app, /textarea,input,select\{font-size:16px !important/,
|
||||
'iOS zooms the page when a focused field is under 16px, which reflows ' +
|
||||
'everything and looks like the layout jumping');
|
||||
|
||||
console.log('mobile-chat-layout: all assertions passed');
|
||||
Reference in new issue
Block a user