From 113bb107d3b6da8f0f8c39ab228de17c814d979d Mon Sep 17 00:00:00 2001 From: lockbitchat Date: Tue, 1 Sep 2026 01:04:11 -0400 Subject: [PATCH] feat(groups): audio and video calls in group chats; release v6.6.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A group call is N-1 ordinary 1:1 calls, one to each other member, each riding the pairwise session that member already has — a transport a human already authenticated by comparing the safety code. No mixer, no SFU, no point at which two people's media meets anywhere but on a device. Call control is separate from call media, because the two reach different sets of people. Who opened a call, who joined and who left travels as group frames signed with the sender's group identity key, so it reaches members currently reachable only through a relay — and a relaying member can drop one but cannot write one. Media flows only where a direct link exists, so a member without one shows as connecting rather than being omitted. Frames carry a per-sender sequence checked before the action, so a captured leave cannot end a later call, and simultaneous calls converge on the lower random call id. One capture is shared across every leg rather than one getUserMedia per member, and legs answer without prompting: the flag permitting that is set only locally, only while this user is in the call, and cleared when they leave. UI: a gallery that sizes itself from the space it has, a spotlight view, an active-speaker indicator read from the waveform, and the call surface in the same visual language as the 1:1 one. Also in this commit, the v6.5.0 language-suggestion work that had not been pushed yet; its notes are in the changelog. And two fixes: the safety-code input asks for digits rather than text, and starting a new chat from inside a group no longer creates it behind the group where it cannot be seen — which had made it impossible to connect to anyone new, or to add anyone to a group, while a group was open. Claude-Session: https://claude.ai/code/session_01XSxAkET3hQTkYDQfbjCQwZ --- CHANGELOG.md | 63 + Dockerfile | 5 + README.md | 4 +- ar/index.html | 48 +- assets/tailwind.css | 2 +- de/index.html | 48 +- deploy/nginx.conf | 44 +- deploy/www-sw.js | 50 + dist/app-boot.js | 476 ++- dist/app-boot.js.map | 4 +- dist/app.js | 3131 +++++++++++++++++--- dist/app.js.map | 8 +- doc/CRYPTOGRAPHY.md | 43 +- es/index.html | 48 +- fa/index.html | 48 +- fr/index.html | 48 +- he/index.html | 48 +- hi/index.html | 48 +- index.html | 48 +- ko/index.html | 48 +- locales/ar.json | 27 +- locales/de.json | 27 +- locales/en.json | 27 +- locales/es.json | 27 +- locales/fa.json | 27 +- locales/fr.json | 27 +- locales/he.json | 27 +- locales/hi.json | 27 +- locales/ko.json | 27 +- locales/ru.json | 27 +- locales/uk.json | 27 +- locales/ur.json | 27 +- locales/zh.json | 27 +- meta.json | 14 +- package.json | 4 +- ru/index.html | 48 +- src/app.jsx | 338 ++- src/components/ui/CallUI.jsx | 5 + src/components/ui/GroupCallUI.jsx | 575 ++++ src/components/ui/GroupChat.jsx | 78 +- src/components/ui/Header.jsx | 12 +- src/components/ui/LanguageSuggestion.jsx | 140 + src/components/ui/callLayout.js | 107 + src/group/GroupSession.js | 308 ++ src/group/groupCallMedia.js | 739 +++++ src/group/groupCrypto.js | 90 + src/i18n/generated.js | 351 ++- src/i18n/index.js | 23 + src/network/EnhancedSecureWebRTCManager.js | 166 +- src/pwa/install-prompt.js | 233 +- src/pwa/pwa-manager.js | 68 +- src/styles/components.css | 7 +- src/styles/pwa.css | 208 +- sw.js | 2 +- tests/group-app-integration.test.mjs | 40 + tests/group-call-autoanswer.test.mjs | 97 + tests/group-call.test.mjs | 927 ++++++ tests/language-suggestion.test.mjs | 100 + tests/mobile-chat-layout.test.mjs | 14 +- uk/index.html | 48 +- ur/index.html | 48 +- zh/index.html | 48 +- 62 files changed, 8412 insertions(+), 1039 deletions(-) create mode 100644 deploy/www-sw.js create mode 100644 src/components/ui/GroupCallUI.jsx create mode 100644 src/components/ui/LanguageSuggestion.jsx create mode 100644 src/components/ui/callLayout.js create mode 100644 src/group/groupCallMedia.js create mode 100644 tests/group-call-autoanswer.test.mjs create mode 100644 tests/group-call.test.mjs create mode 100644 tests/language-suggestion.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 880cd0e..82618bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## v6.6.6 — Group calls + +Groups can call now. Press the handset in a group's header for voice, the camera +for video, and everyone in the group gets a ring. Whoever joins appears as a tile +of their own; tap somebody to give them the whole screen and drop the rest into a +strip along the bottom. Mute, camera, front/back, and a green ring around whoever +is talking. + +**There is no conference server, and there was never going to be one.** The usual +way to do this is to send everyone's audio to a machine in the middle that mixes +it and sends one stream back. It is cheaper, it scales further, and it means a +computer you do not own is holding the room. A group call here is instead a +separate encrypted call to each other member, over the connection you already +built with them and already verified in person by reading the safety code aloud. +Four people means three calls out of your browser. That is the honest cost of the +room only existing on the devices that are in it. + +**Nobody's phone rings for a call they were not invited to.** Who opened a call +and who is in it travels as a message signed with the sender's group key — the +same signature the group's text messages carry. Members who cannot reach each +other directly pass those messages along for one another, which is how a group +works here, and a member passing one along can drop it but cannot write it. So +nobody can add you to a call you never joined, take you out of one you are in, or +hang up on the group. + +**Your microphone opens once, for the whole call, and only after you agree.** One +capture is shared across every connection rather than one per person, which is +also what stops the browser asking a group of six for permission six times. It +stops the moment you leave — including if you close the group, or the tab. + +**A member with no direct link is in the call, not missing from it.** Groups start +as a star, with only the person who created it connected to everyone, and build +the remaining connections quietly afterwards. Someone still being connected shows +as connecting rather than being left out of the list, and joins when their link +comes up. + +**Also in this release:** the safety code now asks for a number instead of text, +so phones open a keypad rather than a full keyboard for an answer that is always +seven digits. And starting a new chat from inside a group works — it used to +create the chat behind the group and leave it invisible, which meant that while +you had a group open you could not connect to anybody new, or add anybody to the +group. + +## v6.5.0 — It asks, in your language + +If your browser reads Arabic and you land on the English page, a small bar now +appears in the corner: "هذه الصفحة متوفرة أيضًا بالعربية" - with a link. One +click and you are on `/ar/`. Dismiss it and it never comes back. + +**What it deliberately does not do is move you.** The obvious version of this +feature reads the browser's language on load and redirects. It looks right from +your own machine and quietly ruins the site everywhere else. Search engines +crawl from one place with one language set, so a redirect answers every one of +the thirteen locale addresses with the same page - and the twelve translations +stop existing as far as the index is concerned, however carefully they are +linked. Shared links break the same way: a link posted in a German group chat +has to open in German for everyone who follows it, including the person whose +laptop is set to English. + +So the address still decides the language, always. The bar is an offer sitting +next to it: written in the language it is offering, laid out in that language's +direction, and gone for good once you say no. + ## v6.4.0 — Now it reads right to left Arabic, Hebrew, Persian and Urdu, at `/ar/`, `/he/`, `/fa/` and `/ur/`. diff --git a/Dockerfile b/Dockerfile index 503f5a8..87d400a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,11 @@ FROM nginx:1.27-alpine # Replace the default nginx config with our static-serving config. COPY deploy/nginx.conf /etc/nginx/nginx.conf +# The www.securebit.chat kill-switch worker. It lives outside the web root because +# it is deployment plumbing, not site content: the apex must never serve it as +# /sw.js, which is the real, caching worker. +COPY deploy/www-sw.js /etc/nginx/www-sw.js + # Serve the repository (src/, assets/, libs/, dist/, config/, logo/, sw.js, ...). COPY . /usr/share/nginx/html diff --git a/README.md b/README.md index 3733645..b2505cf 100644 --- a/README.md +++ b/README.md @@ -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.4.0-3ecf8e.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-6.6.6-3ecf8e.svg)](CHANGELOG.md) [![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) [![Forward secrecy](https://img.shields.io/badge/forward%20secrecy-Double%20Ratchet-3ecf8e.svg)](#forward-secrecy) @@ -50,6 +50,8 @@ It is designed for people who need a small, auditable, zero-infrastructure way t - **Adaptive audio**: Opus with in-band FEC, DTX and RED redundancy for intelligible speech at 15 to 20 percent packet loss; audio is prioritised and never throttled by the network controller. - **Adaptive video**: VP9/AV1 single-encoding SVC (H.264/VP8 fallback) that degrades by spatial/temporal layer, with a runtime controller that trims video bitrate on loss/RTT and recovers as the link clears. - **Live connection-quality indicator** (Excellent, Good, Fair, Weak) shown during a call, plus in-call mute and video controls. +- **Group voice and video calls**, built as one encrypted call per member rather than a conference server: media flows directly between each pair over the link they already verified, so no machine in the middle ever holds the room. Who opened a call and who is in it is signed with each member's group key, so a member relaying that message for two people who cannot reach each other can drop it but never forge it. +- **One microphone capture for the whole group call**, shared across every leg, released the moment you leave — you are asked for the microphone once, not once per participant. **Messaging** - **Encrypted voice messages.** Record in the browser and send over the same end-to-end encrypted transfer channel as files. Audio is captured as PCM/WAV, integrity-protected by a signed hash, and played back inline on the recipient's device without ever touching disk. diff --git a/ar/index.html b/ar/index.html index 1d12e44..e8fda58 100644 --- a/ar/index.html +++ b/ar/index.html @@ -30,7 +30,7 @@ - + @@ -96,7 +96,7 @@ - + @@ -105,7 +105,7 @@ - + @@ -222,7 +222,7 @@ - + - - + + @@ -239,38 +239,38 @@ - - - + + + - + - + - + - + - - - - + + + +
- - + + - - - - - + + + + + \ No newline at end of file diff --git a/assets/tailwind.css b/assets/tailwind.css index 7ccd4d0..7718fec 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.end-0{inset-inline-end:0}.end-4{inset-inline-end:1rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-4{right:1rem}.start-0{inset-inline-start:0}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem}.mb-1,.my-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.me-1{margin-inline-end:.25rem}.me-2{margin-inline-end:.5rem}.me-3{margin-inline-end:.75rem}.me-4{margin-inline-end:1rem}.ms-2{margin-inline-start:.5rem}.ms-6{margin-inline-start:1.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-16{margin-top:4rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-28{height:7rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-\[420px\]{height:420px}.max-h-\[80vh\]{max-height:80vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-28{width:7rem}.w-4{width:1rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[180px\]{min-width:180px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[78vw\]{max-width:78vw}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-purple-500\/30{border-color:rgba(168,85,247,.3)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/20{background-color:rgba(0,0,0,.2)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-center{text-align:center}.text-start{text-align:start}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wide{letter-spacing:.025em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-sky-300{--tw-text-opacity:1;color:rgb(125 211 252/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.line-through{text-decoration-line:line-through}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-700\/40:hover{background-color:rgba(55,65,81,.4)}.hover\:bg-gray-700\/50:hover{background-color:rgba(55,65,81,.5)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-green-400:hover{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.focus\:border-purple-400:focus{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-gray-700\/60:active{background-color:rgba(55,65,81,.6)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-60:disabled{opacity:.6}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-3{margin-bottom:.75rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-12{height:3rem}.sm\:w-12{width:3rem}.sm\:gap-4{gap:1rem}.sm\:p-4{padding:1rem}.sm\:py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:hidden{display:none}.md\:h-\[32rem\]{height:32rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.end-0{inset-inline-end:0}.end-4{inset-inline-end:1rem}.left-1\/2{left:50%}.left-4{left:1rem}.right-4{right:1rem}.start-0{inset-inline-start:0}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem}.mb-1,.my-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.me-1{margin-inline-end:.25rem}.me-2{margin-inline-end:.5rem}.me-3{margin-inline-end:.75rem}.me-4{margin-inline-end:1rem}.ms-2{margin-inline-start:.5rem}.ms-6{margin-inline-start:1.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-16{margin-top:4rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-28{height:7rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-\[420px\]{height:420px}.max-h-\[80vh\]{max-height:80vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-28{width:7rem}.w-4{width:1rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[180px\]{min-width:180px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[78vw\]{max-width:78vw}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.transform,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-purple-500\/30{border-color:rgba(168,85,247,.3)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/20{background-color:rgba(0,0,0,.2)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-center{text-align:center}.text-start{text-align:start}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wide{letter-spacing:.025em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-sky-300{--tw-text-opacity:1;color:rgb(125 211 252/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.line-through{text-decoration-line:line-through}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-700\/40:hover{background-color:rgba(55,65,81,.4)}.hover\:bg-gray-700\/50:hover{background-color:rgba(55,65,81,.5)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-green-400:hover{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.focus\:border-purple-400:focus{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-gray-700\/60:active{background-color:rgba(55,65,81,.6)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-60:disabled{opacity:.6}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-3{margin-bottom:.75rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-12{height:3rem}.sm\:w-12{width:3rem}.sm\:gap-4{gap:1rem}.sm\:p-4{padding:1rem}.sm\:py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:hidden{display:none}.md\:h-\[32rem\]{height:32rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} \ No newline at end of file diff --git a/de/index.html b/de/index.html index 9fd26ba..846c25e 100644 --- a/de/index.html +++ b/de/index.html @@ -30,7 +30,7 @@ - + @@ -96,7 +96,7 @@ - + @@ -105,7 +105,7 @@ - + @@ -222,7 +222,7 @@ - + - - + + @@ -239,38 +239,38 @@ - - - + + + - + - + - + - + - - - - + + + +
- - + + - - - - - + + + + + \ No newline at end of file diff --git a/deploy/nginx.conf b/deploy/nginx.conf index eb5ce5b..70390c1 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -106,8 +106,8 @@ http { } server { - listen 8080; - listen [::]:8080; + listen 8080 default_server; + listen [::]:8080 default_server; server_name _; root /usr/share/nginx/html; index index.html; @@ -140,4 +140,44 @@ http { try_files $uri $uri/ $sb_shell; } } + + # One canonical hostname. www.securebit.chat has its own Fly certificate (so + # Cloudflare's origin handshake succeeds), but it must not serve the app as a + # second address — that splits search ranking and canonical links. Cloudflare + # passes the original Host through, so an exact server_name match catches it + # here; everything else falls through to the default server above. + server { + listen 8080; + listen [::]:8080; + server_name www.securebit.chat; + + # /sw.js is the one path that must NOT redirect. Visitors who reached www in + # the window between its certificate being issued and this redirect shipping + # registered the site's service worker against this origin, and that worker + # still serves its cached app shell — so those browsers never see the 301, + # while their subresource requests do get redirected and are then blocked by + # the page's own `script-src 'self'`. A 301 here would make that permanent: + # a service worker update fails outright if the script URL redirects. So this + # origin answers with a worker that unregisters itself and reloads the tab. + # See deploy/www-sw.js. + location = /sw.js { + alias /etc/nginx/www-sw.js; + # add_header in a location REPLACES the server-level set rather than + # merging, so the security header has to be restated here. + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + # Re-checked on every navigation instead of being trusted for 24 hours, + # which is how quickly a stuck visitor gets rescued. + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + add_header CDN-Cache-Control "no-store" always; + } + + location / { + # The redirect itself must carry HSTS: it is the first response a + # first-time www visitor sees, and the preload list covers subdomains. + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + + return 301 https://securebit.chat$request_uri; + } + } + } diff --git a/deploy/www-sw.js b/deploy/www-sw.js new file mode 100644 index 0000000..d015b2c --- /dev/null +++ b/deploy/www-sw.js @@ -0,0 +1,50 @@ +// Self-destructing service worker, served ONLY at https://www.securebit.chat/sw.js. +// +// www.securebit.chat answered TLS for the first time when its Fly certificate was +// issued, and for the couple of minutes before the redirect below shipped it served +// the real app. That was long enough for visitors to register the site's service +// worker against the www origin. A registered worker outlives the server change: it +// keeps serving its cached app shell, so the 301 is never reached, while every +// subresource request does reach the network, gets redirected to the apex, and is +// then refused by the page's own CSP (`script-src 'self'` — and 'self' is www). +// +// The redirect cannot clear this on its own. A service worker script is re-fetched +// to check for updates, and the spec fails that update if the script URL redirects +// at all, let alone cross-origin — so a plain 301 on /sw.js pins the stale worker in +// place permanently. The only way out is to answer /sw.js with a real script that +// takes over and then removes itself. +// +// It is served with no-store, so browsers re-check it on every navigation rather +// than trusting a cached copy for 24 hours. + +self.addEventListener('install', () => { + // Do not wait for the old worker's clients to go away — they are exactly the + // broken tabs this exists to rescue. + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + // Drop the app shell the previous worker cached under this origin, so nothing + // can be replayed from it if the unregister below is interrupted. + const names = await caches.keys(); + await Promise.all(names.map((name) => caches.delete(name))); + + await self.registration.unregister(); + + // Reload whatever is still open. With no worker left, the navigation reaches + // nginx and follows the 301 to the apex, where the app actually lives. + const clients = await self.clients.matchAll({ type: 'window' }); + for (const client of clients) { + try { + await client.navigate(client.url); + } catch (_) { + // A client that refuses to be navigated (cross-origin owner, or already + // gone) is not worth failing activation over; its next reload is clean. + } + } + })()); +}); + +// Deliberately no fetch handler: until the unregister lands, requests should go +// straight to the network, which is where the redirect is. diff --git a/dist/app-boot.js b/dist/app-boot.js index a927945..9f37b45 100644 --- a/dist/app-boot.js +++ b/dist/app-boot.js @@ -4413,6 +4413,9 @@ var LOCALE_META = { var DICTIONARIES = { "en": { "language.label": "Language", + "language.suggest.text": "This page is also available in English.", + "language.suggest.cta": "Read in English", + "language.suggest.dismiss": "Dismiss", "community.title": "Join the future of privacy", "community.description": "SecureBit grows thanks to its community. Your ideas and feedback shape the future of secure communication - built in the open, with complete ASN.1 validation end-to-end.", "community.github": "GitHub Repository", @@ -5158,10 +5161,35 @@ var DICTIONARIES = { "offline.disconnect": "Disconnect", "offline.learnMore": "Learn more", "pwa.installApp": "Install App", - "chat.onWeb": "You're on Web" + "chat.onWeb": "You're on Web", + "groupCall.startVoice": "Start a group voice call", + "groupCall.startVideo": "Start a group video call", + "groupCall.join": "Join", + "groupCall.dismiss": "Not now", + "groupCall.leave": "Leave call", + "groupCall.you": "You", + "groupCall.connecting": "Connecting\u2026", + "groupCall.waitingLink": "Waiting for a direct link\u2026", + "groupCall.legFailed": "Could not connect", + "groupCall.startedVoice": "{name} started a voice call", + "groupCall.startedVideo": "{name} started a video call", + "groupCall.inCall": "{count} in the call", + "groupCall.err.permission_denied": "The call could not start - microphone and camera access is blocked. Allow it for this site, then try again.", + "groupCall.err.device_not_found": "The call could not start - no microphone was found on this device.", + "groupCall.err.device_busy": "The call could not start - your microphone is in use by another app. Close it and try again.", + "groupCall.err.media_failed": "The call could not start - the microphone could not be opened.", + "groupCall.err.call_in_progress": "A call is already running in this group. Join it instead of starting another.", + "groupCall.err.not_ready": "Confirm the group code before calling.", + "groupCall.speaking": "{name} is speaking", + "groupCall.pin": "Show {name} large", + "groupCall.unpin": "Back to everyone", + "groupCall.showEveryone": "Everyone" }, "de": { "language.label": "Sprache", + "language.suggest.text": "Diese Seite gibt es auch auf Deutsch.", + "language.suggest.cta": "Auf Deutsch lesen", + "language.suggest.dismiss": "Schlie\xDFen", "community.title": "Gestalten Sie die Zukunft der Privatsph\xE4re mit", "community.description": "SecureBit w\xE4chst durch seine Community. Ihre Ideen und Ihr Feedback pr\xE4gen die Zukunft sicherer Kommunikation - offen entwickelt, mit vollst\xE4ndiger ASN.1-Validierung von Ende zu Ende.", "community.github": "GitHub-Repository", @@ -5907,10 +5935,35 @@ var DICTIONARIES = { "offline.disconnect": "Trennen", "offline.learnMore": "Mehr erfahren", "pwa.installApp": "App installieren", - "chat.onWeb": "Sie nutzen die Web-Version" + "chat.onWeb": "Sie nutzen die Web-Version", + "groupCall.startVoice": "Gruppen-Sprachanruf starten", + "groupCall.startVideo": "Gruppen-Videoanruf starten", + "groupCall.join": "Beitreten", + "groupCall.dismiss": "Jetzt nicht", + "groupCall.leave": "Anruf verlassen", + "groupCall.you": "Du", + "groupCall.connecting": "Verbinden\u2026", + "groupCall.waitingLink": "Warten auf eine direkte Verbindung\u2026", + "groupCall.legFailed": "Verbindung fehlgeschlagen", + "groupCall.startedVoice": "{name} hat einen Sprachanruf gestartet", + "groupCall.startedVideo": "{name} hat einen Videoanruf gestartet", + "groupCall.inCall": "{count} im Anruf", + "groupCall.err.permission_denied": "Der Anruf konnte nicht starten - der Zugriff auf Mikrofon und Kamera ist blockiert. Erlaube ihn f\xFCr diese Seite und versuche es erneut.", + "groupCall.err.device_not_found": "Der Anruf konnte nicht starten - auf diesem Ger\xE4t wurde kein Mikrofon gefunden.", + "groupCall.err.device_busy": "Der Anruf konnte nicht starten - dein Mikrofon wird von einer anderen App verwendet. Schlie\xDFe sie und versuche es erneut.", + "groupCall.err.media_failed": "Der Anruf konnte nicht starten - das Mikrofon lie\xDF sich nicht \xF6ffnen.", + "groupCall.err.call_in_progress": "In dieser Gruppe l\xE4uft bereits ein Anruf. Tritt ihm bei, statt einen neuen zu starten.", + "groupCall.err.not_ready": "Best\xE4tige zuerst den Gruppencode, bevor du anrufst.", + "groupCall.speaking": "{name} spricht", + "groupCall.pin": "{name} gro\xDF anzeigen", + "groupCall.unpin": "Zur\xFCck zu allen", + "groupCall.showEveryone": "Alle" }, "fr": { "language.label": "Langue", + "language.suggest.text": "Cette page est aussi disponible en fran\xE7ais.", + "language.suggest.cta": "Lire en fran\xE7ais", + "language.suggest.dismiss": "Fermer", "community.title": "Construisez l'avenir de la vie priv\xE9e", "community.description": "SecureBit grandit gr\xE2ce \xE0 sa communaut\xE9. Vos id\xE9es et vos retours fa\xE7onnent l'avenir des communications s\xE9curis\xE9es, d\xE9velopp\xE9es \xE0 ciel ouvert, avec une validation ASN.1 compl\xE8te de bout en bout.", "community.github": "D\xE9p\xF4t GitHub", @@ -6656,10 +6709,35 @@ var DICTIONARIES = { "offline.disconnect": "Se d\xE9connecter", "offline.learnMore": "En savoir plus", "pwa.installApp": "Installer l'application", - "chat.onWeb": "Vous \xEAtes sur la version web" + "chat.onWeb": "Vous \xEAtes sur la version web", + "groupCall.startVoice": "D\xE9marrer un appel vocal de groupe", + "groupCall.startVideo": "D\xE9marrer un appel vid\xE9o de groupe", + "groupCall.join": "Rejoindre", + "groupCall.dismiss": "Pas maintenant", + "groupCall.leave": "Quitter l'appel", + "groupCall.you": "Vous", + "groupCall.connecting": "Connexion\u2026", + "groupCall.waitingLink": "En attente d'un lien direct\u2026", + "groupCall.legFailed": "Connexion impossible", + "groupCall.startedVoice": "{name} a d\xE9marr\xE9 un appel vocal", + "groupCall.startedVideo": "{name} a d\xE9marr\xE9 un appel vid\xE9o", + "groupCall.inCall": "{count} dans l'appel", + "groupCall.err.permission_denied": "L'appel n'a pas pu d\xE9marrer - l'acc\xE8s au micro et \xE0 la cam\xE9ra est bloqu\xE9. Autorisez-le pour ce site, puis r\xE9essayez.", + "groupCall.err.device_not_found": "L'appel n'a pas pu d\xE9marrer - aucun micro n'a \xE9t\xE9 trouv\xE9 sur cet appareil.", + "groupCall.err.device_busy": "L'appel n'a pas pu d\xE9marrer - votre micro est utilis\xE9 par une autre application. Fermez-la et r\xE9essayez.", + "groupCall.err.media_failed": "L'appel n'a pas pu d\xE9marrer - le micro n'a pas pu \xEAtre ouvert.", + "groupCall.err.call_in_progress": "Un appel est d\xE9j\xE0 en cours dans ce groupe. Rejoignez-le plut\xF4t que d'en d\xE9marrer un autre.", + "groupCall.err.not_ready": "Confirmez le code du groupe avant d'appeler.", + "groupCall.speaking": "{name} parle", + "groupCall.pin": "Afficher {name} en grand", + "groupCall.unpin": "Revenir \xE0 tous", + "groupCall.showEveryone": "Tout le monde" }, "es": { "language.label": "Idioma", + "language.suggest.text": "Esta p\xE1gina tambi\xE9n est\xE1 disponible en espa\xF1ol.", + "language.suggest.cta": "Leer en espa\xF1ol", + "language.suggest.dismiss": "Cerrar", "community.title": "Construye el futuro de la privacidad", "community.description": "SecureBit crece gracias a su comunidad. Tus ideas y comentarios dan forma al futuro de la comunicaci\xF3n segura, desarrollada de forma abierta y con validaci\xF3n ASN.1 completa de extremo a extremo.", "community.github": "Repositorio en GitHub", @@ -7405,10 +7483,35 @@ var DICTIONARIES = { "offline.disconnect": "Desconectar", "offline.learnMore": "M\xE1s informaci\xF3n", "pwa.installApp": "Instalar la app", - "chat.onWeb": "Est\xE1s en la versi\xF3n web" + "chat.onWeb": "Est\xE1s en la versi\xF3n web", + "groupCall.startVoice": "Iniciar llamada de voz grupal", + "groupCall.startVideo": "Iniciar videollamada grupal", + "groupCall.join": "Unirse", + "groupCall.dismiss": "Ahora no", + "groupCall.leave": "Salir de la llamada", + "groupCall.you": "T\xFA", + "groupCall.connecting": "Conectando\u2026", + "groupCall.waitingLink": "Esperando un enlace directo\u2026", + "groupCall.legFailed": "No se pudo conectar", + "groupCall.startedVoice": "{name} inici\xF3 una llamada de voz", + "groupCall.startedVideo": "{name} inici\xF3 una videollamada", + "groupCall.inCall": "{count} en la llamada", + "groupCall.err.permission_denied": "La llamada no pudo iniciarse - el acceso al micr\xF3fono y a la c\xE1mara est\xE1 bloqueado. Perm\xEDtelo para este sitio e int\xE9ntalo de nuevo.", + "groupCall.err.device_not_found": "La llamada no pudo iniciarse - no se encontr\xF3 ning\xFAn micr\xF3fono en este dispositivo.", + "groupCall.err.device_busy": "La llamada no pudo iniciarse - otra aplicaci\xF3n est\xE1 usando tu micr\xF3fono. Ci\xE9rrala e int\xE9ntalo de nuevo.", + "groupCall.err.media_failed": "La llamada no pudo iniciarse - no se pudo abrir el micr\xF3fono.", + "groupCall.err.call_in_progress": "Ya hay una llamada en este grupo. \xDAnete en lugar de iniciar otra.", + "groupCall.err.not_ready": "Confirma el c\xF3digo del grupo antes de llamar.", + "groupCall.speaking": "{name} est\xE1 hablando", + "groupCall.pin": "Ver a {name} en grande", + "groupCall.unpin": "Volver a todos", + "groupCall.showEveryone": "Todos" }, "uk": { "language.label": "\u041C\u043E\u0432\u0430", + "language.suggest.text": "\u0426\u044F \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0430 \u0442\u0430\u043A\u043E\u0436 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0443\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u043E\u044E.", + "language.suggest.cta": "\u0427\u0438\u0442\u0430\u0442\u0438 \u0443\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u043E\u044E", + "language.suggest.dismiss": "\u0417\u0430\u043A\u0440\u0438\u0442\u0438", "community.title": "\u0414\u043E\u043B\u0443\u0447\u0430\u0439\u0442\u0435\u0441\u044F \u0434\u043E \u043C\u0430\u0439\u0431\u0443\u0442\u043D\u044C\u043E\u0433\u043E \u043F\u0440\u0438\u0432\u0430\u0442\u043D\u043E\u0441\u0442\u0456", "community.description": "SecureBit \u0440\u043E\u0441\u0442\u0435 \u0437\u0430\u0432\u0434\u044F\u043A\u0438 \u0441\u0432\u043E\u0457\u0439 \u0441\u043F\u0456\u043B\u044C\u043D\u043E\u0442\u0456. \u0412\u0430\u0448\u0456 \u0456\u0434\u0435\u0457 \u0442\u0430 \u0432\u0456\u0434\u0433\u0443\u043A\u0438 \u0444\u043E\u0440\u043C\u0443\u044E\u0442\u044C \u043C\u0430\u0439\u0431\u0443\u0442\u043D\u0454 \u0437\u0430\u0445\u0438\u0449\u0435\u043D\u043E\u0433\u043E \u0441\u043F\u0456\u043B\u043A\u0443\u0432\u0430\u043D\u043D\u044F - \u0440\u043E\u0437\u0440\u043E\u0431\u043A\u0430 \u0432\u0435\u0434\u0435\u0442\u044C\u0441\u044F \u0432\u0456\u0434\u043A\u0440\u0438\u0442\u043E, \u0437 \u043F\u043E\u0432\u043D\u043E\u044E \u0432\u0430\u043B\u0456\u0434\u0430\u0446\u0456\u0454\u044E ASN.1 \u043D\u0430 \u0432\u0441\u044C\u043E\u043C\u0443 \u0448\u043B\u044F\u0445\u0443.", "community.github": "\u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0456\u0439 \u043D\u0430 GitHub", @@ -8154,10 +8257,35 @@ var DICTIONARIES = { "offline.disconnect": "\u0412\u0456\u0434'\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F", "offline.learnMore": "\u0414\u043E\u043A\u043B\u0430\u0434\u043D\u0456\u0448\u0435", "pwa.installApp": "\u0412\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0438 \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043E\u043A", - "chat.onWeb": "\u0412\u0438 \u0443 \u0432\u0435\u0431\u0432\u0435\u0440\u0441\u0456\u0457" + "chat.onWeb": "\u0412\u0438 \u0443 \u0432\u0435\u0431\u0432\u0435\u0440\u0441\u0456\u0457", + "groupCall.startVoice": "\u041F\u043E\u0447\u0430\u0442\u0438 \u0433\u0440\u0443\u043F\u043E\u0432\u0438\u0439 \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0438\u0439 \u0434\u0437\u0432\u0456\u043D\u043E\u043A", + "groupCall.startVideo": "\u041F\u043E\u0447\u0430\u0442\u0438 \u0433\u0440\u0443\u043F\u043E\u0432\u0438\u0439 \u0432\u0456\u0434\u0435\u043E\u0434\u0437\u0432\u0456\u043D\u043E\u043A", + "groupCall.join": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F", + "groupCall.dismiss": "\u041D\u0435 \u0437\u0430\u0440\u0430\u0437", + "groupCall.leave": "\u0412\u0438\u0439\u0442\u0438 \u0437 \u0434\u0437\u0432\u0456\u043D\u043A\u0430", + "groupCall.you": "\u0412\u0438", + "groupCall.connecting": "\u0417'\u0454\u0434\u043D\u0430\u043D\u043D\u044F\u2026", + "groupCall.waitingLink": "\u041E\u0447\u0456\u043A\u0443\u0432\u0430\u043D\u043D\u044F \u043F\u0440\u044F\u043C\u043E\u0433\u043E \u0437'\u0454\u0434\u043D\u0430\u043D\u043D\u044F\u2026", + "groupCall.legFailed": "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0437'\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F", + "groupCall.startedVoice": "{name} \u043F\u043E\u0447\u0430\u0432 \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0438\u0439 \u0434\u0437\u0432\u0456\u043D\u043E\u043A", + "groupCall.startedVideo": "{name} \u043F\u043E\u0447\u0430\u0432 \u0432\u0456\u0434\u0435\u043E\u0434\u0437\u0432\u0456\u043D\u043E\u043A", + "groupCall.inCall": "{count} \u0443 \u0434\u0437\u0432\u0456\u043D\u043A\u0443", + "groupCall.err.permission_denied": "\u0414\u0437\u0432\u0456\u043D\u043E\u043A \u043D\u0435 \u043F\u043E\u0447\u0430\u0432\u0441\u044F - \u0434\u043E\u0441\u0442\u0443\u043F \u0434\u043E \u043C\u0456\u043A\u0440\u043E\u0444\u043E\u043D\u0430 \u0439 \u043A\u0430\u043C\u0435\u0440\u0438 \u0437\u0430\u0431\u043B\u043E\u043A\u043E\u0432\u0430\u043D\u043E. \u0414\u043E\u0437\u0432\u043E\u043B\u044C\u0442\u0435 \u0439\u043E\u0433\u043E \u0434\u043B\u044F \u0446\u044C\u043E\u0433\u043E \u0441\u0430\u0439\u0442\u0443 \u0439 \u0441\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "groupCall.err.device_not_found": "\u0414\u0437\u0432\u0456\u043D\u043E\u043A \u043D\u0435 \u043F\u043E\u0447\u0430\u0432\u0441\u044F - \u043D\u0430 \u0446\u044C\u043E\u043C\u0443 \u043F\u0440\u0438\u0441\u0442\u0440\u043E\u0457 \u043D\u0435 \u0437\u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043C\u0456\u043A\u0440\u043E\u0444\u043E\u043D\u0430.", + "groupCall.err.device_busy": "\u0414\u0437\u0432\u0456\u043D\u043E\u043A \u043D\u0435 \u043F\u043E\u0447\u0430\u0432\u0441\u044F - \u043C\u0456\u043A\u0440\u043E\u0444\u043E\u043D \u0437\u0430\u0439\u043D\u044F\u0442\u0438\u0439 \u0456\u043D\u0448\u0438\u043C \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043A\u043E\u043C. \u0417\u0430\u043A\u0440\u0438\u0439\u0442\u0435 \u0439\u043E\u0433\u043E \u0439 \u0441\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "groupCall.err.media_failed": "\u0414\u0437\u0432\u0456\u043D\u043E\u043A \u043D\u0435 \u043F\u043E\u0447\u0430\u0432\u0441\u044F - \u043D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0432\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043C\u0456\u043A\u0440\u043E\u0444\u043E\u043D.", + "groupCall.err.call_in_progress": "\u0423 \u0446\u0456\u0439 \u0433\u0440\u0443\u043F\u0456 \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454 \u0434\u0437\u0432\u0456\u043D\u043E\u043A. \u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0439\u0442\u0435\u0441\u044F \u0434\u043E \u043D\u044C\u043E\u0433\u043E, \u0430 \u043D\u0435 \u043F\u043E\u0447\u0438\u043D\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u0438\u0439.", + "groupCall.err.not_ready": "\u0421\u043F\u0435\u0440\u0448\u0443 \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u044C\u0442\u0435 \u043A\u043E\u0434 \u0433\u0440\u0443\u043F\u0438.", + "groupCall.speaking": "{name} \u0433\u043E\u0432\u043E\u0440\u0438\u0442\u044C", + "groupCall.pin": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 {name} \u0432\u0435\u043B\u0438\u043A\u0438\u043C", + "groupCall.unpin": "\u041F\u043E\u0432\u0435\u0440\u043D\u0443\u0442\u0438\u0441\u044F \u0434\u043E \u0432\u0441\u0456\u0445", + "groupCall.showEveryone": "\u0423\u0441\u0456" }, "ru": { "language.label": "\u042F\u0437\u044B\u043A", + "language.suggest.text": "\u042D\u0442\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0442\u0430\u043A\u0436\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u0430 \u0440\u0443\u0441\u0441\u043A\u043E\u043C.", + "language.suggest.cta": "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E-\u0440\u0443\u0441\u0441\u043A\u0438", + "language.suggest.dismiss": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C", "community.title": "\u041F\u0440\u0438\u0441\u043E\u0435\u0434\u0438\u043D\u044F\u0439\u0442\u0435\u0441\u044C \u043A \u0431\u0443\u0434\u0443\u0449\u0435\u043C\u0443 \u043F\u0440\u0438\u0432\u0430\u0442\u043D\u043E\u0441\u0442\u0438", "community.description": "SecureBit \u0440\u0430\u0441\u0442\u0451\u0442 \u0431\u043B\u0430\u0433\u043E\u0434\u0430\u0440\u044F \u0441\u043E\u043E\u0431\u0449\u0435\u0441\u0442\u0432\u0443. \u0412\u0430\u0448\u0438 \u0438\u0434\u0435\u0438 \u0438 \u043E\u0442\u0437\u044B\u0432\u044B \u0444\u043E\u0440\u043C\u0438\u0440\u0443\u044E\u0442 \u0431\u0443\u0434\u0443\u0449\u0435\u0435 \u0437\u0430\u0449\u0438\u0449\u0451\u043D\u043D\u043E\u0433\u043E \u043E\u0431\u0449\u0435\u043D\u0438\u044F - \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u043A\u0430 \u0432\u0435\u0434\u0451\u0442\u0441\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E, \u0441 \u043F\u043E\u043B\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u043E\u0439 ASN.1 \u043D\u0430 \u0432\u0441\u0451\u043C \u043F\u0443\u0442\u0438.", "community.github": "\u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043D\u0430 GitHub", @@ -8903,10 +9031,35 @@ var DICTIONARIES = { "offline.disconnect": "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F", "offline.learnMore": "\u041F\u043E\u0434\u0440\u043E\u0431\u043D\u0435\u0435", "pwa.installApp": "\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435", - "chat.onWeb": "\u0412\u044B \u0432 \u0432\u0435\u0431-\u0432\u0435\u0440\u0441\u0438\u0438" + "chat.onWeb": "\u0412\u044B \u0432 \u0432\u0435\u0431-\u0432\u0435\u0440\u0441\u0438\u0438", + "groupCall.startVoice": "\u041D\u0430\u0447\u0430\u0442\u044C \u0433\u0440\u0443\u043F\u043F\u043E\u0432\u043E\u0439 \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u043E\u0439 \u0437\u0432\u043E\u043D\u043E\u043A", + "groupCall.startVideo": "\u041D\u0430\u0447\u0430\u0442\u044C \u0433\u0440\u0443\u043F\u043F\u043E\u0432\u043E\u0439 \u0432\u0438\u0434\u0435\u043E\u0437\u0432\u043E\u043D\u043E\u043A", + "groupCall.join": "\u041F\u0440\u0438\u0441\u043E\u0435\u0434\u0438\u043D\u0438\u0442\u044C\u0441\u044F", + "groupCall.dismiss": "\u041D\u0435 \u0441\u0435\u0439\u0447\u0430\u0441", + "groupCall.leave": "\u0412\u044B\u0439\u0442\u0438 \u0438\u0437 \u0437\u0432\u043E\u043D\u043A\u0430", + "groupCall.you": "\u0412\u044B", + "groupCall.connecting": "\u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435\u2026", + "groupCall.waitingLink": "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043F\u0440\u044F\u043C\u043E\u0433\u043E \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F\u2026", + "groupCall.legFailed": "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0435\u0434\u0438\u043D\u0438\u0442\u044C\u0441\u044F", + "groupCall.startedVoice": "{name} \u043D\u0430\u0447\u0430\u043B \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u043E\u0439 \u0437\u0432\u043E\u043D\u043E\u043A", + "groupCall.startedVideo": "{name} \u043D\u0430\u0447\u0430\u043B \u0432\u0438\u0434\u0435\u043E\u0437\u0432\u043E\u043D\u043E\u043A", + "groupCall.inCall": "{count} \u0432 \u0437\u0432\u043E\u043D\u043A\u0435", + "groupCall.err.permission_denied": "\u0417\u0432\u043E\u043D\u043E\u043A \u043D\u0435 \u043D\u0430\u0447\u0430\u043B\u0441\u044F - \u0434\u043E\u0441\u0442\u0443\u043F \u043A \u043C\u0438\u043A\u0440\u043E\u0444\u043E\u043D\u0443 \u0438 \u043A\u0430\u043C\u0435\u0440\u0435 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D. \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0435\u0433\u043E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0441\u0430\u0439\u0442\u0430 \u0438 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", + "groupCall.err.device_not_found": "\u0417\u0432\u043E\u043D\u043E\u043A \u043D\u0435 \u043D\u0430\u0447\u0430\u043B\u0441\u044F - \u043D\u0430 \u044D\u0442\u043E\u043C \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043C\u0438\u043A\u0440\u043E\u0444\u043E\u043D.", + "groupCall.err.device_busy": "\u0417\u0432\u043E\u043D\u043E\u043A \u043D\u0435 \u043D\u0430\u0447\u0430\u043B\u0441\u044F - \u043C\u0438\u043A\u0440\u043E\u0444\u043E\u043D \u0437\u0430\u043D\u044F\u0442 \u0434\u0440\u0443\u0433\u0438\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u043C. \u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 \u0435\u0433\u043E \u0438 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", + "groupCall.err.media_failed": "\u0417\u0432\u043E\u043D\u043E\u043A \u043D\u0435 \u043D\u0430\u0447\u0430\u043B\u0441\u044F - \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043C\u0438\u043A\u0440\u043E\u0444\u043E\u043D.", + "groupCall.err.call_in_progress": "\u0412 \u044D\u0442\u043E\u0439 \u0433\u0440\u0443\u043F\u043F\u0435 \u0443\u0436\u0435 \u0438\u0434\u0451\u0442 \u0437\u0432\u043E\u043D\u043E\u043A. \u041F\u0440\u0438\u0441\u043E\u0435\u0434\u0438\u043D\u0438\u0442\u0435\u0441\u044C \u043A \u043D\u0435\u043C\u0443, \u0430 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u044B\u0439.", + "groupCall.err.not_ready": "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u043A\u043E\u0434 \u0433\u0440\u0443\u043F\u043F\u044B.", + "groupCall.speaking": "{name} \u0433\u043E\u0432\u043E\u0440\u0438\u0442", + "groupCall.pin": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C {name} \u043A\u0440\u0443\u043F\u043D\u043E", + "groupCall.unpin": "\u0412\u0435\u0440\u043D\u0443\u0442\u044C\u0441\u044F \u043A\u043E \u0432\u0441\u0435\u043C", + "groupCall.showEveryone": "\u0412\u0441\u0435" }, "zh": { "language.label": "\u8BED\u8A00", + "language.suggest.text": "\u672C\u9875\u9762\u4E5F\u6709\u7B80\u4F53\u4E2D\u6587\u7248\u672C\u3002", + "language.suggest.cta": "\u9605\u8BFB\u7B80\u4F53\u4E2D\u6587", + "language.suggest.dismiss": "\u5173\u95ED", "community.title": "\u4E00\u8D77\u6784\u7B51\u9690\u79C1\u7684\u672A\u6765", "community.description": "SecureBit \u56E0\u793E\u533A\u800C\u6210\u957F\u3002\u4F60\u7684\u60F3\u6CD5\u4E0E\u53CD\u9988\u5851\u9020\u7740\u5B89\u5168\u901A\u8BAF\u7684\u672A\u6765 \u2014\u2014 \u5F00\u653E\u5F00\u53D1\uFF0C\u5168\u7A0B\u5B8C\u6574\u7684 ASN.1 \u6821\u9A8C\u3002", "community.github": "GitHub \u4ED3\u5E93", @@ -9652,10 +9805,35 @@ var DICTIONARIES = { "offline.disconnect": "\u65AD\u5F00\u8FDE\u63A5", "offline.learnMore": "\u4E86\u89E3\u66F4\u591A", "pwa.installApp": "\u5B89\u88C5\u5E94\u7528", - "chat.onWeb": "\u4F60\u6B63\u5728\u4F7F\u7528\u7F51\u9875\u7248" + "chat.onWeb": "\u4F60\u6B63\u5728\u4F7F\u7528\u7F51\u9875\u7248", + "groupCall.startVoice": "\u53D1\u8D77\u7FA4\u7EC4\u8BED\u97F3\u901A\u8BDD", + "groupCall.startVideo": "\u53D1\u8D77\u7FA4\u7EC4\u89C6\u9891\u901A\u8BDD", + "groupCall.join": "\u52A0\u5165", + "groupCall.dismiss": "\u6682\u4E0D\u52A0\u5165", + "groupCall.leave": "\u79BB\u5F00\u901A\u8BDD", + "groupCall.you": "\u4F60", + "groupCall.connecting": "\u8FDE\u63A5\u4E2D\u2026", + "groupCall.waitingLink": "\u6B63\u5728\u7B49\u5F85\u76F4\u8FDE\u2026", + "groupCall.legFailed": "\u65E0\u6CD5\u8FDE\u63A5", + "groupCall.startedVoice": "{name} \u53D1\u8D77\u4E86\u8BED\u97F3\u901A\u8BDD", + "groupCall.startedVideo": "{name} \u53D1\u8D77\u4E86\u89C6\u9891\u901A\u8BDD", + "groupCall.inCall": "{count} \u4EBA\u5728\u901A\u8BDD\u4E2D", + "groupCall.err.permission_denied": "\u901A\u8BDD\u65E0\u6CD5\u5F00\u59CB - \u9EA6\u514B\u98CE\u548C\u6444\u50CF\u5934\u6743\u9650\u88AB\u62D2\u7EDD\u3002\u8BF7\u4E3A\u672C\u7AD9\u70B9\u5141\u8BB8\u540E\u91CD\u8BD5\u3002", + "groupCall.err.device_not_found": "\u901A\u8BDD\u65E0\u6CD5\u5F00\u59CB - \u6B64\u8BBE\u5907\u4E0A\u672A\u627E\u5230\u9EA6\u514B\u98CE\u3002", + "groupCall.err.device_busy": "\u901A\u8BDD\u65E0\u6CD5\u5F00\u59CB - \u9EA6\u514B\u98CE\u6B63\u88AB\u5176\u4ED6\u5E94\u7528\u5360\u7528\u3002\u8BF7\u5173\u95ED\u540E\u91CD\u8BD5\u3002", + "groupCall.err.media_failed": "\u901A\u8BDD\u65E0\u6CD5\u5F00\u59CB - \u65E0\u6CD5\u6253\u5F00\u9EA6\u514B\u98CE\u3002", + "groupCall.err.call_in_progress": "\u8BE5\u7FA4\u7EC4\u5DF2\u6709\u901A\u8BDD\u6B63\u5728\u8FDB\u884C\u3002\u8BF7\u52A0\u5165\uFF0C\u800C\u4E0D\u662F\u53E6\u5F00\u4E00\u4E2A\u3002", + "groupCall.err.not_ready": "\u8BF7\u5148\u786E\u8BA4\u7FA4\u7EC4\u5B89\u5168\u7801\u518D\u901A\u8BDD\u3002", + "groupCall.speaking": "{name} \u6B63\u5728\u8BB2\u8BDD", + "groupCall.pin": "\u653E\u5927\u663E\u793A {name}", + "groupCall.unpin": "\u8FD4\u56DE\u6240\u6709\u4EBA", + "groupCall.showEveryone": "\u6240\u6709\u4EBA" }, "ko": { "language.label": "\uC5B8\uC5B4", + "language.suggest.text": "\uC774 \uD398\uC774\uC9C0\uB294 \uD55C\uAD6D\uC5B4\uB85C\uB3C4 \uBCFC \uC218 \uC788\uC2B5\uB2C8\uB2E4.", + "language.suggest.cta": "\uD55C\uAD6D\uC5B4\uB85C \uBCF4\uAE30", + "language.suggest.dismiss": "\uB2EB\uAE30", "community.title": "\uD504\uB77C\uC774\uBC84\uC2DC\uC758 \uBBF8\uB798\uB97C \uD568\uAED8 \uB9CC\uB4ED\uB2C8\uB2E4", "community.description": "SecureBit\uC740 \uCEE4\uBBA4\uB2C8\uD2F0 \uB355\uBD84\uC5D0 \uC790\uB78D\uB2C8\uB2E4. \uC5EC\uB7EC\uBD84\uC758 \uC544\uC774\uB514\uC5B4\uC640 \uC758\uACAC\uC774 \uC548\uC804\uD55C \uC18C\uD1B5\uC758 \uBBF8\uB798\uB97C \uB9CC\uB4ED\uB2C8\uB2E4 - \uBAA8\uB4E0 \uACFC\uC815\uC774 \uACF5\uAC1C\uB418\uC5B4 \uC788\uACE0, \uC804 \uAD6C\uAC04\uC5D0\uC11C ASN.1 \uAC80\uC99D\uC744 \uC218\uD589\uD569\uB2C8\uB2E4.", "community.github": "GitHub \uC800\uC7A5\uC18C", @@ -10401,10 +10579,35 @@ var DICTIONARIES = { "offline.disconnect": "\uC5F0\uACB0 \uB04A\uAE30", "offline.learnMore": "\uC790\uC138\uD788 \uBCF4\uAE30", "pwa.installApp": "\uC571 \uC124\uCE58", - "chat.onWeb": "\uC6F9 \uBC84\uC804\uC744 \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4" + "chat.onWeb": "\uC6F9 \uBC84\uC804\uC744 \uC4F0\uACE0 \uC788\uC2B5\uB2C8\uB2E4", + "groupCall.startVoice": "\uADF8\uB8F9 \uC74C\uC131 \uD1B5\uD654 \uC2DC\uC791", + "groupCall.startVideo": "\uADF8\uB8F9 \uC601\uC0C1 \uD1B5\uD654 \uC2DC\uC791", + "groupCall.join": "\uCC38\uC5EC", + "groupCall.dismiss": "\uB098\uC911\uC5D0", + "groupCall.leave": "\uD1B5\uD654 \uB098\uAC00\uAE30", + "groupCall.you": "\uB098", + "groupCall.connecting": "\uC5F0\uACB0 \uC911\u2026", + "groupCall.waitingLink": "\uC9C1\uC811 \uC5F0\uACB0\uC744 \uAE30\uB2E4\uB9AC\uB294 \uC911\u2026", + "groupCall.legFailed": "\uC5F0\uACB0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4", + "groupCall.startedVoice": "{name} \uB2D8\uC774 \uC74C\uC131 \uD1B5\uD654\uB97C \uC2DC\uC791\uD588\uC2B5\uB2C8\uB2E4", + "groupCall.startedVideo": "{name} \uB2D8\uC774 \uC601\uC0C1 \uD1B5\uD654\uB97C \uC2DC\uC791\uD588\uC2B5\uB2C8\uB2E4", + "groupCall.inCall": "\uD1B5\uD654 \uC911 {count}\uBA85", + "groupCall.err.permission_denied": "\uD1B5\uD654\uB97C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 - \uB9C8\uC774\uD06C\uC640 \uCE74\uBA54\uB77C \uC811\uADFC\uC774 \uCC28\uB2E8\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC774 \uC0AC\uC774\uD2B8\uC5D0 \uD5C8\uC6A9\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.", + "groupCall.err.device_not_found": "\uD1B5\uD654\uB97C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 - \uC774 \uAE30\uAE30\uC5D0\uC11C \uB9C8\uC774\uD06C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", + "groupCall.err.device_busy": "\uD1B5\uD654\uB97C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 - \uB9C8\uC774\uD06C\uB97C \uB2E4\uB978 \uC571\uC774 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4. \uC885\uB8CC\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.", + "groupCall.err.media_failed": "\uD1B5\uD654\uB97C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 - \uB9C8\uC774\uD06C\uB97C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", + "groupCall.err.call_in_progress": "\uC774 \uADF8\uB8F9\uC5D0\uC11C \uC774\uBBF8 \uD1B5\uD654\uAC00 \uC9C4\uD589 \uC911\uC785\uB2C8\uB2E4. \uC0C8\uB85C \uC2DC\uC791\uD558\uC9C0 \uB9D0\uACE0 \uCC38\uC5EC\uD558\uC138\uC694.", + "groupCall.err.not_ready": "\uD1B5\uD654\uD558\uAE30 \uC804\uC5D0 \uADF8\uB8F9 \uCF54\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.", + "groupCall.speaking": "{name} \uB2D8\uC774 \uB9D0\uD558\uB294 \uC911", + "groupCall.pin": "{name} \uD06C\uAC8C \uBCF4\uAE30", + "groupCall.unpin": "\uC804\uCCB4 \uBCF4\uAE30\uB85C \uB3CC\uC544\uAC00\uAE30", + "groupCall.showEveryone": "\uC804\uCCB4" }, "hi": { "language.label": "\u092D\u093E\u0937\u093E", + "language.suggest.text": "\u092F\u0939 \u092A\u0943\u0937\u094D\u0920 \u0939\u093F\u0928\u094D\u0926\u0940 \u092E\u0947\u0902 \u092D\u0940 \u0909\u092A\u0932\u092C\u094D\u0927 \u0939\u0948\u0964", + "language.suggest.cta": "\u0939\u093F\u0928\u094D\u0926\u0940 \u092E\u0947\u0902 \u092A\u0922\u093C\u0947\u0902", + "language.suggest.dismiss": "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902", "community.title": "\u0928\u093F\u091C\u0924\u093E \u0915\u0947 \u092D\u0935\u093F\u0937\u094D\u092F \u092E\u0947\u0902 \u0936\u093E\u092E\u093F\u0932 \u0939\u094B\u0902", "community.description": "SecureBit \u0905\u092A\u0928\u0947 \u0938\u092E\u0941\u0926\u093E\u092F \u0938\u0947 \u092C\u0922\u093C\u0924\u093E \u0939\u0948\u0964 \u0906\u092A\u0915\u0947 \u0935\u093F\u091A\u093E\u0930 \u0914\u0930 \u0938\u0941\u091D\u093E\u0935 \u0938\u0941\u0930\u0915\u094D\u0937\u093F\u0924 \u0938\u0902\u0935\u093E\u0926 \u0915\u093E \u092D\u0935\u093F\u0937\u094D\u092F \u0917\u0922\u093C\u0924\u0947 \u0939\u0948\u0902 - \u0916\u0941\u0932\u0947 \u092E\u0947\u0902 \u092C\u0928\u093E, \u0914\u0930 \u092A\u0942\u0930\u0947 \u0930\u093E\u0938\u094D\u0924\u0947 \u092A\u0942\u0930\u0940 ASN.1 \u091C\u093E\u0901\u091A \u0915\u0947 \u0938\u093E\u0925\u0964", "community.github": "GitHub \u0930\u093F\u092A\u0949\u091C\u093C\u093F\u091F\u0930\u0940", @@ -11150,10 +11353,35 @@ var DICTIONARIES = { "offline.disconnect": "\u091C\u0941\u0921\u093C\u093E\u0935 \u0924\u094B\u0921\u093C\u0947\u0902", "offline.learnMore": "\u0914\u0930 \u091C\u093E\u0928\u0947\u0902", "pwa.installApp": "\u0910\u092A \u0907\u0902\u0938\u094D\u091F\u0949\u0932 \u0915\u0930\u0947\u0902", - "chat.onWeb": "\u0906\u092A \u0935\u0947\u092C \u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u092A\u0930 \u0939\u0948\u0902" + "chat.onWeb": "\u0906\u092A \u0935\u0947\u092C \u0938\u0902\u0938\u094D\u0915\u0930\u0923 \u092A\u0930 \u0939\u0948\u0902", + "groupCall.startVoice": "\u0938\u092E\u0942\u0939 \u0935\u0949\u0907\u0938 \u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902", + "groupCall.startVideo": "\u0938\u092E\u0942\u0939 \u0935\u0940\u0921\u093F\u092F\u094B \u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0915\u0930\u0947\u0902", + "groupCall.join": "\u0936\u093E\u092E\u093F\u0932 \u0939\u094B\u0902", + "groupCall.dismiss": "\u0905\u092D\u0940 \u0928\u0939\u0940\u0902", + "groupCall.leave": "\u0915\u0949\u0932 \u091B\u094B\u0921\u093C\u0947\u0902", + "groupCall.you": "\u0906\u092A", + "groupCall.connecting": "\u091C\u0941\u0921\u093C \u0930\u0939\u093E \u0939\u0948\u2026", + "groupCall.waitingLink": "\u0938\u0940\u0927\u0947 \u0932\u093F\u0902\u0915 \u0915\u0940 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E\u2026", + "groupCall.legFailed": "\u0915\u0928\u0947\u0915\u094D\u091F \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u093E", + "groupCall.startedVoice": "{name} \u0928\u0947 \u0935\u0949\u0907\u0938 \u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0915\u0940", + "groupCall.startedVideo": "{name} \u0928\u0947 \u0935\u0940\u0921\u093F\u092F\u094B \u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0915\u0940", + "groupCall.inCall": "\u0915\u0949\u0932 \u092E\u0947\u0902 {count}", + "groupCall.err.permission_denied": "\u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940 - \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0914\u0930 \u0915\u0948\u092E\u0930\u0947 \u0915\u0940 \u0905\u0928\u0941\u092E\u0924\u093F \u0905\u0935\u0930\u0941\u0926\u094D\u0927 \u0939\u0948\u0964 \u0907\u0938 \u0938\u093E\u0907\u091F \u0915\u0947 \u0932\u093F\u090F \u0905\u0928\u0941\u092E\u0924\u093F \u0926\u0947\u0902 \u0914\u0930 \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964", + "groupCall.err.device_not_found": "\u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940 - \u0907\u0938 \u0921\u093F\u0935\u093E\u0907\u0938 \u092A\u0930 \u0915\u094B\u0908 \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0928\u0939\u0940\u0902 \u092E\u093F\u0932\u093E\u0964", + "groupCall.err.device_busy": "\u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940 - \u0906\u092A\u0915\u093E \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0915\u093F\u0938\u0940 \u0905\u0928\u094D\u092F \u0910\u092A \u0915\u0947 \u0909\u092A\u092F\u094B\u0917 \u092E\u0947\u0902 \u0939\u0948\u0964 \u0909\u0938\u0947 \u092C\u0902\u0926 \u0915\u0930\u0915\u0947 \u092B\u093F\u0930 \u0915\u094B\u0936\u093F\u0936 \u0915\u0930\u0947\u0902\u0964", + "groupCall.err.media_failed": "\u0915\u0949\u0932 \u0936\u0941\u0930\u0942 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940 - \u092E\u093E\u0907\u0915\u094D\u0930\u094B\u092B\u093C\u094B\u0928 \u0928\u0939\u0940\u0902 \u0916\u0941\u0932 \u0938\u0915\u093E\u0964", + "groupCall.err.call_in_progress": "\u0907\u0938 \u0938\u092E\u0942\u0939 \u092E\u0947\u0902 \u092A\u0939\u0932\u0947 \u0938\u0947 \u090F\u0915 \u0915\u0949\u0932 \u091A\u0932 \u0930\u0939\u0940 \u0939\u0948\u0964 \u0928\u0908 \u0936\u0941\u0930\u0942 \u0915\u0930\u0928\u0947 \u0915\u0947 \u092C\u091C\u093E\u092F \u0909\u0938\u092E\u0947\u0902 \u0936\u093E\u092E\u093F\u0932 \u0939\u094B\u0902\u0964", + "groupCall.err.not_ready": "\u0915\u0949\u0932 \u0915\u0930\u0928\u0947 \u0938\u0947 \u092A\u0939\u0932\u0947 \u0938\u092E\u0942\u0939 \u0915\u094B\u0921 \u0915\u0940 \u092A\u0941\u0937\u094D\u091F\u093F \u0915\u0930\u0947\u0902\u0964", + "groupCall.speaking": "{name} \u092C\u094B\u0932 \u0930\u0939\u0947 \u0939\u0948\u0902", + "groupCall.pin": "{name} \u0915\u094B \u092C\u0921\u093C\u093E \u0926\u093F\u0916\u093E\u090F\u0901", + "groupCall.unpin": "\u0938\u092D\u0940 \u092A\u0930 \u0932\u094C\u091F\u0947\u0902", + "groupCall.showEveryone": "\u0938\u092D\u0940" }, "ar": { "language.label": "\u0627\u0644\u0644\u063A\u0629", + "language.suggest.text": "\u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629 \u0645\u062A\u0648\u0641\u0631\u0629 \u0623\u064A\u0636\u064B\u0627 \u0628\u0627\u0644\u0639\u0631\u0628\u064A\u0629.", + "language.suggest.cta": "\u0627\u0642\u0631\u0623 \u0628\u0627\u0644\u0639\u0631\u0628\u064A\u0629", + "language.suggest.dismiss": "\u0625\u063A\u0644\u0627\u0642", "community.title": "\u0627\u0646\u0636\u0645\u0651 \u0625\u0644\u0649 \u0645\u0633\u062A\u0642\u0628\u0644 \u0627\u0644\u062E\u0635\u0648\u0635\u064A\u0629", "community.description": "\u064A\u0646\u0645\u0648 SecureBit \u0628\u0641\u0636\u0644 \u0645\u062C\u062A\u0645\u0639\u0647. \u0623\u0641\u0643\u0627\u0631\u0643\u0645 \u0648\u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643\u0645 \u0647\u064A \u0645\u0627 \u064A\u0635\u0648\u063A \u0645\u0633\u062A\u0642\u0628\u0644 \u0627\u0644\u062A\u0648\u0627\u0635\u0644 \u0627\u0644\u0622\u0645\u0646 - \u0645\u0628\u0646\u064A\u0651 \u0641\u064A \u0627\u0644\u0639\u0644\u0646\u060C \u0645\u0639 \u062A\u062D\u0642\u0651\u0642 ASN.1 \u0643\u0627\u0645\u0644 \u0645\u0646 \u0637\u0631\u0641 \u0625\u0644\u0649 \u0637\u0631\u0641.", "community.github": "\u0645\u0633\u062A\u0648\u062F\u0639 GitHub", @@ -11899,10 +12127,35 @@ var DICTIONARIES = { "offline.disconnect": "\u0642\u0637\u0639 \u0627\u0644\u0627\u062A\u0635\u0627\u0644", "offline.learnMore": "\u0627\u0639\u0631\u0641 \u0627\u0644\u0645\u0632\u064A\u062F", "pwa.installApp": "\u062B\u0628\u0651\u062A \u0627\u0644\u062A\u0637\u0628\u064A\u0642", - "chat.onWeb": "\u0623\u0646\u062A \u0639\u0644\u0649 \u0627\u0644\u0648\u064A\u0628" + "chat.onWeb": "\u0623\u0646\u062A \u0639\u0644\u0649 \u0627\u0644\u0648\u064A\u0628", + "groupCall.startVoice": "\u0628\u062F\u0621 \u0645\u0643\u0627\u0644\u0645\u0629 \u0635\u0648\u062A\u064A\u0629 \u062C\u0645\u0627\u0639\u064A\u0629", + "groupCall.startVideo": "\u0628\u062F\u0621 \u0645\u0643\u0627\u0644\u0645\u0629 \u0641\u064A\u062F\u064A\u0648 \u062C\u0645\u0627\u0639\u064A\u0629", + "groupCall.join": "\u0627\u0646\u0636\u0645\u0627\u0645", + "groupCall.dismiss": "\u0644\u064A\u0633 \u0627\u0644\u0622\u0646", + "groupCall.leave": "\u0645\u063A\u0627\u062F\u0631\u0629 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629", + "groupCall.you": "\u0623\u0646\u062A", + "groupCall.connecting": "\u062C\u0627\u0631\u064D \u0627\u0644\u0627\u062A\u0635\u0627\u0644\u2026", + "groupCall.waitingLink": "\u0641\u064A \u0627\u0646\u062A\u0638\u0627\u0631 \u0631\u0627\u0628\u0637 \u0645\u0628\u0627\u0634\u0631\u2026", + "groupCall.legFailed": "\u062A\u0639\u0630\u0651\u0631 \u0627\u0644\u0627\u062A\u0635\u0627\u0644", + "groupCall.startedVoice": "\u0628\u062F\u0623 {name} \u0645\u0643\u0627\u0644\u0645\u0629 \u0635\u0648\u062A\u064A\u0629", + "groupCall.startedVideo": "\u0628\u062F\u0623 {name} \u0645\u0643\u0627\u0644\u0645\u0629 \u0641\u064A\u062F\u064A\u0648", + "groupCall.inCall": "{count} \u0641\u064A \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629", + "groupCall.err.permission_denied": "\u062A\u0639\u0630\u0651\u0631 \u0628\u062F\u0621 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629 - \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0648\u0627\u0644\u0643\u0627\u0645\u064A\u0631\u0627 \u0645\u062D\u0638\u0648\u0631. \u0627\u0633\u0645\u062D \u0628\u0647 \u0644\u0647\u0630\u0627 \u0627\u0644\u0645\u0648\u0642\u0639 \u062B\u0645 \u0623\u0639\u062F \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629.", + "groupCall.err.device_not_found": "\u062A\u0639\u0630\u0651\u0631 \u0628\u062F\u0621 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629 - \u0644\u0645 \u064A\u064F\u0639\u062B\u0631 \u0639\u0644\u0649 \u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0641\u064A \u0647\u0630\u0627 \u0627\u0644\u062C\u0647\u0627\u0632.", + "groupCall.err.device_busy": "\u062A\u0639\u0630\u0651\u0631 \u0628\u062F\u0621 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629 - \u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0645\u0633\u062A\u062E\u062F\u064E\u0645 \u0645\u0646 \u062A\u0637\u0628\u064A\u0642 \u0622\u062E\u0631. \u0623\u063A\u0644\u0642\u0647 \u062B\u0645 \u0623\u0639\u062F \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629.", + "groupCall.err.media_failed": "\u062A\u0639\u0630\u0651\u0631 \u0628\u062F\u0621 \u0627\u0644\u0645\u0643\u0627\u0644\u0645\u0629 - \u0644\u0645 \u064A\u0645\u0643\u0646 \u0641\u062A\u062D \u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646.", + "groupCall.err.call_in_progress": "\u0647\u0646\u0627\u0643 \u0645\u0643\u0627\u0644\u0645\u0629 \u062C\u0627\u0631\u064A\u0629 \u0628\u0627\u0644\u0641\u0639\u0644 \u0641\u064A \u0647\u0630\u0647 \u0627\u0644\u0645\u062C\u0645\u0648\u0639\u0629. \u0627\u0646\u0636\u0645 \u0625\u0644\u064A\u0647\u0627 \u0628\u062F\u0644 \u0628\u062F\u0621 \u0623\u062E\u0631\u0649.", + "groupCall.err.not_ready": "\u0623\u0643\u0651\u062F \u0631\u0645\u0632 \u0627\u0644\u0645\u062C\u0645\u0648\u0639\u0629 \u0642\u0628\u0644 \u0627\u0644\u0627\u062A\u0635\u0627\u0644.", + "groupCall.speaking": "{name} \u064A\u062A\u062D\u062F\u0651\u062B \u0627\u0644\u0622\u0646", + "groupCall.pin": "\u0639\u0631\u0636 {name} \u0628\u062D\u062C\u0645 \u0643\u0628\u064A\u0631", + "groupCall.unpin": "\u0627\u0644\u0639\u0648\u062F\u0629 \u0625\u0644\u0649 \u0627\u0644\u062C\u0645\u064A\u0639", + "groupCall.showEveryone": "\u0627\u0644\u062C\u0645\u064A\u0639" }, "he": { "language.label": "\u05E9\u05E4\u05D4", + "language.suggest.text": "\u05D4\u05D3\u05E3 \u05D4\u05D6\u05D4 \u05D6\u05DE\u05D9\u05DF \u05D2\u05DD \u05D1\u05E2\u05D1\u05E8\u05D9\u05EA.", + "language.suggest.cta": "\u05DC\u05E7\u05E8\u05D9\u05D0\u05D4 \u05D1\u05E2\u05D1\u05E8\u05D9\u05EA", + "language.suggest.dismiss": "\u05E1\u05D2\u05D9\u05E8\u05D4", "community.title": "\u05D4\u05E6\u05D8\u05E8\u05E4\u05D5 \u05DC\u05E2\u05EA\u05D9\u05D3 \u05E9\u05DC \u05D4\u05E4\u05E8\u05D8\u05D9\u05D5\u05EA", "community.description": "SecureBit \u05D2\u05D3\u05DC \u05D1\u05D6\u05DB\u05D5\u05EA \u05D4\u05E7\u05D4\u05D9\u05DC\u05D4 \u05E9\u05DC\u05D5. \u05D4\u05E8\u05E2\u05D9\u05D5\u05E0\u05D5\u05EA \u05D5\u05D4\u05DE\u05E9\u05D5\u05D1 \u05E9\u05DC\u05DB\u05DD \u05DE\u05E2\u05E6\u05D1\u05D9\u05DD \u05D0\u05EA \u05E2\u05EA\u05D9\u05D3 \u05D4\u05EA\u05E7\u05E9\u05D5\u05E8\u05EA \u05D4\u05DE\u05D0\u05D5\u05D1\u05D8\u05D7\u05EA - \u05E0\u05D1\u05E0\u05D4 \u05D1\u05D2\u05DC\u05D5\u05D9, \u05E2\u05DD \u05D0\u05D9\u05DE\u05D5\u05EA ASN.1 \u05DE\u05DC\u05D0 \u05DE\u05E7\u05E6\u05D4 \u05DC\u05E7\u05E6\u05D4.", "community.github": "\u05DE\u05D0\u05D2\u05E8 GitHub", @@ -12648,10 +12901,35 @@ var DICTIONARIES = { "offline.disconnect": "\u05E0\u05D9\u05EA\u05D5\u05E7", "offline.learnMore": "\u05DE\u05D9\u05D3\u05E2 \u05E0\u05D5\u05E1\u05E3", "pwa.installApp": "\u05D4\u05EA\u05E7\u05E0\u05EA \u05D4\u05D0\u05E4\u05DC\u05D9\u05E7\u05E6\u05D9\u05D4", - "chat.onWeb": "\u05D0\u05EA\u05DD \u05D1\u05D2\u05E8\u05E1\u05EA \u05D4\u05D5\u05D5\u05D1" + "chat.onWeb": "\u05D0\u05EA\u05DD \u05D1\u05D2\u05E8\u05E1\u05EA \u05D4\u05D5\u05D5\u05D1", + "groupCall.startVoice": "\u05D4\u05EA\u05D7\u05DC\u05EA \u05E9\u05D9\u05D7\u05D4 \u05E7\u05D5\u05DC\u05D9\u05EA \u05E7\u05D1\u05D5\u05E6\u05EA\u05D9\u05EA", + "groupCall.startVideo": "\u05D4\u05EA\u05D7\u05DC\u05EA \u05E9\u05D9\u05D7\u05EA \u05D5\u05D9\u05D3\u05D0\u05D5 \u05E7\u05D1\u05D5\u05E6\u05EA\u05D9\u05EA", + "groupCall.join": "\u05D4\u05E6\u05D8\u05E8\u05E4\u05D5\u05EA", + "groupCall.dismiss": "\u05DC\u05D0 \u05E2\u05DB\u05E9\u05D9\u05D5", + "groupCall.leave": "\u05D9\u05E6\u05D9\u05D0\u05D4 \u05DE\u05D4\u05E9\u05D9\u05D7\u05D4", + "groupCall.you": "\u05D0\u05EA/\u05D4", + "groupCall.connecting": "\u05DE\u05EA\u05D7\u05D1\u05E8\u2026", + "groupCall.waitingLink": "\u05DE\u05DE\u05EA\u05D9\u05DF \u05DC\u05E7\u05D9\u05E9\u05D5\u05E8 \u05D9\u05E9\u05D9\u05E8\u2026", + "groupCall.legFailed": "\u05D4\u05D4\u05EA\u05D7\u05D1\u05E8\u05D5\u05EA \u05E0\u05DB\u05E9\u05DC\u05D4", + "groupCall.startedVoice": "{name} \u05D4\u05EA\u05D7\u05D9\u05DC \u05E9\u05D9\u05D7\u05D4 \u05E7\u05D5\u05DC\u05D9\u05EA", + "groupCall.startedVideo": "{name} \u05D4\u05EA\u05D7\u05D9\u05DC \u05E9\u05D9\u05D7\u05EA \u05D5\u05D9\u05D3\u05D0\u05D5", + "groupCall.inCall": "{count} \u05D1\u05E9\u05D9\u05D7\u05D4", + "groupCall.err.permission_denied": "\u05D4\u05E9\u05D9\u05D7\u05D4 \u05DC\u05D0 \u05D4\u05EA\u05D7\u05D9\u05DC\u05D4 - \u05D4\u05D2\u05D9\u05E9\u05D4 \u05DC\u05DE\u05D9\u05E7\u05E8\u05D5\u05E4\u05D5\u05DF \u05D5\u05DC\u05DE\u05E6\u05DC\u05DE\u05D4 \u05D7\u05E1\u05D5\u05DE\u05D4. \u05D0\u05E4\u05E9\u05E8\u05D5 \u05D0\u05D5\u05EA\u05D4 \u05DC\u05D0\u05EA\u05E8 \u05D4\u05D6\u05D4 \u05D5\u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.", + "groupCall.err.device_not_found": "\u05D4\u05E9\u05D9\u05D7\u05D4 \u05DC\u05D0 \u05D4\u05EA\u05D7\u05D9\u05DC\u05D4 - \u05DC\u05D0 \u05E0\u05DE\u05E6\u05D0 \u05DE\u05D9\u05E7\u05E8\u05D5\u05E4\u05D5\u05DF \u05D1\u05DE\u05DB\u05E9\u05D9\u05E8 \u05D4\u05D6\u05D4.", + "groupCall.err.device_busy": "\u05D4\u05E9\u05D9\u05D7\u05D4 \u05DC\u05D0 \u05D4\u05EA\u05D7\u05D9\u05DC\u05D4 - \u05D4\u05DE\u05D9\u05E7\u05E8\u05D5\u05E4\u05D5\u05DF \u05D1\u05E9\u05D9\u05DE\u05D5\u05E9 \u05D0\u05E4\u05DC\u05D9\u05E7\u05E6\u05D9\u05D4 \u05D0\u05D7\u05E8\u05EA. \u05E1\u05D2\u05E8\u05D5 \u05D0\u05D5\u05EA\u05D4 \u05D5\u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.", + "groupCall.err.media_failed": "\u05D4\u05E9\u05D9\u05D7\u05D4 \u05DC\u05D0 \u05D4\u05EA\u05D7\u05D9\u05DC\u05D4 - \u05DC\u05D0 \u05E0\u05D9\u05EA\u05DF \u05D4\u05D9\u05D4 \u05DC\u05E4\u05EA\u05D5\u05D7 \u05D0\u05EA \u05D4\u05DE\u05D9\u05E7\u05E8\u05D5\u05E4\u05D5\u05DF.", + "groupCall.err.call_in_progress": "\u05DB\u05D1\u05E8 \u05DE\u05EA\u05E7\u05D9\u05D9\u05DE\u05EA \u05E9\u05D9\u05D7\u05D4 \u05D1\u05E7\u05D1\u05D5\u05E6\u05D4 \u05D4\u05D6\u05D5. \u05D4\u05E6\u05D8\u05E8\u05E4\u05D5 \u05D0\u05DC\u05D9\u05D4 \u05D1\u05DE\u05E7\u05D5\u05DD \u05DC\u05D4\u05EA\u05D7\u05D9\u05DC \u05D0\u05D7\u05EA \u05D7\u05D3\u05E9\u05D4.", + "groupCall.err.not_ready": "\u05D0\u05E9\u05E8\u05D5 \u05D0\u05EA \u05E7\u05D5\u05D3 \u05D4\u05E7\u05D1\u05D5\u05E6\u05D4 \u05DC\u05E4\u05E0\u05D9 \u05E9\u05D9\u05D7\u05D4.", + "groupCall.speaking": "{name} \u05DE\u05D3\u05D1\u05E8/\u05EA", + "groupCall.pin": "\u05D4\u05E6\u05D2\u05EA {name} \u05D1\u05D2\u05D3\u05D5\u05DC", + "groupCall.unpin": "\u05D7\u05D6\u05E8\u05D4 \u05DC\u05DB\u05D5\u05DC\u05DD", + "groupCall.showEveryone": "\u05DB\u05D5\u05DC\u05DD" }, "fa": { "language.label": "\u0632\u0628\u0627\u0646", + "language.suggest.text": "\u0627\u06CC\u0646 \u0635\u0641\u062D\u0647 \u0628\u0647 \u0641\u0627\u0631\u0633\u06CC \u0647\u0645 \u062F\u0631 \u062F\u0633\u062A\u0631\u0633 \u0627\u0633\u062A.", + "language.suggest.cta": "\u062E\u0648\u0627\u0646\u062F\u0646 \u0628\u0647 \u0641\u0627\u0631\u0633\u06CC", + "language.suggest.dismiss": "\u0628\u0633\u062A\u0646", "community.title": "\u0628\u0647 \u0622\u06CC\u0646\u062F\u0647\u0654 \u062D\u0631\u06CC\u0645 \u062E\u0635\u0648\u0635\u06CC \u0628\u067E\u06CC\u0648\u0646\u062F\u06CC\u062F", "community.description": "\u200FSecureBit \u0628\u0647\u200C\u0644\u0637\u0641 \u062C\u0627\u0645\u0639\u0647\u200C\u0627\u0634 \u0631\u0634\u062F \u0645\u06CC\u200C\u06A9\u0646\u062F. \u0627\u06CC\u062F\u0647\u200C\u0647\u0627 \u0648 \u0628\u0627\u0632\u062E\u0648\u0631\u062F\u0647\u0627\u06CC \u0634\u0645\u0627 \u0622\u06CC\u0646\u062F\u0647\u0654 \u0627\u0631\u062A\u0628\u0627\u0637 \u0627\u0645\u0646 \u0631\u0627 \u0634\u06A9\u0644 \u0645\u06CC\u200C\u062F\u0647\u062F - \u0633\u0627\u062E\u062A\u0647\u200C\u0634\u062F\u0647 \u062F\u0631 \u0641\u0636\u0627\u06CC \u0628\u0627\u0632\u060C \u0628\u0627 \u0627\u0639\u062A\u0628\u0627\u0631\u0633\u0646\u062C\u06CC \u06A9\u0627\u0645\u0644 ASN.1 \u0627\u0632 \u06CC\u06A9 \u0633\u0631 \u062A\u0627 \u0633\u0631 \u062F\u06CC\u06AF\u0631.", "community.github": "\u0645\u062E\u0632\u0646 GitHub", @@ -13397,10 +13675,35 @@ var DICTIONARIES = { "offline.disconnect": "\u0642\u0637\u0639 \u0627\u062A\u0635\u0627\u0644", "offline.learnMore": "\u0628\u06CC\u0634\u062A\u0631 \u0628\u062F\u0627\u0646\u06CC\u062F", "pwa.installApp": "\u0646\u0635\u0628 \u0628\u0631\u0646\u0627\u0645\u0647", - "chat.onWeb": "\u0634\u0645\u0627 \u0631\u0648\u06CC \u0646\u0633\u062E\u0647\u0654 \u0648\u0628 \u0647\u0633\u062A\u06CC\u062F" + "chat.onWeb": "\u0634\u0645\u0627 \u0631\u0648\u06CC \u0646\u0633\u062E\u0647\u0654 \u0648\u0628 \u0647\u0633\u062A\u06CC\u062F", + "groupCall.startVoice": "\u0634\u0631\u0648\u0639 \u062A\u0645\u0627\u0633 \u0635\u0648\u062A\u06CC \u06AF\u0631\u0648\u0647\u06CC", + "groupCall.startVideo": "\u0634\u0631\u0648\u0639 \u062A\u0645\u0627\u0633 \u062A\u0635\u0648\u06CC\u0631\u06CC \u06AF\u0631\u0648\u0647\u06CC", + "groupCall.join": "\u067E\u06CC\u0648\u0633\u062A\u0646", + "groupCall.dismiss": "\u0627\u0644\u0627\u0646 \u0646\u0647", + "groupCall.leave": "\u062E\u0631\u0648\u062C \u0627\u0632 \u062A\u0645\u0627\u0633", + "groupCall.you": "\u0634\u0645\u0627", + "groupCall.connecting": "\u062F\u0631 \u062D\u0627\u0644 \u0627\u062A\u0635\u0627\u0644\u2026", + "groupCall.waitingLink": "\u062F\u0631 \u0627\u0646\u062A\u0638\u0627\u0631 \u06CC\u06A9 \u067E\u06CC\u0648\u0646\u062F \u0645\u0633\u062A\u0642\u06CC\u0645\u2026", + "groupCall.legFailed": "\u0627\u062A\u0635\u0627\u0644 \u0628\u0631\u0642\u0631\u0627\u0631 \u0646\u0634\u062F", + "groupCall.startedVoice": "{name} \u06CC\u06A9 \u062A\u0645\u0627\u0633 \u0635\u0648\u062A\u06CC \u0622\u063A\u0627\u0632 \u06A9\u0631\u062F", + "groupCall.startedVideo": "{name} \u06CC\u06A9 \u062A\u0645\u0627\u0633 \u062A\u0635\u0648\u06CC\u0631\u06CC \u0622\u063A\u0627\u0632 \u06A9\u0631\u062F", + "groupCall.inCall": "{count} \u0646\u0641\u0631 \u062F\u0631 \u062A\u0645\u0627\u0633", + "groupCall.err.permission_denied": "\u062A\u0645\u0627\u0633 \u0622\u063A\u0627\u0632 \u0646\u0634\u062F - \u062F\u0633\u062A\u0631\u0633\u06CC \u0628\u0647 \u0645\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u0648 \u062F\u0648\u0631\u0628\u06CC\u0646 \u0645\u0633\u062F\u0648\u062F \u0627\u0633\u062A. \u0622\u0646 \u0631\u0627 \u0628\u0631\u0627\u06CC \u0627\u06CC\u0646 \u0633\u0627\u06CC\u062A \u0645\u062C\u0627\u0632 \u06A9\u0646\u06CC\u062F \u0648 \u062F\u0648\u0628\u0627\u0631\u0647 \u062A\u0644\u0627\u0634 \u06A9\u0646\u06CC\u062F.", + "groupCall.err.device_not_found": "\u062A\u0645\u0627\u0633 \u0622\u063A\u0627\u0632 \u0646\u0634\u062F - \u0645\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646\u06CC \u0631\u0648\u06CC \u0627\u06CC\u0646 \u062F\u0633\u062A\u06AF\u0627\u0647 \u067E\u06CC\u062F\u0627 \u0646\u0634\u062F.", + "groupCall.err.device_busy": "\u062A\u0645\u0627\u0633 \u0622\u063A\u0627\u0632 \u0646\u0634\u062F - \u0645\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u0634\u0645\u0627 \u062F\u0631 \u062D\u0627\u0644 \u0627\u0633\u062A\u0641\u0627\u062F\u0647 \u062A\u0648\u0633\u0637 \u0628\u0631\u0646\u0627\u0645\u0647 \u062F\u06CC\u06AF\u0631\u06CC \u0627\u0633\u062A. \u0622\u0646 \u0631\u0627 \u0628\u0628\u0646\u062F\u06CC\u062F \u0648 \u062F\u0648\u0628\u0627\u0631\u0647 \u062A\u0644\u0627\u0634 \u06A9\u0646\u06CC\u062F.", + "groupCall.err.media_failed": "\u062A\u0645\u0627\u0633 \u0622\u063A\u0627\u0632 \u0646\u0634\u062F - \u0645\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u0628\u0627\u0632 \u0646\u0634\u062F.", + "groupCall.err.call_in_progress": "\u0647\u0645\u06CC\u0646 \u062D\u0627\u0644\u0627 \u062A\u0645\u0627\u0633\u06CC \u062F\u0631 \u0627\u06CC\u0646 \u06AF\u0631\u0648\u0647 \u062F\u0631 \u062C\u0631\u06CC\u0627\u0646 \u0627\u0633\u062A. \u0628\u0647\u200C\u062C\u0627\u06CC \u0634\u0631\u0648\u0639 \u062A\u0645\u0627\u0633 \u062A\u0627\u0632\u0647\u060C \u0628\u0647 \u0622\u0646 \u0628\u067E\u06CC\u0648\u0646\u062F\u06CC\u062F.", + "groupCall.err.not_ready": "\u067E\u06CC\u0634 \u0627\u0632 \u062A\u0645\u0627\u0633\u060C \u06A9\u062F \u06AF\u0631\u0648\u0647 \u0631\u0627 \u062A\u0623\u06CC\u06CC\u062F \u06A9\u0646\u06CC\u062F.", + "groupCall.speaking": "{name} \u062F\u0631 \u062D\u0627\u0644 \u0635\u062D\u0628\u062A \u0627\u0633\u062A", + "groupCall.pin": "\u0646\u0645\u0627\u06CC\u0634 \u0628\u0632\u0631\u06AF {name}", + "groupCall.unpin": "\u0628\u0627\u0632\u06AF\u0634\u062A \u0628\u0647 \u0647\u0645\u0647", + "groupCall.showEveryone": "\u0647\u0645\u0647" }, "ur": { "language.label": "\u0632\u0628\u0627\u0646", + "language.suggest.text": "\u06CC\u06C1 \u0635\u0641\u062D\u06C1 \u0627\u0631\u062F\u0648 \u0645\u06CC\u06BA \u0628\u06BE\u06CC \u062F\u0633\u062A\u06CC\u0627\u0628 \u06C1\u06D2\u06D4", + "language.suggest.cta": "\u0627\u0631\u062F\u0648 \u0645\u06CC\u06BA \u067E\u0691\u06BE\u06CC\u06BA", + "language.suggest.dismiss": "\u0628\u0646\u062F \u06A9\u0631\u06CC\u06BA", "community.title": "\u0646\u062C\u06CC\u062A \u06A9\u06D2 \u0645\u0633\u062A\u0642\u0628\u0644 \u0645\u06CC\u06BA \u0634\u0627\u0645\u0644 \u06C1\u0648\u06BA", "community.description": "\u200FSecureBit \u0627\u067E\u0646\u06CC \u0628\u0631\u0627\u062F\u0631\u06CC \u06A9\u06CC \u0628\u062F\u0648\u0644\u062A \u0628\u0691\u06BE\u062A\u0627 \u06C1\u06D2\u06D4 \u0622\u067E \u06A9\u06D2 \u062E\u06CC\u0627\u0644\u0627\u062A \u0627\u0648\u0631 \u0622\u0631\u0627\u0621 \u0645\u062D\u0641\u0648\u0638 \u0631\u0627\u0628\u0637\u06D2 \u06A9\u0627 \u0645\u0633\u062A\u0642\u0628\u0644 \u062A\u0634\u06A9\u06CC\u0644 \u062F\u06CC\u062A\u06D2 \u06C1\u06CC\u06BA - \u06A9\u06BE\u0644\u06D2 \u0639\u0627\u0645 \u0628\u0646\u0627\u06CC\u0627 \u06AF\u06CC\u0627\u060C \u0633\u0631\u06D2 \u0633\u06D2 \u0633\u0631\u06D2 \u062A\u06A9 \u0645\u06A9\u0645\u0644 ASN.1 \u062A\u0648\u062B\u06CC\u0642 \u06A9\u06D2 \u0633\u0627\u062A\u06BE\u06D4", "community.github": "\u200FGitHub \u0645\u062E\u0632\u0646", @@ -14146,7 +14449,29 @@ var DICTIONARIES = { "offline.disconnect": "\u0645\u0646\u0642\u0637\u0639 \u06A9\u0631\u06CC\u06BA", "offline.learnMore": "\u0645\u0632\u06CC\u062F \u062C\u0627\u0646\u06CC\u06BA", "pwa.installApp": "\u0627\u06CC\u067E \u0646\u0635\u0628 \u06A9\u0631\u06CC\u06BA", - "chat.onWeb": "\u0622\u067E \u0648\u06CC\u0628 \u067E\u0631 \u06C1\u06CC\u06BA" + "chat.onWeb": "\u0622\u067E \u0648\u06CC\u0628 \u067E\u0631 \u06C1\u06CC\u06BA", + "groupCall.startVoice": "\u06AF\u0631\u0648\u067E \u0648\u0627\u0626\u0633 \u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u06A9\u0631\u06CC\u06BA", + "groupCall.startVideo": "\u06AF\u0631\u0648\u067E \u0648\u06CC\u0688\u06CC\u0648 \u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u06A9\u0631\u06CC\u06BA", + "groupCall.join": "\u0634\u0627\u0645\u0644 \u06C1\u0648\u06BA", + "groupCall.dismiss": "\u0627\u0628\u06BE\u06CC \u0646\u06C1\u06CC\u06BA", + "groupCall.leave": "\u06A9\u0627\u0644 \u0686\u06BE\u0648\u0691\u06CC\u06BA", + "groupCall.you": "\u0622\u067E", + "groupCall.connecting": "\u0645\u0646\u0633\u0644\u06A9 \u06C1\u0648 \u0631\u06C1\u0627 \u06C1\u06D2\u2026", + "groupCall.waitingLink": "\u0628\u0631\u0627\u06C1\u0650 \u0631\u0627\u0633\u062A \u0631\u0627\u0628\u0637\u06D2 \u06A9\u0627 \u0627\u0646\u062A\u0638\u0627\u0631\u2026", + "groupCall.legFailed": "\u0645\u0646\u0633\u0644\u06A9 \u0646\u06C1\u06CC\u06BA \u06C1\u0648 \u0633\u06A9\u0627", + "groupCall.startedVoice": "{name} \u0646\u06D2 \u0648\u0627\u0626\u0633 \u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u06A9\u06CC", + "groupCall.startedVideo": "{name} \u0646\u06D2 \u0648\u06CC\u0688\u06CC\u0648 \u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u06A9\u06CC", + "groupCall.inCall": "\u06A9\u0627\u0644 \u0645\u06CC\u06BA {count}", + "groupCall.err.permission_denied": "\u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u0646\u06C1\u06CC\u06BA \u06C1\u0648 \u0633\u06A9\u06CC - \u0645\u0627\u0626\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u0627\u0648\u0631 \u06A9\u06CC\u0645\u0631\u06D2 \u062A\u06A9 \u0631\u0633\u0627\u0626\u06CC \u0628\u0646\u062F \u06C1\u06D2\u06D4 \u0627\u0633 \u0633\u0627\u0626\u0679 \u06A9\u06D2 \u0644\u06CC\u06D2 \u0627\u062C\u0627\u0632\u062A \u062F\u06CC\u06BA \u0627\u0648\u0631 \u062F\u0648\u0628\u0627\u0631\u06C1 \u06A9\u0648\u0634\u0634 \u06A9\u0631\u06CC\u06BA\u06D4", + "groupCall.err.device_not_found": "\u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u0646\u06C1\u06CC\u06BA \u06C1\u0648 \u0633\u06A9\u06CC - \u0627\u0633 \u0688\u06CC\u0648\u0627\u0626\u0633 \u067E\u0631 \u06A9\u0648\u0626\u06CC \u0645\u0627\u0626\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u0646\u06C1\u06CC\u06BA \u0645\u0644\u0627\u06D4", + "groupCall.err.device_busy": "\u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u0646\u06C1\u06CC\u06BA \u06C1\u0648 \u0633\u06A9\u06CC - \u0622\u067E \u06A9\u0627 \u0645\u0627\u0626\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u06A9\u0633\u06CC \u0627\u0648\u0631 \u0627\u06CC\u067E \u06A9\u06D2 \u0632\u06CC\u0631\u0650 \u0627\u0633\u062A\u0639\u0645\u0627\u0644 \u06C1\u06D2\u06D4 \u0627\u0633\u06D2 \u0628\u0646\u062F \u06A9\u0631 \u06A9\u06D2 \u062F\u0648\u0628\u0627\u0631\u06C1 \u06A9\u0648\u0634\u0634 \u06A9\u0631\u06CC\u06BA\u06D4", + "groupCall.err.media_failed": "\u06A9\u0627\u0644 \u0634\u0631\u0648\u0639 \u0646\u06C1\u06CC\u06BA \u06C1\u0648 \u0633\u06A9\u06CC - \u0645\u0627\u0626\u06CC\u06A9\u0631\u0648\u0641\u0648\u0646 \u0646\u06C1\u06CC\u06BA \u06A9\u06BE\u0644 \u0633\u06A9\u0627\u06D4", + "groupCall.err.call_in_progress": "\u0627\u0633 \u06AF\u0631\u0648\u067E \u0645\u06CC\u06BA \u067E\u06C1\u0644\u06D2 \u06C1\u06CC \u0627\u06CC\u06A9 \u06A9\u0627\u0644 \u062C\u0627\u0631\u06CC \u06C1\u06D2\u06D4 \u0646\u0626\u06CC \u0634\u0631\u0648\u0639 \u06A9\u0631\u0646\u06D2 \u06A9\u06D2 \u0628\u062C\u0627\u0626\u06D2 \u0627\u0633 \u0645\u06CC\u06BA \u0634\u0627\u0645\u0644 \u06C1\u0648\u06BA\u06D4", + "groupCall.err.not_ready": "\u06A9\u0627\u0644 \u0633\u06D2 \u067E\u06C1\u0644\u06D2 \u06AF\u0631\u0648\u067E \u06A9\u0648\u0688 \u06A9\u06CC \u062A\u0635\u062F\u06CC\u0642 \u06A9\u0631\u06CC\u06BA\u06D4", + "groupCall.speaking": "{name} \u0628\u0648\u0644 \u0631\u06C1\u06D2 \u06C1\u06CC\u06BA", + "groupCall.pin": "{name} \u06A9\u0648 \u0628\u0691\u0627 \u062F\u06A9\u06BE\u0627\u0626\u06CC\u06BA", + "groupCall.unpin": "\u0633\u0628 \u067E\u0631 \u0648\u0627\u067E\u0633 \u062C\u0627\u0626\u06CC\u06BA", + "groupCall.showEveryone": "\u0633\u0628" } }; @@ -18486,6 +18811,8 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { callId: null, quality: null, // 'excellent'|'good'|'fair'|'poor'|null — link quality for the UI + groupCallId: null, + // set while this call is one leg of a group call error: null }; this.localMediaStream = null; @@ -18496,6 +18823,9 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { this._callVideoSender = null; this._callFacingMode = "user"; this._adaptationController = null; + this._callGroupContext = null; + this._externalMediaStream = null; + this._callStateListeners = /* @__PURE__ */ new Set(); this.keyRotationInterval = null; this.lastKeyRotation = Date.now(); this.currentKeyVersion = 0; @@ -29219,6 +29549,9 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { } this._callAudioSender = null; this._callVideoSender = null; + this._callGroupContext = null; + this._externalMediaStream = null; + this._callStateListeners?.clear?.(); this.intentionalDisconnect = true; window.EnhancedSecureCryptoUtils.secureLog.log("info", "Starting intentional disconnect"); this.sendDisconnectNotification(); @@ -29740,6 +30073,57 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { getCallState() { return { ...this.callState }; } + /** + * Subscribe to call-state changes without taking the onCallStateChanged slot. + * @param {(state: object) => void} listener + * @returns {() => void} unsubscribe + */ + addCallStateListener(listener) { + if (typeof listener !== "function") return () => { + }; + this._callStateListeners.add(listener); + return () => this._callStateListeners.delete(listener); + } + removeCallStateListener(listener) { + this._callStateListeners.delete(listener); + } + /** + * Mark this session as one leg of a group call, or clear it. + * + * While set, an inbound call offer is answered without ringing. That is safe + * only because of who sets it: the group-call controller, and only while the + * local user is in that group's call. Nothing on the wire can set it, so a + * peer cannot cause this session to open a microphone on its own. + * + * @param {string|null} groupCallId + */ + setCallGroupContext(groupCallId) { + this._callGroupContext = typeof groupCallId === "string" && groupCallId ? groupCallId : null; + this._updateCallState({}); + if (this._callGroupContext && this._pendingCallOffer && this.callState.phase === "incoming") { + this.acceptCall().catch((error) => { + this._secureLog("warn", "\u26A0\uFE0F Failed to answer a group call leg that was already ringing", { + errorType: error?.constructor?.name + }); + }); + } + } + /** + * Use a capture owned by somebody else for the next call on this session. + * + * The group-call controller captures the microphone and camera ONCE and + * hands the same stream to every leg. This manager attaches its tracks and + * must never stop them: the other legs are still sending them. + * + * @param {MediaStream|null} stream + */ + setExternalMediaStream(stream) { + this._externalMediaStream = stream || null; + } + /** True when the live capture belongs to somebody else. */ + _usingExternalMedia() { + return !!this._externalMediaStream && this.localMediaStream === this._externalMediaStream; + } getRemoteMediaStream() { return this.remoteMediaStream; } @@ -29779,7 +30163,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { }, 1200); } _updateCallState(patch) { - this.callState = { ...this.callState, ...patch }; + this.callState = { ...this.callState, ...patch, groupCallId: this._callGroupContext }; const snapshot = this.getCallState(); if (snapshot.phase === "active") this._startAdaptation(); else if (snapshot.phase === "idle") this._stopAdaptation(); @@ -29787,6 +30171,12 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { this.onCallStateChanged?.(snapshot); } catch (_) { } + for (const listener of this._callStateListeners) { + try { + listener(snapshot); + } catch (_) { + } + } if (typeof document !== "undefined") { try { this._dispatchAppEvent?.(new CustomEvent("securebit-call-state", { @@ -29813,7 +30203,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { return { facingMode: this._callFacingMode, width: { ideal: 1280 }, height: { ideal: 720 } }; } async _acquireLocalMedia(withVideo) { - const stream = await navigator.mediaDevices.getUserMedia({ + const stream = this._externalMediaStream || await navigator.mediaDevices.getUserMedia({ audio: this._audioConstraints(), video: withVideo ? this._videoConstraints() : false }); @@ -29835,7 +30225,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { // Fully release the mic/camera — only on session disconnect, not between calls. _stopLocalMediaPermanently() { try { - if (this.localMediaStream) { + if (this.localMediaStream && !this._usingExternalMedia()) { for (const t2 of this.localMediaStream.getTracks()) { try { t2.stop(); @@ -30011,6 +30401,9 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { remoteHasVideo: !!data.withVideo, error: null }); + if (this._callGroupContext) { + await this.acceptCall(); + } } async _answerCallOffer(data, renegotiation = false) { await this.peerConnection.setRemoteDescription({ type: "offer", sdp: data.sdp }); @@ -30078,7 +30471,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { this._stopAdaptation(); const pc = this.peerConnection; try { - if (this.localMediaStream) { + if (this.localMediaStream && !this._usingExternalMedia()) { for (const track of this.localMediaStream.getTracks()) { try { track.stop(); @@ -30150,17 +30543,44 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { const camStream = await navigator.mediaDevices.getUserMedia({ video: this._videoConstraints() }); const videoTrack = camStream.getVideoTracks()[0]; if (!videoTrack) return; - this.localMediaStream.addTrack(videoTrack); - if (this._callVideoSender) await this._callVideoSender.replaceTrack(videoTrack); - else this._callVideoSender = this.peerConnection.addTrack(videoTrack, this.localMediaStream); - this._applyCallCodecPrefs(); - this._updateCallState({ cameraEnabled: true, withVideo: true }); - await this._renegotiateCall(); + await this.addVideoTrack(videoTrack); } catch (error) { this._secureLog("error", "\u274C upgradeToVideo failed", { errorType: error?.constructor?.name }); this._updateCallState({ cameraEnabled: false, error: "camera_failed" }); } } + /** + * Put an already-captured camera track on this call and renegotiate. + * + * Split out of upgradeToVideo so a group call can capture its camera once + * and add the SAME track to every leg — N captures of one camera is both + * wasteful and, on several devices, simply not permitted. + * + * @param {MediaStreamTrack} videoTrack + */ + async addVideoTrack(videoTrack) { + if (!videoTrack || !this.localMediaStream || !this.peerConnection) return; + if (!this.localMediaStream.getVideoTracks().includes(videoTrack)) { + this.localMediaStream.addTrack(videoTrack); + } + if (this._callVideoSender) await this._callVideoSender.replaceTrack(videoTrack); + else this._callVideoSender = this.peerConnection.addTrack(videoTrack, this.localMediaStream); + this._applyCallCodecPrefs(); + this._updateCallState({ cameraEnabled: true, withVideo: true }); + await this._renegotiateCall(); + } + /** + * Swap the outgoing camera track without renegotiating. + * + * The group's camera flip captures one new track and hands it to every leg; + * replaceTrack keeps the transceiver, so no leg has to renegotiate for it. + * + * @param {MediaStreamTrack} videoTrack + */ + async replaceVideoTrack(videoTrack) { + if (!videoTrack || !this._callVideoSender) return; + await this._callVideoSender.replaceTrack(videoTrack); + } // Flip between front/back cameras without renegotiation (replaceTrack). async switchCamera() { if (!this._callVideoSender || !this.localMediaStream) return; @@ -31264,7 +31684,7 @@ var SecureMasterKeyManager = class { var import_NotificationIntegration = __toESM(require_NotificationIntegration()); // package.json -var version = "6.4.0"; +var version = "6.6.6"; // src/components/ui/LanguageSwitcher.jsx var LanguageSwitcher = () => { @@ -31815,8 +32235,9 @@ Right-click or Ctrl+click to disconnect`, }, []); const GLASS = "blur(20px) saturate(180%)"; const MATERIALISE = "background .25s ease, backdrop-filter .25s ease, -webkit-backdrop-filter .25s ease, border-color .25s ease"; - const overlay = { position: "fixed", top: 0, left: 0, right: 0 }; - const headerStyle = onLanding ? scrolled ? { ...overlay, background: "rgba(15,15,17,0.72)", backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: "1px solid rgba(255,255,255,0.06)", transition: MATERIALISE } : { ...overlay, background: "transparent", backdropFilter: "blur(0px) saturate(100%)", WebkitBackdropFilter: "blur(0px) saturate(100%)", borderBottom: "1px solid transparent", transition: MATERIALISE } : { background: "rgba(18,18,20,0.72)", backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: "1px solid rgba(255,255,255,0.06)" }; + const safeTop = { paddingTop: "var(--sb-safe-top, 0px)", paddingBottom: "var(--sb-bar-extra, 0px)" }; + const overlay = { position: "fixed", top: 0, left: 0, right: 0, ...safeTop }; + const headerStyle = onLanding ? scrolled ? { ...overlay, background: "rgba(15,15,17,0.72)", backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: "1px solid rgba(255,255,255,0.06)", transition: MATERIALISE } : { ...overlay, background: "transparent", backdropFilter: "blur(0px) saturate(100%)", WebkitBackdropFilter: "blur(0px) saturate(100%)", borderBottom: "1px solid transparent", transition: MATERIALISE } : { ...safeTop, background: "rgba(18,18,20,0.72)", backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: "1px solid rgba(255,255,255,0.06)" }; return React.createElement("header", { className: onLanding ? "header-minimal z-50" : "header-minimal sticky top-0 z-50", style: headerStyle @@ -31829,7 +32250,7 @@ Right-click or Ctrl+click to disconnect`, React.createElement("div", { key: "content", className: "flex items-center justify-between", - style: { height: "64px", gap: "16px" } + style: { height: "var(--sb-bar-h, 64px)", gap: "16px" } }, [ // Left: logo + wordmark React.createElement("div", { key: "left", style: { display: "flex", alignItems: "center", gap: "12px", minWidth: 0 } }, [ @@ -33584,6 +34005,7 @@ var CallUIComponent = ({ webrtcManager, peerTitle }) => { if (phase === "idle") setMinimized(false); }, [phase]); if (!active || phase === "idle" || phase === "ended") return null; + if (call.groupCallId) return null; const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`; const ringing = phase === "outgoing" || phase === "connecting"; const callStatus = phase === "outgoing" ? t("call.ringing") : phase === "connecting" ? t("call.connecting") : phase === "active" ? fmt(seconds) : t("call.ringing"); diff --git a/dist/app-boot.js.map b/dist/app-boot.js.map index f472d81..861503b 100644 --- a/dist/app-boot.js.map +++ b/dist/app-boot.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../node_modules/dompurify/src/utils.ts", "../node_modules/dompurify/src/tags.ts", "../node_modules/dompurify/src/attrs.ts", "../node_modules/dompurify/src/regexp.ts", "../node_modules/dompurify/src/purify.ts", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/i18n/generated.js", "../src/i18n/index.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/webrtc/config.js", "../src/network/webrtc/sdp.js", "../src/network/webrtc/audio.js", "../src/network/webrtc/video.js", "../src/network/webrtc/adaptation/metrics.js", "../src/network/webrtc/adaptation/controller.js", "../src/crypto/DoubleRatchet.js", "../src/network/descriptor/sbq2.js", "../src/network/descriptor/keyexchange.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../package.json", "../src/components/ui/LanguageSwitcher.jsx", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/CommunityCTA.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.jsx", "../src/components/ui/CallUI.jsx"], - "sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n * XSS Protection: Sanitize input text\n * @param {string} text - Text to sanitize\n * @returns {string} Sanitized text\n * @private\n */\n sanitizeText(text) {\n if (typeof text !== 'string') {\n return '';\n }\n \n // Remove HTML tags and potentially dangerous characters\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .substring(0, 500); // Length limit\n }\n\n /**\n * Validate icon URL (XSS protection)\n * @param {string} url - URL to validate\n * @returns {string|null} Validated URL or null\n * @private\n */\n validateIconUrl(url) {\n if (!url) return null;\n \n try {\n const parsedUrl = new URL(url, window.location.origin);\n \n // Only allow HTTPS and data URLs\n if (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'data:') {\n // Check trusted origins if specified\n if (this.trustedOrigins.length > 0) {\n const isTrusted = this.trustedOrigins.some(origin => \n parsedUrl.origin === origin\n );\n return isTrusted ? parsedUrl.href : null;\n }\n return parsedUrl.href;\n }\n \n return null;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Rate limiting for spam protection\n * @returns {boolean} Rate limit check passed\n * @private\n */\n checkRateLimit() {\n const now = Date.now();\n if (now - this.lastNotificationTime < this.rateLimitMs) {\n return false;\n }\n this.lastNotificationTime = now;\n return true;\n }\n\n /**\n * Send secure notification\n * @param {string} senderName - Name of message sender\n * @param {string} message - Message content\n * @param {Object} options - Notification options\n * @returns {Notification|null} Created notification or null\n */\n notify(senderName, message, options = {}) {\n // Abort if Notifications API is not available (e.g., iOS Safari)\n if (typeof Notification === 'undefined') {\n return null;\n }\n // Update tab active state before checking\n this.isTabActive = this.checkTabActive();\n \n // Only show if tab is NOT active (user is on another tab or minimized)\n if (this.isTabActive) {\n return null;\n }\n\n // Permission check\n if (this.permission !== 'granted') {\n return null;\n }\n\n // Rate limiting\n if (!this.checkRateLimit()) {\n return null;\n }\n\n // Data sanitization (XSS Protection)\n const safeSenderName = this.sanitizeText(senderName || 'Unknown');\n const safeMessage = this.sanitizeText(message || '');\n const safeIcon = this.validateIconUrl(options.icon) || '/logo/icon-192x192.png';\n\n // Queue overflow protection\n if (this.notificationQueue.length >= this.maxQueueSize) {\n this.clearNotificationQueue();\n }\n\n try {\n \n const notification = new Notification(\n `${safeSenderName}`,\n {\n body: safeMessage.substring(0, 200), // Length limit\n icon: safeIcon,\n badge: safeIcon,\n tag: `chat-${options.senderId || 'unknown'}`, // Grouping\n requireInteraction: false, // Don't block user\n silent: options.silent || false,\n // Vibrate only for mobile and if supported\n vibrate: navigator.vibrate ? [200, 100, 200] : undefined,\n // Safe metadata\n data: {\n senderId: this.sanitizeText(options.senderId),\n timestamp: Date.now(),\n // Don't include sensitive data!\n }\n }\n );\n\n // Increment counter\n this.unreadCount++;\n this.updateTitle();\n\n // Add to queue for management\n this.notificationQueue.push(notification);\n\n // Safe click handler\n notification.onclick = (event) => {\n event.preventDefault(); // Prevent default behavior\n window.focus();\n notification.close();\n \n // Safe callback\n if (typeof options.onClick === 'function') {\n try {\n options.onClick(options.senderId);\n } catch (error) {\n console.error('[Notifications] Error in onClick handler:', error);\n }\n }\n };\n\n // Error handler\n notification.onerror = (event) => {\n console.error('[Notifications] Error showing notification:', event);\n };\n\n // Auto-close after reasonable time\n const autoCloseTimeout = Math.min(options.autoClose || 5000, 10000);\n setTimeout(() => {\n notification.close();\n this.removeFromQueue(notification);\n }, autoCloseTimeout);\n\n return notification;\n \n } catch (error) {\n console.error('[Notifications] Failed to create notification:', error);\n return null;\n }\n }\n\n /**\n * Remove notification from queue\n * @param {Notification} notification - Notification to remove\n * @private\n */\n removeFromQueue(notification) {\n const index = this.notificationQueue.indexOf(notification);\n if (index > -1) {\n this.notificationQueue.splice(index, 1);\n }\n }\n\n /**\n * Clear all notifications\n */\n clearNotificationQueue() {\n this.notificationQueue.forEach(notification => {\n try {\n notification.close();\n } catch (error) {\n // Ignore errors when closing\n }\n });\n this.notificationQueue = [];\n }\n\n /**\n * Reset unread counter\n */\n resetUnreadCount() {\n this.unreadCount = 0;\n this.updateTitle();\n }\n\n /**\n * Get current status\n * @returns {Object} Current notification status\n */\n getStatus() {\n return {\n permission: this.permission,\n isTabActive: this.isTabActive,\n unreadCount: this.unreadCount,\n isSecureContext: this.isSecureContext,\n queueSize: this.notificationQueue.length\n };\n }\n}\n\n/**\n * Secure integration with WebRTC\n */\nclass SecureP2PChat {\n constructor() {\n this.notificationManager = new SecureChatNotificationManager({\n maxQueueSize: 5,\n rateLimitMs: 2000,\n trustedOrigins: [\n window.location.origin,\n // Add other trusted origins for CDN icons\n ]\n });\n \n this.dataChannel = null;\n this.peerConnection = null;\n this.remotePeerName = 'Peer';\n this.messageHistory = [];\n this.maxHistorySize = 100;\n }\n\n /**\n * Initialize when user connects\n */\n async init() {\n // Initialize notification manager silently\n }\n\n /**\n * Method for manual permission request (called on click)\n * @returns {Promise} Permission granted status\n */\n async enableNotifications() {\n const granted = await this.notificationManager.requestPermission();\n return granted;\n }\n\n /**\n * Setup DataChannel with security checks\n * @param {RTCDataChannel} dataChannel - WebRTC data channel\n */\n setupDataChannel(dataChannel) {\n if (!dataChannel) {\n console.error('[Chat] Invalid DataChannel');\n return;\n }\n\n this.dataChannel = dataChannel;\n \n // Setup handlers\n this.dataChannel.onmessage = (event) => {\n this.handleIncomingMessage(event.data);\n };\n\n this.dataChannel.onerror = (error) => {\n // Handle error silently\n };\n }\n\n /**\n * XSS Protection: Validate incoming messages\n * @param {string|Object} data - Message data\n * @returns {Object|null} Validated message or null\n * @private\n */\n validateMessage(data) {\n try {\n const message = typeof data === 'string' ? JSON.parse(data) : data;\n \n // Check message structure\n if (!message || typeof message !== 'object') {\n throw new Error('Invalid message structure');\n }\n\n // Check required fields\n if (!message.text || typeof message.text !== 'string') {\n throw new Error('Invalid message text');\n }\n\n // Message length limit (DoS protection)\n if (message.text.length > 10000) {\n throw new Error('Message too long');\n }\n\n return {\n text: message.text,\n senderName: message.senderName || 'Unknown',\n senderId: message.senderId || 'unknown',\n timestamp: message.timestamp || Date.now(),\n senderAvatar: message.senderAvatar || null\n };\n \n } catch (error) {\n console.error('[Chat] Message validation failed:', error);\n return null;\n }\n }\n\n /**\n * Secure handling of incoming messages\n * @param {string|Object} data - Message data\n * @private\n */\n handleIncomingMessage(data) {\n const message = this.validateMessage(data);\n \n if (!message) {\n return;\n }\n\n // Save to history (with limit)\n this.messageHistory.push(message);\n if (this.messageHistory.length > this.maxHistorySize) {\n this.messageHistory.shift();\n }\n\n // Display in UI (with sanitization)\n this.displayMessage(message);\n\n // Send notification only if tab is inactive\n this.notificationManager.notify(\n message.senderName,\n message.text,\n {\n icon: message.senderAvatar,\n senderId: message.senderId,\n onClick: (senderId) => {\n this.scrollToLatestMessage();\n }\n }\n );\n\n // Optional: sound (with check)\n if (!this.notificationManager.isTabActive) {\n this.playNotificationSound();\n }\n }\n\n /**\n * XSS Protection: Safe message display\n * @param {Object} message - Message to display\n * @private\n */\n displayMessage(message) {\n const container = document.getElementById('messages');\n if (!container) {\n return;\n }\n\n const messageEl = document.createElement('div');\n messageEl.className = 'message';\n \n // Use textContent to prevent XSS\n const nameEl = document.createElement('strong');\n nameEl.textContent = message.senderName + ': ';\n \n const textEl = document.createElement('span');\n textEl.textContent = message.text;\n textEl.style.wordWrap = 'break-word';\n textEl.style.overflowWrap = 'break-word';\n textEl.style.whiteSpace = 'normal';\n \n const timeEl = document.createElement('small');\n timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();\n \n messageEl.appendChild(nameEl);\n messageEl.appendChild(textEl);\n messageEl.appendChild(document.createElement('br'));\n messageEl.appendChild(timeEl);\n \n container.appendChild(messageEl);\n this.scrollToLatestMessage();\n }\n\n /**\n * Safe sound playback\n * @private\n */\n playNotificationSound() {\n try {\n // Use only local audio files\n const audio = new Audio('/assets/audio/notification.mp3');\n audio.volume = 0.3; // Moderate volume\n \n // Error handling\n audio.play().catch(error => {\n // Handle audio error silently\n });\n } catch (error) {\n // Handle audio creation error silently\n }\n }\n\n /**\n * Scroll to latest message\n * @private\n */\n scrollToLatestMessage() {\n const container = document.getElementById('messages');\n if (container) {\n container.scrollTop = container.scrollHeight;\n }\n }\n\n /**\n * Get status\n * @returns {Object} Current chat status\n */\n getStatus() {\n return {\n notifications: this.notificationManager.getStatus(),\n messageCount: this.messageHistory.length,\n connected: this.dataChannel?.readyState === 'open'\n };\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = { SecureChatNotificationManager, SecureP2PChat };\n}\n\n// Global export for browser usage\nif (typeof window !== 'undefined') {\n window.SecureChatNotificationManager = SecureChatNotificationManager;\n window.SecureP2PChat = SecureP2PChat;\n}\n", "/**\n * Notification Integration Module for SecureBit WebRTC Chat\n * Integrates secure notifications with existing WebRTC architecture\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nimport { SecureChatNotificationManager } from './SecureNotificationManager.js';\n\nclass NotificationIntegration {\n constructor(webrtcManager) {\n this.webrtcManager = webrtcManager;\n this.notificationManager = new SecureChatNotificationManager({\n maxQueueSize: 10,\n rateLimitMs: 1000, // Reduced from 2000ms to 1000ms\n trustedOrigins: [\n window.location.origin,\n // Add other trusted origins for CDN icons\n ]\n });\n \n this.isInitialized = false;\n this.originalOnMessage = null;\n this.originalOnStatusChange = null;\n this.processedMessages = new Set(); // Track processed messages to avoid duplicates\n }\n\n /**\n * Initialize notification integration\n * @returns {Promise} Initialization success\n */\n async init() {\n try {\n if (this.isInitialized) {\n return true;\n }\n\n // Store original callbacks\n this.originalOnMessage = this.webrtcManager.onMessage;\n this.originalOnStatusChange = this.webrtcManager.onStatusChange;\n\n\n // Wrap the original onMessage callback.\n // IMPORTANT: forward ALL arguments (incl. per-message `meta`) so the app\n // still receives view-once / disappearing / unsend metadata.\n this.webrtcManager.onMessage = (message, type, ...rest) => {\n this.handleIncomingMessage(message, type, rest[0]);\n\n // Call original callback if it exists\n if (this.originalOnMessage) {\n this.originalOnMessage(message, type, ...rest);\n }\n };\n\n // Wrap the original onStatusChange callback\n this.webrtcManager.onStatusChange = (status) => {\n this.handleStatusChange(status);\n \n // Call original callback if it exists\n if (this.originalOnStatusChange) {\n this.originalOnStatusChange(status);\n }\n };\n\n // Also hook into the deliverMessageToUI method if it exists.\n // IMPORTANT: forward ALL arguments (incl. per-message `meta`) to the\n // original, otherwise view-once / disappearing / unsend metadata is lost.\n if (this.webrtcManager.deliverMessageToUI) {\n this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);\n this.webrtcManager.deliverMessageToUI = (message, type, ...rest) => {\n this.handleIncomingMessage(message, type, rest[0]);\n this.originalDeliverMessageToUI(message, type, ...rest);\n };\n }\n\n this.isInitialized = true;\n return true;\n\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Handle incoming messages and trigger notifications\n * @param {*} message - Message content\n * @param {string} type - Message type\n * @private\n */\n handleIncomingMessage(message, type, meta) {\n try {\n // Create a unique key for this message to avoid duplicates\n const messageKey = `${type}:${typeof message === 'string' ? message : JSON.stringify(message)}`;\n \n // Skip if we've already processed this message\n if (this.processedMessages.has(messageKey)) {\n return;\n }\n \n // Mark message as processed\n this.processedMessages.add(messageKey);\n \n // Clean up old processed messages (keep only last 100)\n if (this.processedMessages.size > 100) {\n const messagesArray = Array.from(this.processedMessages);\n this.processedMessages.clear();\n messagesArray.slice(-50).forEach(msg => this.processedMessages.add(msg));\n }\n \n \n // Only process chat messages, not system messages\n if (type === 'system' || type === 'file-transfer' || type === 'heartbeat') {\n return;\n }\n\n // Extract message information\n const messageInfo = this.extractMessageInfo(message, type);\n if (!messageInfo) {\n return;\n }\n\n // PRIVACY: a view-once or disappearing message must not be copied into the\n // OS notification. Notifications are shown only while the tab is in the\n // background \u2014 i.e. typically on a lock screen \u2014 and once the OS has the\n // text it lands in the notification centre, in backups and on the user's\n // other synced devices. From there the app can no longer delete it, so the\n // message the UI destroys after 30 seconds outlives itself indefinitely.\n // Show that something arrived; never what it said.\n const isEphemeral = !!meta && typeof meta === 'object' &&\n (meta.once === true || (Number.isFinite(meta.ttl) && meta.ttl > 0));\n const notificationText = isEphemeral ? 'Sent you a private message' : messageInfo.text;\n\n // Send notification\n const notificationResult = this.notificationManager.notify(\n messageInfo.senderName,\n notificationText,\n {\n icon: messageInfo.senderAvatar,\n senderId: messageInfo.senderId,\n onClick: (senderId) => {\n this.focusChatWindow();\n }\n }\n );\n\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Handle status changes\n * @param {string} status - Connection status\n * @private\n */\n handleStatusChange(status) {\n try {\n // Clear notifications when connection is lost\n if (status === 'disconnected' || status === 'failed') {\n this.notificationManager.clearNotificationQueue();\n this.notificationManager.resetUnreadCount();\n }\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Extract message information for notifications\n * @param {*} message - Message content\n * @param {string} type - Message type\n * @returns {Object|null} Extracted message info or null\n * @private\n */\n extractMessageInfo(message, type) {\n try {\n let messageData = message;\n\n // Handle different message formats\n if (typeof message === 'string') {\n try {\n messageData = JSON.parse(message);\n } catch (e) {\n // Plain text message\n return {\n senderName: 'Peer',\n text: message,\n senderId: 'peer',\n senderAvatar: null\n };\n }\n }\n\n // Handle structured message data\n if (typeof messageData === 'object' && messageData !== null) {\n return {\n senderName: messageData.senderName || messageData.name || 'Peer',\n text: messageData.text || messageData.message || messageData.content || '',\n senderId: messageData.senderId || messageData.id || 'peer',\n senderAvatar: messageData.senderAvatar || messageData.avatar || null\n };\n }\n\n return null;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Focus chat window when notification is clicked\n * @private\n */\n focusChatWindow() {\n try {\n window.focus();\n \n // Scroll to bottom of messages if container exists\n const messagesContainer = document.getElementById('messages');\n if (messagesContainer) {\n messagesContainer.scrollTop = messagesContainer.scrollHeight;\n }\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Request notification permission\n * @returns {Promise} Permission granted status\n */\n async requestPermission() {\n try {\n return await this.notificationManager.requestPermission();\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Get notification status\n * @returns {Object} Notification status\n */\n getStatus() {\n return this.notificationManager.getStatus();\n }\n\n /**\n * Clear all notifications\n */\n clearNotifications() {\n this.notificationManager.clearNotificationQueue();\n this.notificationManager.resetUnreadCount();\n }\n\n /**\n * Cleanup integration\n */\n cleanup() {\n try {\n if (this.isInitialized) {\n // Restore original callbacks\n if (this.originalOnMessage) {\n this.webrtcManager.onMessage = this.originalOnMessage;\n }\n if (this.originalOnStatusChange) {\n this.webrtcManager.onStatusChange = this.originalOnStatusChange;\n }\n if (this.originalDeliverMessageToUI) {\n this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;\n }\n\n // Clear notifications\n this.clearNotifications();\n\n this.isInitialized = false;\n }\n } catch (error) {\n // Handle error silently\n }\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = { NotificationIntegration };\n}\n\n// Global export for browser usage\nif (typeof window !== 'undefined') {\n window.NotificationIntegration = NotificationIntegration;\n}\n", "const {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n} = Object;\n\nlet { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports\nlet { apply, construct } = typeof Reflect !== 'undefined' && Reflect;\n\nif (!freeze) {\n freeze = function (x: T): T {\n return x;\n };\n}\n\nif (!seal) {\n seal = function (x: T): T {\n return x;\n };\n}\n\nif (!apply) {\n apply = function (\n func: (thisArg: any, ...args: any[]) => T,\n thisArg: any,\n ...args: any[]\n ): T {\n return func.apply(thisArg, args);\n };\n}\n\nif (!construct) {\n construct = function (Func: new (...args: any[]) => T, ...args: any[]): T {\n return new Func(...args);\n };\n}\n\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayIndexOf = unapply(Array.prototype.indexOf);\nconst arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySlice = unapply(Array.prototype.slice);\nconst arraySplice = unapply(Array.prototype.splice);\nconst arrayIsArray = Array.isArray;\n\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\n\nconst numberToString = unapply(Number.prototype.toString);\nconst booleanToString = unapply(Boolean.prototype.toString);\nconst bigintToString =\n typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);\nconst symbolToString =\n typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);\n\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\nconst objectToString = unapply(Object.prototype.toString);\n\nconst regExpTest = unapply(RegExp.prototype.test);\n\nconst typeErrorCreate = unconstruct(TypeError);\n\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param func - The function to be wrapped and called.\n * @returns A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(\n func: (thisArg: any, ...args: any[]) => T\n): (thisArg: any, ...args: any[]) => T {\n return (thisArg: any, ...args: any[]): T => {\n if (thisArg instanceof RegExp) {\n thisArg.lastIndex = 0;\n }\n\n return apply(func, thisArg, args);\n };\n}\n\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param func - The constructor function to be wrapped and called.\n * @returns A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(\n Func: new (...args: any[]) => T\n): (...args: any[]) => T {\n return (...args: any[]): T => construct(Func, args);\n}\n\n/**\n * Add properties to a lookup table\n *\n * @param set - The set to which elements will be added.\n * @param array - The array containing elements to be added to the set.\n * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns The modified set with added elements.\n */\nfunction addToSet(\n set: Record,\n array: readonly unknown[],\n transformCaseFunc: ReturnType> = stringToLowerCase\n): Record {\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n\n if (!arrayIsArray(array)) {\n return set;\n }\n\n let l = array.length;\n while (l--) {\n let element = array[l];\n\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n (array as unknown[])[l] = lcElement;\n }\n\n element = lcElement;\n }\n }\n\n set[element as string] = true;\n }\n\n return set;\n}\n\n/**\n * Clean up an array to harden against CSPP\n *\n * @param array - The array to be cleaned.\n * @returns The cleaned version of the array\n */\nfunction cleanArray(array: T[]): Array {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n\n return array;\n}\n\n/**\n * Shallow clone an object\n *\n * @param object - The object to be cloned.\n * @returns A new object that copies the original.\n */\nfunction clone>(object: T): T {\n const newObject = create(null);\n\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n\n if (isPropertyExist) {\n if (arrayIsArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (\n value &&\n typeof value === 'object' &&\n value.constructor === Object\n ) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n\n return newObject;\n}\n\n/**\n * Convert non-node values into strings without depending on direct property access.\n *\n * @param value - The value to stringify.\n * @returns A string representation of the provided value.\n */\nfunction stringifyValue(value: unknown): string {\n switch (typeof value) {\n case 'string': {\n return value;\n }\n\n case 'number': {\n return numberToString(value);\n }\n\n case 'boolean': {\n return booleanToString(value);\n }\n\n case 'bigint': {\n return bigintToString ? bigintToString(value) : '0';\n }\n\n case 'symbol': {\n return symbolToString ? symbolToString(value) : 'Symbol()';\n }\n\n case 'undefined': {\n return objectToString(value);\n }\n\n case 'function':\n case 'object': {\n if (value === null) {\n return objectToString(value);\n }\n\n const valueAsRecord = value as Record;\n const valueToString = lookupGetter(valueAsRecord, 'toString');\n\n if (typeof valueToString === 'function') {\n const stringified = valueToString(valueAsRecord);\n\n return typeof stringified === 'string'\n ? stringified\n : objectToString(stringified);\n }\n\n return objectToString(value);\n }\n\n default: {\n return objectToString(value);\n }\n }\n}\n\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param object - The object to look up the getter function in its prototype chain.\n * @param prop - The property name for which to find the getter function.\n * @returns The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter>(\n object: T,\n prop: string\n): ReturnType> | (() => null) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n\n object = getPrototypeOf(object);\n }\n\n function fallbackValue(): null {\n return null;\n }\n\n return fallbackValue;\n}\n\nfunction isRegex(value: unknown): value is RegExp {\n try {\n regExpTest(value as RegExp, '');\n return true;\n } catch {\n return false;\n }\n}\n\nexport {\n // Array\n arrayForEach,\n arrayIndexOf,\n arrayIsArray,\n arrayLastIndexOf,\n arrayPop,\n arrayPush,\n arraySlice,\n arraySplice,\n // Object\n entries,\n freeze,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n isFrozen,\n setPrototypeOf,\n seal,\n clone,\n create,\n objectHasOwnProperty,\n objectToString,\n // RegExp\n regExpTest,\n isRegex,\n // String\n stringIndexOf,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringTrim,\n // Other conversion\n stringifyValue,\n // Errors\n typeErrorCreate,\n // Other\n lookupGetter,\n addToSet,\n // Reflect\n unapply,\n unconstruct,\n};\n", "import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'a',\n 'abbr',\n 'acronym',\n 'address',\n 'area',\n 'article',\n 'aside',\n 'audio',\n 'b',\n 'bdi',\n 'bdo',\n 'big',\n 'blink',\n 'blockquote',\n 'body',\n 'br',\n 'button',\n 'canvas',\n 'caption',\n 'center',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'content',\n 'data',\n 'datalist',\n 'dd',\n 'decorator',\n 'del',\n 'details',\n 'dfn',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'element',\n 'em',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'font',\n 'footer',\n 'form',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hgroup',\n 'hr',\n 'html',\n 'i',\n 'img',\n 'input',\n 'ins',\n 'kbd',\n 'label',\n 'legend',\n 'li',\n 'main',\n 'map',\n 'mark',\n 'marquee',\n 'menu',\n 'menuitem',\n 'meter',\n 'nav',\n 'nobr',\n 'ol',\n 'optgroup',\n 'option',\n 'output',\n 'p',\n 'picture',\n 'pre',\n 'progress',\n 'q',\n 'rp',\n 'rt',\n 'ruby',\n 's',\n 'samp',\n 'search',\n 'section',\n 'select',\n 'shadow',\n 'slot',\n 'small',\n 'source',\n 'spacer',\n 'span',\n 'strike',\n 'strong',\n 'style',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'template',\n 'textarea',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'tt',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'wbr',\n] as const);\n\nexport const svg = freeze([\n 'svg',\n 'a',\n 'altglyph',\n 'altglyphdef',\n 'altglyphitem',\n 'animatecolor',\n 'animatemotion',\n 'animatetransform',\n 'circle',\n 'clippath',\n 'defs',\n 'desc',\n 'ellipse',\n 'enterkeyhint',\n 'exportparts',\n 'filter',\n 'font',\n 'g',\n 'glyph',\n 'glyphref',\n 'hkern',\n 'image',\n 'inputmode',\n 'line',\n 'lineargradient',\n 'marker',\n 'mask',\n 'metadata',\n 'mpath',\n 'part',\n 'path',\n 'pattern',\n 'polygon',\n 'polyline',\n 'radialgradient',\n 'rect',\n 'stop',\n 'style',\n 'switch',\n 'symbol',\n 'text',\n 'textpath',\n 'title',\n 'tref',\n 'tspan',\n 'view',\n 'vkern',\n] as const);\n\nexport const svgFilters = freeze([\n 'feBlend',\n 'feColorMatrix',\n 'feComponentTransfer',\n 'feComposite',\n 'feConvolveMatrix',\n 'feDiffuseLighting',\n 'feDisplacementMap',\n 'feDistantLight',\n 'feDropShadow',\n 'feFlood',\n 'feFuncA',\n 'feFuncB',\n 'feFuncG',\n 'feFuncR',\n 'feGaussianBlur',\n 'feImage',\n 'feMerge',\n 'feMergeNode',\n 'feMorphology',\n 'feOffset',\n 'fePointLight',\n 'feSpecularLighting',\n 'feSpotLight',\n 'feTile',\n 'feTurbulence',\n] as const);\n\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nexport const svgDisallowed = freeze([\n 'animate',\n 'color-profile',\n 'cursor',\n 'discard',\n 'font-face',\n 'font-face-format',\n 'font-face-name',\n 'font-face-src',\n 'font-face-uri',\n 'foreignobject',\n 'hatch',\n 'hatchpath',\n 'mesh',\n 'meshgradient',\n 'meshpatch',\n 'meshrow',\n 'missing-glyph',\n 'script',\n 'set',\n 'solidcolor',\n 'unknown',\n 'use',\n] as const);\n\nexport const mathMl = freeze([\n 'math',\n 'menclose',\n 'merror',\n 'mfenced',\n 'mfrac',\n 'mglyph',\n 'mi',\n 'mlabeledtr',\n 'mmultiscripts',\n 'mn',\n 'mo',\n 'mover',\n 'mpadded',\n 'mphantom',\n 'mroot',\n 'mrow',\n 'ms',\n 'mspace',\n 'msqrt',\n 'mstyle',\n 'msub',\n 'msup',\n 'msubsup',\n 'mtable',\n 'mtd',\n 'mtext',\n 'mtr',\n 'munder',\n 'munderover',\n 'mprescripts',\n] as const);\n\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nexport const mathMlDisallowed = freeze([\n 'maction',\n 'maligngroup',\n 'malignmark',\n 'mlongdiv',\n 'mscarries',\n 'mscarry',\n 'msgroup',\n 'mstack',\n 'msline',\n 'msrow',\n 'semantics',\n 'annotation',\n 'annotation-xml',\n 'mprescripts',\n 'none',\n] as const);\n\nexport const text = freeze(['#text'] as const);\n", "import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'accept',\n 'action',\n 'align',\n 'alt',\n 'autocapitalize',\n 'autocomplete',\n 'autopictureinpicture',\n 'autoplay',\n 'background',\n 'bgcolor',\n 'border',\n 'capture',\n 'cellpadding',\n 'cellspacing',\n 'checked',\n 'cite',\n 'class',\n 'clear',\n 'color',\n 'cols',\n 'colspan',\n 'command',\n 'commandfor',\n 'controls',\n 'controlslist',\n 'coords',\n 'crossorigin',\n 'datetime',\n 'decoding',\n 'default',\n 'dir',\n 'disabled',\n 'disablepictureinpicture',\n 'disableremoteplayback',\n 'download',\n 'draggable',\n 'enctype',\n 'enterkeyhint',\n 'exportparts',\n 'face',\n 'for',\n 'headers',\n 'height',\n 'hidden',\n 'high',\n 'href',\n 'hreflang',\n 'id',\n 'inert',\n 'inputmode',\n 'integrity',\n 'ismap',\n 'kind',\n 'label',\n 'lang',\n 'list',\n 'loading',\n 'loop',\n 'low',\n 'max',\n 'maxlength',\n 'media',\n 'method',\n 'min',\n 'minlength',\n 'multiple',\n 'muted',\n 'name',\n 'nonce',\n 'noshade',\n 'novalidate',\n 'nowrap',\n 'open',\n 'optimum',\n 'part',\n 'pattern',\n 'placeholder',\n 'playsinline',\n 'popover',\n 'popovertarget',\n 'popovertargetaction',\n 'poster',\n 'preload',\n 'pubdate',\n 'radiogroup',\n 'readonly',\n 'rel',\n 'required',\n 'rev',\n 'reversed',\n 'role',\n 'rows',\n 'rowspan',\n 'spellcheck',\n 'scope',\n 'selected',\n 'shape',\n 'size',\n 'sizes',\n 'slot',\n 'span',\n 'srclang',\n 'start',\n 'src',\n 'srcset',\n 'step',\n 'style',\n 'summary',\n 'tabindex',\n 'title',\n 'translate',\n 'type',\n 'usemap',\n 'valign',\n 'value',\n 'width',\n 'wrap',\n 'xmlns',\n] as const);\n\nexport const svg = freeze([\n 'accent-height',\n 'accumulate',\n 'additive',\n 'alignment-baseline',\n 'amplitude',\n 'ascent',\n 'attributename',\n 'attributetype',\n 'azimuth',\n 'basefrequency',\n 'baseline-shift',\n 'begin',\n 'bias',\n 'by',\n 'class',\n 'clip',\n 'clippathunits',\n 'clip-path',\n 'clip-rule',\n 'color',\n 'color-interpolation',\n 'color-interpolation-filters',\n 'color-profile',\n 'color-rendering',\n 'cx',\n 'cy',\n 'd',\n 'dx',\n 'dy',\n 'diffuseconstant',\n 'direction',\n 'display',\n 'divisor',\n 'dur',\n 'edgemode',\n 'elevation',\n 'end',\n 'exponent',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'filter',\n 'filterunits',\n 'flood-color',\n 'flood-opacity',\n 'font-family',\n 'font-size',\n 'font-size-adjust',\n 'font-stretch',\n 'font-style',\n 'font-variant',\n 'font-weight',\n 'fx',\n 'fy',\n 'g1',\n 'g2',\n 'glyph-name',\n 'glyphref',\n 'gradientunits',\n 'gradienttransform',\n 'height',\n 'href',\n 'id',\n 'image-rendering',\n 'in',\n 'in2',\n 'intercept',\n 'k',\n 'k1',\n 'k2',\n 'k3',\n 'k4',\n 'kerning',\n 'keypoints',\n 'keysplines',\n 'keytimes',\n 'lang',\n 'lengthadjust',\n 'letter-spacing',\n 'kernelmatrix',\n 'kernelunitlength',\n 'lighting-color',\n 'local',\n 'marker-end',\n 'marker-mid',\n 'marker-start',\n 'markerheight',\n 'markerunits',\n 'markerwidth',\n 'maskcontentunits',\n 'maskunits',\n 'max',\n 'mask',\n 'mask-type',\n 'media',\n 'method',\n 'mode',\n 'min',\n 'name',\n 'numoctaves',\n 'offset',\n 'operator',\n 'opacity',\n 'order',\n 'orient',\n 'orientation',\n 'origin',\n 'overflow',\n 'paint-order',\n 'path',\n 'pathlength',\n 'patterncontentunits',\n 'patterntransform',\n 'patternunits',\n 'points',\n 'preservealpha',\n 'preserveaspectratio',\n 'primitiveunits',\n 'r',\n 'rx',\n 'ry',\n 'radius',\n 'refx',\n 'refy',\n 'repeatcount',\n 'repeatdur',\n 'restart',\n 'result',\n 'rotate',\n 'scale',\n 'seed',\n 'shape-rendering',\n 'slope',\n 'specularconstant',\n 'specularexponent',\n 'spreadmethod',\n 'startoffset',\n 'stddeviation',\n 'stitchtiles',\n 'stop-color',\n 'stop-opacity',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-miterlimit',\n 'stroke-opacity',\n 'stroke',\n 'stroke-width',\n 'style',\n 'surfacescale',\n 'systemlanguage',\n 'tabindex',\n 'tablevalues',\n 'targetx',\n 'targety',\n 'transform',\n 'transform-origin',\n 'text-anchor',\n 'text-decoration',\n 'text-rendering',\n 'textlength',\n 'type',\n 'u1',\n 'u2',\n 'unicode',\n 'values',\n 'viewbox',\n 'visibility',\n 'version',\n 'vert-adv-y',\n 'vert-origin-x',\n 'vert-origin-y',\n 'width',\n 'word-spacing',\n 'wrap',\n 'writing-mode',\n 'xchannelselector',\n 'ychannelselector',\n 'x',\n 'x1',\n 'x2',\n 'xmlns',\n 'y',\n 'y1',\n 'y2',\n 'z',\n 'zoomandpan',\n] as const);\n\nexport const mathMl = freeze([\n 'accent',\n 'accentunder',\n 'align',\n 'bevelled',\n 'close',\n 'columnalign',\n 'columnlines',\n 'columnspacing',\n 'columnspan',\n 'denomalign',\n 'depth',\n 'dir',\n 'display',\n 'displaystyle',\n 'encoding',\n 'fence',\n 'frame',\n 'height',\n 'href',\n 'id',\n 'largeop',\n 'length',\n 'linethickness',\n 'lquote',\n 'lspace',\n 'mathbackground',\n 'mathcolor',\n 'mathsize',\n 'mathvariant',\n 'maxsize',\n 'minsize',\n 'movablelimits',\n 'notation',\n 'numalign',\n 'open',\n 'rowalign',\n 'rowlines',\n 'rowspacing',\n 'rowspan',\n 'rspace',\n 'rquote',\n 'scriptlevel',\n 'scriptminsize',\n 'scriptsizemultiplier',\n 'selection',\n 'separator',\n 'separators',\n 'stretchy',\n 'subscriptshift',\n 'supscriptshift',\n 'symmetric',\n 'voffset',\n 'width',\n 'xmlns',\n]);\n\nexport const xml = freeze([\n 'xlink:href',\n 'xml:id',\n 'xlink:title',\n 'xml:space',\n 'xmlns:xlink',\n] as const);\n", "import { seal } from './utils.js';\n\nexport const MUSTACHE_EXPR = seal(/{{[\\w\\W]*|^[\\w\\W]*}}/g);\nexport const ERB_EXPR = seal(/<%[\\w\\W]*|^[\\w\\W]*%>/g);\nexport const TMPLIT_EXPR = seal(/\\${[\\w\\W]*/g);\nexport const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]+$/); // eslint-disable-line no-useless-escape\nexport const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nexport const IS_ALLOWED_URI = seal(\n /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nexport const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nexport const ATTR_WHITESPACE = seal(\n /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nexport const DOCTYPE_NAME = seal(/^html$/i);\nexport const CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\n// Markup-significant character probes used by _sanitizeElements.\n// Shared module-level instances are safe despite the sticky /g flags:\n// unapply() resets lastIndex for RegExp receivers before every call.\nexport const ELEMENT_MARKUP_PROBE = seal(/<[/\\w!]/g);\nexport const COMMENT_MARKUP_PROBE = seal(/<[/\\w]/g);\nexport const FALLBACK_TAG_CLOSE = seal(/<\\/no(script|embed|frames)/i);\nexport const SELF_CLOSING_TAG = seal(/\\/>/i);\n", "/* eslint-disable @typescript-eslint/indent */\n\nimport type { Config, UseProfilesConfig } from './config';\nimport type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';\nimport * as TAGS from './tags.js';\nimport * as ATTRS from './attrs.js';\nimport * as EXPRESSIONS from './regexp.js';\nimport {\n addToSet,\n clone,\n entries,\n freeze,\n seal,\n arrayForEach,\n arrayIsArray,\n arrayLastIndexOf,\n arrayPop,\n arrayPush,\n arraySplice,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringIndexOf,\n stringTrim,\n regExpTest,\n isRegex,\n typeErrorCreate,\n lookupGetter,\n create,\n objectHasOwnProperty,\n stringifyValue,\n} from './utils.js';\n\nexport type { Config } from './config';\n\nexport type {\n DOMPurify,\n RemovedElement,\n RemovedAttribute,\n HookName,\n NodeHook,\n ElementHook,\n DocumentFragmentHook,\n UponSanitizeElementHook,\n UponSanitizeAttributeHook,\n UponSanitizeElementHookEvent,\n UponSanitizeAttributeHookEvent,\n WindowLike,\n} from './types';\n\ndeclare const VERSION: string;\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\nconst NODE_TYPE = {\n element: 1,\n attribute: 2,\n text: 3,\n cdataSection: 4,\n entityReference: 5, // Deprecated\n entityNode: 6, // Deprecated\n processingInstruction: 7,\n comment: 8,\n document: 9,\n documentType: 10,\n documentFragment: 11,\n notation: 12, // Deprecated\n};\n\nconst getGlobal = function (): WindowLike {\n return typeof window === 'undefined' ? null : window;\n};\n\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param trustedTypes The policy factory.\n * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function (\n trustedTypes: TrustedTypePolicyFactory,\n purifyHostElement: HTMLScriptElement\n) {\n if (\n typeof trustedTypes !== 'object' ||\n typeof trustedTypes.createPolicy !== 'function'\n ) {\n return null;\n }\n\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n },\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn(\n 'TrustedTypes policy ' + policyName + ' could not be created.'\n );\n return null;\n }\n};\n\nconst _createHooksMap = function (): HooksMap {\n return {\n afterSanitizeAttributes: [],\n afterSanitizeElements: [],\n afterSanitizeShadowDOM: [],\n beforeSanitizeAttributes: [],\n beforeSanitizeElements: [],\n beforeSanitizeShadowDOM: [],\n uponSanitizeAttribute: [],\n uponSanitizeElement: [],\n uponSanitizeShadowNode: [],\n };\n};\n\n/**\n * Resolve a set-valued configuration option: a fresh set built from\n * cfg[key] when it is an own array property (seeded with a clone of\n * options.base when given, case-normalized via options.transform),\n * the fallback set otherwise.\n *\n * @param cfg the cloned, prototype-free configuration object\n * @param key the configuration property to read\n * @param fallback the set to use when the option is absent or not an array\n * @param options transform and optional base set to merge into\n * @returns the resolved set\n */\nconst _resolveSetOption = function (\n cfg: Config,\n key: keyof Config,\n fallback: Record,\n options: {\n transform: Parameters[2];\n base?: Record;\n }\n): Record {\n return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key])\n ? addToSet(\n options.base ? clone(options.base) : {},\n cfg[key] as readonly unknown[],\n options.transform\n )\n : fallback;\n};\n\nfunction createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {\n const DOMPurify: DOMPurify = (root: WindowLike) => createDOMPurify(root);\n\n DOMPurify.version = VERSION;\n\n DOMPurify.removed = [];\n\n if (\n !window ||\n !window.document ||\n window.document.nodeType !== NODE_TYPE.document ||\n !window.Element\n ) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n\n return DOMPurify;\n }\n\n let { document } = window;\n\n const originalDocument = document;\n const currentScript: HTMLScriptElement =\n originalDocument.currentScript as HTMLScriptElement;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || (window as any).MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes,\n } = window;\n\n const ElementPrototype = Element.prototype;\n\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const remove = lookupGetter(ElementPrototype, 'remove');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');\n const getAttributes = lookupGetter(ElementPrototype, 'attributes');\n const getNodeType =\n Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;\n const getNodeName =\n Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;\n\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n\n let trustedTypesPolicy;\n let emptyHTML = '';\n\n // The instance's own internal Trusted Types policy. Unlike a caller-supplied\n // `TRUSTED_TYPES_POLICY`, this is created at most once \u2014 Trusted Types throws\n // on duplicate policy names \u2014 and is the only policy allowed to persist\n // across configurations and survive `clearConfig()`.\n let defaultTrustedTypesPolicy;\n let defaultTrustedTypesPolicyResolved = false;\n\n // Tracks whether we are already inside a call to the configured Trusted Types\n // policy (`createHTML` or `createScriptURL`). If a supplied policy callback\n // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would\n // re-enter the policy and recurse until the stack overflows. We detect that\n // re-entry and throw a clear, actionable error instead. The guard is shared\n // across both callbacks, because either one re-entering `sanitize` triggers\n // the same unbounded recursion.\n let IN_TRUSTED_TYPES_POLICY = 0;\n const _assertNotInTrustedTypesPolicy = function (): void {\n if (IN_TRUSTED_TYPES_POLICY > 0) {\n throw typeErrorCreate(\n 'A configured TRUSTED_TYPES_POLICY callback (createHTML or ' +\n 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' +\n 'infinite recursion. Do not pass a policy whose callbacks wrap ' +\n 'DOMPurify as TRUSTED_TYPES_POLICY; see the \"DOMPurify and Trusted ' +\n 'Types\" section of the README.'\n );\n }\n };\n\n const _createTrustedHTML = function (html: string): string {\n _assertNotInTrustedTypesPolicy();\n\n IN_TRUSTED_TYPES_POLICY++;\n try {\n return trustedTypesPolicy.createHTML(html);\n } finally {\n IN_TRUSTED_TYPES_POLICY--;\n }\n };\n\n const _createTrustedScriptURL = function (scriptUrl: string): string {\n _assertNotInTrustedTypesPolicy();\n\n IN_TRUSTED_TYPES_POLICY++;\n try {\n return trustedTypesPolicy.createScriptURL(scriptUrl);\n } finally {\n IN_TRUSTED_TYPES_POLICY--;\n }\n };\n\n // Lazily resolve (and cache) the instance's internal default policy.\n // Resolution is attempted at most once: a successful `createPolicy` cannot be\n // repeated (Trusted Types throws on duplicate names), and a failed or\n // unsupported attempt must not be retried on every parse.\n const _getDefaultTrustedTypesPolicy = function () {\n if (!defaultTrustedTypesPolicyResolved) {\n defaultTrustedTypesPolicy = _createTrustedTypesPolicy(\n trustedTypes,\n currentScript\n );\n defaultTrustedTypesPolicyResolved = true;\n }\n\n return defaultTrustedTypesPolicy;\n };\n\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName,\n } = document;\n const { importNode } = originalDocument;\n\n let hooks = _createHooksMap();\n\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported =\n typeof entries === 'function' &&\n typeof getParentNode === 'function' &&\n implementation &&\n implementation.createHTMLDocument !== undefined;\n\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n CUSTOM_ELEMENT,\n } = EXPRESSIONS;\n\n let { IS_ALLOWED_URI } = EXPRESSIONS;\n\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [\n ...TAGS.html,\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.mathMl,\n ...TAGS.text,\n ]);\n\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [\n ...ATTRS.html,\n ...ATTRS.svg,\n ...ATTRS.mathMl,\n ...ATTRS.xml,\n ]);\n\n /*\n * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false,\n },\n })\n );\n\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n\n /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */\n const EXTRA_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n })\n );\n\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n\n /* Output should be safe even for XML used within HTML and alike.\n * This means, DOMPurify removes comments when containing risky content.\n */\n let SAFE_FOR_XML = true;\n\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (\u00A77.3.3)\n * - DOM Tree Accessors (\u00A73.1.5)\n * - Form Element Parent-Child Relations (\u00A74.10.3)\n * - Iframe srcdoc / Nested WindowProxies (\u00A74.8.5)\n * - HTMLCollection (\u00A74.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES: UseProfilesConfig | false = {};\n\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, [\n 'annotation-xml',\n 'audio',\n 'colgroup',\n 'desc',\n 'foreignobject',\n 'head',\n 'iframe',\n 'math',\n 'mi',\n 'mn',\n 'mo',\n 'ms',\n 'mtext',\n 'noembed',\n 'noframes',\n 'noscript',\n 'plaintext',\n 'script',\n // mirrors the selected