v6.8.12: new design for exchanging invitations
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

This commit is contained in:
lockbitchat
2026-09-27 16:23:03 -04:00
parent cd9d78d398
commit b0444a52c6
57 changed files with 919 additions and 693 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## v6.8.12 — New design for exchanging invitations
New design for the screens where you exchange invitations and answers with your
peer, and for the safety code check. The download buttons now give you desktop
version 1.0.5.
## v6.8.8 — Project support section
Added a section to support the project.
+2 -2
View File
@@ -9,7 +9,7 @@
No accounts. No servers storing your messages. No installation required.
[![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-6.8.8-3ecf8e.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-6.8.12-3ecf8e.svg)](CHANGELOG.md)
[![Get it from the Snap Store](https://snapcraft.io/securebit-chat/badge.svg)](https://snapcraft.io/securebit-chat)
[![PWA](https://img.shields.io/badge/PWA-installable-3ecf8e.svg)](#install-as-an-app)
[![Encryption](https://img.shields.io/badge/crypto-ECDH%20P--384%20%C2%B7%20AES--256--GCM-blue.svg)](#security-model)
@@ -31,7 +31,7 @@ From the web app at [securebit.chat](https://securebit.chat) — no install, no
| Open a channel | Verify the safety code |
| :---: | :---: |
| ![The landing page, with the option to create a channel or join one with an invitation](assets/screenshots/web-start.png) | ![The security verification screen, showing a seven-digit safety code to compare with the other person](assets/screenshots/web-verification.png) |
| ![The landing page, with the option to create a channel or join one with an invitation](assets/screenshots/web-start.png) | ![The Verify your peer screen: a seven-digit safety code to read aloud, boxes to type it in, and You and Peer confirmation marks](assets/screenshots/web-verification.png) |
| Your device generates the keys and a one-time invitation. Nothing touches a server. | Compare the code out of band. Until both sides confirm, the chat stays locked. |
| Encrypted conversation | Voice and video |
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ar/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ar.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/ar.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+1 -1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 98 KiB

+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/de/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/de.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/de.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+3 -3
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
Vendored
+9 -9
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,7 +7,7 @@ this document describes.
| | |
| --- | --- |
| Release | v6.8.8 |
| Release | v6.8.12 |
| Protocol version | 4.1 |
| Ratchet wire version | 1 |
@@ -347,5 +347,5 @@ no leg may stop a capture it borrowed.
## Scope
This describes the browser implementation as it stands in v6.8.8. It is not a
This describes the browser implementation as it stands in v6.8.12. It is not a
substitute for independent cryptographic review.
+2 -2
View File
@@ -153,7 +153,7 @@
</thead>
<tbody><tr>
<td>Release</td>
<td>v6.8.8</td>
<td>v6.8.12</td>
</tr>
<tr>
<td>Protocol version</td>
@@ -486,7 +486,7 @@
shared across every leg, and stopped when the call, the group or the tab ends;
no leg may stop a capture it borrowed.</p>
<h2 id="scope">Scope</h2>
<p>This describes the browser implementation as it stands in v6.8.8. It is not a
<p>This describes the browser implementation as it stands in v6.8.12. It is not a
substitute for independent cryptographic review.</p>
<nav class="more">
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/es/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/es.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/es.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/fa/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/fa.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/fa.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/fr/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/fr.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/fr.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/he/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/he.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/he.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/hi/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/hi.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/hi.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+18 -18
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,18 +287,18 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -486,12 +486,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ko/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ko.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/ko.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "يولّد جهازك المفاتيح ودعوة تُستعمل مرّة واحدة. لا شيء يمرّ عبر خادم.",
"intro.createCta": "توليد المفاتيح والدعوة",
"intro.joinTitle": "انضمّ إلى قناة",
"intro.joinDesc": "امسح رمز QR الخاص بالطرف الآخر بالكاميرا، أو الصق رمز الدعوة.",
"intro.scanTitle": "امسح رمز QR بالكاميرا",
"intro.scanSubtitle": "الأسرع - وجّهها إلى شاشة الطرف الآخر",
"intro.orPasteCode": "أو ألصق الرمز",
"intro.orPasteCode": "أو الصق الرمز",
"intro.pastePlaceholder": "ألصق رمز الدعوة هنا…",
"intro.connect": "اتّصل",
"intro.connecting": "جارٍ المعالجة…",
@@ -303,22 +304,26 @@
"handshake.establish": "إنشاء الاتصال",
"handshake.thenReceive": "ثمّ استقبل الردّ الذي يرسله الطرف الآخر",
"handshake.pasteAnswerPlaceholder": "ألصق رمز ردّ الطرف الآخر…",
"handshake.answerSentNote": "أرسل هذا الردّ إلى المُنشئ ثمّ انتظر - تُفتح الدردشة حالما يتّصل.",
"handshake.answerSentNote": "بانتظار اتصال المُنشئ. ستُفتح المحادثة تلقائياً.",
"handshake.qrHint": "أبقِ هذا مفتوحاً حتى يلتقط الطرف الآخر الرمز.",
"handshake.qrHintFrames": "المصافحة موزّعة على {frames} إطارات - أبقِ هذا مفتوحاً حتى يلتقطها الطرف الآخر كلّها.",
"verify.title": "التحقّق الأمني",
"verify.desc": "قارِن رمز الأمان هذا مع الطرف الآخر عبر قناة منفصلة (مكالمة أو لقاء مباشر)، ثمّ اكتبه لفتح الدردشة.",
"verify.enterLabel": "أدخل الرمز المُتحقَّق منه",
"verify.placeholder": "اكتب الرمز هنا",
"verify.confirm": "تأكيد الرمز",
"verify.confirmed": "مؤكَّد",
"verify.mismatch": "غير متطابقين",
"verify.yours": "تأكيدك",
"verify.peer": "تأكيد الطرف الآخر",
"verify.pending": "قيد الانتظار",
"verify.waiting": "بانتظار الرمز…",
"verify.verified": "تمّ التحقّق من القناة",
"verify.bothConfirmed": "أكّد الطرفان. جارٍ فتح الدردشة الآمنة…",
"sasv.title": "تحقّق من الطرف الآخر",
"sasv.titleDone": "تم التحقّق من الطرف الآخر",
"sasv.desc": "اقرأ الرمز بصوت عالٍ عبر مكالمة أو وجهًا لوجه. لا تتابع إلا إذا رأى الطرف الآخر الأرقام نفسها.",
"sasv.descDone": "أكّدتما كلاكما الرمز نفسه. الاتصال موثَّق.",
"sasv.enter": "أدخل الرمز",
"sasv.match": "متطابق",
"sasv.noMatch": "غير متطابق",
"sasv.you": "أنت",
"sasv.peer": "الطرف الآخر",
"sasv.waitingPeer": "بانتظار الطرف الآخر…",
"sasv.codesDiffer": "الرموز غير متطابقة",
"sasv.opening": "جارٍ فتح المحادثة الآمنة",
"pwa.bannerTitle": "ثبّت SecureBit.chat",
"pwa.bannerDesc": "احصل على تجربة تطبيق أصلي بأمان أعلى",
"pwa.install": "تثبيت",
@@ -353,9 +358,8 @@
"action.scan": "مسح",
"action.downloadDesktop": "نزّل تطبيق سطح المكتب",
"action.advancedSettings": "إعدادات متقدّمة",
"cred.offerTag": "عرض · أو انسخ النصّ",
"cred.answerTag": "ردّ · أو انسخ النصّ",
"cred.reveal": "اضغط للكشف - أبقِ هذا الرمز سرّياً",
"cred.orCopy": "أو انسخ الرمز",
"qr.title": "امسح رمز QR",
"qr.subtitle": "وجّه كاميرتك إلى رمزهم",
"qr.scanning": "جارٍ المسح…",
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "Ihr Gerät erzeugt die Schlüssel und eine einmalige Einladung. Nichts davon berührt einen Server.",
"intro.createCta": "Schlüssel & Einladung erzeugen",
"intro.joinTitle": "Kanal beitreten",
"intro.joinDesc": "Scannen Sie den QR-Code Ihres Gegenübers mit der Kamera oder fügen Sie seinen Einladungscode ein.",
"intro.scanTitle": "QR-Code mit der Kamera scannen",
"intro.scanSubtitle": "Am schnellsten - auf den Bildschirm Ihres Gegenübers halten",
"intro.orPasteCode": "oder Code einfügen",
"intro.orPasteCode": "Oder den Code einfügen",
"intro.pastePlaceholder": "Einladungscode hier einfügen …",
"intro.connect": "Verbinden",
"intro.connecting": "Wird verarbeitet …",
@@ -303,22 +304,26 @@
"handshake.establish": "Verbindung herstellen",
"handshake.thenReceive": "Nehmen Sie danach die Antwort Ihres Gegenübers entgegen",
"handshake.pasteAnswerPlaceholder": "Antwortcode des Gegenübers einfügen …",
"handshake.answerSentNote": "Senden Sie diese Antwort an den Ersteller und warten Sie - der Chat öffnet sich, sobald er sich verbindet.",
"handshake.answerSentNote": "Warten, bis sich der Ersteller verbindet. Der Chat öffnet sich automatisch.",
"handshake.qrHint": "Lassen Sie dies geöffnet, bis Ihr Gegenüber den Code erfasst hat.",
"handshake.qrHintFrames": "Der Handschlag ist auf {frames} Einzelbilder verteilt - lassen Sie dies geöffnet, bis Ihr Gegenüber alle erfasst hat.",
"verify.title": "Sicherheitsprüfung",
"verify.desc": "Vergleichen Sie diesen Sicherheitscode über einen anderen Weg mit Ihrem Gegenüber (Anruf oder persönlich) und geben Sie ihn dann ein, um den Chat freizuschalten.",
"verify.enterLabel": "Geprüften Code eingeben",
"verify.placeholder": "Code hier eingeben",
"verify.confirm": "Code bestätigen",
"verify.confirmed": "Bestätigt",
"verify.mismatch": "Stimmt nicht überein",
"verify.yours": "Ihre Bestätigung",
"verify.peer": "Bestätigung des Gegenübers",
"verify.pending": "Ausstehend",
"verify.waiting": "Warten auf den Code …",
"verify.verified": "Kanal geprüft",
"verify.bothConfirmed": "Beide Seiten haben bestätigt. Der sichere Chat wird geöffnet …",
"sasv.title": "Gegenüber überprüfen",
"sasv.titleDone": "Gegenüber verifiziert",
"sasv.desc": "Lies den Code bei einem Anruf oder persönlich laut vor. Fahre nur fort, wenn dein Gegenüber dieselben Ziffern sieht.",
"sasv.descDone": "Ihr habt beide denselben Code bestätigt. Die Verbindung ist authentifiziert.",
"sasv.enter": "Code eingeben",
"sasv.match": "Stimmt überein",
"sasv.noMatch": "Stimmt nicht überein",
"sasv.you": "Du",
"sasv.peer": "Gegenüber",
"sasv.waitingPeer": "Warte auf Gegenüber…",
"sasv.codesDiffer": "Codes stimmen nicht überein",
"sasv.opening": "Sicherer Chat wird geöffnet",
"pwa.bannerTitle": "SecureBit.chat installieren",
"pwa.bannerDesc": "Die App-Erfahrung mit zusätzlicher Sicherheit",
"pwa.install": "Installieren",
@@ -353,9 +358,8 @@
"action.scan": "Scannen",
"action.downloadDesktop": "Desktop-App laden",
"action.advancedSettings": "Erweiterte Einstellungen",
"cred.offerTag": "Angebot · oder Text kopieren",
"cred.answerTag": "Antwort · oder Text kopieren",
"cred.reveal": "Zum Anzeigen klicken - halten Sie diesen Code geheim",
"cred.orCopy": "Oder den Code kopieren",
"qr.title": "QR-Code scannen",
"qr.subtitle": "Richten Sie die Kamera auf den QR-Code",
"qr.scanning": "Wird gescannt …",
+16 -12
View File
@@ -120,9 +120,10 @@
"intro.createDesc": "Your device generates the keys and a one-time invitation. Nothing touches a server.",
"intro.createCta": "Generate keys & invitation",
"intro.joinTitle": "Join a channel",
"intro.joinDesc": "Scan your peer's QR with your camera, or paste their invitation code.",
"intro.scanTitle": "Scan QR with camera",
"intro.scanSubtitle": "Fastest - point at your peer's screen",
"intro.orPasteCode": "or paste code",
"intro.orPasteCode": "Or paste the code",
"intro.pastePlaceholder": "Paste invitation code here…",
"intro.connect": "Connect",
"intro.connecting": "Processing…",
@@ -298,22 +299,26 @@
"handshake.establish": "Establish connection",
"handshake.thenReceive": "Then receive the answer your peer sends back",
"handshake.pasteAnswerPlaceholder": "Paste peer's answer code…",
"handshake.answerSentNote": "Send this answer to the creator, then wait - the chat opens once they connect.",
"handshake.answerSentNote": "Waiting for the creator to connect. The chat opens automatically.",
"handshake.qrHint": "Keep this open until your peer captures the code.",
"handshake.qrHintFrames": "The handshake is split across {frames} frames - keep this open until your peer captures all of them.",
"verify.title": "Security verification",
"verify.desc": "Compare this safety code with your peer over a separate channel (voice / in person), then type it to unlock the chat.",
"verify.enterLabel": "Enter the verified code",
"verify.placeholder": "Type code here",
"verify.confirm": "Confirm code",
"verify.confirmed": "Confirmed",
"verify.mismatch": "Don't match",
"verify.yours": "Your confirmation",
"verify.peer": "Peer confirmation",
"verify.pending": "Pending",
"verify.waiting": "Waiting for code…",
"verify.verified": "Channel verified",
"verify.bothConfirmed": "Both parties confirmed. Opening the secure chat…",
"sasv.title": "Verify your peer",
"sasv.titleDone": "Peer verified",
"sasv.desc": "Read the code aloud over a call or in person. Continue only if your peer sees the same digits.",
"sasv.descDone": "You both confirmed the same code. The connection is authenticated.",
"sasv.enter": "Enter the code",
"sasv.match": "Match",
"sasv.noMatch": "Doesn't match",
"sasv.you": "You",
"sasv.peer": "Peer",
"sasv.waitingPeer": "Waiting for peer…",
"sasv.codesDiffer": "Codes don't match",
"sasv.opening": "Opening secure chat",
"pwa.bannerTitle": "Install SecureBit.chat",
"pwa.bannerDesc": "Get the native app experience with enhanced security",
"pwa.install": "Install",
@@ -348,9 +353,8 @@
"action.scan": "Scan",
"action.downloadDesktop": "Download desktop app",
"action.advancedSettings": "Advanced settings",
"cred.offerTag": "offer · or copy text",
"cred.answerTag": "answer · or copy text",
"cred.reveal": "Click to reveal - keep this code private",
"cred.orCopy": "Or copy the code",
"qr.title": "Scan QR code",
"qr.subtitle": "Point your camera at their QR",
"qr.scanning": "Scanning…",
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "Tu dispositivo genera las claves y una invitación de un solo uso. Nada de esto pasa por un servidor.",
"intro.createCta": "Generar claves e invitación",
"intro.joinTitle": "Unirse a un canal",
"intro.joinDesc": "Escanea el QR de tu contacto con la cámara o pega su código de invitación.",
"intro.scanTitle": "Escanear el QR con la cámara",
"intro.scanSubtitle": "Lo más rápido: apunta a la pantalla de la otra persona",
"intro.orPasteCode": "o pega el código",
"intro.orPasteCode": "O pega el código",
"intro.pastePlaceholder": "Pega aquí el código de invitación…",
"intro.connect": "Conectar",
"intro.connecting": "Procesando…",
@@ -303,22 +304,26 @@
"handshake.establish": "Establecer la conexión",
"handshake.thenReceive": "Después recibe la respuesta que te devuelva la otra persona",
"handshake.pasteAnswerPlaceholder": "Pega aquí el código de respuesta…",
"handshake.answerSentNote": "Envía esta respuesta a quien creó el canal y espera: el chat se abrirá cuando conecte.",
"handshake.answerSentNote": "Esperando a que el creador se conecte. El chat se abrirá automáticamente.",
"handshake.qrHint": "Mantén esto abierto hasta que la otra persona capture el código.",
"handshake.qrHintFrames": "El saludo se reparte en {frames} imágenes: mantén esto abierto hasta que las capture todas.",
"verify.title": "Verificación de seguridad",
"verify.desc": "Compara este código de seguridad con la otra persona por otra vía (voz o en persona) y escríbelo para desbloquear el chat.",
"verify.enterLabel": "Introduce el código verificado",
"verify.placeholder": "Escribe el código aquí",
"verify.confirm": "Confirmar el código",
"verify.confirmed": "Confirmado",
"verify.mismatch": "No coinciden",
"verify.yours": "Tu confirmación",
"verify.peer": "Confirmación de la otra persona",
"verify.pending": "Pendiente",
"verify.waiting": "Esperando el código…",
"verify.verified": "Canal verificado",
"verify.bothConfirmed": "Ambas partes han confirmado. Abriendo el chat seguro…",
"sasv.title": "Verifica a tu contacto",
"sasv.titleDone": "Contacto verificado",
"sasv.desc": "Lee el código en voz alta por llamada o en persona. Continúa solo si tu contacto ve los mismos dígitos.",
"sasv.descDone": "Ambos confirmasteis el mismo código. La conexión está autenticada.",
"sasv.enter": "Introduce el código",
"sasv.match": "Coincide",
"sasv.noMatch": "No coincide",
"sasv.you": "Tú",
"sasv.peer": "Contacto",
"sasv.waitingPeer": "Esperando a tu contacto…",
"sasv.codesDiffer": "Los códigos no coinciden",
"sasv.opening": "Abriendo el chat seguro",
"pwa.bannerTitle": "Instala SecureBit.chat",
"pwa.bannerDesc": "Disfruta de la experiencia de app nativa con más seguridad",
"pwa.install": "Instalar",
@@ -353,9 +358,8 @@
"action.scan": "Escanear",
"action.downloadDesktop": "Descargar la app de escritorio",
"action.advancedSettings": "Ajustes avanzados",
"cred.offerTag": "oferta · o copia el texto",
"cred.answerTag": "respuesta · o copia el texto",
"cred.reveal": "Haz clic para mostrarlo: mantén este código en privado",
"cred.orCopy": "O copia el código",
"qr.title": "Escanear código QR",
"qr.subtitle": "Apunta la cámara a su código QR",
"qr.scanning": "Escaneando…",
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "دستگاه شما کلیدها و یک دعوت‌نامهٔ یک‌بارمصرف می‌سازد. هیچ‌چیز به سرور نمی‌رسد.",
"intro.createCta": "ساخت کلید و دعوت‌نامه",
"intro.joinTitle": "به کانالی بپیوندید",
"intro.joinDesc": "QR طرف مقابل را با دوربین اسکن کنید یا کد دعوت او را جای‌گذاری کنید.",
"intro.scanTitle": "‏QR را با دوربین بخوانید",
"intro.scanSubtitle": "سریع‌ترین راه - به صفحهٔ همتایتان بگیرید",
"intro.orPasteCode": "یا کد را بچسبانید",
"intro.orPasteCode": "یا کد را جای‌گذاری کنید",
"intro.pastePlaceholder": "کد دعوت را اینجا بچسبانید…",
"intro.connect": "اتصال",
"intro.connecting": "در حال پردازش…",
@@ -303,22 +304,26 @@
"handshake.establish": "برقراری اتصال",
"handshake.thenReceive": "سپس پاسخی را که همتایتان برمی‌گرداند دریافت کنید",
"handshake.pasteAnswerPlaceholder": "کد پاسخ همتا را بچسبانید…",
"handshake.answerSentNote": "این پاسخ را برای سازنده بفرستید و منتظر بمانید - گفتگو به‌محض اتصال او باز می‌شود.",
"handshake.answerSentNote": "در انتظار اتصال سازنده. گفتگو به‌طور خودکار باز می‌شود.",
"handshake.qrHint": "این را باز نگه دارید تا همتایتان کد را بگیرد.",
"handshake.qrHintFrames": "دست‌دادن میان {frames} فریم پخش شده است - این را باز نگه دارید تا همتایتان همهٔ آن‌ها را بگیرد.",
"verify.title": "تأیید امنیتی",
"verify.desc": "این کد ایمنی را از راه کانالی جداگانه (تماس صوتی / حضوری) با همتایتان مقایسه کنید، سپس آن را بنویسید تا گفتگو باز شود.",
"verify.enterLabel": "کد تأییدشده را وارد کنید",
"verify.placeholder": "کد را اینجا بنویسید",
"verify.confirm": "تأیید کد",
"verify.confirmed": "تأیید شد",
"verify.mismatch": "هم‌خوان نیستند",
"verify.yours": "تأیید شما",
"verify.peer": "تأیید همتا",
"verify.pending": "در انتظار",
"verify.waiting": "در انتظار کد…",
"verify.verified": "کانال تأیید شد",
"verify.bothConfirmed": "هر دو طرف تأیید کردند. گفتگوی امن در حال باز شدن…",
"sasv.title": "طرف مقابل را تأیید کنید",
"sasv.titleDone": "طرف مقابل تأیید شد",
"sasv.desc": "کد را در تماس یا حضوری با صدای بلند بخوانید. فقط وقتی ادامه دهید که طرف مقابل همان ارقام را ببیند.",
"sasv.descDone": "هر دو یک کد را تأیید کردید. اتصال احراز هویت شد.",
"sasv.enter": "کد را وارد کنید",
"sasv.match": "مطابق است",
"sasv.noMatch": "مطابق نیست",
"sasv.you": "شما",
"sasv.peer": "طرف مقابل",
"sasv.waitingPeer": "در انتظار طرف مقابل…",
"sasv.codesDiffer": "کدها مطابق نیستند",
"sasv.opening": "در حال باز کردن گفتگوی امن",
"pwa.bannerTitle": "‏SecureBit.chat را نصب کنید",
"pwa.bannerDesc": "تجربهٔ برنامهٔ بومی با امنیت بیشتر",
"pwa.install": "نصب",
@@ -353,9 +358,8 @@
"action.scan": "خواندن",
"action.downloadDesktop": "دریافت برنامهٔ رومیزی",
"action.advancedSettings": "تنظیمات پیشرفته",
"cred.offerTag": "پیشنهاد · یا متن را رونوشت کنید",
"cred.answerTag": "پاسخ · یا متن را رونوشت کنید",
"cred.reveal": "برای نمایش کلیک کنید - این کد را خصوصی نگه دارید",
"cred.orCopy": "یا کد را رونوشت کنید",
"qr.title": "خواندن کد QR",
"qr.subtitle": "دوربین را به QR آن‌ها بگیرید",
"qr.scanning": "در حال خواندن…",
+16 -12
View File
@@ -120,9 +120,10 @@
"intro.createDesc": "Votre appareil génère les clés et une invitation à usage unique. Rien de tout cela ne passe par un serveur.",
"intro.createCta": "Générer les clés et l'invitation",
"intro.joinTitle": "Rejoindre un canal",
"intro.joinDesc": "Scannez le QR de votre correspondant avec l'appareil photo, ou collez son code d'invitation.",
"intro.scanTitle": "Scanner le QR avec la caméra",
"intro.scanSubtitle": "Le plus rapide : visez l'écran de votre correspondant",
"intro.orPasteCode": "ou collez le code",
"intro.orPasteCode": "Ou collez le code",
"intro.pastePlaceholder": "Collez ici le code d'invitation…",
"intro.connect": "Se connecter",
"intro.connecting": "Traitement…",
@@ -298,22 +299,26 @@
"handshake.establish": "Établir la connexion",
"handshake.thenReceive": "Recevez ensuite la réponse que votre correspondant renvoie",
"handshake.pasteAnswerPlaceholder": "Collez le code de réponse…",
"handshake.answerSentNote": "Envoyez cette réponse à la personne qui a créé le canal, puis patientez : la discussion s'ouvre dès qu'elle se connecte.",
"handshake.answerSentNote": "En attente de la connexion du créateur. Le chat s'ouvrira automatiquement.",
"handshake.qrHint": "Gardez ceci ouvert jusqu'à ce que votre correspondant ait capturé le code.",
"handshake.qrHintFrames": "La poignée de main est répartie sur {frames} images : gardez ceci ouvert jusqu'à ce que votre correspondant les ait toutes capturées.",
"verify.title": "Vérification de sécurité",
"verify.desc": "Comparez ce code de sécurité avec votre correspondant par un autre canal (voix ou en personne), puis saisissez-le pour ouvrir la discussion.",
"verify.enterLabel": "Saisissez le code vérifié",
"verify.placeholder": "Tapez le code ici",
"verify.confirm": "Confirmer le code",
"verify.confirmed": "Confirmé",
"verify.mismatch": "Ne correspond pas",
"verify.yours": "Votre confirmation",
"verify.peer": "Confirmation du correspondant",
"verify.pending": "En attente",
"verify.waiting": "En attente du code…",
"verify.verified": "Canal vérifié",
"verify.bothConfirmed": "Les deux parties ont confirmé. Ouverture de la discussion sécurisée…",
"sasv.title": "Vérifiez votre correspondant",
"sasv.titleDone": "Correspondant vérifié",
"sasv.desc": "Lisez le code à voix haute par appel ou en personne. Ne continuez que si votre correspondant voit les mêmes chiffres.",
"sasv.descDone": "Vous avez tous les deux confirmé le même code. La connexion est authentifiée.",
"sasv.enter": "Saisissez le code",
"sasv.match": "Correspond",
"sasv.noMatch": "Ne correspond pas",
"sasv.you": "Vous",
"sasv.peer": "Correspondant",
"sasv.waitingPeer": "En attente du correspondant…",
"sasv.codesDiffer": "Les codes ne correspondent pas",
"sasv.opening": "Ouverture du chat sécurisé",
"pwa.bannerTitle": "Installer SecureBit.chat",
"pwa.bannerDesc": "Profitez de l'expérience d'une application native, avec une sécurité renforcée",
"pwa.install": "Installer",
@@ -348,9 +353,8 @@
"action.scan": "Scanner",
"action.downloadDesktop": "Télécharger l'application",
"action.advancedSettings": "Réglages avancés",
"cred.offerTag": "offre · ou copiez le texte",
"cred.answerTag": "réponse · ou copiez le texte",
"cred.reveal": "Cliquez pour afficher - gardez ce code privé",
"cred.orCopy": "Ou copiez le code",
"qr.title": "Scanner le QR",
"qr.subtitle": "Visez le QR de votre correspondant",
"qr.scanning": "Lecture…",
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "המכשיר שלכם יוצר את המפתחות והזמנה חד־פעמית. שום דבר לא נוגע בשרת.",
"intro.createCta": "יצירת מפתחות והזמנה",
"intro.joinTitle": "הצטרפו לערוץ",
"intro.joinDesc": "סרקו את קוד ה-QR של הצד השני במצלמה, או הדביקו את קוד ההזמנה.",
"intro.scanTitle": "סרקו QR עם המצלמה",
"intro.scanSubtitle": "הכי מהיר - כוונו למסך של העמית",
"intro.orPasteCode": "או הדביקו קוד",
"intro.orPasteCode": "או הדביקו את הקוד",
"intro.pastePlaceholder": "הדביקו כאן את קוד ההזמנה…",
"intro.connect": "התחברו",
"intro.connecting": "מעבד…",
@@ -303,22 +304,26 @@
"handshake.establish": "יצירת חיבור",
"handshake.thenReceive": "ואז קבלו את התשובה שהעמית שולח בחזרה",
"handshake.pasteAnswerPlaceholder": "הדביקו את קוד התשובה של העמית…",
"handshake.answerSentNote": "שלחו את התשובה ליוצר ואז המתינו - הצ'אט נפתח ברגע שהם מתחברים.",
"handshake.answerSentNote": "ממתינים שהיוצר יתחבר. הצ'אט ייפתח אוטומטית.",
"handshake.qrHint": "השאירו פתוח עד שהעמית יקלוט את הקוד.",
"handshake.qrHintFrames": "לחיצת היד מפוצלת ל-{frames} פריימים - השאירו פתוח עד שהעמית יקלוט את כולם.",
"verify.title": "אימות אבטחה",
"verify.desc": "השוו את קוד הבטיחות הזה עם העמית בערוץ נפרד (שיחה קולית / פנים אל פנים), ואז הקלידו אותו כדי לפתוח את הצ'אט.",
"verify.enterLabel": "הזינו את הקוד המאומת",
"verify.placeholder": "הקלידו כאן את הקוד",
"verify.confirm": "אשרו את הקוד",
"verify.confirmed": "אושר",
"verify.mismatch": "לא תואמים",
"verify.yours": "האישור שלכם",
"verify.peer": "אישור העמית",
"verify.pending": "ממתין",
"verify.waiting": "ממתין לקוד…",
"verify.verified": "הערוץ אומת",
"verify.bothConfirmed": "שני הצדדים אישרו. פותח את הצ'אט המאובטח…",
"sasv.title": "אמתו את הצד השני",
"sasv.titleDone": "הצד השני אומת",
"sasv.desc": "הקריאו את הקוד בקול בשיחה או פנים אל פנים. המשיכו רק אם הצד השני רואה את אותן ספרות.",
"sasv.descDone": "שניכם אישרתם את אותו קוד. החיבור מאומת.",
"sasv.enter": "הזינו את הקוד",
"sasv.match": "תואם",
"sasv.noMatch": "לא תואם",
"sasv.you": "אתם",
"sasv.peer": "הצד השני",
"sasv.waitingPeer": "ממתינים לצד השני…",
"sasv.codesDiffer": "הקודים לא תואמים",
"sasv.opening": "פותחים צ'אט מאובטח",
"pwa.bannerTitle": "התקינו את SecureBit.chat",
"pwa.bannerDesc": "קבלו חוויית אפליקציה מקורית עם אבטחה משופרת",
"pwa.install": "התקנה",
@@ -353,9 +358,8 @@
"action.scan": "סריקה",
"action.downloadDesktop": "הורידו את אפליקציית שולחן העבודה",
"action.advancedSettings": "הגדרות מתקדמות",
"cred.offerTag": "הצעה · או העתיקו טקסט",
"cred.answerTag": "תשובה · או העתיקו טקסט",
"cred.reveal": "לחצו לחשיפה - שמרו את הקוד הזה פרטי",
"cred.orCopy": "או העתיקו את הקוד",
"qr.title": "סריקת קוד QR",
"qr.subtitle": "כוונו את המצלמה ל-QR שלהם",
"qr.scanning": "סורק…",
+16 -12
View File
@@ -120,9 +120,10 @@
"intro.createDesc": "आपका उपकरण कुंजियाँ और एक बार काम आने वाला निमंत्रण बनाता है। इनमें से कुछ भी किसी सर्वर तक नहीं जाता।",
"intro.createCta": "कुंजियाँ और निमंत्रण बनाएँ",
"intro.joinTitle": "चैनल में शामिल हों",
"intro.joinDesc": "कैमरे से अपने साथी का QR स्कैन करें, या उनका आमंत्रण कोड पेस्ट करें।",
"intro.scanTitle": "कैमरे से QR स्कैन करें",
"intro.scanSubtitle": "सबसे तेज़ - सामने वाले की स्क्रीन पर कैमरा रखें",
"intro.orPasteCode": "या कोड चिपकाएँ",
"intro.orPasteCode": "या कोड पेस्ट करें",
"intro.pastePlaceholder": "निमंत्रण कोड यहाँ चिपकाएँ…",
"intro.connect": "जोड़ें",
"intro.connecting": "प्रक्रिया जारी…",
@@ -298,22 +299,26 @@
"handshake.establish": "जुड़ाव बनाएँ",
"handshake.thenReceive": "फिर सामने वाले का भेजा उत्तर लीजिए",
"handshake.pasteAnswerPlaceholder": "सामने वाले का उत्तर कोड चिपकाएँ…",
"handshake.answerSentNote": "यह उत्तर चैनल बनाने वाले को भेजिए और रुकिए - जैसे ही वे जुड़ेंगे, चैट खुल जाएगी।",
"handshake.answerSentNote": "निर्माता के जुड़ने की प्रतीक्षा है। चैट अपने-आप खुल जाएगी।",
"handshake.qrHint": "जब तक सामने वाला कोड पढ़ न ले, इसे खुला रखिए।",
"handshake.qrHintFrames": "हाथ मिलाना {frames} फ़्रेमों में बँटा है - जब तक सामने वाला सब पढ़ न ले, इसे खुला रखिए।",
"verify.title": "सुरक्षा सत्यापन",
"verify.desc": "इस सुरक्षा कोड को किसी दूसरे रास्ते से (आवाज़ पर या आमने-सामने) मिलाइए, फिर उसे लिखकर चैट खोलिए।",
"verify.enterLabel": "मिलाया हुआ कोड लिखें",
"verify.placeholder": "कोड यहाँ लिखें",
"verify.confirm": "कोड पक्का करें",
"verify.confirmed": "पक्का हुआ",
"verify.mismatch": "मेल नहीं खाता",
"verify.yours": "आपकी पुष्टि",
"verify.peer": "सामने वाले की पुष्टि",
"verify.pending": "प्रतीक्षा में",
"verify.waiting": "कोड की प्रतीक्षा…",
"verify.verified": "चैनल सत्यापित",
"verify.bothConfirmed": "दोनों पक्षों ने पुष्टि कर दी। सुरक्षित चैट खुल रही है…",
"sasv.title": "अपने साथी की पुष्टि करें",
"sasv.titleDone": "साथी की पुष्टि हो गई",
"sasv.desc": "कॉल पर या आमने-सामने कोड ज़ोर से पढ़ें। तभी आगे बढ़ें जब आपके साथी को वही अंक दिखें।",
"sasv.descDone": "आप दोनों ने एक ही कोड की पुष्टि की। कनेक्शन प्रमाणित है।",
"sasv.enter": "कोड दर्ज करें",
"sasv.match": "मेल खाता है",
"sasv.noMatch": "मेल नहीं खाता",
"sasv.you": "आप",
"sasv.peer": "साथी",
"sasv.waitingPeer": "साथी की प्रतीक्षा…",
"sasv.codesDiffer": "कोड मेल नहीं खाते",
"sasv.opening": "सुरक्षित चैट खुल रही है",
"pwa.bannerTitle": "SecureBit.chat इंस्टॉल करें",
"pwa.bannerDesc": "बेहतर सुरक्षा के साथ मूल ऐप जैसा अनुभव पाइए",
"pwa.install": "इंस्टॉल करें",
@@ -348,9 +353,8 @@
"action.scan": "स्कैन करें",
"action.downloadDesktop": "डेस्कटॉप ऐप डाउनलोड करें",
"action.advancedSettings": "उन्नत सेटिंग्स",
"cred.offerTag": "निमंत्रण · या पाठ कॉपी करें",
"cred.answerTag": "उत्तर · या पाठ कॉपी करें",
"cred.reveal": "देखने के लिए दबाएँ - यह कोड किसी को न बताएँ",
"cred.orCopy": "या कोड कॉपी करें",
"qr.title": "QR कोड स्कैन करें",
"qr.subtitle": "कैमरा सामने वाले के QR पर रखें",
"qr.scanning": "पढ़ा जा रहा है…",
+15 -11
View File
@@ -120,6 +120,7 @@
"intro.createDesc": "여러분의 기기가 키와 일회용 초대장을 만듭니다. 그 어느 것도 서버를 거치지 않습니다.",
"intro.createCta": "키와 초대장 만들기",
"intro.joinTitle": "채널 참여하기",
"intro.joinDesc": "카메라로 상대방의 QR을 스캔하거나 초대 코드를 붙여넣으세요.",
"intro.scanTitle": "카메라로 QR 스캔",
"intro.scanSubtitle": "가장 빠릅니다 - 상대의 화면을 비추세요",
"intro.orPasteCode": "또는 코드 붙여넣기",
@@ -298,22 +299,26 @@
"handshake.establish": "연결 맺기",
"handshake.thenReceive": "그다음 상대가 보내는 응답을 받으세요",
"handshake.pasteAnswerPlaceholder": "상대의 응답 코드를 붙여넣으세요…",
"handshake.answerSentNote": "이 응답을 만든 사람에게 보내고 기다리세요 - 상대가 연결되면 대화가 열립니다.",
"handshake.answerSentNote": "생성자의 연결을 기다리는 중입니다. 채팅이 자동으로 열립니다.",
"handshake.qrHint": "상대가 코드를 다 읽을 때까지 이 화면을 열어 두세요.",
"handshake.qrHintFrames": "악수가 {frames}개 화면으로 나뉘어 있습니다 - 상대가 전부 읽을 때까지 열어 두세요.",
"verify.title": "보안 확인",
"verify.desc": "다른 경로(통화나 대면)로 상대와 이 안전 코드를 대조한 뒤, 입력해서 대화를 여세요.",
"verify.enterLabel": "대조한 코드를 입력하세요",
"verify.placeholder": "여기에 코드 입력",
"verify.confirm": "코드 확인",
"verify.confirmed": "확인됨",
"verify.mismatch": "일치하지 않음",
"verify.yours": "내 확인",
"verify.peer": "상대의 확인",
"verify.pending": "대기 중",
"verify.waiting": "코드 대기 중…",
"verify.verified": "채널 확인 완료",
"verify.bothConfirmed": "양쪽 모두 확인했습니다. 안전한 대화를 여는 중…",
"sasv.title": "상대방 확인",
"sasv.titleDone": "상대방 확인 완료",
"sasv.desc": "통화나 대면으로 코드를 소리 내어 읽어 주세요. 상대방이 같은 숫자를 볼 때만 계속하세요.",
"sasv.descDone": "두 사람 모두 같은 코드를 확인했습니다. 연결이 인증되었습니다.",
"sasv.enter": "코드 입력",
"sasv.match": "일치",
"sasv.noMatch": "일치하지 않음",
"sasv.you": "나",
"sasv.peer": "상대방",
"sasv.waitingPeer": "상대방을 기다리는 중…",
"sasv.codesDiffer": "코드가 일치하지 않음",
"sasv.opening": "보안 채팅을 여는 중",
"pwa.bannerTitle": "SecureBit.chat 설치",
"pwa.bannerDesc": "보안이 강화된 네이티브 앱 경험을 누리세요",
"pwa.install": "설치",
@@ -348,9 +353,8 @@
"action.scan": "스캔",
"action.downloadDesktop": "데스크톱 앱 내려받기",
"action.advancedSettings": "고급 설정",
"cred.offerTag": "초대 · 또는 텍스트 복사",
"cred.answerTag": "응답 · 또는 텍스트 복사",
"cred.reveal": "눌러서 보기 - 이 코드는 남에게 알리지 마세요",
"cred.orCopy": "또는 코드 복사",
"qr.title": "QR 코드 스캔",
"qr.subtitle": "상대의 QR에 카메라를 맞추세요",
"qr.scanning": "읽는 중…",
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "Ваше устройство создаёт ключи и одноразовое приглашение. Ничто из этого не попадает на сервер.",
"intro.createCta": "Создать ключи и приглашение",
"intro.joinTitle": "Присоединиться к каналу",
"intro.joinDesc": "Отсканируйте QR собеседника камерой или вставьте его код приглашения.",
"intro.scanTitle": "Сканировать QR камерой",
"intro.scanSubtitle": "Быстрее всего - наведите на экран собеседника",
"intro.orPasteCode": "или вставьте код",
"intro.orPasteCode": "Или вставьте код",
"intro.pastePlaceholder": "Вставьте код приглашения сюда…",
"intro.connect": "Соединиться",
"intro.connecting": "Обработка…",
@@ -303,22 +304,26 @@
"handshake.establish": "Установить соединение",
"handshake.thenReceive": "Затем получите ответ, который пришлёт собеседник",
"handshake.pasteAnswerPlaceholder": "Вставьте код ответа собеседника…",
"handshake.answerSentNote": "Отправьте этот ответ создателю и подождите - чат откроется, как только он подключится.",
"handshake.answerSentNote": "Ждём, пока создатель подключится. Чат откроется автоматически.",
"handshake.qrHint": "Держите это открытым, пока собеседник не считает код.",
"handshake.qrHintFrames": "Рукопожатие разбито на {frames} кадров - держите это открытым, пока собеседник не считает все.",
"verify.title": "Проверка безопасности",
"verify.desc": "Сверьте этот код безопасности с собеседником по другому каналу (голосом или лично), затем введите его, чтобы открыть чат.",
"verify.enterLabel": "Введите сверенный код",
"verify.placeholder": "Введите код здесь",
"verify.confirm": "Подтвердить код",
"verify.confirmed": "Подтверждено",
"verify.mismatch": "Не совпадают",
"verify.yours": "Ваше подтверждение",
"verify.peer": "Подтверждение собеседника",
"verify.pending": "Ожидается",
"verify.waiting": "Ожидание кода…",
"verify.verified": "Канал проверен",
"verify.bothConfirmed": "Обе стороны подтвердили. Открываем защищённый чат…",
"sasv.title": "Проверьте собеседника",
"sasv.titleDone": "Собеседник проверен",
"sasv.desc": "Прочитайте код вслух по звонку или при встрече. Продолжайте, только если собеседник видит те же цифры.",
"sasv.descDone": "Вы оба подтвердили один и тот же код. Соединение подлинное.",
"sasv.enter": "Введите код",
"sasv.match": "Совпадает",
"sasv.noMatch": "Не совпадает",
"sasv.you": "Вы",
"sasv.peer": "Собеседник",
"sasv.waitingPeer": "Ждём собеседника…",
"sasv.codesDiffer": "Коды не совпадают",
"sasv.opening": "Открываем защищённый чат",
"pwa.bannerTitle": "Установите SecureBit.chat",
"pwa.bannerDesc": "Опыт полноценного приложения с усиленной защитой",
"pwa.install": "Установить",
@@ -353,9 +358,8 @@
"action.scan": "Сканировать",
"action.downloadDesktop": "Скачать приложение",
"action.advancedSettings": "Дополнительные настройки",
"cred.offerTag": "предложение · или скопируйте текст",
"cred.answerTag": "ответ · или скопируйте текст",
"cred.reveal": "Нажмите, чтобы показать - держите этот код в тайне",
"cred.orCopy": "Или скопируйте код",
"qr.title": "Сканировать QR-код",
"qr.subtitle": "Наведите камеру на их QR-код",
"qr.scanning": "Сканирование…",
+16 -12
View File
@@ -125,9 +125,10 @@
"intro.createDesc": "Ваш пристрій створює ключі та одноразове запрошення. Ніщо з цього не потрапляє на сервер.",
"intro.createCta": "Створити ключі та запрошення",
"intro.joinTitle": "Приєднатися до каналу",
"intro.joinDesc": "Відскануйте QR співрозмовника камерою або вставте його код запрошення.",
"intro.scanTitle": "Сканувати QR камерою",
"intro.scanSubtitle": "Найшвидше - наведіть на екран співрозмовника",
"intro.orPasteCode": "або вставте код",
"intro.orPasteCode": "Або вставте код",
"intro.pastePlaceholder": "Вставте код запрошення сюди…",
"intro.connect": "З'єднатися",
"intro.connecting": "Обробка…",
@@ -303,22 +304,26 @@
"handshake.establish": "Встановити з'єднання",
"handshake.thenReceive": "Потім отримайте відповідь, яку надішле співрозмовник",
"handshake.pasteAnswerPlaceholder": "Вставте код відповіді співрозмовника…",
"handshake.answerSentNote": "Надішліть цю відповідь створювачу й зачекайте - чат відкриється, щойно він приєднається.",
"handshake.answerSentNote": "Чекаємо, поки творець під'єднається. Чат відкриється автоматично.",
"handshake.qrHint": "Тримайте це відкритим, доки співрозмовник не зчитає код.",
"handshake.qrHintFrames": "Рукостискання розбите на {frames} кадрів - тримайте це відкритим, доки співрозмовник не зчитає всі.",
"verify.title": "Перевірка безпеки",
"verify.desc": "Звірте цей код безпеки зі співрозмовником іншим каналом (голосом або особисто), потім введіть його, щоб відкрити чат.",
"verify.enterLabel": "Введіть звірений код",
"verify.placeholder": "Введіть код тут",
"verify.confirm": "Підтвердити код",
"verify.confirmed": "Підтверджено",
"verify.mismatch": "Не збігаються",
"verify.yours": "Ваше підтвердження",
"verify.peer": "Підтвердження співрозмовника",
"verify.pending": "Очікується",
"verify.waiting": "Очікування коду…",
"verify.verified": "Канал перевірено",
"verify.bothConfirmed": "Обидві сторони підтвердили. Відкриваємо захищений чат…",
"sasv.title": "Перевірте співрозмовника",
"sasv.titleDone": "Співрозмовника перевірено",
"sasv.desc": "Прочитайте код уголос під час дзвінка або зустрічі. Продовжуйте, лише якщо співрозмовник бачить ті самі цифри.",
"sasv.descDone": "Ви обидва підтвердили той самий код. З'єднання справжнє.",
"sasv.enter": "Введіть код",
"sasv.match": "Збігається",
"sasv.noMatch": "Не збігається",
"sasv.you": "Ви",
"sasv.peer": "Співрозмовник",
"sasv.waitingPeer": "Чекаємо на співрозмовника…",
"sasv.codesDiffer": "Коди не збігаються",
"sasv.opening": "Відкриваємо захищений чат",
"pwa.bannerTitle": "Встановіть SecureBit.chat",
"pwa.bannerDesc": "Досвід власного застосунку з посиленим захистом",
"pwa.install": "Встановити",
@@ -353,9 +358,8 @@
"action.scan": "Сканувати",
"action.downloadDesktop": "Завантажити застосунок",
"action.advancedSettings": "Додаткові налаштування",
"cred.offerTag": "пропозиція · або скопіюйте текст",
"cred.answerTag": "відповідь · або скопіюйте текст",
"cred.reveal": "Натисніть, щоб показати - тримайте цей код у таємниці",
"cred.orCopy": "Або скопіюйте код",
"qr.title": "Сканувати QR-код",
"qr.subtitle": "Наведіть камеру на їхній QR-код",
"qr.scanning": "Сканування…",
+15 -11
View File
@@ -125,6 +125,7 @@
"intro.createDesc": "آپ کا آلہ کلیدیں اور ایک بار کی دعوت بناتا ہے۔ کچھ بھی سرور کو نہیں چھوتا۔",
"intro.createCta": "کلیدیں اور دعوت بنائیں",
"intro.joinTitle": "چینل میں شامل ہوں",
"intro.joinDesc": "کیمرے سے اپنے ساتھی کا QR اسکین کریں، یا ان کا دعوتی کوڈ چسپاں کریں۔",
"intro.scanTitle": "کیمرے سے QR پڑھیں",
"intro.scanSubtitle": "سب سے تیز - ساتھی کی سکرین کی طرف کریں",
"intro.orPasteCode": "یا کوڈ چسپاں کریں",
@@ -303,22 +304,26 @@
"handshake.establish": "اتصال قائم کریں",
"handshake.thenReceive": "پھر وہ جواب وصول کریں جو آپ کا ساتھی واپس بھیجتا ہے",
"handshake.pasteAnswerPlaceholder": "ساتھی کا جوابی کوڈ چسپاں کریں…",
"handshake.answerSentNote": "یہ جواب بنانے والے کو بھیجیں، پھر انتظار کریں - ان کے جڑتے ہی گفتگو کھل جائے گی۔",
"handshake.answerSentNote": "تخلیق کار کے جڑنے کا انتظار ہے۔ چیٹ خود بخود کھل جائے گی۔",
"handshake.qrHint": "یہ اس وقت تک کھلا رکھیں جب تک ساتھی کوڈ نہ پکڑ لے۔",
"handshake.qrHintFrames": "مصافحہ {frames} فریموں میں بٹا ہے - یہ اس وقت تک کھلا رکھیں جب تک ساتھی سب نہ پکڑ لے۔",
"verify.title": "حفاظتی تصدیق",
"verify.desc": "یہ حفاظتی کوڈ کسی الگ ذریعے (آواز / بالمشافہ) سے اپنے ساتھی کے ساتھ ملائیں، پھر گفتگو کھولنے کے لیے لکھیں۔",
"verify.enterLabel": "تصدیق شدہ کوڈ درج کریں",
"verify.placeholder": "کوڈ یہاں لکھیں",
"verify.confirm": "کوڈ کی تصدیق",
"verify.confirmed": "تصدیق ہو گئی",
"verify.mismatch": "مطابقت نہیں",
"verify.yours": "آپ کی تصدیق",
"verify.peer": "ساتھی کی تصدیق",
"verify.pending": "زیرِ التوا",
"verify.waiting": "کوڈ کا انتظار…",
"verify.verified": "چینل تصدیق شدہ",
"verify.bothConfirmed": "دونوں فریقوں نے تصدیق کی۔ محفوظ گفتگو کھولی جا رہی ہے…",
"sasv.title": "اپنے ساتھی کی تصدیق کریں",
"sasv.titleDone": "ساتھی کی تصدیق ہو گئی",
"sasv.desc": "کال پر یا آمنے سامنے کوڈ بلند آواز سے پڑھیں۔ صرف تب آگے بڑھیں جب آپ کے ساتھی کو وہی ہندسے نظر آئیں۔",
"sasv.descDone": "آپ دونوں نے ایک ہی کوڈ کی تصدیق کی۔ کنکشن مستند ہے۔",
"sasv.enter": "کوڈ درج کریں",
"sasv.match": "مماثل ہے",
"sasv.noMatch": "مماثل نہیں",
"sasv.you": "آپ",
"sasv.peer": "ساتھی",
"sasv.waitingPeer": "ساتھی کا انتظار…",
"sasv.codesDiffer": "کوڈ مماثل نہیں",
"sasv.opening": "محفوظ چیٹ کھل رہی ہے",
"pwa.bannerTitle": "‏SecureBit.chat نصب کریں",
"pwa.bannerDesc": "بہتر سلامتی کے ساتھ مقامی ایپ کا تجربہ حاصل کریں",
"pwa.install": "نصب",
@@ -353,9 +358,8 @@
"action.scan": "پڑھیں",
"action.downloadDesktop": "ڈیسک ٹاپ ایپ ڈاؤن لوڈ کریں",
"action.advancedSettings": "اعلیٰ ترتیبات",
"cred.offerTag": "پیشکش · یا متن نقل کریں",
"cred.answerTag": "جواب · یا متن نقل کریں",
"cred.reveal": "دکھانے کے لیے دبائیں - یہ کوڈ نجی رکھیں",
"cred.orCopy": "یا کوڈ نقل کریں",
"qr.title": "‏QR کوڈ پڑھیں",
"qr.subtitle": "اپنا کیمرہ ان کے QR کی طرف کریں",
"qr.scanning": "پڑھا جا رہا ہے…",
+15 -11
View File
@@ -120,6 +120,7 @@
"intro.createDesc": "你的设备会生成密钥和一次性邀请。这些都不会经过任何服务器。",
"intro.createCta": "生成密钥与邀请",
"intro.joinTitle": "加入通道",
"intro.joinDesc": "用相机扫描对方的二维码,或粘贴对方的邀请代码。",
"intro.scanTitle": "用摄像头扫描二维码",
"intro.scanSubtitle": "最快的方式 —— 对准对方的屏幕",
"intro.orPasteCode": "或粘贴代码",
@@ -298,22 +299,26 @@
"handshake.establish": "建立连接",
"handshake.thenReceive": "然后接收对方发回的应答",
"handshake.pasteAnswerPlaceholder": "粘贴对方的应答码…",
"handshake.answerSentNote": "把这段应答发给创建者,然后等待 —— 对方一连上,聊天就会打开。",
"handshake.answerSentNote": "正在等待创建者连接。聊天将自动打开。",
"handshake.qrHint": "在对方读取完代码之前,请保持此界面打开。",
"handshake.qrHintFrames": "握手被拆成 {frames} 帧 —— 在对方读完全部帧之前,请保持此界面打开。",
"verify.title": "安全校验",
"verify.desc": "通过另一条渠道(通话或当面)与对方核对这个安全码,然后输入它以解锁聊天。",
"verify.enterLabel": "输入核对过的安全码",
"verify.placeholder": "在此输入安全码",
"verify.confirm": "确认安全码",
"verify.confirmed": "已确认",
"verify.mismatch": "不一致",
"verify.yours": "你的确认",
"verify.peer": "对方的确认",
"verify.pending": "等待中",
"verify.waiting": "等待安全码…",
"verify.verified": "通道已校验",
"verify.bothConfirmed": "双方均已确认。正在打开安全聊天…",
"sasv.title": "验证对方",
"sasv.titleDone": "对方已验证",
"sasv.desc": "通过通话或当面大声读出代码。只有对方看到相同的数字时才继续。",
"sasv.descDone": "你们双方确认了相同的代码。连接已通过认证。",
"sasv.enter": "输入代码",
"sasv.match": "一致",
"sasv.noMatch": "不一致",
"sasv.you": "你",
"sasv.peer": "对方",
"sasv.waitingPeer": "正在等待对方…",
"sasv.codesDiffer": "代码不一致",
"sasv.opening": "正在打开安全聊天",
"pwa.bannerTitle": "安装 SecureBit.chat",
"pwa.bannerDesc": "获得原生应用般的体验,安全性更进一步",
"pwa.install": "安装",
@@ -348,9 +353,8 @@
"action.scan": "扫描",
"action.downloadDesktop": "下载桌面应用",
"action.advancedSettings": "高级设置",
"cred.offerTag": "邀请 · 或复制文本",
"cred.answerTag": "应答 · 或复制文本",
"cred.reveal": "点击显示 - 请勿泄露此代码",
"cred.orCopy": "或复制代码",
"qr.title": "扫描二维码",
"qr.subtitle": "把摄像头对准对方的二维码",
"qr.scanning": "扫描中…",
+7 -7
View File
@@ -1,10 +1,10 @@
{
"version": "1790225533889",
"buildVersion": "1790225533889",
"appVersion": "6.8.8",
"buildTime": "2026-09-24T04:52:13.935Z",
"buildId": "1790225533889-eac3009",
"gitHash": "eac3009",
"version": "1790540575787",
"buildVersion": "1790540575787",
"appVersion": "6.8.12",
"buildTime": "2026-09-27T20:22:55.824Z",
"buildId": "1790540575787-cd9d78d",
"gitHash": "cd9d78d",
"generated": true,
"generatedAt": "2026-09-24T04:52:13.936Z"
"generatedAt": "2026-09-27T20:22:55.825Z"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "securebit-chat",
"version": "6.8.8",
"version": "6.8.12",
"description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html",
"scripts": {
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ru/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ru.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/ru.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+23 -23
View File
@@ -17,7 +17,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -37,7 +37,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -57,7 +57,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -77,7 +77,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -97,7 +97,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -117,7 +117,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -137,7 +137,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -157,7 +157,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -177,7 +177,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -197,7 +197,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -217,7 +217,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -237,7 +237,7 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@@ -257,67 +257,67 @@
<xhtml:link rel="alternate" hreflang="fa" href="https://securebit.chat/fa/"/>
<xhtml:link rel="alternate" hreflang="ur" href="https://securebit.chat/ur/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://securebit.chat/"/>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://securebit.chat/docs/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/faq/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/architecture/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/cryptography/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/descriptor-sbq2/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/configuration/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/calls/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/api/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/contributing/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://securebit.chat/docs/use-policy/</loc>
<lastmod>2026-09-24</lastmod>
<lastmod>2026-09-27</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
+192 -84
View File
@@ -1170,9 +1170,23 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
const [copied, setCopied] = React.useState(false);
const [sasInput, setSasInput] = React.useState('');
const [sasError, setSasError] = React.useState('');
const [sasFocused, setSasFocused] = React.useState(false);
const [sasShake, setSasShake] = React.useState(0);
const [platformsOpen, setPlatformsOpen] = React.useState(false);
const [codeRevealed, setCodeRevealed] = React.useState(false);
const [genProgress, setGenProgress] = React.useState(0);
// Countdown on the invitation: the joining side refuses an offer older
// than 30 minutes (MAX_OFFER_AGE in EnhancedSecureWebRTCManager), measured
// from the offer's own timestamp.
const OFFER_TTL_MS = 30 * 60 * 1000;
const offerTs = (offerData && typeof offerData === 'object' && Number(offerData.ts || offerData.timestamp)) || null;
const [nowTick, setNowTick] = React.useState(() => Date.now());
React.useEffect(() => {
if (!showOfferStep || !offerTs) return undefined;
setNowTick(Date.now());
const id = setInterval(() => setNowTick(Date.now()), 1000);
return () => clearInterval(id);
}, [showOfferStep, offerTs]);
// Reset the typed SAS whenever a fresh verification code arrives
React.useEffect(() => { setSasInput(''); setSasError(''); }, [verificationCode]);
@@ -1549,72 +1563,151 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
const backButton = (key) => h('button', { key: key || 'back', className: 'sb-soft-btn', onClick: resetToSelect, style: { display: 'inline-flex', alignItems: 'center', gap: '6px', marginBottom: '14px', paddingBlock: '6px', paddingInlineStart: '8px', paddingInlineEnd: '11px', borderRadius: '8px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'transparent', color: 'var(--sb-text-6)', fontFamily: 'inherit', fontSize: '12.5px', fontWeight: 600, cursor: 'pointer' } }, [fa('fa-chevron-left', { key: 'i' }), t('action.back')]);
// credential code block (offer/answer text fallback + copy)
const credBlock = h('div', { key: 'codeblock', style: { borderRadius: '13px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)', overflow: 'hidden', marginBottom: '16px' } }, [
h('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '9px 12px', borderBottom: '1px solid rgba(var(--sb-ink), 0.06)', background: 'rgba(0,0,0,0.2)' } }, [
h('span', { key: 'dot', style: { width: '7px', height: '7px', borderRadius: '50%', background: accent } }),
h('span', { key: 'tag', style: { fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: 'var(--sb-text-7)' } }, isCreate ? t('cred.offerTag') : t('cred.answerTag')),
h('button', { key: 'copy', onClick: copyCred, style: { marginInlineStart: 'auto', padding: '4px 9px', borderRadius: '6px', border: `1px solid ${copied ? 'rgba(var(--sb-green-rgb), 0.4)' : 'rgba(var(--sb-ink), 0.1)'}`, background: copied ? 'rgba(var(--sb-green-rgb), 0.1)' : 'rgba(var(--sb-ink), 0.04)', color: copied ? C_GREEN : 'var(--sb-text-5)', fontFamily: 'inherit', fontSize: '11px', fontWeight: 600, cursor: 'pointer', transition: 'all .14s' } }, copied ? t('action.copied') : t('action.copy'))
// Invitation card (Share Invitation v3 design): QR preview row on top,
// the text code with copy + countdown below, one bordered surface.
const offerLeft = (isCreate && offerTs) ? Math.max(0, Math.ceil((offerTs + OFFER_TTL_MS - nowTick) / 1000)) : null;
const offerExpires = offerLeft === null ? null : `${Math.floor(offerLeft / 60)}:${String(offerLeft % 60).padStart(2, '0')}`;
const credCard = h('div', { key: 'card', style: { borderRadius: '16px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'var(--sb-surface)', overflow: 'hidden', marginBottom: isCreate ? '26px' : '16px' } }, [
qrCodeUrl && h('button', { key: 'showqr', className: 'sb-qr-row', onClick: () => setQrModalOpen(true), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '14px', padding: '14px 16px', border: 'none', background: 'transparent', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start', transition: 'background .15s' } }, [
h('span', { key: 'ic', style: { flex: 'none', width: '48px', height: '48px', padding: '5px', borderRadius: '9px', background: '#fff' } },
h('img', { src: qrCodeUrl, alt: '', style: { width: '100%', height: '100%', objectFit: 'contain', display: 'block', imageRendering: 'pixelated' } })),
h('span', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
h('span', { key: 't', style: { display: 'block', fontSize: '15px', fontWeight: 700, color: 'var(--sb-text-1)' } }, t('qr.showTitle')),
h('span', { key: 's', style: { display: 'block', fontSize: '12.5px', color: 'var(--sb-text-7)', marginTop: '2px' } }, (qrFramesTotal || 0) > 1 ? t('qr.showSubtitleFrames', { frames: qrFramesTotal }) : t('qr.showSubtitle'))
]),
fa('fa-chevron-right', { color: 'var(--sb-text-9)' })
]),
// The handshake code is sensitive — keep it blurred until the
// user deliberately reveals it, underscoring that it must be
// shared only over a channel they trust.
h('div', { key: 'codewrap', style: { position: 'relative' } }, [
h('div', { key: 'code', className: 'sb-sc', style: { fontFamily: MONO, fontSize: '11px', lineHeight: 1.55, color: 'var(--sb-text-code)', wordBreak: 'break-all', padding: '11px 12px', maxHeight: '72px', overflowY: 'auto', filter: codeRevealed ? 'none' : 'blur(6px)', userSelect: codeRevealed ? 'text' : 'none', transition: 'filter .2s' } }, credCode),
!codeRevealed && h('button', { key: 'reveal', onClick: () => setCodeRevealed(true), style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', border: 'none', background: 'rgba(var(--sb-surface-rgb), 0.25)', color: 'var(--sb-text-4)', fontFamily: 'inherit', fontSize: '12px', fontWeight: 600, cursor: 'pointer' } }, [
fa('fa-eye', { key: 'i', fontSize: '15px' }),
t('cred.reveal')
qrCodeUrl && h('div', { key: 'div', style: { height: '1px', background: 'rgba(var(--sb-ink), 0.06)' } }),
h('div', { key: 'code', style: { padding: '12px 16px 16px' } }, [
h('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px', marginBottom: '10px' } }, [
h('span', { key: 'l', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-6)' } }, t('cred.orCopy')),
h('span', { key: 'r', style: { display: 'flex', alignItems: 'center', gap: '12px' } }, [
offerExpires && h('span', { key: 'exp', dir: 'ltr', style: { fontFamily: MONO, fontSize: '11px', color: offerLeft < 60 ? 'var(--sb-red)' : 'var(--sb-text-9)' } }, offerExpires),
h('button', { key: 'copy', className: 'sb-soft-btn', onClick: copyCred, style: { display: 'inline-flex', alignItems: 'center', gap: '6px', padding: '6px 11px', borderRadius: '8px', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.03)', color: 'var(--sb-text-2)', fontFamily: 'inherit', fontSize: '12.5px', fontWeight: 700, cursor: 'pointer' } }, [
copied && fa('fa-check', { key: 'i', color: C_GREEN_SOLID }),
copied ? t('action.copied') : t('action.copy')
])
])
]),
// The handshake code is sensitive — keep it blurred until the
// user deliberately reveals it, underscoring that it must be
// shared only over a channel they trust.
h('div', { key: 'codewrap', onClick: () => setCodeRevealed((v) => !v), style: { position: 'relative', borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.05)', background: 'var(--sb-bg)', overflow: 'hidden', cursor: 'pointer' } }, [
h('div', { key: 'code', dir: 'ltr', className: 'sb-sc', style: { fontFamily: MONO, fontSize: '11.5px', lineHeight: 1.6, color: 'var(--sb-text-code)', wordBreak: 'break-all', padding: '11px 13px', maxHeight: '78px', overflowY: codeRevealed ? 'auto' : 'hidden', filter: codeRevealed ? 'none' : 'blur(5px)', userSelect: codeRevealed ? 'text' : 'none', transition: 'filter .25s' } }, credCode),
!codeRevealed && h('span', { key: 'reveal', style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '0 12px', textAlign: 'center', color: 'var(--sb-text-3)', fontSize: '12.5px', fontWeight: 600 } }, [
fa('fa-eye', { key: 'i' }),
t('cred.reveal')
])
])
])
]);
const showQrButton = qrCodeUrl && h('button', { key: 'showqr', onClick: () => setQrModalOpen(true), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '15px 16px', borderRadius: '14px', border: `1px solid ${isCreate ? 'rgba(var(--sb-orange-rgb), 0.3)' : 'rgba(var(--sb-green-rgb), 0.3)'}`, background: isCreate ? 'rgba(var(--sb-orange-rgb), 0.06)' : 'rgba(var(--sb-green-rgb), 0.06)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start', marginBottom: '14px' } }, [
h('span', { key: 'ic', style: { flex: 'none', width: '42px', height: '42px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: isCreate ? 'rgba(var(--sb-orange-rgb), 0.12)' : 'rgba(var(--sb-green-rgb), 0.12)', border: `1px solid ${isCreate ? 'rgba(var(--sb-orange-rgb), 0.28)' : 'rgba(var(--sb-green-rgb), 0.28)'}` } }, fa('fa-qrcode', { color: accentSolid, fontSize: '18px' })),
h('span', { key: 'tx', style: { flex: 1 } }, [
h('span', { key: 't', style: { display: 'block', fontSize: '14.5px', fontWeight: 700, color: 'var(--sb-text-1)' } }, t('qr.showTitle')),
h('span', { key: 's', style: { display: 'block', fontSize: '12.5px', color: 'var(--sb-text-7)', marginTop: '1px' } }, (qrFramesTotal || 0) > 1 ? t('qr.showSubtitleFrames', { frames: qrFramesTotal }) : t('qr.showSubtitle'))
]),
fa('fa-chevron-right', { color: 'var(--sb-text-9)' })
]);
let inner;
if (showVerification) {
// Transcribed from the Claude Design component (SAS Verification.dc.html),
// without its card: it sits straight in the setup column like the other steps.
const verified = bothVerificationsConfirmed;
const cells = (verificationCode || '').split('').map((ch, i) =>
h('div', { key: i, style: { flex: 1, maxWidth: '46px', aspectRatio: '0.82', display: 'grid', placeItems: 'center', borderRadius: '10px', border: '1px solid rgba(var(--sb-green-rgb), 0.25)', background: 'rgba(var(--sb-green-rgb), 0.05)', fontFamily: MONO, fontSize: '22px', fontWeight: 700, color: C_GREEN } }, ch));
const code = (verificationCode || '').replace(/[-\s]/g, '');
const n = code.length;
const typed = sasInput.replace(/[-\s]/g, '');
const full = n > 0 && typed.length === n;
// Comparing against the code already on this screen reveals nothing; it
// only spares a typo one of the limited real attempts in onVerifyConnection.
const match = full && typed === code;
const wrong = full && !match;
const youOk = localVerificationConfirmed;
const peerOk = remoteVerificationConfirmed;
const waiting = youOk && !peerOk;
const split = Math.min(3, n);
const confirmable = canConfirm && match;
const check = (k) => h('svg', { key: k, width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'var(--sb-on-green-2)', strokeWidth: 3.2, strokeLinecap: 'round', strokeLinejoin: 'round' },
h('path', { d: 'M5 12.5l4.2 4.2L19 7', strokeDasharray: 24, style: { animation: 'svDraw .35s ease-out .1s both' } }));
const ring = (ok, soft) => ({
position: 'relative', width: '22px', height: '22px', flex: 'none', borderRadius: '50%', display: 'grid', placeItems: 'center', transition: 'all .3s',
border: `1.5px solid ${ok ? C_GREEN_SOLID : (soft ? 'rgba(var(--sb-green-rgb), 0.5)' : 'rgba(var(--sb-ink), 0.18)')}`,
background: ok ? C_GREEN_SOLID : 'transparent'
});
const who = (k, label) => h('span', { key: k, style: { fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-4)' } }, label);
const cells = code.split('').map((_, i) => {
const v = typed[i] || '';
const active = sasFocused && !youOk && i === typed.length && !full;
let border = 'rgba(var(--sb-ink), 0.09)';
let color = 'var(--sb-text-1)';
let anim = 'none';
if (v) { border = 'rgba(var(--sb-ink), 0.2)'; anim = 'otpPop .28s cubic-bezier(.2,.8,.3,1.2) both'; }
if (active) border = 'rgba(var(--sb-orange-rgb), 0.65)';
if (wrong) { border = 'rgba(var(--sb-red-rgb), 0.6)'; color = 'var(--sb-red)'; }
if (match) anim = `otpOk .45s cubic-bezier(.2,.8,.3,1) ${i * 55}ms both`;
return h('span', { key: i, style: {
position: 'relative', flex: 1, minWidth: 0, height: '56px', display: 'grid', placeItems: 'center', borderRadius: '12px',
border: `1px solid ${border}`, background: 'var(--sb-surface)', fontFamily: MONO, fontSize: '22px', fontWeight: 600, color,
transition: 'border-color .15s, color .15s, box-shadow .15s', animation: anim,
marginInlineStart: i === split && n > split ? '10px' : 0,
boxShadow: active ? '0 0 0 3px rgba(var(--sb-orange-rgb), 0.12)' : 'none'
} }, [
v,
active && h('span', { key: 'caret', style: { position: 'absolute', left: '50%', top: '50%', width: '2px', height: '22px', margin: '-11px 0 0 -1px', borderRadius: '1px', background: C_ORANGE_SOLID, animation: 'otpCaret 1s steps(1) infinite' } })
]);
});
inner = h('div', { key: 'verify', style: { animation: 'sbUp .3s ease' } }, [
!verified && backButton('vback'),
h('div', { key: 'head', style: { display: 'flex', alignItems: 'center', gap: '11px', marginBottom: '8px' } }, [
h('div', { key: 'i', style: { width: '34px', height: '34px', flex: 'none', borderRadius: '10px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.1)', border: '1px solid rgba(var(--sb-green-rgb), 0.25)' } }, fa('fa-shield-alt', { color: C_GREEN_SOLID })),
h('h2', { key: 't', style: { margin: 0, fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, t('verify.title'))
h('h2', { key: 't', style: { margin: '0 0 8px', fontSize: '22px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, verified ? t('sasv.titleDone') : t('sasv.title')),
h('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '13.5px', lineHeight: 1.55, color: 'var(--sb-text-7)', textWrap: 'pretty' } }, verified ? t('sasv.descDone') : t('sasv.desc')),
h('div', { key: 'code', style: { padding: '20px 0 22px', borderTop: '1px solid rgba(var(--sb-ink), 0.06)', borderBottom: '1px solid rgba(var(--sb-ink), 0.06)', marginBottom: '22px', textAlign: 'center' } },
h('div', { dir: 'ltr', style: { display: 'inline-flex', alignItems: 'center', gap: '18px', fontFamily: MONO, fontSize: '34px', fontWeight: 600, letterSpacing: '8px', lineHeight: 1, color: verified ? C_GREEN : 'var(--sb-text-1)', transition: 'color .4s' } },
n ? [h('span', { key: 'a' }, code.slice(0, split)), h('span', { key: 'b' }, code.slice(split))]
: h('span', { style: { fontSize: '14px', letterSpacing: 0, color: 'var(--sb-text-8)' } }, t('verify.waiting')))),
!verified && h('div', { key: 'form' }, [
h('div', { key: 'lbl', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '10px' } }, [
h('span', { key: 'l', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-6)' } }, t('sasv.enter')),
h('span', { key: 'h', style: { fontFamily: MONO, fontSize: '11px', transition: 'color .2s', color: full ? (match ? C_GREEN : 'var(--sb-red)') : 'var(--sb-text-faint)' } },
full ? (match ? t('sasv.match') : t('sasv.noMatch')) : `${typed.length} / ${n || SAS_CODE_LENGTH}`)
]),
h('label', { key: 'cells', dir: 'ltr', style: { position: 'relative', display: 'flex', gap: '6px', marginBottom: sasError ? '8px' : '18px', cursor: 'text', animation: wrong ? `otpShake .4s ease ${sasShake % 2 ? '0s' : '0.001s'}` : 'none' } }, [
...cells,
h('input', { key: 'in', dir: 'ltr', value: sasInput, disabled: youOk || !n, autoFocus: true, autoComplete: 'one-time-code', spellCheck: false, type: 'text', inputMode: 'numeric', pattern: '[0-9]*', maxLength: n || SAS_CODE_LENGTH, 'aria-label': t('sasv.enter'),
onChange: (e) => {
const v = e.target.value.replace(/\D/g, '').slice(0, n || SAS_CODE_LENGTH);
if (n && v.length === n && v !== code) setSasShake((k) => k + 1);
setSasInput(v);
if (sasError) setSasError('');
},
onFocus: () => setSasFocused(true), onBlur: () => setSasFocused(false),
style: { position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0, border: 'none', outline: 'none', background: 'transparent', color: 'transparent', caretColor: 'transparent', fontSize: '16px' } })
]),
sasError && h('p', { key: 'err', style: { color: 'var(--sb-red)', fontSize: '12.5px', margin: '0 0 16px' } }, sasError)
]),
h('p', { key: 'sub', style: { margin: '0 0 18px', fontSize: '13.5px', lineHeight: 1.55, color: 'var(--sb-text-7)' } }, t('verify.desc')),
h('div', { key: 'cells', dir: 'ltr', style: { display: 'flex', gap: '6px', justifyContent: 'center', marginBottom: '20px', flexWrap: 'wrap' } }, cells),
verified
? h('div', { key: 'ok', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', padding: '24px 16px', borderRadius: '16px', border: '1px solid rgba(var(--sb-green-rgb), 0.25)', background: 'rgba(var(--sb-green-rgb), 0.06)', animation: 'sbUp .3s ease' } }, [
h('div', { key: 'i', style: { width: '54px', height: '54px', borderRadius: '16px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.14)', border: '1px solid rgba(var(--sb-green-rgb), 0.35)', marginBottom: '14px' } }, fa('fa-check', { color: C_GREEN_SOLID, fontSize: '24px' })),
h('div', { key: 't', style: { fontSize: '18px', fontWeight: 800, color: 'var(--sb-text-1)' } }, t('verify.verified')),
h('div', { key: 's', style: { fontSize: '13.5px', color: 'var(--sb-text-7)', marginTop: '5px' } }, t('verify.bothConfirmed'))
h('div', { key: 'link', style: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' } }, [
h('div', { key: 'you', style: { display: 'flex', alignItems: 'center', gap: '9px', flex: 'none' } }, [
h('span', { key: 'r', style: ring(youOk, false) }, youOk && check('c')),
who('l', t('sasv.you'))
]),
h('div', { key: 'bar', style: { position: 'relative', flex: 1, height: '2px', borderRadius: '2px', background: 'rgba(var(--sb-ink), 0.08)', overflow: 'hidden' } }, [
h('div', { key: 'fill', style: { position: 'absolute', insetInlineStart: 0, top: 0, bottom: 0, width: peerOk ? '100%' : (youOk ? '50%' : '0%'), background: C_GREEN_SOLID, transition: 'width .6s cubic-bezier(.4,0,.2,1)' } }),
waiting && h('div', { key: 'scan', style: { position: 'absolute', top: 0, bottom: 0, width: '30%', background: 'linear-gradient(90deg, transparent, rgba(var(--sb-green-rgb), 0.9), transparent)', animation: 'svScan 1.1s ease-in-out infinite' } })
]),
h('div', { key: 'peer', style: { display: 'flex', alignItems: 'center', gap: '9px', flex: 'none' } }, [
who('l', t('sasv.peer')),
h('span', { key: 'r', style: ring(peerOk, youOk) }, peerOk && check('c'))
])
: h('div', { key: 'form' }, [
h('div', { key: 'lbl', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-6)', marginBottom: '8px' } }, t('verify.enterLabel')),
h('input', { key: 'in', dir: 'ltr', value: sasInput, onChange: (e) => { setSasInput(e.target.value.replace(/\D/g, '').slice(0, SAS_CODE_LENGTH)); if (sasError) setSasError(''); }, disabled: localVerificationConfirmed, autoFocus: true, autoComplete: 'one-time-code', spellCheck: false, type: 'text', inputMode: 'numeric', pattern: '[0-9]*', maxLength: SAS_CODE_LENGTH, placeholder: verificationCode ? t('verify.placeholder') : t('verify.waiting'), style: { width: '100%', textAlign: 'center', letterSpacing: '6px', borderRadius: '12px', border: `1px solid ${sasInput.length ? (canConfirm || localVerificationConfirmed ? 'rgba(var(--sb-green-rgb), 0.5)' : 'rgba(var(--sb-ink), 0.14)') : 'rgba(var(--sb-ink), 0.08)'}`, background: 'var(--sb-surface)', color: 'var(--sb-text-1)', fontFamily: MONO, fontSize: '20px', fontWeight: 700, padding: '14px', outline: 'none', marginBottom: sasError ? '8px' : '16px' } }),
sasError && h('p', { key: 'err', style: { color: 'var(--sb-red)', fontSize: '12.5px', margin: '0 0 16px' } }, sasError),
h('div', { key: 'status', style: { display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' } }, [
h('div', { key: 'you', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '11px 14px', borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.06)', background: 'var(--sb-surface)' } }, [
h('span', { key: 'l', style: { fontSize: '13px', color: 'var(--sb-text-4)', fontWeight: 600 } }, t('verify.yours')),
h('span', { key: 'v', style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '12.5px', fontWeight: 600, color: localVerificationConfirmed ? C_GREEN : 'var(--sb-text-8)' } }, [fa(localVerificationConfirmed ? 'fa-check-circle' : 'fa-clock', { key: 'i' }), localVerificationConfirmed ? t('verify.confirmed') : t('verify.pending')])
]),
h('div', { key: 'peer', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '11px 14px', borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.06)', background: 'var(--sb-surface)' } }, [
h('span', { key: 'l', style: { fontSize: '13px', color: 'var(--sb-text-4)', fontWeight: 600 } }, t('verify.peer')),
h('span', { key: 'v', style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '12.5px', fontWeight: 600, color: remoteVerificationConfirmed ? C_GREEN : 'var(--sb-text-8)' } }, [fa(remoteVerificationConfirmed ? 'fa-check-circle' : 'fa-clock', { key: 'i' }), remoteVerificationConfirmed ? t('verify.confirmed') : t('verify.pending')])
])
]),
h('div', { key: 'btns', style: { display: 'flex', gap: '10px' } }, [
h('button', { key: 'ok', onClick: handleSasConfirm, disabled: !canConfirm, style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '14px', borderRadius: '13px', border: 'none', background: canConfirm ? C_GREEN_SOLID : 'rgba(var(--sb-ink), 0.05)', color: canConfirm ? 'var(--sb-on-green-2)' : 'var(--sb-text-faint)', fontFamily: 'inherit', fontSize: '14.5px', fontWeight: 700, cursor: canConfirm ? 'pointer' : 'not-allowed', boxShadow: canConfirm ? '0 8px 24px rgba(var(--sb-green-rgb), 0.25)' : 'none' } }, [fa(localVerificationConfirmed ? 'fa-check-circle' : 'fa-check', { key: 'i' }), localVerificationConfirmed ? t('verify.confirmed') : t('verify.confirm')]),
h('button', { key: 'no', onClick: handleVerificationReject, style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '7px', padding: '14px 16px', borderRadius: '13px', border: '1px solid rgba(var(--sb-red-rgb), 0.3)', background: 'transparent', color: 'var(--sb-red)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, [fa('fa-times', { key: 'i' }), t('verify.mismatch')])
])
]),
!verified
? h('div', { key: 'btns' }, [
h('button', { key: 'ok', onClick: handleSasConfirm, disabled: !confirmable, style: { width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '14px', borderRadius: '13px', border: 'none', fontFamily: 'inherit', fontSize: '14.5px', fontWeight: 700, transition: 'background .25s, color .25s',
background: youOk ? 'rgba(var(--sb-green-rgb), 0.12)' : (confirmable ? C_GREEN_SOLID : 'rgba(var(--sb-ink), 0.05)'),
color: youOk ? C_GREEN : (confirmable ? 'var(--sb-on-green-2)' : 'var(--sb-text-faint)'),
cursor: confirmable ? 'pointer' : 'default' } }, youOk ? t('sasv.waitingPeer') : t('verify.confirm')),
h('button', { key: 'no', onClick: handleVerificationReject, className: 'sb-sas-mismatch', style: { display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', marginTop: '10px', padding: '8px', border: 'none', background: 'transparent', color: 'var(--sb-text-7)', fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: 'pointer', transition: 'color .15s' },
onMouseEnter: (e) => { e.currentTarget.style.color = 'var(--sb-red)'; }, onMouseLeave: (e) => { e.currentTarget.style.color = 'var(--sb-text-7)'; } }, t('sasv.codesDiffer'))
])
: h('div', { key: 'opening', style: { animation: 'svFade .35s ease .3s both' } },
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '14px 0 4px', fontSize: '13.5px', fontWeight: 600, color: 'var(--sb-text-4)' } }, [
t('sasv.opening'),
h('span', { key: 'd', style: { display: 'inline-flex', gap: '4px' } }, [0, 0.15, 0.3].map((d, i) =>
h('span', { key: i, style: { width: '4px', height: '4px', borderRadius: '50%', background: C_GREEN_SOLID, animation: `svDot 1.2s ease-in-out ${d}s infinite` } })))
]))
]);
} else if (isGenerating) {
const genSteps = [t('handshake.step1'), t('handshake.step2'), t('handshake.step3')];
@@ -1643,22 +1736,24 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
} else if (isOfferCred || isAnswerCred) {
inner = h('div', { key: 'cred', style: { animation: 'sbUp .3s ease' } }, [
backButton('cback'),
h('h2', { key: 'h', style: { margin: '0 0 6px', fontSize: '23px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, isCreate ? t('handshake.shareTitle') : t('handshake.sendAnswerTitle')),
h('p', { key: 'p', style: { margin: '0 0 18px', fontSize: '14px', lineHeight: 1.55, color: 'var(--sb-text-7)' } }, isCreate ? t('handshake.shareDesc') : t('handshake.sendAnswerDesc')),
showQrButton,
credBlock,
isOfferCred && h('div', { key: 'offerextra', style: { marginTop: '4px' } }, [
h('div', { key: 'lbl', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-6)', marginBottom: '8px' } }, t('handshake.thenReceive')),
h('div', { key: 'ta', style: { borderRadius: '12px', border: `1px solid ${hasAnswer ? 'rgba(var(--sb-ink), 0.18)' : 'rgba(var(--sb-ink), 0.07)'}`, background: 'var(--sb-surface)', padding: '11px 14px', marginBottom: '10px' } },
h('textarea', { dir: 'ltr', value: answerInput, onChange: (e) => { setAnswerInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 2, placeholder: t('handshake.pasteAnswerPlaceholder'), style: { width: '100%', resize: 'none', border: 'none', outline: 'none', background: 'transparent', color: 'var(--sb-text-3)', fontFamily: MONO, fontSize: '12px', lineHeight: 1.55, minHeight: '44px' } })),
h('h2', { key: 'h', style: { margin: '0 0 8px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: 'var(--sb-text-1)' } }, isCreate ? t('handshake.shareTitle') : t('handshake.sendAnswerTitle')),
h('p', { key: 'p', style: { margin: '0 0 24px', fontSize: '14px', lineHeight: 1.55, color: 'var(--sb-text-7)' } }, isCreate ? t('handshake.shareDesc') : t('handshake.sendAnswerDesc')),
credCard,
isOfferCred && h('div', { key: 'offerextra' }, [
h('div', { key: 'lbl', style: { fontSize: '13.5px', fontWeight: 600, color: 'var(--sb-text-3)', marginBottom: '10px' } }, t('handshake.thenReceive')),
h('textarea', { key: 'ta', dir: 'ltr', value: answerInput, onChange: (e) => { setAnswerInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 3, placeholder: t('handshake.pasteAnswerPlaceholder'), style: { display: 'block', width: '100%', resize: 'none', borderRadius: '14px', border: `1px solid ${hasAnswer ? 'rgba(var(--sb-orange-rgb), 0.45)' : 'rgba(var(--sb-ink), 0.08)'}`, background: 'var(--sb-surface)', outline: 'none', color: 'var(--sb-text-2)', fontFamily: MONO, fontSize: '12.5px', lineHeight: 1.5, padding: '13px 15px', marginBottom: '12px', transition: 'border-color .15s' } }),
h('div', { key: 'btns', style: { display: 'flex', gap: '10px' } }, [
h('button', { key: 'scan', className: 'sb-scan-btn', onClick: () => setShowQRScannerModal(true), style: { flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '14px 16px', borderRadius: '13px', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.04)', color: 'var(--sb-text-4)', fontFamily: 'inherit', fontSize: '14px', fontWeight: 700, cursor: 'pointer' } }, [fa('fa-camera', { key: 'i' }), t('action.scan')]),
h('button', { key: 'est', onClick: onConnect, disabled: !hasAnswer, style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '9px', padding: '14px', borderRadius: '13px', border: 'none', background: hasAnswer ? C_ORANGE_SOLID : 'rgba(var(--sb-ink), 0.05)', color: hasAnswer ? 'var(--sb-on-accent)' : 'var(--sb-text-faint)', fontFamily: 'inherit', fontSize: '14.5px', fontWeight: 700, cursor: hasAnswer ? 'pointer' : 'not-allowed', boxShadow: hasAnswer ? '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)' : 'none' } }, t('handshake.establish'))
h('button', { key: 'scan', className: 'sb-scan-btn', onClick: () => setShowQRScannerModal(true), style: { flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '0 18px', height: '50px', borderRadius: '13px', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'transparent', color: 'var(--sb-text-2)', fontFamily: 'inherit', fontSize: '14px', fontWeight: 700, cursor: 'pointer' } }, [
h('svg', { key: 'i', width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
h('path', { d: 'M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3M7 12h10' })),
t('action.scan')
]),
h('button', { key: 'est', onClick: onConnect, disabled: !hasAnswer, style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '9px', height: '50px', borderRadius: '13px', border: 'none', background: hasAnswer ? C_ORANGE_SOLID : 'rgba(var(--sb-ink), 0.05)', color: hasAnswer ? 'var(--sb-on-accent)' : 'var(--sb-text-faint)', fontFamily: 'inherit', fontSize: '14.5px', fontWeight: 700, cursor: hasAnswer ? 'pointer' : 'not-allowed', transition: 'background .2s, color .2s' } }, t('handshake.establish'))
])
]),
isAnswerCred && h('div', { key: 'answerextra', style: { marginTop: '4px', display: 'flex', alignItems: 'center', gap: '10px', padding: '12px 14px', borderRadius: '12px', border: '1px solid rgba(var(--sb-green-rgb), 0.18)', background: 'rgba(var(--sb-green-rgb), 0.05)' } }, [
fa('fa-circle-notch', { key: 'i', color: C_GREEN_SOLID, animation: 'sbSpin 1.4s linear infinite' }),
h('span', { key: 't', style: { fontSize: '13px', color: 'var(--sb-text-4)', fontWeight: 500 } }, t('handshake.answerSentNote'))
isAnswerCred && h('div', { key: 'answerextra', style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '4px 2px' } }, [
h('span', { key: 'i', style: { flex: 'none', width: '16px', height: '16px', borderRadius: '50%', border: '2px solid rgba(var(--sb-ink), 0.1)', borderTopColor: C_ORANGE_SOLID, animation: 'sbSpin .9s linear infinite' } }),
h('span', { key: 't', style: { fontSize: '13px', lineHeight: 1.5, color: 'var(--sb-text-7)' } }, t('handshake.answerSentNote'))
])
]);
} else if (isCreate) {
@@ -1669,26 +1764,30 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
h('button', { key: 'gen', className: 'sb-gen-btn', onClick: () => { requestNotificationPermissionOnInteraction(); if (webrtcManagerRef.current) handleCreateOffer(); }, style: { width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '9px', padding: '15px', borderRadius: '13px', border: 'none', background: C_ORANGE_SOLID, color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '15px', fontWeight: 700, cursor: 'pointer', boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)' } }, [fa('fa-bolt', { key: 'i' }), t('intro.createCta')])
]);
} else {
// JOIN intro
// JOIN intro — same card language as the invitation/answer screens:
// scan row on top, the paste field below, one bordered surface.
const canJoin = hasInvite && connectionStatus !== 'connecting';
inner = h('div', { key: 'introJ', style: { animation: 'sbUp .28s ease' } }, [
h('h2', { key: 'h', style: { margin: '0 0 6px', fontSize: '23px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, t('intro.joinTitle')),
h('p', { key: 'p', style: { margin: '0 0 16px', fontSize: '14px', lineHeight: 1.55, color: 'var(--sb-text-7)' } }, "Scan your peer's QR with your camera, or paste their invitation code."),
h('button', { key: 'scan', className: 'sb-scan-btn', onClick: () => { requestNotificationPermissionOnInteraction(); setShowQRScannerModal(true); }, style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '15px 16px', borderRadius: '14px', border: '1px solid rgba(var(--sb-green-rgb), 0.3)', background: 'rgba(var(--sb-green-rgb), 0.06)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start', marginBottom: '14px' } }, [
h('span', { key: 'ic', style: { flex: 'none', width: '42px', height: '42px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.12)', border: '1px solid rgba(var(--sb-green-rgb), 0.28)' } }, fa('fa-camera', { color: C_GREEN_SOLID, fontSize: '18px' })),
h('span', { key: 'tx', style: { flex: 1 } }, [
h('span', { key: 't', style: { display: 'block', fontSize: '14.5px', fontWeight: 700, color: 'var(--sb-text-1)' } }, t('intro.scanTitle')),
h('span', { key: 's', style: { display: 'block', fontSize: '12.5px', color: 'var(--sb-text-7)', marginTop: '1px' } }, t('intro.scanSubtitle'))
h('h2', { key: 'h', style: { margin: '0 0 8px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: 'var(--sb-text-1)' } }, t('intro.joinTitle')),
h('p', { key: 'p', style: { margin: '0 0 24px', fontSize: '14px', lineHeight: 1.55, color: 'var(--sb-text-7)' } }, t('intro.joinDesc')),
h('div', { key: 'card', style: { borderRadius: '16px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'var(--sb-surface)', overflow: 'hidden', marginBottom: '12px' } }, [
h('button', { key: 'scan', className: 'sb-qr-row', onClick: () => { requestNotificationPermissionOnInteraction(); setShowQRScannerModal(true); }, style: { width: '100%', display: 'flex', alignItems: 'center', gap: '14px', padding: '14px 16px', border: 'none', background: 'transparent', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start', transition: 'background .15s' } }, [
h('span', { key: 'ic', style: { flex: 'none', width: '48px', height: '48px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-ink), 0.05)', border: '1px solid rgba(var(--sb-ink), 0.08)', color: 'var(--sb-text-2)' } },
h('svg', { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
h('path', { d: 'M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3M7 12h10' }))),
h('span', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
h('span', { key: 't', style: { display: 'block', fontSize: '15px', fontWeight: 700, color: 'var(--sb-text-1)' } }, t('intro.scanTitle')),
h('span', { key: 's', style: { display: 'block', fontSize: '12.5px', color: 'var(--sb-text-7)', marginTop: '2px' } }, t('intro.scanSubtitle'))
]),
fa('fa-chevron-right', { color: 'var(--sb-text-9)' })
]),
fa('fa-chevron-right', { color: 'var(--sb-text-9)' })
h('div', { key: 'div', style: { height: '1px', background: 'rgba(var(--sb-ink), 0.06)' } }),
h('div', { key: 'paste', style: { padding: '12px 16px 16px' } }, [
h('div', { key: 'l', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-6)', marginBottom: '10px' } }, t('intro.orPasteCode')),
h('textarea', { key: 'ta', dir: 'ltr', value: offerInput, onChange: (e) => { setOfferInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 3, placeholder: t('intro.pastePlaceholder'), style: { display: 'block', width: '100%', resize: 'none', borderRadius: '11px', border: `1px solid ${hasInvite ? 'rgba(var(--sb-orange-rgb), 0.45)' : 'rgba(var(--sb-ink), 0.05)'}`, background: 'var(--sb-bg)', outline: 'none', color: 'var(--sb-text-2)', fontFamily: MONO, fontSize: '12.5px', lineHeight: 1.5, padding: '11px 13px', transition: 'border-color .15s' } })
])
]),
h('div', { key: 'or', style: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '14px' } }, [
h('span', { key: 'a', style: { flex: 1, height: '1px', background: 'rgba(var(--sb-ink), 0.07)' } }),
h('span', { key: 'm', style: { fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-faint)', textTransform: 'uppercase', letterSpacing: '0.7px' } }, t('intro.orPasteCode')),
h('span', { key: 'b', style: { flex: 1, height: '1px', background: 'rgba(var(--sb-ink), 0.07)' } })
]),
h('div', { key: 'ta', style: { borderRadius: '13px', border: `1px solid ${hasInvite ? 'rgba(var(--sb-ink), 0.18)' : 'rgba(var(--sb-ink), 0.07)'}`, background: 'var(--sb-surface)', padding: '13px 15px', marginBottom: '12px' } },
h('textarea', { dir: 'ltr', value: offerInput, onChange: (e) => { setOfferInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 3, placeholder: t('intro.pastePlaceholder'), style: { width: '100%', resize: 'none', border: 'none', outline: 'none', background: 'transparent', color: 'var(--sb-text-3)', fontFamily: MONO, fontSize: '12.5px', lineHeight: 1.6, minHeight: '66px' } })),
h('button', { key: 'connect', onClick: () => { requestNotificationPermissionOnInteraction(); onCreateAnswer(); }, disabled: !hasInvite || connectionStatus === 'connecting', style: { width: '100%', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '9px', padding: '14px', borderRadius: '13px', border: 'none', background: (hasInvite && connectionStatus !== 'connecting') ? C_ORANGE_SOLID : 'rgba(var(--sb-ink), 0.05)', color: (hasInvite && connectionStatus !== 'connecting') ? 'var(--sb-on-accent)' : 'var(--sb-text-faint)', fontFamily: 'inherit', fontSize: '15px', fontWeight: 700, cursor: (hasInvite && connectionStatus !== 'connecting') ? 'pointer' : 'not-allowed', boxShadow: (hasInvite && connectionStatus !== 'connecting') ? '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)' : 'none' } }, connectionStatus === 'connecting' ? t('intro.connecting') : t('intro.connect'))
h('button', { key: 'connect', onClick: () => { requestNotificationPermissionOnInteraction(); onCreateAnswer(); }, disabled: !canJoin, style: { width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '9px', height: '50px', borderRadius: '13px', border: 'none', background: canJoin ? C_ORANGE_SOLID : 'rgba(var(--sb-ink), 0.05)', color: canJoin ? 'var(--sb-on-accent)' : 'var(--sb-text-faint)', fontFamily: 'inherit', fontSize: '14.5px', fontWeight: 700, cursor: canJoin ? 'pointer' : 'not-allowed', transition: 'background .2s, color .2s' } }, connectionStatus === 'connecting' ? t('intro.connecting') : t('intro.connect'))
]);
}
@@ -1704,7 +1803,7 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
// /download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe, a file that never
// existed, and the download 404s. A pinned tag keeps serving a real
// installer instead.
const SB_DESKTOP_VERSION = '1.0.4';
const SB_DESKTOP_VERSION = '1.0.5';
const SB_DESKTOP_RELEASE = `https://github.com/SecureBitChat/securebit-desktop/releases/download/v${SB_DESKTOP_VERSION}`;
const DOWNLOADS = {
mac: { name: 'macOS', format: '.dmg · Apple Silicon & Intel', icon: 'fab fa-apple', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64.dmg` },
@@ -1847,6 +1946,15 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
'@keyframes sbNode{0%,100%{box-shadow:0 0 0 0 rgba(var(--sb-green-rgb), 0)}50%{box-shadow:0 0 0 6px rgba(var(--sb-green-rgb), .06)}}' +
'@keyframes sbScan{0%{top:8%}100%{top:88%}}' +
'@keyframes sbBlink{0%,100%{opacity:1}50%{opacity:.35}}' +
// SAS verification step (SAS Verification.dc.html).
'@keyframes otpPop{0%{transform:translateY(6px) scale(.9);opacity:0}60%{transform:translateY(-1px) scale(1.04);opacity:1}100%{transform:none;opacity:1}}' +
'@keyframes otpOk{0%{transform:none}45%{transform:translateY(-4px)}100%{transform:none;background:rgba(var(--sb-green-rgb),.07);border-color:rgba(var(--sb-green-rgb),.55);color:var(--sb-green)}}' +
'@keyframes otpShake{0%,100%{transform:translateX(0)}20%{transform:translateX(-7px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(2px)}}' +
'@keyframes otpCaret{0%,100%{opacity:1}50%{opacity:0}}' +
'@keyframes svDot{0%,80%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}}' +
'@keyframes svDraw{from{stroke-dashoffset:24}to{stroke-dashoffset:0}}' +
'@keyframes svScan{from{left:-30%}to{left:100%}}' +
'@keyframes svFade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}' +
// P2P / mesh animation (left panel). One 14s loop: the direct line is
// already up, then two peers join it. `sbTrav` moves a dot along the
// same geometry the line is drawn from, via offset-path, so the packet
+1 -1
View File
@@ -5,7 +5,7 @@
// The tag is pinned rather than using /releases/latest/download/: the filenames
// carry the version, so a `latest` link 404s the moment a newer release exists,
// whereas a pinned tag keeps serving a working (if older) installer.
const DESKTOP_VERSION = '1.0.4';
const DESKTOP_VERSION = '1.0.5';
const DESKTOP_RELEASE = `https://github.com/SecureBitChat/securebit-desktop/releases/download/v${DESKTOP_VERSION}`;
const DownloadApps = () => {
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "يولّد جهازك المفاتيح ودعوة تُستعمل مرّة واحدة. لا شيء يمرّ عبر خادم.",
"intro.createCta": "توليد المفاتيح والدعوة",
"intro.joinTitle": "انضمّ إلى قناة",
"intro.joinDesc": "امسح رمز QR الخاص بالطرف الآخر بالكاميرا، أو الصق رمز الدعوة.",
"intro.scanTitle": "امسح رمز QR بالكاميرا",
"intro.scanSubtitle": "الأسرع - وجّهها إلى شاشة الطرف الآخر",
"intro.orPasteCode": "أو ألصق الرمز",
"intro.orPasteCode": "أو الصق الرمز",
"intro.pastePlaceholder": "ألصق رمز الدعوة هنا…",
"intro.connect": "اتّصل",
"intro.connecting": "جارٍ المعالجة…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "إنشاء الاتصال",
"handshake.thenReceive": "ثمّ استقبل الردّ الذي يرسله الطرف الآخر",
"handshake.pasteAnswerPlaceholder": "ألصق رمز ردّ الطرف الآخر…",
"handshake.answerSentNote": "أرسل هذا الردّ إلى المُنشئ ثمّ انتظر - تُفتح الدردشة حالما يتّصل.",
"handshake.answerSentNote": "بانتظار اتصال المُنشئ. ستُفتح المحادثة تلقائياً.",
"handshake.qrHint": "أبقِ هذا مفتوحاً حتى يلتقط الطرف الآخر الرمز.",
"handshake.qrHintFrames": "المصافحة موزّعة على {frames} إطارات - أبقِ هذا مفتوحاً حتى يلتقطها الطرف الآخر كلّها.",
"verify.title": "التحقّق الأمني",
"verify.desc": "قارِن رمز الأمان هذا مع الطرف الآخر عبر قناة منفصلة (مكالمة أو لقاء مباشر)، ثمّ اكتبه لفتح الدردشة.",
"verify.enterLabel": "أدخل الرمز المُتحقَّق منه",
"verify.placeholder": "اكتب الرمز هنا",
"verify.confirm": "تأكيد الرمز",
"verify.confirmed": "مؤكَّد",
"verify.mismatch": "غير متطابقين",
"verify.yours": "تأكيدك",
"verify.peer": "تأكيد الطرف الآخر",
"verify.pending": "قيد الانتظار",
"verify.waiting": "بانتظار الرمز…",
"verify.verified": "تمّ التحقّق من القناة",
"verify.bothConfirmed": "أكّد الطرفان. جارٍ فتح الدردشة الآمنة…",
"sasv.title": "تحقّق من الطرف الآخر",
"sasv.titleDone": "تم التحقّق من الطرف الآخر",
"sasv.desc": "اقرأ الرمز بصوت عالٍ عبر مكالمة أو وجهًا لوجه. لا تتابع إلا إذا رأى الطرف الآخر الأرقام نفسها.",
"sasv.descDone": "أكّدتما كلاكما الرمز نفسه. الاتصال موثَّق.",
"sasv.enter": "أدخل الرمز",
"sasv.match": "متطابق",
"sasv.noMatch": "غير متطابق",
"sasv.you": "أنت",
"sasv.peer": "الطرف الآخر",
"sasv.waitingPeer": "بانتظار الطرف الآخر…",
"sasv.codesDiffer": "الرموز غير متطابقة",
"sasv.opening": "جارٍ فتح المحادثة الآمنة",
"pwa.bannerTitle": "ثبّت SecureBit.chat",
"pwa.bannerDesc": "احصل على تجربة تطبيق أصلي بأمان أعلى",
"pwa.install": "تثبيت",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "مسح",
"action.downloadDesktop": "نزّل تطبيق سطح المكتب",
"action.advancedSettings": "إعدادات متقدّمة",
"cred.offerTag": "عرض · أو انسخ النصّ",
"cred.answerTag": "ردّ · أو انسخ النصّ",
"cred.reveal": "اضغط للكشف - أبقِ هذا الرمز سرّياً",
"cred.orCopy": "أو انسخ الرمز",
"qr.title": "امسح رمز QR",
"qr.subtitle": "وجّه كاميرتك إلى رمزهم",
"qr.scanning": "جارٍ المسح…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "Ihr Gerät erzeugt die Schlüssel und eine einmalige Einladung. Nichts davon berührt einen Server.",
"intro.createCta": "Schlüssel & Einladung erzeugen",
"intro.joinTitle": "Kanal beitreten",
"intro.joinDesc": "Scannen Sie den QR-Code Ihres Gegenübers mit der Kamera oder fügen Sie seinen Einladungscode ein.",
"intro.scanTitle": "QR-Code mit der Kamera scannen",
"intro.scanSubtitle": "Am schnellsten - auf den Bildschirm Ihres Gegenübers halten",
"intro.orPasteCode": "oder Code einfügen",
"intro.orPasteCode": "Oder den Code einfügen",
"intro.pastePlaceholder": "Einladungscode hier einfügen …",
"intro.connect": "Verbinden",
"intro.connecting": "Wird verarbeitet …",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "Verbindung herstellen",
"handshake.thenReceive": "Nehmen Sie danach die Antwort Ihres Gegenübers entgegen",
"handshake.pasteAnswerPlaceholder": "Antwortcode des Gegenübers einfügen …",
"handshake.answerSentNote": "Senden Sie diese Antwort an den Ersteller und warten Sie - der Chat öffnet sich, sobald er sich verbindet.",
"handshake.answerSentNote": "Warten, bis sich der Ersteller verbindet. Der Chat öffnet sich automatisch.",
"handshake.qrHint": "Lassen Sie dies geöffnet, bis Ihr Gegenüber den Code erfasst hat.",
"handshake.qrHintFrames": "Der Handschlag ist auf {frames} Einzelbilder verteilt - lassen Sie dies geöffnet, bis Ihr Gegenüber alle erfasst hat.",
"verify.title": "Sicherheitsprüfung",
"verify.desc": "Vergleichen Sie diesen Sicherheitscode über einen anderen Weg mit Ihrem Gegenüber (Anruf oder persönlich) und geben Sie ihn dann ein, um den Chat freizuschalten.",
"verify.enterLabel": "Geprüften Code eingeben",
"verify.placeholder": "Code hier eingeben",
"verify.confirm": "Code bestätigen",
"verify.confirmed": "Bestätigt",
"verify.mismatch": "Stimmt nicht überein",
"verify.yours": "Ihre Bestätigung",
"verify.peer": "Bestätigung des Gegenübers",
"verify.pending": "Ausstehend",
"verify.waiting": "Warten auf den Code …",
"verify.verified": "Kanal geprüft",
"verify.bothConfirmed": "Beide Seiten haben bestätigt. Der sichere Chat wird geöffnet …",
"sasv.title": "Gegenüber überprüfen",
"sasv.titleDone": "Gegenüber verifiziert",
"sasv.desc": "Lies den Code bei einem Anruf oder persönlich laut vor. Fahre nur fort, wenn dein Gegenüber dieselben Ziffern sieht.",
"sasv.descDone": "Ihr habt beide denselben Code bestätigt. Die Verbindung ist authentifiziert.",
"sasv.enter": "Code eingeben",
"sasv.match": "Stimmt überein",
"sasv.noMatch": "Stimmt nicht überein",
"sasv.you": "Du",
"sasv.peer": "Gegenüber",
"sasv.waitingPeer": "Warte auf Gegenüber…",
"sasv.codesDiffer": "Codes stimmen nicht überein",
"sasv.opening": "Sicherer Chat wird geöffnet",
"pwa.bannerTitle": "SecureBit.chat installieren",
"pwa.bannerDesc": "Die App-Erfahrung mit zusätzlicher Sicherheit",
"pwa.install": "Installieren",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "Scannen",
"action.downloadDesktop": "Desktop-App laden",
"action.advancedSettings": "Erweiterte Einstellungen",
"cred.offerTag": "Angebot · oder Text kopieren",
"cred.answerTag": "Antwort · oder Text kopieren",
"cred.reveal": "Zum Anzeigen klicken - halten Sie diesen Code geheim",
"cred.orCopy": "Oder den Code kopieren",
"qr.title": "QR-Code scannen",
"qr.subtitle": "Richten Sie die Kamera auf den QR-Code",
"qr.scanning": "Wird gescannt …",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "Your device generates the keys and a one-time invitation. Nothing touches a server.",
"intro.createCta": "Generate keys & invitation",
"intro.joinTitle": "Join a channel",
"intro.joinDesc": "Scan your peer's QR with your camera, or paste their invitation code.",
"intro.scanTitle": "Scan QR with camera",
"intro.scanSubtitle": "Fastest - point at your peer's screen",
"intro.orPasteCode": "or paste code",
"intro.orPasteCode": "Or paste the code",
"intro.pastePlaceholder": "Paste invitation code here…",
"intro.connect": "Connect",
"intro.connecting": "Processing…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "Establish connection",
"handshake.thenReceive": "Then receive the answer your peer sends back",
"handshake.pasteAnswerPlaceholder": "Paste peer's answer code…",
"handshake.answerSentNote": "Send this answer to the creator, then wait - the chat opens once they connect.",
"handshake.answerSentNote": "Waiting for the creator to connect. The chat opens automatically.",
"handshake.qrHint": "Keep this open until your peer captures the code.",
"handshake.qrHintFrames": "The handshake is split across {frames} frames - keep this open until your peer captures all of them.",
"verify.title": "Security verification",
"verify.desc": "Compare this safety code with your peer over a separate channel (voice / in person), then type it to unlock the chat.",
"verify.enterLabel": "Enter the verified code",
"verify.placeholder": "Type code here",
"verify.confirm": "Confirm code",
"verify.confirmed": "Confirmed",
"verify.mismatch": "Don't match",
"verify.yours": "Your confirmation",
"verify.peer": "Peer confirmation",
"verify.pending": "Pending",
"verify.waiting": "Waiting for code…",
"verify.verified": "Channel verified",
"verify.bothConfirmed": "Both parties confirmed. Opening the secure chat…",
"sasv.title": "Verify your peer",
"sasv.titleDone": "Peer verified",
"sasv.desc": "Read the code aloud over a call or in person. Continue only if your peer sees the same digits.",
"sasv.descDone": "You both confirmed the same code. The connection is authenticated.",
"sasv.enter": "Enter the code",
"sasv.match": "Match",
"sasv.noMatch": "Doesn't match",
"sasv.you": "You",
"sasv.peer": "Peer",
"sasv.waitingPeer": "Waiting for peer…",
"sasv.codesDiffer": "Codes don't match",
"sasv.opening": "Opening secure chat",
"pwa.bannerTitle": "Install SecureBit.chat",
"pwa.bannerDesc": "Get the native app experience with enhanced security",
"pwa.install": "Install",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "Scan",
"action.downloadDesktop": "Download desktop app",
"action.advancedSettings": "Advanced settings",
"cred.offerTag": "offer · or copy text",
"cred.answerTag": "answer · or copy text",
"cred.reveal": "Click to reveal - keep this code private",
"cred.orCopy": "Or copy the code",
"qr.title": "Scan QR code",
"qr.subtitle": "Point your camera at their QR",
"qr.scanning": "Scanning…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "Tu dispositivo genera las claves y una invitación de un solo uso. Nada de esto pasa por un servidor.",
"intro.createCta": "Generar claves e invitación",
"intro.joinTitle": "Unirse a un canal",
"intro.joinDesc": "Escanea el QR de tu contacto con la cámara o pega su código de invitación.",
"intro.scanTitle": "Escanear el QR con la cámara",
"intro.scanSubtitle": "Lo más rápido: apunta a la pantalla de la otra persona",
"intro.orPasteCode": "o pega el código",
"intro.orPasteCode": "O pega el código",
"intro.pastePlaceholder": "Pega aquí el código de invitación…",
"intro.connect": "Conectar",
"intro.connecting": "Procesando…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "Establecer la conexión",
"handshake.thenReceive": "Después recibe la respuesta que te devuelva la otra persona",
"handshake.pasteAnswerPlaceholder": "Pega aquí el código de respuesta…",
"handshake.answerSentNote": "Envía esta respuesta a quien creó el canal y espera: el chat se abrirá cuando conecte.",
"handshake.answerSentNote": "Esperando a que el creador se conecte. El chat se abrirá automáticamente.",
"handshake.qrHint": "Mantén esto abierto hasta que la otra persona capture el código.",
"handshake.qrHintFrames": "El saludo se reparte en {frames} imágenes: mantén esto abierto hasta que las capture todas.",
"verify.title": "Verificación de seguridad",
"verify.desc": "Compara este código de seguridad con la otra persona por otra vía (voz o en persona) y escríbelo para desbloquear el chat.",
"verify.enterLabel": "Introduce el código verificado",
"verify.placeholder": "Escribe el código aquí",
"verify.confirm": "Confirmar el código",
"verify.confirmed": "Confirmado",
"verify.mismatch": "No coinciden",
"verify.yours": "Tu confirmación",
"verify.peer": "Confirmación de la otra persona",
"verify.pending": "Pendiente",
"verify.waiting": "Esperando el código…",
"verify.verified": "Canal verificado",
"verify.bothConfirmed": "Ambas partes han confirmado. Abriendo el chat seguro…",
"sasv.title": "Verifica a tu contacto",
"sasv.titleDone": "Contacto verificado",
"sasv.desc": "Lee el código en voz alta por llamada o en persona. Continúa solo si tu contacto ve los mismos dígitos.",
"sasv.descDone": "Ambos confirmasteis el mismo código. La conexión está autenticada.",
"sasv.enter": "Introduce el código",
"sasv.match": "Coincide",
"sasv.noMatch": "No coincide",
"sasv.you": "Tú",
"sasv.peer": "Contacto",
"sasv.waitingPeer": "Esperando a tu contacto…",
"sasv.codesDiffer": "Los códigos no coinciden",
"sasv.opening": "Abriendo el chat seguro",
"pwa.bannerTitle": "Instala SecureBit.chat",
"pwa.bannerDesc": "Disfruta de la experiencia de app nativa con más seguridad",
"pwa.install": "Instalar",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "Escanear",
"action.downloadDesktop": "Descargar la app de escritorio",
"action.advancedSettings": "Ajustes avanzados",
"cred.offerTag": "oferta · o copia el texto",
"cred.answerTag": "respuesta · o copia el texto",
"cred.reveal": "Haz clic para mostrarlo: mantén este código en privado",
"cred.orCopy": "O copia el código",
"qr.title": "Escanear código QR",
"qr.subtitle": "Apunta la cámara a su código QR",
"qr.scanning": "Escaneando…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "دستگاه شما کلیدها و یک دعوت‌نامهٔ یک‌بارمصرف می‌سازد. هیچ‌چیز به سرور نمی‌رسد.",
"intro.createCta": "ساخت کلید و دعوت‌نامه",
"intro.joinTitle": "به کانالی بپیوندید",
"intro.joinDesc": "QR طرف مقابل را با دوربین اسکن کنید یا کد دعوت او را جای‌گذاری کنید.",
"intro.scanTitle": "‏QR را با دوربین بخوانید",
"intro.scanSubtitle": "سریع‌ترین راه - به صفحهٔ همتایتان بگیرید",
"intro.orPasteCode": "یا کد را بچسبانید",
"intro.orPasteCode": "یا کد را جای‌گذاری کنید",
"intro.pastePlaceholder": "کد دعوت را اینجا بچسبانید…",
"intro.connect": "اتصال",
"intro.connecting": "در حال پردازش…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "برقراری اتصال",
"handshake.thenReceive": "سپس پاسخی را که همتایتان برمی‌گرداند دریافت کنید",
"handshake.pasteAnswerPlaceholder": "کد پاسخ همتا را بچسبانید…",
"handshake.answerSentNote": "این پاسخ را برای سازنده بفرستید و منتظر بمانید - گفتگو به‌محض اتصال او باز می‌شود.",
"handshake.answerSentNote": "در انتظار اتصال سازنده. گفتگو به‌طور خودکار باز می‌شود.",
"handshake.qrHint": "این را باز نگه دارید تا همتایتان کد را بگیرد.",
"handshake.qrHintFrames": "دست‌دادن میان {frames} فریم پخش شده است - این را باز نگه دارید تا همتایتان همهٔ آن‌ها را بگیرد.",
"verify.title": "تأیید امنیتی",
"verify.desc": "این کد ایمنی را از راه کانالی جداگانه (تماس صوتی / حضوری) با همتایتان مقایسه کنید، سپس آن را بنویسید تا گفتگو باز شود.",
"verify.enterLabel": "کد تأییدشده را وارد کنید",
"verify.placeholder": "کد را اینجا بنویسید",
"verify.confirm": "تأیید کد",
"verify.confirmed": "تأیید شد",
"verify.mismatch": "هم‌خوان نیستند",
"verify.yours": "تأیید شما",
"verify.peer": "تأیید همتا",
"verify.pending": "در انتظار",
"verify.waiting": "در انتظار کد…",
"verify.verified": "کانال تأیید شد",
"verify.bothConfirmed": "هر دو طرف تأیید کردند. گفتگوی امن در حال باز شدن…",
"sasv.title": "طرف مقابل را تأیید کنید",
"sasv.titleDone": "طرف مقابل تأیید شد",
"sasv.desc": "کد را در تماس یا حضوری با صدای بلند بخوانید. فقط وقتی ادامه دهید که طرف مقابل همان ارقام را ببیند.",
"sasv.descDone": "هر دو یک کد را تأیید کردید. اتصال احراز هویت شد.",
"sasv.enter": "کد را وارد کنید",
"sasv.match": "مطابق است",
"sasv.noMatch": "مطابق نیست",
"sasv.you": "شما",
"sasv.peer": "طرف مقابل",
"sasv.waitingPeer": "در انتظار طرف مقابل…",
"sasv.codesDiffer": "کدها مطابق نیستند",
"sasv.opening": "در حال باز کردن گفتگوی امن",
"pwa.bannerTitle": "‏SecureBit.chat را نصب کنید",
"pwa.bannerDesc": "تجربهٔ برنامهٔ بومی با امنیت بیشتر",
"pwa.install": "نصب",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "خواندن",
"action.downloadDesktop": "دریافت برنامهٔ رومیزی",
"action.advancedSettings": "تنظیمات پیشرفته",
"cred.offerTag": "پیشنهاد · یا متن را رونوشت کنید",
"cred.answerTag": "پاسخ · یا متن را رونوشت کنید",
"cred.reveal": "برای نمایش کلیک کنید - این کد را خصوصی نگه دارید",
"cred.orCopy": "یا کد را رونوشت کنید",
"qr.title": "خواندن کد QR",
"qr.subtitle": "دوربین را به QR آن‌ها بگیرید",
"qr.scanning": "در حال خواندن…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "Votre appareil génère les clés et une invitation à usage unique. Rien de tout cela ne passe par un serveur.",
"intro.createCta": "Générer les clés et l'invitation",
"intro.joinTitle": "Rejoindre un canal",
"intro.joinDesc": "Scannez le QR de votre correspondant avec l'appareil photo, ou collez son code d'invitation.",
"intro.scanTitle": "Scanner le QR avec la caméra",
"intro.scanSubtitle": "Le plus rapide : visez l'écran de votre correspondant",
"intro.orPasteCode": "ou collez le code",
"intro.orPasteCode": "Ou collez le code",
"intro.pastePlaceholder": "Collez ici le code d'invitation…",
"intro.connect": "Se connecter",
"intro.connecting": "Traitement…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "Établir la connexion",
"handshake.thenReceive": "Recevez ensuite la réponse que votre correspondant renvoie",
"handshake.pasteAnswerPlaceholder": "Collez le code de réponse…",
"handshake.answerSentNote": "Envoyez cette réponse à la personne qui a créé le canal, puis patientez : la discussion s'ouvre dès qu'elle se connecte.",
"handshake.answerSentNote": "En attente de la connexion du créateur. Le chat s'ouvrira automatiquement.",
"handshake.qrHint": "Gardez ceci ouvert jusqu'à ce que votre correspondant ait capturé le code.",
"handshake.qrHintFrames": "La poignée de main est répartie sur {frames} images : gardez ceci ouvert jusqu'à ce que votre correspondant les ait toutes capturées.",
"verify.title": "Vérification de sécurité",
"verify.desc": "Comparez ce code de sécurité avec votre correspondant par un autre canal (voix ou en personne), puis saisissez-le pour ouvrir la discussion.",
"verify.enterLabel": "Saisissez le code vérifié",
"verify.placeholder": "Tapez le code ici",
"verify.confirm": "Confirmer le code",
"verify.confirmed": "Confirmé",
"verify.mismatch": "Ne correspond pas",
"verify.yours": "Votre confirmation",
"verify.peer": "Confirmation du correspondant",
"verify.pending": "En attente",
"verify.waiting": "En attente du code…",
"verify.verified": "Canal vérifié",
"verify.bothConfirmed": "Les deux parties ont confirmé. Ouverture de la discussion sécurisée…",
"sasv.title": "Vérifiez votre correspondant",
"sasv.titleDone": "Correspondant vérifié",
"sasv.desc": "Lisez le code à voix haute par appel ou en personne. Ne continuez que si votre correspondant voit les mêmes chiffres.",
"sasv.descDone": "Vous avez tous les deux confirmé le même code. La connexion est authentifiée.",
"sasv.enter": "Saisissez le code",
"sasv.match": "Correspond",
"sasv.noMatch": "Ne correspond pas",
"sasv.you": "Vous",
"sasv.peer": "Correspondant",
"sasv.waitingPeer": "En attente du correspondant…",
"sasv.codesDiffer": "Les codes ne correspondent pas",
"sasv.opening": "Ouverture du chat sécurisé",
"pwa.bannerTitle": "Installer SecureBit.chat",
"pwa.bannerDesc": "Profitez de l'expérience d'une application native, avec une sécurité renforcée",
"pwa.install": "Installer",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "Scanner",
"action.downloadDesktop": "Télécharger l'application",
"action.advancedSettings": "Réglages avancés",
"cred.offerTag": "offre · ou copiez le texte",
"cred.answerTag": "réponse · ou copiez le texte",
"cred.reveal": "Cliquez pour afficher - gardez ce code privé",
"cred.orCopy": "Ou copiez le code",
"qr.title": "Scanner le QR",
"qr.subtitle": "Visez le QR de votre correspondant",
"qr.scanning": "Lecture…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "המכשיר שלכם יוצר את המפתחות והזמנה חד־פעמית. שום דבר לא נוגע בשרת.",
"intro.createCta": "יצירת מפתחות והזמנה",
"intro.joinTitle": "הצטרפו לערוץ",
"intro.joinDesc": "סרקו את קוד ה-QR של הצד השני במצלמה, או הדביקו את קוד ההזמנה.",
"intro.scanTitle": "סרקו QR עם המצלמה",
"intro.scanSubtitle": "הכי מהיר - כוונו למסך של העמית",
"intro.orPasteCode": "או הדביקו קוד",
"intro.orPasteCode": "או הדביקו את הקוד",
"intro.pastePlaceholder": "הדביקו כאן את קוד ההזמנה…",
"intro.connect": "התחברו",
"intro.connecting": "מעבד…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "יצירת חיבור",
"handshake.thenReceive": "ואז קבלו את התשובה שהעמית שולח בחזרה",
"handshake.pasteAnswerPlaceholder": "הדביקו את קוד התשובה של העמית…",
"handshake.answerSentNote": "שלחו את התשובה ליוצר ואז המתינו - הצ'אט נפתח ברגע שהם מתחברים.",
"handshake.answerSentNote": "ממתינים שהיוצר יתחבר. הצ'אט ייפתח אוטומטית.",
"handshake.qrHint": "השאירו פתוח עד שהעמית יקלוט את הקוד.",
"handshake.qrHintFrames": "לחיצת היד מפוצלת ל-{frames} פריימים - השאירו פתוח עד שהעמית יקלוט את כולם.",
"verify.title": "אימות אבטחה",
"verify.desc": "השוו את קוד הבטיחות הזה עם העמית בערוץ נפרד (שיחה קולית / פנים אל פנים), ואז הקלידו אותו כדי לפתוח את הצ'אט.",
"verify.enterLabel": "הזינו את הקוד המאומת",
"verify.placeholder": "הקלידו כאן את הקוד",
"verify.confirm": "אשרו את הקוד",
"verify.confirmed": "אושר",
"verify.mismatch": "לא תואמים",
"verify.yours": "האישור שלכם",
"verify.peer": "אישור העמית",
"verify.pending": "ממתין",
"verify.waiting": "ממתין לקוד…",
"verify.verified": "הערוץ אומת",
"verify.bothConfirmed": "שני הצדדים אישרו. פותח את הצ'אט המאובטח…",
"sasv.title": "אמתו את הצד השני",
"sasv.titleDone": "הצד השני אומת",
"sasv.desc": "הקריאו את הקוד בקול בשיחה או פנים אל פנים. המשיכו רק אם הצד השני רואה את אותן ספרות.",
"sasv.descDone": "שניכם אישרתם את אותו קוד. החיבור מאומת.",
"sasv.enter": "הזינו את הקוד",
"sasv.match": "תואם",
"sasv.noMatch": "לא תואם",
"sasv.you": "אתם",
"sasv.peer": "הצד השני",
"sasv.waitingPeer": "ממתינים לצד השני…",
"sasv.codesDiffer": "הקודים לא תואמים",
"sasv.opening": "פותחים צ'אט מאובטח",
"pwa.bannerTitle": "התקינו את SecureBit.chat",
"pwa.bannerDesc": "קבלו חוויית אפליקציה מקורית עם אבטחה משופרת",
"pwa.install": "התקנה",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "סריקה",
"action.downloadDesktop": "הורידו את אפליקציית שולחן העבודה",
"action.advancedSettings": "הגדרות מתקדמות",
"cred.offerTag": "הצעה · או העתיקו טקסט",
"cred.answerTag": "תשובה · או העתיקו טקסט",
"cred.reveal": "לחצו לחשיפה - שמרו את הקוד הזה פרטי",
"cred.orCopy": "או העתיקו את הקוד",
"qr.title": "סריקת קוד QR",
"qr.subtitle": "כוונו את המצלמה ל-QR שלהם",
"qr.scanning": "סורק…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "आपका उपकरण कुंजियाँ और एक बार काम आने वाला निमंत्रण बनाता है। इनमें से कुछ भी किसी सर्वर तक नहीं जाता।",
"intro.createCta": "कुंजियाँ और निमंत्रण बनाएँ",
"intro.joinTitle": "चैनल में शामिल हों",
"intro.joinDesc": "कैमरे से अपने साथी का QR स्कैन करें, या उनका आमंत्रण कोड पेस्ट करें।",
"intro.scanTitle": "कैमरे से QR स्कैन करें",
"intro.scanSubtitle": "सबसे तेज़ - सामने वाले की स्क्रीन पर कैमरा रखें",
"intro.orPasteCode": "या कोड चिपकाएँ",
"intro.orPasteCode": "या कोड पेस्ट करें",
"intro.pastePlaceholder": "निमंत्रण कोड यहाँ चिपकाएँ…",
"intro.connect": "जोड़ें",
"intro.connecting": "प्रक्रिया जारी…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "जुड़ाव बनाएँ",
"handshake.thenReceive": "फिर सामने वाले का भेजा उत्तर लीजिए",
"handshake.pasteAnswerPlaceholder": "सामने वाले का उत्तर कोड चिपकाएँ…",
"handshake.answerSentNote": "यह उत्तर चैनल बनाने वाले को भेजिए और रुकिए - जैसे ही वे जुड़ेंगे, चैट खुल जाएगी।",
"handshake.answerSentNote": "निर्माता के जुड़ने की प्रतीक्षा है। चैट अपने-आप खुल जाएगी।",
"handshake.qrHint": "जब तक सामने वाला कोड पढ़ न ले, इसे खुला रखिए।",
"handshake.qrHintFrames": "हाथ मिलाना {frames} फ़्रेमों में बँटा है - जब तक सामने वाला सब पढ़ न ले, इसे खुला रखिए।",
"verify.title": "सुरक्षा सत्यापन",
"verify.desc": "इस सुरक्षा कोड को किसी दूसरे रास्ते से (आवाज़ पर या आमने-सामने) मिलाइए, फिर उसे लिखकर चैट खोलिए।",
"verify.enterLabel": "मिलाया हुआ कोड लिखें",
"verify.placeholder": "कोड यहाँ लिखें",
"verify.confirm": "कोड पक्का करें",
"verify.confirmed": "पक्का हुआ",
"verify.mismatch": "मेल नहीं खाता",
"verify.yours": "आपकी पुष्टि",
"verify.peer": "सामने वाले की पुष्टि",
"verify.pending": "प्रतीक्षा में",
"verify.waiting": "कोड की प्रतीक्षा…",
"verify.verified": "चैनल सत्यापित",
"verify.bothConfirmed": "दोनों पक्षों ने पुष्टि कर दी। सुरक्षित चैट खुल रही है…",
"sasv.title": "अपने साथी की पुष्टि करें",
"sasv.titleDone": "साथी की पुष्टि हो गई",
"sasv.desc": "कॉल पर या आमने-सामने कोड ज़ोर से पढ़ें। तभी आगे बढ़ें जब आपके साथी को वही अंक दिखें।",
"sasv.descDone": "आप दोनों ने एक ही कोड की पुष्टि की। कनेक्शन प्रमाणित है।",
"sasv.enter": "कोड दर्ज करें",
"sasv.match": "मेल खाता है",
"sasv.noMatch": "मेल नहीं खाता",
"sasv.you": "आप",
"sasv.peer": "साथी",
"sasv.waitingPeer": "साथी की प्रतीक्षा…",
"sasv.codesDiffer": "कोड मेल नहीं खाते",
"sasv.opening": "सुरक्षित चैट खुल रही है",
"pwa.bannerTitle": "SecureBit.chat इंस्टॉल करें",
"pwa.bannerDesc": "बेहतर सुरक्षा के साथ मूल ऐप जैसा अनुभव पाइए",
"pwa.install": "इंस्टॉल करें",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "स्कैन करें",
"action.downloadDesktop": "डेस्कटॉप ऐप डाउनलोड करें",
"action.advancedSettings": "उन्नत सेटिंग्स",
"cred.offerTag": "निमंत्रण · या पाठ कॉपी करें",
"cred.answerTag": "उत्तर · या पाठ कॉपी करें",
"cred.reveal": "देखने के लिए दबाएँ - यह कोड किसी को न बताएँ",
"cred.orCopy": "या कोड कॉपी करें",
"qr.title": "QR कोड स्कैन करें",
"qr.subtitle": "कैमरा सामने वाले के QR पर रखें",
"qr.scanning": "पढ़ा जा रहा है…",
+15 -11
View File
@@ -93,6 +93,7 @@ export const DICTIONARY = {
"intro.createDesc": "여러분의 기기가 키와 일회용 초대장을 만듭니다. 그 어느 것도 서버를 거치지 않습니다.",
"intro.createCta": "키와 초대장 만들기",
"intro.joinTitle": "채널 참여하기",
"intro.joinDesc": "카메라로 상대방의 QR을 스캔하거나 초대 코드를 붙여넣으세요.",
"intro.scanTitle": "카메라로 QR 스캔",
"intro.scanSubtitle": "가장 빠릅니다 - 상대의 화면을 비추세요",
"intro.orPasteCode": "또는 코드 붙여넣기",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "연결 맺기",
"handshake.thenReceive": "그다음 상대가 보내는 응답을 받으세요",
"handshake.pasteAnswerPlaceholder": "상대의 응답 코드를 붙여넣으세요…",
"handshake.answerSentNote": "이 응답을 만든 사람에게 보내고 기다리세요 - 상대가 연결되면 대화가 열립니다.",
"handshake.answerSentNote": "생성자의 연결을 기다리는 중입니다. 채팅이 자동으로 열립니다.",
"handshake.qrHint": "상대가 코드를 다 읽을 때까지 이 화면을 열어 두세요.",
"handshake.qrHintFrames": "악수가 {frames}개 화면으로 나뉘어 있습니다 - 상대가 전부 읽을 때까지 열어 두세요.",
"verify.title": "보안 확인",
"verify.desc": "다른 경로(통화나 대면)로 상대와 이 안전 코드를 대조한 뒤, 입력해서 대화를 여세요.",
"verify.enterLabel": "대조한 코드를 입력하세요",
"verify.placeholder": "여기에 코드 입력",
"verify.confirm": "코드 확인",
"verify.confirmed": "확인됨",
"verify.mismatch": "일치하지 않음",
"verify.yours": "내 확인",
"verify.peer": "상대의 확인",
"verify.pending": "대기 중",
"verify.waiting": "코드 대기 중…",
"verify.verified": "채널 확인 완료",
"verify.bothConfirmed": "양쪽 모두 확인했습니다. 안전한 대화를 여는 중…",
"sasv.title": "상대방 확인",
"sasv.titleDone": "상대방 확인 완료",
"sasv.desc": "통화나 대면으로 코드를 소리 내어 읽어 주세요. 상대방이 같은 숫자를 볼 때만 계속하세요.",
"sasv.descDone": "두 사람 모두 같은 코드를 확인했습니다. 연결이 인증되었습니다.",
"sasv.enter": "코드 입력",
"sasv.match": "일치",
"sasv.noMatch": "일치하지 않음",
"sasv.you": "나",
"sasv.peer": "상대방",
"sasv.waitingPeer": "상대방을 기다리는 중…",
"sasv.codesDiffer": "코드가 일치하지 않음",
"sasv.opening": "보안 채팅을 여는 중",
"pwa.bannerTitle": "SecureBit.chat 설치",
"pwa.bannerDesc": "보안이 강화된 네이티브 앱 경험을 누리세요",
"pwa.install": "설치",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "스캔",
"action.downloadDesktop": "데스크톱 앱 내려받기",
"action.advancedSettings": "고급 설정",
"cred.offerTag": "초대 · 또는 텍스트 복사",
"cred.answerTag": "응답 · 또는 텍스트 복사",
"cred.reveal": "눌러서 보기 - 이 코드는 남에게 알리지 마세요",
"cred.orCopy": "또는 코드 복사",
"qr.title": "QR 코드 스캔",
"qr.subtitle": "상대의 QR에 카메라를 맞추세요",
"qr.scanning": "읽는 중…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "Ваше устройство создаёт ключи и одноразовое приглашение. Ничто из этого не попадает на сервер.",
"intro.createCta": "Создать ключи и приглашение",
"intro.joinTitle": "Присоединиться к каналу",
"intro.joinDesc": "Отсканируйте QR собеседника камерой или вставьте его код приглашения.",
"intro.scanTitle": "Сканировать QR камерой",
"intro.scanSubtitle": "Быстрее всего - наведите на экран собеседника",
"intro.orPasteCode": "или вставьте код",
"intro.orPasteCode": "Или вставьте код",
"intro.pastePlaceholder": "Вставьте код приглашения сюда…",
"intro.connect": "Соединиться",
"intro.connecting": "Обработка…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "Установить соединение",
"handshake.thenReceive": "Затем получите ответ, который пришлёт собеседник",
"handshake.pasteAnswerPlaceholder": "Вставьте код ответа собеседника…",
"handshake.answerSentNote": "Отправьте этот ответ создателю и подождите - чат откроется, как только он подключится.",
"handshake.answerSentNote": "Ждём, пока создатель подключится. Чат откроется автоматически.",
"handshake.qrHint": "Держите это открытым, пока собеседник не считает код.",
"handshake.qrHintFrames": "Рукопожатие разбито на {frames} кадров - держите это открытым, пока собеседник не считает все.",
"verify.title": "Проверка безопасности",
"verify.desc": "Сверьте этот код безопасности с собеседником по другому каналу (голосом или лично), затем введите его, чтобы открыть чат.",
"verify.enterLabel": "Введите сверенный код",
"verify.placeholder": "Введите код здесь",
"verify.confirm": "Подтвердить код",
"verify.confirmed": "Подтверждено",
"verify.mismatch": "Не совпадают",
"verify.yours": "Ваше подтверждение",
"verify.peer": "Подтверждение собеседника",
"verify.pending": "Ожидается",
"verify.waiting": "Ожидание кода…",
"verify.verified": "Канал проверен",
"verify.bothConfirmed": "Обе стороны подтвердили. Открываем защищённый чат…",
"sasv.title": "Проверьте собеседника",
"sasv.titleDone": "Собеседник проверен",
"sasv.desc": "Прочитайте код вслух по звонку или при встрече. Продолжайте, только если собеседник видит те же цифры.",
"sasv.descDone": "Вы оба подтвердили один и тот же код. Соединение подлинное.",
"sasv.enter": "Введите код",
"sasv.match": "Совпадает",
"sasv.noMatch": "Не совпадает",
"sasv.you": "Вы",
"sasv.peer": "Собеседник",
"sasv.waitingPeer": "Ждём собеседника…",
"sasv.codesDiffer": "Коды не совпадают",
"sasv.opening": "Открываем защищённый чат",
"pwa.bannerTitle": "Установите SecureBit.chat",
"pwa.bannerDesc": "Опыт полноценного приложения с усиленной защитой",
"pwa.install": "Установить",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "Сканировать",
"action.downloadDesktop": "Скачать приложение",
"action.advancedSettings": "Дополнительные настройки",
"cred.offerTag": "предложение · или скопируйте текст",
"cred.answerTag": "ответ · или скопируйте текст",
"cred.reveal": "Нажмите, чтобы показать - держите этот код в тайне",
"cred.orCopy": "Или скопируйте код",
"qr.title": "Сканировать QR-код",
"qr.subtitle": "Наведите камеру на их QR-код",
"qr.scanning": "Сканирование…",
+16 -12
View File
@@ -93,9 +93,10 @@ export const DICTIONARY = {
"intro.createDesc": "Ваш пристрій створює ключі та одноразове запрошення. Ніщо з цього не потрапляє на сервер.",
"intro.createCta": "Створити ключі та запрошення",
"intro.joinTitle": "Приєднатися до каналу",
"intro.joinDesc": "Відскануйте QR співрозмовника камерою або вставте його код запрошення.",
"intro.scanTitle": "Сканувати QR камерою",
"intro.scanSubtitle": "Найшвидше - наведіть на екран співрозмовника",
"intro.orPasteCode": "або вставте код",
"intro.orPasteCode": "Або вставте код",
"intro.pastePlaceholder": "Вставте код запрошення сюди…",
"intro.connect": "З'єднатися",
"intro.connecting": "Обробка…",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "Встановити з'єднання",
"handshake.thenReceive": "Потім отримайте відповідь, яку надішле співрозмовник",
"handshake.pasteAnswerPlaceholder": "Вставте код відповіді співрозмовника…",
"handshake.answerSentNote": "Надішліть цю відповідь створювачу й зачекайте - чат відкриється, щойно він приєднається.",
"handshake.answerSentNote": "Чекаємо, поки творець під'єднається. Чат відкриється автоматично.",
"handshake.qrHint": "Тримайте це відкритим, доки співрозмовник не зчитає код.",
"handshake.qrHintFrames": "Рукостискання розбите на {frames} кадрів - тримайте це відкритим, доки співрозмовник не зчитає всі.",
"verify.title": "Перевірка безпеки",
"verify.desc": "Звірте цей код безпеки зі співрозмовником іншим каналом (голосом або особисто), потім введіть його, щоб відкрити чат.",
"verify.enterLabel": "Введіть звірений код",
"verify.placeholder": "Введіть код тут",
"verify.confirm": "Підтвердити код",
"verify.confirmed": "Підтверджено",
"verify.mismatch": "Не збігаються",
"verify.yours": "Ваше підтвердження",
"verify.peer": "Підтвердження співрозмовника",
"verify.pending": "Очікується",
"verify.waiting": "Очікування коду…",
"verify.verified": "Канал перевірено",
"verify.bothConfirmed": "Обидві сторони підтвердили. Відкриваємо захищений чат…",
"sasv.title": "Перевірте співрозмовника",
"sasv.titleDone": "Співрозмовника перевірено",
"sasv.desc": "Прочитайте код уголос під час дзвінка або зустрічі. Продовжуйте, лише якщо співрозмовник бачить ті самі цифри.",
"sasv.descDone": "Ви обидва підтвердили той самий код. З'єднання справжнє.",
"sasv.enter": "Введіть код",
"sasv.match": "Збігається",
"sasv.noMatch": "Не збігається",
"sasv.you": "Ви",
"sasv.peer": "Співрозмовник",
"sasv.waitingPeer": "Чекаємо на співрозмовника…",
"sasv.codesDiffer": "Коди не збігаються",
"sasv.opening": "Відкриваємо захищений чат",
"pwa.bannerTitle": "Встановіть SecureBit.chat",
"pwa.bannerDesc": "Досвід власного застосунку з посиленим захистом",
"pwa.install": "Встановити",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "Сканувати",
"action.downloadDesktop": "Завантажити застосунок",
"action.advancedSettings": "Додаткові налаштування",
"cred.offerTag": "пропозиція · або скопіюйте текст",
"cred.answerTag": "відповідь · або скопіюйте текст",
"cred.reveal": "Натисніть, щоб показати - тримайте цей код у таємниці",
"cred.orCopy": "Або скопіюйте код",
"qr.title": "Сканувати QR-код",
"qr.subtitle": "Наведіть камеру на їхній QR-код",
"qr.scanning": "Сканування…",
+15 -11
View File
@@ -93,6 +93,7 @@ export const DICTIONARY = {
"intro.createDesc": "آپ کا آلہ کلیدیں اور ایک بار کی دعوت بناتا ہے۔ کچھ بھی سرور کو نہیں چھوتا۔",
"intro.createCta": "کلیدیں اور دعوت بنائیں",
"intro.joinTitle": "چینل میں شامل ہوں",
"intro.joinDesc": "کیمرے سے اپنے ساتھی کا QR اسکین کریں، یا ان کا دعوتی کوڈ چسپاں کریں۔",
"intro.scanTitle": "کیمرے سے QR پڑھیں",
"intro.scanSubtitle": "سب سے تیز - ساتھی کی سکرین کی طرف کریں",
"intro.orPasteCode": "یا کوڈ چسپاں کریں",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "اتصال قائم کریں",
"handshake.thenReceive": "پھر وہ جواب وصول کریں جو آپ کا ساتھی واپس بھیجتا ہے",
"handshake.pasteAnswerPlaceholder": "ساتھی کا جوابی کوڈ چسپاں کریں…",
"handshake.answerSentNote": "یہ جواب بنانے والے کو بھیجیں، پھر انتظار کریں - ان کے جڑتے ہی گفتگو کھل جائے گی۔",
"handshake.answerSentNote": "تخلیق کار کے جڑنے کا انتظار ہے۔ چیٹ خود بخود کھل جائے گی۔",
"handshake.qrHint": "یہ اس وقت تک کھلا رکھیں جب تک ساتھی کوڈ نہ پکڑ لے۔",
"handshake.qrHintFrames": "مصافحہ {frames} فریموں میں بٹا ہے - یہ اس وقت تک کھلا رکھیں جب تک ساتھی سب نہ پکڑ لے۔",
"verify.title": "حفاظتی تصدیق",
"verify.desc": "یہ حفاظتی کوڈ کسی الگ ذریعے (آواز / بالمشافہ) سے اپنے ساتھی کے ساتھ ملائیں، پھر گفتگو کھولنے کے لیے لکھیں۔",
"verify.enterLabel": "تصدیق شدہ کوڈ درج کریں",
"verify.placeholder": "کوڈ یہاں لکھیں",
"verify.confirm": "کوڈ کی تصدیق",
"verify.confirmed": "تصدیق ہو گئی",
"verify.mismatch": "مطابقت نہیں",
"verify.yours": "آپ کی تصدیق",
"verify.peer": "ساتھی کی تصدیق",
"verify.pending": "زیرِ التوا",
"verify.waiting": "کوڈ کا انتظار…",
"verify.verified": "چینل تصدیق شدہ",
"verify.bothConfirmed": "دونوں فریقوں نے تصدیق کی۔ محفوظ گفتگو کھولی جا رہی ہے…",
"sasv.title": "اپنے ساتھی کی تصدیق کریں",
"sasv.titleDone": "ساتھی کی تصدیق ہو گئی",
"sasv.desc": "کال پر یا آمنے سامنے کوڈ بلند آواز سے پڑھیں۔ صرف تب آگے بڑھیں جب آپ کے ساتھی کو وہی ہندسے نظر آئیں۔",
"sasv.descDone": "آپ دونوں نے ایک ہی کوڈ کی تصدیق کی۔ کنکشن مستند ہے۔",
"sasv.enter": "کوڈ درج کریں",
"sasv.match": "مماثل ہے",
"sasv.noMatch": "مماثل نہیں",
"sasv.you": "آپ",
"sasv.peer": "ساتھی",
"sasv.waitingPeer": "ساتھی کا انتظار…",
"sasv.codesDiffer": "کوڈ مماثل نہیں",
"sasv.opening": "محفوظ چیٹ کھل رہی ہے",
"pwa.bannerTitle": "‏SecureBit.chat نصب کریں",
"pwa.bannerDesc": "بہتر سلامتی کے ساتھ مقامی ایپ کا تجربہ حاصل کریں",
"pwa.install": "نصب",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "پڑھیں",
"action.downloadDesktop": "ڈیسک ٹاپ ایپ ڈاؤن لوڈ کریں",
"action.advancedSettings": "اعلیٰ ترتیبات",
"cred.offerTag": "پیشکش · یا متن نقل کریں",
"cred.answerTag": "جواب · یا متن نقل کریں",
"cred.reveal": "دکھانے کے لیے دبائیں - یہ کوڈ نجی رکھیں",
"cred.orCopy": "یا کوڈ نقل کریں",
"qr.title": "‏QR کوڈ پڑھیں",
"qr.subtitle": "اپنا کیمرہ ان کے QR کی طرف کریں",
"qr.scanning": "پڑھا جا رہا ہے…",
+15 -11
View File
@@ -93,6 +93,7 @@ export const DICTIONARY = {
"intro.createDesc": "你的设备会生成密钥和一次性邀请。这些都不会经过任何服务器。",
"intro.createCta": "生成密钥与邀请",
"intro.joinTitle": "加入通道",
"intro.joinDesc": "用相机扫描对方的二维码,或粘贴对方的邀请代码。",
"intro.scanTitle": "用摄像头扫描二维码",
"intro.scanSubtitle": "最快的方式 —— 对准对方的屏幕",
"intro.orPasteCode": "或粘贴代码",
@@ -271,22 +272,26 @@ export const DICTIONARY = {
"handshake.establish": "建立连接",
"handshake.thenReceive": "然后接收对方发回的应答",
"handshake.pasteAnswerPlaceholder": "粘贴对方的应答码…",
"handshake.answerSentNote": "把这段应答发给创建者,然后等待 —— 对方一连上,聊天就会打开。",
"handshake.answerSentNote": "正在等待创建者连接。聊天将自动打开。",
"handshake.qrHint": "在对方读取完代码之前,请保持此界面打开。",
"handshake.qrHintFrames": "握手被拆成 {frames} 帧 —— 在对方读完全部帧之前,请保持此界面打开。",
"verify.title": "安全校验",
"verify.desc": "通过另一条渠道(通话或当面)与对方核对这个安全码,然后输入它以解锁聊天。",
"verify.enterLabel": "输入核对过的安全码",
"verify.placeholder": "在此输入安全码",
"verify.confirm": "确认安全码",
"verify.confirmed": "已确认",
"verify.mismatch": "不一致",
"verify.yours": "你的确认",
"verify.peer": "对方的确认",
"verify.pending": "等待中",
"verify.waiting": "等待安全码…",
"verify.verified": "通道已校验",
"verify.bothConfirmed": "双方均已确认。正在打开安全聊天…",
"sasv.title": "验证对方",
"sasv.titleDone": "对方已验证",
"sasv.desc": "通过通话或当面大声读出代码。只有对方看到相同的数字时才继续。",
"sasv.descDone": "你们双方确认了相同的代码。连接已通过认证。",
"sasv.enter": "输入代码",
"sasv.match": "一致",
"sasv.noMatch": "不一致",
"sasv.you": "你",
"sasv.peer": "对方",
"sasv.waitingPeer": "正在等待对方…",
"sasv.codesDiffer": "代码不一致",
"sasv.opening": "正在打开安全聊天",
"pwa.bannerTitle": "安装 SecureBit.chat",
"pwa.bannerDesc": "获得原生应用般的体验,安全性更进一步",
"pwa.install": "安装",
@@ -321,9 +326,8 @@ export const DICTIONARY = {
"action.scan": "扫描",
"action.downloadDesktop": "下载桌面应用",
"action.advancedSettings": "高级设置",
"cred.offerTag": "邀请 · 或复制文本",
"cred.answerTag": "应答 · 或复制文本",
"cred.reveal": "点击显示 - 请勿泄露此代码",
"cred.orCopy": "或复制代码",
"qr.title": "扫描二维码",
"qr.subtitle": "把摄像头对准对方的二维码",
"qr.scanning": "扫描中…",
+1
View File
@@ -798,6 +798,7 @@ button i {
.sb-start .sb-sc::-webkit-scrollbar-thumb { background: rgba(var(--sb-ink), 0.1); border-radius: 99px; }
.sb-seg-btn { transition: color .2s; }
.sb-soft-btn { transition: all .15s; }
.sb-qr-row:hover { background: rgba(var(--sb-ink), 0.025) !important; }
.sb-soft-btn:hover { border-color: rgba(var(--sb-ink), 0.18) !important; color: var(--sb-text-2) !important; }
.sb-scan-btn:hover { border-color: rgba(var(--sb-green-rgb), 0.5) !important; background: rgba(var(--sb-green-rgb), 0.1) !important; }
.sb-gen-btn:hover { background: var(--sb-orange-hi) !important; }
+1 -1
View File
@@ -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 = '1790225533889';
const SW_BUILD_VERSION = '1790540575787';
// Locale subdirectories, rewritten by scripts/build-i18n.js. Each localized page is a
// separate document at its own URL, so the shell has to be cached and served per
+7
View File
@@ -176,6 +176,13 @@ function sources() {
assert.ok(app.slice(cells, cells + 60).includes("dir: 'ltr'"),
'the safety-code tiles sit in a flex row, so they must be pinned left-to-right');
// The same code printed large above the tiles, in two groups side by side — another
// flex row, so in a right-to-left locale the groups would swap places.
const big = app.indexOf("key: 'code', style: { padding: '20px 0 22px'");
assert.notEqual(big, -1, 'the large safety-code display is gone — this guard now protects nothing');
assert.ok(app.slice(big, big + 400).includes("dir: 'ltr'"),
'the large safety-code display must be pinned left-to-right');
// The group code, wherever it is put on screen. A plain `group.sasCode` also appears
// in guard conditions, which render nothing — so the window looks both ways and only
// the sites that are actually inside an element have to carry the direction.
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/uk/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/uk.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/uk.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ur/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ur.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/ur.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/zh/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790225533889"></script>
<script src="/src/scripts/theme-boot.js?v=1790540575787"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790225533889"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790540575787"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790225533889">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790540575787">
<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">
@@ -125,7 +125,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=1790225533889">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790540575787">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790225533889"></script>
<script defer src="/config/ice-servers.js?v=1790540575787"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790225533889">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790225533889">
<link rel="stylesheet" href="/assets/app.css?v=1790540575787">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790540575787">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790225533889"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790540575787"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790225533889"></script>
<script defer src="/src/scripts/fa-check.js?v=1790540575787"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/zh.js?v=1790225533889"></script>
<script type="module" src="/src/i18n/dict/zh.js?v=1790540575787"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790225533889"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790225533889"></script>
<script defer src="/src/utils/updateManager.js?v=1790540575787"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790540575787"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790225533889"></script>
<script type="module" src="/dist/app.js?v=1790225533889"></script>
<script type="module" src="/dist/app-boot.js?v=1790540575787"></script>
<script type="module" src="/dist/app.js?v=1790540575787"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790225533889"></script>
<script src="/src/pwa/install-prompt.js?v=1790225533889" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790225533889" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790225533889"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790540575787"></script>
<script src="/src/pwa/install-prompt.js?v=1790540575787" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790540575787" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790540575787"></script>
</body>
</html>