2026-05-19 09:49:22 -04:00
// src/utils/debugWindowHooks.js
function isSecureBitDebugEnabled ( targetWindow = globalThis . window ) {
return targetWindow ? . SECUREBIT_DEBUG === true ;
}
function installDebugWindowHooks ({
targetWindow = globalThis . window ,
webrtcManagerRef ,
onClearData ,
clearConsole = () => {
if ( typeof console . clear === "function" ) {
console . clear ();
}
}
}) {
if ( ! isSecureBitDebugEnabled ( targetWindow )) {
return () => {
};
}
targetWindow . forceCleanup = () => {
onClearData ();
if ( webrtcManagerRef . current ) {
webrtcManagerRef . current . disconnect ();
}
};
targetWindow . clearLogs = clearConsole ;
targetWindow . webrtcManagerRef = webrtcManagerRef ;
return () => {
delete targetWindow . forceCleanup ;
delete targetWindow . clearLogs ;
delete targetWindow . webrtcManagerRef ;
};
}
2026-06-15 15:39:13 -04:00
// src/network/iceSettingsStore.js
var DB_NAME = "securebit-net" ;
var DB_VERSION = 1 ;
var STORE = "kv" ;
var KEY_RECORD = "ice-device-key" ;
var SETTINGS_RECORD = "ice-settings" ;
var SETTINGS_VERSION = 1 ;
function isSupported () {
return typeof indexedDB !== "undefined" && typeof crypto !== "undefined" && !! crypto . subtle ;
}
function openDb () {
return new Promise (( resolve , reject ) => {
let request ;
try {
request = indexedDB . open ( DB_NAME , DB_VERSION );
} catch ( error ) {
reject ( error );
return ;
}
request . onupgradeneeded = () => {
const db = request . result ;
if ( ! db . objectStoreNames . contains ( STORE )) {
db . createObjectStore ( STORE );
}
};
request . onsuccess = () => resolve ( request . result );
request . onerror = () => reject ( request . error );
});
}
function idbGet ( db , key ) {
return new Promise (( resolve , reject ) => {
const tx = db . transaction ( STORE , "readonly" );
const req = tx . objectStore ( STORE ). get ( key );
req . onsuccess = () => resolve ( req . result );
req . onerror = () => reject ( req . error );
});
}
function idbPut ( db , key , value ) {
return new Promise (( resolve , reject ) => {
const tx = db . transaction ( STORE , "readwrite" );
tx . objectStore ( STORE ). put ( value , key );
tx . oncomplete = () => resolve ();
tx . onerror = () => reject ( tx . error );
});
}
function idbDelete ( db , key ) {
return new Promise (( resolve , reject ) => {
const tx = db . transaction ( STORE , "readwrite" );
tx . objectStore ( STORE ). delete ( key );
tx . oncomplete = () => resolve ();
tx . onerror = () => reject ( tx . error );
});
}
async function getOrCreateDeviceKey ( db ) {
const existing = await idbGet ( db , KEY_RECORD );
if ( existing instanceof CryptoKey ) {
return existing ;
}
const key = await crypto . subtle . generateKey (
{ name : "AES-GCM" , length : 256 },
false ,
// non-extractable
[ "encrypt" , "decrypt" ]
);
await idbPut ( db , KEY_RECORD , key );
return key ;
}
async function saveIceSettings ( settings ) {
if ( ! isSupported ()) throw new Error ( "Persistent storage is not available in this browser" );
const db = await openDb ();
const key = await getOrCreateDeviceKey ( db );
const payload = JSON . stringify ({
version : SETTINGS_VERSION ,
servers : Array . isArray ( settings ? . servers ) ? settings . servers : [],
privacyMode : settings ? . privacyMode === "relay-only" ? "relay-only" : "standard"
});
const iv = crypto . getRandomValues ( new Uint8Array ( 12 ));
const ciphertext = await crypto . subtle . encrypt (
{ name : "AES-GCM" , iv },
key ,
new TextEncoder (). encode ( payload )
);
await idbPut ( db , SETTINGS_RECORD , {
iv : Array . from ( iv ),
data : Array . from ( new Uint8Array ( ciphertext ))
});
}
async function loadIceSettings () {
if ( ! isSupported ()) return null ;
try {
const db = await openDb ();
const record = await idbGet ( db , SETTINGS_RECORD );
if ( ! record || ! Array . isArray ( record . iv ) || ! Array . isArray ( record . data )) {
return null ;
}
const key = await idbGet ( db , KEY_RECORD );
if ( ! ( key instanceof CryptoKey )) return null ;
const plaintext = await crypto . subtle . decrypt (
{ name : "AES-GCM" , iv : new Uint8Array ( record . iv ) },
key ,
new Uint8Array ( record . data )
);
const parsed = JSON . parse ( new TextDecoder (). decode ( plaintext ));
return {
servers : Array . isArray ( parsed . servers ) ? parsed . servers : [],
privacyMode : parsed . privacyMode === "relay-only" ? "relay-only" : "standard"
};
} catch {
return null ;
}
}
async function clearIceSettings () {
if ( ! isSupported ()) return ;
try {
const db = await openDb ();
await idbDelete ( db , SETTINGS_RECORD );
} catch {
}
}
2026-06-26 00:00:13 -04:00
// src/state/sessionsStore.js
var SESSION_ACTIONS = Object . freeze ({
CREATE_SESSION : "CREATE_SESSION" ,
REMOVE_SESSION : "REMOVE_SESSION" ,
SET_ACTIVE : "SET_ACTIVE" ,
SET_STATUS : "SET_STATUS" ,
SET_FINGERPRINT : "SET_FINGERPRINT" ,
SET_VERIFICATION : "SET_VERIFICATION" ,
SET_SAS : "SET_SAS" ,
ADD_MESSAGE : "ADD_MESSAGE" ,
SET_MESSAGES : "SET_MESSAGES" ,
UPDATE_MESSAGE_STATUS : "UPDATE_MESSAGE_STATUS" ,
DELETE_MESSAGE : "DELETE_MESSAGE" ,
EXPIRE_MESSAGE : "EXPIRE_MESSAGE" ,
INCREMENT_UNREAD : "INCREMENT_UNREAD" ,
CLEAR_UNREAD : "CLEAR_UNREAD" ,
SET_PENDING_FILES : "SET_PENDING_FILES" ,
PATCH_SETUP : "PATCH_SETUP" ,
RENAME : "RENAME" ,
SET_PEER_PRESENCE : "SET_PEER_PRESENCE"
});
var PRESENCE_DOT = { available : "#3ecf8e" , away : "#e3b341" , busy : "#e5727a" , offline : "#6b6b73" };
var PRESENCE_WORD = { available : "Available" , away : "Away" , busy : "Busy" , offline : "Offline" };
var MY_STATUS_OPTIONS = [
{ key : "available" , word : "Available" , desc : "Online and reachable" , dot : "#3ecf8e" },
{ key : "away" , word : "Away" , desc : "Idle \xB7 stepped away" , dot : "#e3b341" },
{ key : "busy" , word : "Busy" , desc : "Do not disturb" , dot : "#e5727a" },
{ key : "invisible" , word : "Invisible" , desc : "Appear offline to peers" , dot : "#6b6b73" }
];
function shortLabelFromId ( id ) {
const hex = String ( id || "" ). replace ( /[^a-z0-9]/gi , "" );
return "Chat " + ( hex . slice ( 0 , 4 ) || "0000" ). toUpperCase ();
}
function monoInitials ( label ) {
const words = String ( label || "" ). trim (). split ( /\s+/ ). filter ( Boolean );
const a = words [ 0 ] ? .[ 0 ] || "" ;
const b = words [ 1 ] ? .[ 0 ] || words [ 0 ] ? .[ 1 ] || "" ;
return ( a + b ). toUpperCase () || "\xB7\xB7" ;
}
function statusSub ( status ) {
switch ( status ) {
case "connected" :
case "verified" :
return "P2P \xB7 connected" ;
case "verifying" :
return "Verifying\u2026" ;
case "connecting" :
case "new" :
return "Connecting\u2026" ;
case "peer_disconnected" :
return "Peer disconnected" ;
default :
return "Disconnected" ;
}
}
function emptySetup () {
return {
offerData : "" ,
answerData : "" ,
offerInput : "" ,
answerInput : "" ,
showOfferStep : false ,
showAnswerStep : false ,
showVerification : false ,
showQRCode : false ,
qrCodeUrl : "" ,
isGeneratingKeys : false ,
qrFramesTotal : 0 ,
qrFrameIndex : 0 ,
qrManualMode : false
};
}
function createSessionEntry ( opts = {}) {
const id = opts . id || ( typeof crypto !== "undefined" && crypto . randomUUID ? crypto . randomUUID () : String ( Date . now ()) + Math . random ());
return {
id ,
peerLabel : opts . peerLabel || shortLabelFromId ( id ),
labelIsCustom : false ,
// becomes true once the user renames; blocks SAS auto-relabel
createdAt : opts . createdAt || Date . now (),
role : opts . role || "offer" ,
// 'offer' | 'answer'
status : opts . status || "new" ,
keyFingerprint : "" ,
verificationCode : "" ,
sas : { localConfirmed : false , remoteConfirmed : false , bothConfirmed : false , isVerified : false },
messages : [],
unreadCount : 0 ,
pendingIncomingFiles : [],
peerPresence : null ,
// peer's advertised availability ('available'|'away'|'busy'|'offline'); null = unknown
setup : emptySetup ()
};
}
function createInitialState () {
return { sessions : {}, order : [], activeSessionId : null };
}
function patchSession ( state , id , patch ) {
const session = state . sessions [ id ];
if ( ! session ) return state ;
return {
... state ,
sessions : { ... state . sessions , [ id ] : { ... session , ... patch } }
};
}
function sessionsReducer ( state , action ) {
const A = SESSION_ACTIONS ;
switch ( action . type ) {
case A . CREATE_SESSION : {
const entry = action . entry || createSessionEntry ( action );
if ( state . sessions [ entry . id ]) return state ;
return {
sessions : { ... state . sessions , [ entry . id ] : entry },
order : [... state . order , entry . id ],
activeSessionId : action . activate === false ? state . activeSessionId : entry . id
};
}
case A . REMOVE_SESSION : {
const { id } = action ;
if ( ! state . sessions [ id ]) return state ;
const sessions = { ... state . sessions };
delete sessions [ id ];
const order = state . order . filter (( x ) => x !== id );
let activeSessionId = state . activeSessionId ;
if ( activeSessionId === id ) {
const removedIdx = state . order . indexOf ( id );
activeSessionId = order [ Math . max ( 0 , removedIdx - 1 )] || order [ 0 ] || null ;
}
return { sessions , order , activeSessionId };
}
case A . SET_ACTIVE : {
if ( ! state . sessions [ action . id ]) return state ;
if ( state . activeSessionId === action . id ) return state ;
return { ... state , activeSessionId : action . id };
}
case A . SET_STATUS : {
const session = state . sessions [ action . id ];
if ( ! session || session . status === action . status ) return state ;
return patchSession ( state , action . id , { status : action . status });
}
case A . SET_FINGERPRINT :
return patchSession ( state , action . id , { keyFingerprint : action . fingerprint });
case A . SET_VERIFICATION :
return patchSession ( state , action . id , { verificationCode : action . code });
case A . SET_SAS : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
return patchSession ( state , action . id , { sas : { ... session . sas , ... action . sas } });
}
case A . ADD_MESSAGE : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
return patchSession ( state , action . id , { messages : [... session . messages , action . message ] });
}
case A . SET_MESSAGES : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
const next = typeof action . updater === "function" ? action . updater ( session . messages ) : action . messages ;
return patchSession ( state , action . id , { messages : Array . isArray ( next ) ? next : [] });
}
case A . UPDATE_MESSAGE_STATUS : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
let changed = false ;
const messages = session . messages . map (( m ) => {
if ( String ( m . mid ) === String ( action . mid ) && m . status !== action . status ) {
changed = true ;
return { ... m , status : action . status };
}
return m ;
});
return changed ? patchSession ( state , action . id , { messages }) : state ;
}
case A . DELETE_MESSAGE : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
const messages = session . messages . filter (( m ) => String ( m . mid ) !== String ( action . mid ));
if ( messages . length === session . messages . length ) return state ;
return patchSession ( state , action . id , { messages });
}
case A . EXPIRE_MESSAGE : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
let changed = false ;
const messages = session . messages . map (( m ) => {
if ( String ( m . id ) === String ( action . messageId ) && ! m . expired ) {
changed = true ;
return { ... m , expired : true , message : "" , expiresAt : void 0 };
}
return m ;
});
return changed ? patchSession ( state , action . id , { messages }) : state ;
}
case A . INCREMENT_UNREAD : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
return patchSession ( state , action . id , { unreadCount : session . unreadCount + 1 });
}
case A . CLEAR_UNREAD : {
const session = state . sessions [ action . id ];
if ( ! session || session . unreadCount === 0 ) return state ;
return patchSession ( state , action . id , { unreadCount : 0 });
}
case A . SET_PENDING_FILES : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
const next = typeof action . updater === "function" ? action . updater ( session . pendingIncomingFiles ) : action . files ;
return patchSession ( state , action . id , { pendingIncomingFiles : Array . isArray ( next ) ? next : [] });
}
case A . PATCH_SETUP : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
return patchSession ( state , action . id , { setup : { ... session . setup , ... action . patch } });
}
case A . RENAME : {
const session = state . sessions [ action . id ];
if ( ! session ) return state ;
const label = String ( action . label || "" ). trim () || session . peerLabel ;
return patchSession ( state , action . id , { peerLabel : label , labelIsCustom : true });
}
case A . SET_PEER_PRESENCE : {
const session = state . sessions [ action . id ];
if ( ! session || session . peerPresence === action . presence ) return state ;
return patchSession ( state , action . id , { peerPresence : action . presence });
}
default :
return state ;
}
}
function decorateSession ( session , activeSessionId ) {
const lastMessage = [... session . messages ]. reverse (). find (( m ) => ! m . expired && typeof m . message === "string" && m . message . trim ());
const s = session . status ;
const isUp = s === "connected" || s === "verified" ;
const isPending = s === "connecting" || s === "verifying" || s === "new" ;
let dot , headerSub ;
if ( isPending ) {
dot = "#e3b341" ;
headerSub = statusSub ( s );
} else if ( isUp ) {
dot = session . peerPresence ? PRESENCE_DOT [ session . peerPresence ] || "#6b6b73" : "#3ecf8e" ;
headerSub = session . peerPresence ? PRESENCE_WORD [ session . peerPresence ] || "Online" : "P2P \xB7 connected" ;
} else {
dot = "#e5727a" ;
headerSub = statusSub ( s );
}
const preview = lastMessage ? lastMessage . message : headerSub ;
return {
id : session . id ,
name : session . peerLabel ,
mono : monoInitials ( session . peerLabel ),
dot ,
headerSub ,
status : session . status ,
peerPresence : session . peerPresence ,
preview ,
unread : session . unreadCount > 0 ? session . unreadCount > 99 ? "99+" : String ( session . unreadCount ) : null ,
verified : !! session . sas . isVerified ,
active : session . id === activeSessionId ,
inactive : session . id !== activeSessionId
};
}
function decorateSessions ( state ) {
return state . order . map (( id ) => state . sessions [ id ]). filter ( Boolean ). map (( s ) => decorateSession ( s , state . activeSessionId ));
}
2025-09-08 16:04:58 -04:00
// src/app.jsx
2026-06-18 20:37:50 -04:00
var copyToClipboardSecure = async ( text , autoClearMs = 0 ) => {
let ok = false ;
try {
await navigator . clipboard . writeText ( text );
ok = true ;
} catch ( e ) {
try {
const ta = document . createElement ( "textarea" );
ta . value = text ;
ta . style . position = "fixed" ;
ta . style . opacity = "0" ;
document . body . appendChild ( ta );
ta . select ();
ok = document . execCommand ( "copy" );
document . body . removeChild ( ta );
} catch ( _ ) {
ok = false ;
}
}
if ( ok && autoClearMs > 0 && navigator . clipboard && navigator . clipboard . writeText ) {
setTimeout ( async () => {
let current = null ;
let readable = true ;
try {
current = await navigator . clipboard . readText ();
} catch ( _ ) {
readable = false ;
}
if ( ! readable || current === text ) {
try {
await navigator . clipboard . writeText ( "" );
} catch ( _ ) {
}
}
}, autoClearMs );
}
return ok ;
};
var parseMessageSegments = ( text ) => {
if ( typeof text !== "string" || text . indexOf ( "```" ) === - 1 ) return null ;
const segments = [];
const re = /```([a-zA-Z0-9_+#.-]*)\n?([\s\S]*?)```/g ;
let last = 0 ;
let m ;
while (( m = re . exec ( text )) !== null ) {
if ( m . index > last ) segments . push ({ kind : "text" , content : text . slice ( last , m . index ) });
segments . push ({ kind : "code" , lang : ( m [ 1 ] || "" ). toLowerCase (), content : m [ 2 ]. replace ( /\n$/ , "" ) });
last = re . lastIndex ;
}
if ( last < text . length ) segments . push ({ kind : "text" , content : text . slice ( last ) });
return segments . some (( s ) => s . kind === "code" ) ? segments : null ;
};
2026-06-19 02:58:03 -04:00
var HL_KEYWORDS = /* @__PURE__ */ new Set ([
"const" ,
"let" ,
"var" ,
"function" ,
"return" ,
"if" ,
"else" ,
"for" ,
"while" ,
"do" ,
"switch" ,
"case" ,
"break" ,
"continue" ,
"class" ,
"extends" ,
"new" ,
"this" ,
"super" ,
"import" ,
"export" ,
"from" ,
"as" ,
"default" ,
"async" ,
"await" ,
"try" ,
"catch" ,
"finally" ,
"throw" ,
"typeof" ,
"instanceof" ,
"delete" ,
"yield" ,
"in" ,
"of" ,
"def" ,
"elif" ,
"lambda" ,
"pass" ,
"with" ,
"global" ,
"public" ,
"private" ,
"protected" ,
"static" ,
"final" ,
"void" ,
"int" ,
"long" ,
"float" ,
"double" ,
"char" ,
"bool" ,
"boolean" ,
"string" ,
"struct" ,
"enum" ,
"interface" ,
"package" ,
"func" ,
"fn" ,
"type" ,
"where" ,
"select" ,
"update" ,
"insert" ,
"delete" ,
"where" ,
"and" ,
"or" ,
"not" ,
"end" ,
"then" ,
"fi" ,
"done" ,
"echo" ,
"use" ,
"mut" ,
"impl" ,
"trait" ,
"match" ,
"module" ,
"require"
]);
var HL_LITERALS = /* @__PURE__ */ new Set ([ "true" , "false" , "null" , "undefined" , "None" , "True" , "False" , "nil" , "NaN" , "Infinity" ]);
var highlightCode = ( code ) => {
const re = /(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/|--[^\n]*)|(`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(\b\d[\d_.]*(?:[eE][+-]?\d+)?\b|\b0[xX][0-9a-fA-F]+\b)|([A-Za-z_$][A-Za-z0-9_$]*)/g ;
const nodes = [];
let buffer = "" ;
let last = 0 ;
let key = 0 ;
const flush = () => {
if ( buffer ) {
nodes . push ( buffer );
buffer = "" ;
}
};
let m ;
while (( m = re . exec ( code )) !== null ) {
if ( m . index > last ) buffer += code . slice ( last , m . index );
last = re . lastIndex ;
let cls = null ;
if ( m [ 1 ]) cls = "text-gray-500 italic" ;
else if ( m [ 2 ]) cls = "text-amber-300" ;
else if ( m [ 3 ]) cls = "text-sky-300" ;
else if ( m [ 4 ]) {
if ( HL_KEYWORDS . has ( m [ 4 ])) cls = "text-purple-300" ;
else if ( HL_LITERALS . has ( m [ 4 ])) cls = "text-sky-300" ;
}
if ( cls ) {
flush ();
nodes . push ( React . createElement ( "span" , { key : `h ${ key ++ } ` , className : cls }, m [ 0 ]));
} else {
buffer += m [ 0 ];
}
}
if ( last < code . length ) buffer += code . slice ( last );
flush ();
return nodes ;
};
2026-06-26 00:00:13 -04:00
var PRISM_ALIAS = {
js : "javascript" ,
mjs : "javascript" ,
javascript : "javascript" ,
node : "javascript" ,
ts : "typescript" ,
typescript : "typescript" ,
jsx : "jsx" ,
tsx : "tsx" ,
py : "python" ,
python : "python" ,
sh : "bash" ,
shell : "bash" ,
zsh : "bash" ,
bash : "bash" ,
"c++" : "cpp" ,
cpp : "cpp" ,
cc : "cpp" ,
cxx : "cpp" ,
c : "c" ,
h : "c" ,
cs : "csharp" ,
csharp : "csharp" ,
java : "java" ,
go : "go" ,
golang : "go" ,
rs : "rust" ,
rust : "rust" ,
json : "json" ,
yml : "yaml" ,
yaml : "yaml" ,
sql : "sql" ,
md : "markdown" ,
markdown : "markdown" ,
html : "markup" ,
xml : "markup" ,
svg : "markup" ,
css : "css"
};
2026-06-18 20:37:50 -04:00
var CodeBlock = ({ code , lang }) => {
const [ copied , setCopied ] = React . useState ( false );
const handleCopy = async () => {
const ok = await copyToClipboardSecure ( code , 3e4 );
if ( ok ) {
setCopied ( true );
setTimeout (() => setCopied ( false ), 2e3 );
}
};
2026-06-26 00:00:13 -04:00
const norm = PRISM_ALIAS [( lang || "" ). toLowerCase ()] || ( lang || "" ). toLowerCase ();
const prism = typeof window !== "undefined" ? window . Prism : null ;
const grammar = prism && prism . languages ? prism . languages [ norm ] : null ;
const usePrism = !! ( prism && grammar && typeof prism . highlight === "function" );
let highlightedHtml = null ;
if ( usePrism ) {
try {
highlightedHtml = prism . highlight ( code , grammar , norm );
} catch ( _ ) {
highlightedHtml = null ;
}
}
const displayLang = usePrism ? norm : lang || "code" ;
const codeEl = usePrism && highlightedHtml != null ? React . createElement ( "code" , { className : "language-" + norm , dangerouslySetInnerHTML : { __html : highlightedHtml } }) : React . createElement ( "code" , null , highlightCode ( code ));
2026-06-18 20:37:50 -04:00
return React . createElement ( "div" , {
2026-06-19 02:58:03 -04:00
className : "my-1 rounded-lg overflow-hidden" ,
style : { backgroundColor : "#1b1c1b" , border : "0 solid #e5e7eb" }
2026-06-18 20:37:50 -04:00
}, [
React . createElement ( "div" , {
key : "hdr" ,
2026-06-19 02:58:03 -04:00
className : "flex items-center justify-between px-3 py-1.5" ,
style : { backgroundColor : "#222322" , border : "0 solid #e5e7eb" }
2026-06-18 20:37:50 -04:00
}, [
React . createElement ( "span" , {
key : "lang" ,
className : "text-[11px] uppercase tracking-wide text-gray-500 font-mono"
2026-06-26 00:00:13 -04:00
}, displayLang ),
2026-06-18 20:37:50 -04:00
React . createElement ( "button" , {
key : "copy" ,
onClick : handleCopy ,
title : "Copy \u2014 clipboard auto-clears in 30s" ,
className : "flex items-center text-[11px] text-gray-400 hover:text-green-400 transition-colors"
}, [
React . createElement ( "i" , {
key : "ic" ,
className : ` ${ copied ? "fas fa-check text-green-400" : "far fa-copy" } mr-1`
}),
copied ? "Copied" : "Copy"
])
]),
React . createElement ( "pre" , {
key : "pre" ,
className : "px-3 py-2 overflow-x-auto text-xs leading-relaxed text-gray-200 custom-scrollbar" ,
style : { whiteSpace : "pre" , fontFamily : "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" , margin : 0 }
2026-06-26 00:00:13 -04:00
}, codeEl )
2026-06-18 20:37:50 -04:00
]);
};
var MessageBody = ({ text }) => {
const segments = parseMessageSegments ( text );
if ( ! segments ) {
return React . createElement ( "div" , {
className : "text-sm break-words" ,
style : { whiteSpace : "pre-wrap" , wordBreak : "break-word" }
}, text );
}
return React . createElement (
"div" ,
{ className : "text-sm" },
segments . map (
( seg , i ) => seg . kind === "code" ? React . createElement ( CodeBlock , { key : i , code : seg . content , lang : seg . lang }) : seg . content . trim () ? React . createElement ( "div" , {
key : i ,
className : "break-words" ,
style : { whiteSpace : "pre-wrap" , wordBreak : "break-word" }
}, seg . content ) : null
)
);
};
2026-06-23 16:52:30 -04:00
var GRAIN_URL = `url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='100'%20height='100'%3E%3Cfilter%20id='n'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3C/filter%3E%3Crect%20width='100%25'%20height='100%25'%20filter='url(%23n)'/%3E%3C/svg%3E")` ;
var SB_MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace" ;
var EnhancedChatMessage = ({ message , type , timestamp , mid , status , viewOnce , viewOnceTtl , expiresAt , expired , nowTick , canUnsend , onUnsend , onExpire }) => {
2026-06-18 20:37:50 -04:00
const [ revealed , setRevealed ] = React . useState ( false );
const revealTimerRef = React . useRef ( null );
2026-06-23 16:52:30 -04:00
const formatTime = ( ts ) => new Date ( ts ). toLocaleTimeString ( "en-GB" , { hour : "2-digit" , minute : "2-digit" , second : "2-digit" });
2026-06-18 20:37:50 -04:00
React . useEffect (() => () => {
if ( revealTimerRef . current ) clearTimeout ( revealTimerRef . current );
}, []);
2026-06-23 16:52:30 -04:00
if ( type === "system" || type === "notice" ) {
const isNotice = type === "notice" ;
return React . createElement (
"div" ,
{ className : "message-slide" , style : { display : "flex" , justifyContent : "center" , margin : "4px 0" } },
React . createElement ( "div" , {
style : { maxWidth : "80%" , padding : "8px 14px" , borderRadius : "10px" , border : "1px solid " + ( isNotice ? "rgba(62,207,142,0.25)" : "rgba(240,137,42,0.22)" ), background : isNotice ? "rgba(62,207,142,0.08)" : "rgba(240,137,42,0.08)" , color : isNotice ? "#8fe0bb" : "#e8b27a" , fontSize : "12.5px" , textAlign : "center" , lineHeight : 1.5 }
}, message )
);
}
const isMe = type === "sent" ;
const encrypted = isMe ;
2026-06-18 20:37:50 -04:00
const isViewOnce = type === "received" && viewOnce === true ;
2026-06-23 16:52:30 -04:00
const remaining = typeof expiresAt === "number" ? Math . max ( 0 , Math . ceil (( expiresAt - ( nowTick || Date . now ())) / 1e3 )) : null ;
const fmtRemaining = ( sec ) => {
if ( sec == null ) return "" ;
const h = Math . floor ( sec / 3600 ), m = Math . floor ( sec % 3600 / 60 ), s = sec % 60 ;
const pad = ( n ) => String ( n ). padStart ( 2 , "0" );
return h > 0 ? h + ":" + pad ( m ) + ":" + pad ( s ) : m + ":" + pad ( s );
};
2026-06-18 20:37:50 -04:00
const handleReveal = () => {
if ( revealed ) return ;
setRevealed ( true );
2026-06-19 02:58:03 -04:00
const ms = Math . max ( 1 , typeof viewOnceTtl === "number" ? viewOnceTtl : 15 ) * 1e3 ;
2026-06-18 20:37:50 -04:00
revealTimerRef . current = setTimeout (() => {
onExpire && onExpire ();
2026-06-19 02:58:03 -04:00
}, ms );
2026-06-18 20:37:50 -04:00
};
2026-06-23 16:52:30 -04:00
const radius = isMe ? "14px 14px 4px 14px" : "14px 14px 14px 4px" ;
const border = isMe ? "1px solid rgba(255,255,255,0.10)" : "1px solid rgba(255,255,255,0.06)" ;
const bg = isMe ? "#26262b" : "#161618" ;
const isExpired = expired === true || typeof expiresAt === "number" && ( nowTick || Date . now ()) >= expiresAt ;
if ( isExpired ) {
return React . createElement ( "div" , {
className : "message-slide" ,
style : { display : "flex" , width : "100%" , justifyContent : isMe ? "flex-end" : "flex-start" }
}, React . createElement (
"div" ,
{ style : { maxWidth : "74%" , minWidth : "170px" } },
React . createElement ( "div" , { style : { display : "flex" , alignItems : "center" , gap : "9px" , padding : "12px 15px" , borderRadius : radius , border : "1px dashed rgba(255,255,255,0.1)" , background : "rgba(255,255,255,0.018)" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-clock" , style : { color : "#6b6b73" , fontSize : "13px" } }),
React . createElement ( "span" , { key : "t" , style : { fontSize : "13px" , color : "#6b6b73" , fontStyle : "italic" } }, "This message has expired" )
])
));
}
2026-06-18 20:37:50 -04:00
let body ;
if ( isViewOnce && ! revealed ) {
2026-06-23 16:52:30 -04:00
body = React . createElement ( "div" , {
key : "cover" ,
2026-06-18 20:37:50 -04:00
onClick : handleReveal ,
2026-06-23 16:52:30 -04:00
style : { position : "relative" , cursor : "pointer" , padding : "12px 15px 10px" , overflow : "hidden" }
2026-06-18 20:37:50 -04:00
}, [
2026-06-23 16:52:30 -04:00
React . createElement ( "div" , { key : "blur" , style : { fontSize : "14.5px" , lineHeight : 1.55 , color : "#b3b3ba" , filter : "blur(7px)" , userSelect : "none" , pointerEvents : "none" , wordBreak : "break-word" , minHeight : "22px" } }, message ),
React . createElement ( "div" , { key : "grain" , style : { position : "absolute" , inset : 0 , backgroundImage : GRAIN_URL , backgroundSize : "90px" , opacity : 0.18 , mixBlendMode : "screen" , pointerEvents : "none" } }),
React . createElement ( "div" , { key : "lbl" , style : { position : "absolute" , inset : 0 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "7px" , pointerEvents : "none" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-eye-slash" , style : { color : "#e8e8eb" , fontSize : "13px" } }),
React . createElement ( "span" , { key : "t" , style : { fontSize : "12px" , fontWeight : 600 , color : "#e8e8eb" , textShadow : "0 1px 5px rgba(0,0,0,0.75)" } }, "View once \xB7 tap to reveal" )
])
2026-06-18 20:37:50 -04:00
]);
} else {
2026-06-23 16:52:30 -04:00
body = React . createElement (
"div" ,
{ key : "body" , style : { padding : "12px 15px 10px" , color : "#e9e9ec" } },
React . createElement ( MessageBody , { text : message })
);
2026-06-18 20:37:50 -04:00
}
2026-06-23 16:52:30 -04:00
const metaLeft = [
React . createElement ( "span" , { key : "time" , style : { fontFamily : SB_MONO , fontSize : "10.5px" , color : "#6b6b73" } }, formatTime ( timestamp ))
2026-06-18 20:37:50 -04:00
];
2026-06-23 16:52:30 -04:00
if ( isMe ) {
const stCfg = {
sending : { icon : "fa-clock" , color : "#6b6b73" , label : "Sending" },
sent : { icon : "fa-check" , color : "#8a8a92" , label : "Sent" },
2026-06-26 00:00:13 -04:00
// Two GREY ticks = delivered to the peer's device but not yet read.
delivered : { icon : "fa-check-double" , color : "#8a8a92" , label : "Delivered" },
// Two GREEN ticks = the peer actually opened the chat and read it.
read : { icon : "fa-check-double" , color : "#3ecf8e" , label : "Read" },
2026-06-23 16:52:30 -04:00
failed : { icon : "fa-triangle-exclamation" , color : "#e5727a" , label : "Not sent" }
}[ status || "sent" ] || { icon : "fa-check" , color : "#8a8a92" , label : "Sent" };
metaLeft . push ( React . createElement ( "span" , {
key : "dlv" ,
title : stCfg . label ,
style : { display : "inline-flex" , alignItems : "center" , color : stCfg . color }
}, React . createElement ( "i" , { className : "fas " + stCfg . icon , style : { fontSize : "10.5px" } })));
}
2026-06-18 20:37:50 -04:00
if ( isViewOnce && revealed ) {
2026-06-23 16:52:30 -04:00
metaLeft . push ( React . createElement ( "span" , { key : "vo" , style : { display : "inline-flex" , alignItems : "center" , gap : "4px" , fontSize : "10px" , fontWeight : 600 , color : "#8a8a92" } }, [
React . createElement ( "span" , { key : "d" , style : { width : "4px" , height : "4px" , borderRadius : "50%" , background : "#8a8a92" } }),
"Viewed once"
2026-06-18 20:37:50 -04:00
]));
} else if ( remaining !== null ) {
2026-06-23 16:52:30 -04:00
metaLeft . push ( React . createElement ( "span" , { key : "ttl" , style : { display : "inline-flex" , alignItems : "center" , gap : "4px" , fontFamily : SB_MONO , fontSize : "10.5px" , fontWeight : 500 , color : "#f0892a" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-clock" , style : { fontSize : "10px" } }),
fmtRemaining ( remaining )
2026-06-18 20:37:50 -04:00
]));
}
2026-06-23 16:52:30 -04:00
const metaRight = [];
metaRight . push ( React . createElement ( "span" , { key : "status" , style : { display : "inline-flex" , alignItems : "center" , gap : "5px" , fontSize : "10.5px" , fontWeight : 600 , color : "#3ecf8e" , flex : "none" } }, [
React . createElement ( "i" , { key : "i" , className : encrypted ? "fas fa-lock" : "fas fa-lock-open" , style : { fontSize : "10px" } }),
encrypted ? "Encrypted" : "Decrypted"
]));
if ( canUnsend && isMe && mid ) {
metaRight . push ( React . createElement ( "button" , {
2026-06-18 20:37:50 -04:00
key : "unsend" ,
onClick : () => onUnsend && onUnsend ( mid ),
title : "Delete for everyone" ,
2026-06-23 16:52:30 -04:00
className : "sb-unsend" ,
style : { background : "none" , border : "none" , cursor : "pointer" , color : "#56565e" , fontSize : "11px" , padding : 0 , lineHeight : 1 }
2026-06-18 20:37:50 -04:00
}, React . createElement ( "i" , { className : "fas fa-trash-can" })));
}
2026-06-23 16:52:30 -04:00
const meta = React . createElement ( "div" , {
key : "meta" ,
style : { display : "flex" , alignItems : "center" , justifyContent : "space-between" , gap : "14px" , padding : "0 15px 10px" }
}, [
React . createElement ( "div" , { key : "l" , style : { display : "flex" , alignItems : "center" , gap : "9px" , minWidth : 0 } }, metaLeft ),
React . createElement ( "div" , { key : "r" , style : { display : "flex" , alignItems : "center" , gap : "9px" , flex : "none" } }, metaRight )
]);
2025-09-08 16:04:58 -04:00
return React . createElement ( "div" , {
2026-06-23 16:52:30 -04:00
className : "message-slide" ,
style : { display : "flex" , width : "100%" , justifyContent : isMe ? "flex-end" : "flex-start" }
2025-09-08 16:04:58 -04:00
}, [
2026-06-23 16:52:30 -04:00
React . createElement (
"div" ,
{ key : "wrap" , style : { maxWidth : "74%" , minWidth : "170px" } },
React . createElement ( "div" , { style : { borderRadius : radius , border , background : bg , overflow : "hidden" } }, [ body , meta ])
)
2025-09-08 16:04:58 -04:00
]);
};
var EnhancedConnectionSetup = ({
messages ,
onCreateOffer ,
onCreateAnswer ,
onConnect ,
onClearData ,
onVerifyConnection ,
connectionStatus ,
offerData ,
answerData ,
offerInput ,
setOfferInput ,
answerInput ,
setAnswerInput ,
showOfferStep ,
showAnswerStep ,
verificationCode ,
showVerification ,
2025-09-23 20:01:02 -04:00
showQRCode ,
qrCodeUrl ,
showQRScanner ,
setShowQRCode ,
setShowQRScanner ,
setShowQRScannerModal ,
2025-09-08 16:04:58 -04:00
offerPassword ,
answerPassword ,
localVerificationConfirmed ,
remoteVerificationConfirmed ,
2025-09-27 19:07:17 -04:00
bothVerificationsConfirmed ,
// QR control props
qrFramesTotal ,
qrFrameIndex ,
qrManualMode ,
toggleQrManualMode ,
nextQrFrame ,
2025-10-02 01:43:32 -04:00
prevQrFrame ,
2025-10-15 19:58:28 -04:00
markAnswerCreated ,
2025-10-19 15:23:02 -04:00
notificationIntegrationRef ,
isGeneratingKeys ,
2026-05-19 09:49:22 -04:00
setIsGeneratingKeys ,
2026-05-17 14:48:52 -04:00
handleCreateOffer ,
relayOnlyMode ,
2026-05-19 09:49:22 -04:00
setRelayOnlyMode ,
2026-06-23 16:52:30 -04:00
webrtcManagerRef ,
showIceSettings ,
setShowIceSettings ,
iceServersText ,
iceSettingsPersisted ,
customIceServers ,
handleApplyIceSettings ,
2026-06-26 00:00:13 -04:00
handleForgetIceSettings ,
// When true, render ONLY the create/connect card (no marketing landing,
// no hero) so it slots into the chat column for an additional session.
compact = false
2025-09-08 16:04:58 -04:00
}) => {
2026-06-23 16:52:30 -04:00
const [ mode , setMode ] = React . useState ( "create" );
2025-10-15 19:58:28 -04:00
const [ notificationPermissionRequested , setNotificationPermissionRequested ] = React . useState ( false );
2026-06-23 16:52:30 -04:00
const [ qrModalOpen , setQrModalOpen ] = React . useState ( false );
const [ copied , setCopied ] = React . useState ( false );
const [ sasInput , setSasInput ] = React . useState ( "" );
const [ sasError , setSasError ] = React . useState ( "" );
const [ platformsOpen , setPlatformsOpen ] = React . useState ( false );
const [ codeRevealed , setCodeRevealed ] = React . useState ( false );
const [ genProgress , setGenProgress ] = React . useState ( 0 );
React . useEffect (() => {
setSasInput ( "" );
setSasError ( "" );
}, [ verificationCode ]);
React . useEffect (() => {
if ( ! showOfferStep && ! showAnswerStep ) setQrModalOpen ( false );
setCodeRevealed ( false );
}, [ showOfferStep , showAnswerStep ]);
React . useEffect (() => {
const generating = isGeneratingKeys && ! showOfferStep && ! showAnswerStep && ! showVerification ;
if ( ! generating ) {
setGenProgress ( 0 );
return ;
}
setGenProgress ( 0 );
let p = 0 ;
const id = setInterval (() => {
p += 1 ;
setGenProgress ( p );
if ( p >= 3 ) clearInterval ( id );
}, 520 );
return () => clearInterval ( id );
}, [ isGeneratingKeys , showOfferStep , showAnswerStep , showVerification ]);
React . useEffect (() => {
if ( ! platformsOpen ) return ;
const onDoc = () => setPlatformsOpen ( false );
const id = setTimeout (() => document . addEventListener ( "click" , onDoc ), 0 );
return () => {
clearTimeout ( id );
document . removeEventListener ( "click" , onDoc );
};
}, [ platformsOpen ]);
2025-09-08 16:04:58 -04:00
const resetToSelect = () => {
2025-10-19 15:23:02 -04:00
setIsGeneratingKeys ( false );
2026-06-23 16:52:30 -04:00
setQrModalOpen ( false );
2025-09-08 16:04:58 -04:00
onClearData ();
};
2026-05-17 14:48:52 -04:00
const handleVerificationConfirm = ( userCode ) => {
return onVerifyConnection ( userCode );
2025-09-08 16:04:58 -04:00
};
const handleVerificationReject = () => {
2026-05-17 14:48:52 -04:00
onVerifyConnection ( null , false );
2025-09-08 16:04:58 -04:00
};
2025-10-15 19:58:28 -04:00
const requestNotificationPermissionOnInteraction = async () => {
if ( notificationPermissionRequested ) {
return ;
}
try {
if ( ! ( "Notification" in window )) {
return ;
}
if ( ! window . isSecureContext && window . location . protocol !== "https:" && window . location . hostname !== "localhost" ) {
return ;
}
2025-10-19 20:51:44 -04:00
const currentPermission = typeof Notification !== "undefined" && Notification ? Notification . permission : "denied" ;
if ( currentPermission === "default" && typeof Notification !== "undefined" && Notification ) {
2025-10-15 19:58:28 -04:00
const permission = await Notification . requestPermission ();
if ( permission === "granted" ) {
try {
if ( window . NotificationIntegration && webrtcManagerRef . current ) {
const integration = new window . NotificationIntegration ( webrtcManagerRef . current );
await integration . init ();
notificationIntegrationRef . current = integration ;
}
} catch ( error ) {
}
setTimeout (() => {
try {
const welcomeNotification = new Notification ( "SecureBit Chat" , {
body : "Notifications enabled! You will receive alerts for new messages." ,
icon : "/logo/icon-192x192.png" ,
tag : "welcome-notification"
});
welcomeNotification . onclick = () => {
welcomeNotification . close ();
};
setTimeout (() => {
welcomeNotification . close ();
}, 5e3 );
} catch ( error ) {
}
}, 1e3 );
}
2025-10-19 15:23:02 -04:00
} else if ( currentPermission === "granted" ) {
2025-10-15 19:58:28 -04:00
try {
if ( window . NotificationIntegration && webrtcManagerRef . current && ! notificationIntegrationRef . current ) {
const integration = new window . NotificationIntegration ( webrtcManagerRef . current );
await integration . init ();
notificationIntegrationRef . current = integration ;
}
} catch ( error ) {
}
setTimeout (() => {
try {
const testNotification = new Notification ( "SecureBit Chat" , {
body : "Notifications are working! You will receive alerts for new messages." ,
icon : "/logo/icon-192x192.png" ,
tag : "test-notification"
});
testNotification . onclick = () => {
testNotification . close ();
};
setTimeout (() => {
testNotification . close ();
}, 5e3 );
} catch ( error ) {
}
}, 1e3 );
}
setNotificationPermissionRequested ( true );
} catch ( error ) {
}
};
2026-06-23 16:52:30 -04:00
const h = React . createElement ;
const C_ORANGE = "#f0892a" ;
const C_GREEN = "#3ecf8e" ;
const MONO = SB_MONO ;
const encode = ( data ) => {
try {
const min = typeof data === "object" ? JSON . stringify ( data ) : data || "" ;
if ( ! min ) return "" ;
if ( typeof window . encodeBinaryToPrefixed === "function" ) return window . encodeBinaryToPrefixed ( min );
if ( typeof window . compressToPrefixedGzip === "function" ) return window . compressToPrefixedGzip ( min );
return min ;
} catch {
return typeof data === "object" ? JSON . stringify ( data ) : data || "" ;
}
};
const isCreate = mode === "create" ;
const isGenerating = isGeneratingKeys && ! showOfferStep && ! showAnswerStep && ! showVerification ;
const isOfferCred = isCreate && showOfferStep && ! showVerification ;
const isAnswerCred = ! isCreate && showAnswerStep && ! showVerification ;
const atIntro = ! showVerification && ! isGenerating && ! isOfferCred && ! isAnswerCred ;
const accent = isCreate ? C_ORANGE : C_GREEN ;
const kicker = showVerification ? "Step 3 \xB7 verification" : isOfferCred || isAnswerCred ? "Step 2 \xB7 exchange" : "Step 1 \xB7 open a channel" ;
const credCode = isCreate ? encode ( offerData ) : encode ( answerData );
const hasInvite = ( offerInput || "" ). trim (). length > 0 ;
const hasAnswer = ( answerInput || "" ). trim (). length > 0 ;
const copyCred = async () => {
try {
if ( typeof copyToClipboardSecure === "function" ) await copyToClipboardSecure ( credCode );
else await navigator . clipboard . writeText ( credCode );
} catch ( e ) {
}
setCopied ( true );
setTimeout (() => setCopied ( false ), 1600 );
};
const normExpected = ( verificationCode || "" ). replace ( /[-\s]/g , "" ). length ;
const normInput = sasInput . replace ( /[-\s]/g , "" ). length ;
const canConfirm = ! localVerificationConfirmed && normExpected > 0 && normInput === normExpected ;
const handleSasConfirm = async () => {
try {
setSasError ( "" );
await onVerifyConnection ( sasInput );
} catch ( err ) {
setSasInput ( "" );
setSasError ( err ? . message === "SAS_MAX_ATTEMPTS" ? "Too many incorrect attempts. Session reset for safety." : "Incorrect code. Check it with your peer and try again." );
}
};
const ICON_DEFS = {
"fa-user" : { sw : 1.9 , e : [[ "circle" , { cx : 12 , cy : 8 , r : 3.6 }], [ "path" , { d : "M5 20c0-3.5 3-5.5 7-5.5s7 2 7 5.5" }]] },
"fa-lock" : { sw : 2 , e : [[ "path" , { d : "M7 11V7a5 5 0 0 1 10 0v4" }], [ "rect" , { x : 4.5 , y : 11 , width : 15 , height : 9 , rx : 2.2 }]] },
"fa-plus" : { sw : 2.1 , e : [[ "path" , { d : "M12 5v14M5 12h14" }]] },
"fa-link" : { sw : 2 , e : [[ "path" , { d : "M9.5 14.5l5-5M8 11l-2.2 2.2a3.5 3.5 0 0 0 4.95 4.95L13 16M16 13l2.2-2.2a3.5 3.5 0 0 0-4.95-4.95L11 8" }]] },
"fa-bolt" : { sw : 2.1 , e : [[ "path" , { d : "M13 2L4.5 13H11l-1 9 8.5-11H12l1-9z" }]] },
"fa-camera" : { sw : 1.8 , e : [[ "path" , { d : "M2 8.5V6.5A2.5 2.5 0 0 1 4.5 4h2M17.5 4h2A2.5 2.5 0 0 1 22 6.5v2M22 15.5v2a2.5 2.5 0 0 1-2.5 2.5h-2M6.5 20h-2A2.5 2.5 0 0 1 2 17.5v-2" }], [ "circle" , { cx : 12 , cy : 12 , r : 3.2 }]] },
"fa-qrcode" : { sw : 1.9 , e : [[ "rect" , { x : 3 , y : 3 , width : 7 , height : 7 , rx : 1.3 }], [ "rect" , { x : 14 , y : 3 , width : 7 , height : 7 , rx : 1.3 }], [ "rect" , { x : 3 , y : 14 , width : 7 , height : 7 , rx : 1.3 }], [ "path" , { d : "M14 14h3v3M21 14v.01M14 21h.01M21 21v-4M17.5 21H21" }]] },
"fa-chevron-right" : { sw : 2 , e : [[ "path" , { d : "M9 6l6 6-6 6" }]] },
"fa-chevron-left" : { sw : 2 , e : [[ "path" , { d : "M15 6l-6 6 6 6" }]] },
"fa-chevron-down" : { sw : 2 , e : [[ "path" , { d : "M6 9l6 6 6-6" }]] },
"fa-circle-notch" : { sw : 2 , e : [[ "path" , { d : "M21 12a9 9 0 1 1-6.2-8.6" }]] },
"fa-check" : { sw : 2.4 , e : [[ "path" , { d : "M20 6L9 17l-5-5" }]] },
"fa-check-circle" : { sw : 2 , e : [[ "circle" , { cx : 12 , cy : 12 , r : 9 }], [ "path" , { d : "M8.5 12.4l2.4 2.4 4.6-5" }]] },
"fa-shield-alt" : { sw : 1.9 , e : [[ "path" , { d : "M12 2.6l7 3v5.1c0 4.5-3 8.3-7 10.2-4-1.9-7-5.7-7-10.2V5.6l7-3z" }], [ "path" , { d : "M9 12l2 2 4-4.1" }]] },
"fa-download" : { sw : 2 , e : [[ "path" , { d : "M12 3v12M12 15l-4.5-4.5M12 15l4.5-4.5" }], [ "path" , { d : "M4 20h16" }]] },
"fa-clock" : { sw : 1.8 , e : [[ "circle" , { cx : 12 , cy : 13 , r : 8 }], [ "path" , { d : "M12 9v4l2.5 2M9 2h6" }]] },
"fa-times" : { sw : 2.2 , e : [[ "path" , { d : "M18 6L6 18M6 6l12 12" }]] },
"fa-eye" : { sw : 1.9 , e : [[ "path" , { d : "M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" }], [ "circle" , { cx : 12 , cy : 12 , r : 3 }]] }
};
const fa = ( name , opts ) => {
opts = opts || {};
const def = ICON_DEFS [ name ];
if ( ! def ) {
const st = {};
if ( opts . color ) st . color = opts . color ;
if ( opts . fontSize ) st . fontSize = opts . fontSize ;
if ( opts . animation ) st . animation = opts . animation ;
if ( opts . style ) Object . assign ( st , opts . style );
return h ( "i" , { key : opts . key , className : `fas ${ name } ` , style : st });
}
const size = opts . fontSize ? parseFloat ( opts . fontSize ) : 16 ;
const svgStyle = {};
if ( opts . animation ) {
svgStyle . animation = opts . animation ;
svgStyle . transformOrigin = "center" ;
svgStyle . transformBox = "fill-box" ;
}
if ( opts . style ) Object . assign ( svgStyle , opts . style );
return h ( "svg" , {
key : opts . key ,
width : size ,
height : size ,
viewBox : "0 0 24 24" ,
fill : "none" ,
stroke : opts . color || "currentColor" ,
strokeWidth : def . sw || 2 ,
strokeLinecap : "round" ,
strokeLinejoin : "round" ,
style : svgStyle
}, def . e . map (( el , i ) => h ( el [ 0 ], Object . assign ({ key : i }, el [ 1 ]))));
};
const leftPanel = h ( "div" , {
key : "left" ,
className : "sb-start-left" ,
style : {
flex : "1.05 1 380px" ,
position : "relative" ,
overflow : "hidden" ,
// Full viewport height even when the panels stack on mobile, so the
// branding column isn't collapsed/cramped (looked broken otherwise).
minHeight : "100vh" ,
boxSizing : "border-box" ,
padding : "46px" ,
display : "flex" ,
flexDirection : "column" ,
justifyContent : "space-between" ,
gap : "36px" ,
borderRight : "1px solid rgba(255,255,255,0.06)" ,
background : "radial-gradient(900px 600px at 25% 18%, rgba(240,137,42,0.07), transparent 62%), radial-gradient(800px 700px at 80% 92%, rgba(62,207,142,0.06), transparent 60%), #0c0c0e"
}
}, [
h (
"div" ,
{ key : "herowrap" , style : { flex : 1 , display : "flex" , flexDirection : "column" , justifyContent : "center" , position : "relative" , zIndex : 2 } },
h ( "div" , { key : "hero" , style : { maxWidth : "470px" } }, [
h ( "h1" , { key : "h1" , style : { margin : "0 0 14px" , fontSize : "34px" , fontWeight : 800 , letterSpacing : "-1.1px" , lineHeight : 1.1 , color : "#f4f4f6" } }, [
"A direct line" ,
h ( "br" , { key : "br" }),
"only you two can read."
2026-06-15 15:39:13 -04:00
]),
2026-06-23 16:52:30 -04:00
h (
"p" ,
{ key : "p" , style : { margin : "0 0 38px" , fontSize : "14.5px" , lineHeight : 1.6 , color : "#8a8a92" , maxWidth : "390px" } },
"Keys are generated on your device and exchanged peer-to-peer. No accounts, no servers storing your messages."
),
// animated channel
h ( "div" , { key : "channel" , style : { display : "flex" , alignItems : "center" , height : "74px" } }, [
h ( "div" , { key : "you" , style : { flex : "none" , display : "flex" , flexDirection : "column" , alignItems : "center" , gap : "8px" , width : "74px" } }, [
h ( "div" , { key : "n" , style : { width : "50px" , height : "50px" , borderRadius : "15px" , display : "grid" , placeItems : "center" , background : "rgba(240,137,42,0.13)" , border : "1px solid rgba(240,137,42,0.3)" , animation : "sbNode 3s ease-in-out infinite" } }, fa ( "fa-user" , { color : C_ORANGE , fontSize : "20px" })),
h ( "span" , { key : "l" , style : { fontSize : "11px" , fontWeight : 600 , color : "#9a9aa2" } }, "You" )
2025-09-08 16:04:58 -04:00
]),
2026-06-23 16:52:30 -04:00
h ( "div" , { key : "wire" , style : { flex : 1 , position : "relative" , height : "52px" , margin : "0 -6px" } }, [
h ( "div" , { key : "line" , style : { position : "absolute" , top : "50%" , left : 0 , right : 0 , height : "2px" , transform : "translateY(-50%)" , background : "linear-gradient(90deg, rgba(240,137,42,0.45), rgba(62,207,142,0.45))" } }),
h ( "span" , { key : "d1" , style : { position : "absolute" , top : "50%" , transform : "translateY(-50%)" , width : "6px" , height : "6px" , borderRadius : "50%" , background : C_ORANGE , boxShadow : `0 0 8px ${ C_ORANGE } ` , animation : "sbFlowR 2.6s linear infinite" } }),
h ( "span" , { key : "d2" , style : { position : "absolute" , top : "50%" , transform : "translateY(-50%)" , width : "6px" , height : "6px" , borderRadius : "50%" , background : C_GREEN , boxShadow : `0 0 8px ${ C_GREEN } ` , animation : "sbFlowL 2.6s linear infinite" , animationDelay : "0.6s" } }),
h ( "div" , { key : "hub" , style : { position : "absolute" , top : "50%" , left : "50%" , transform : "translate(-50%,-50%)" , width : "38px" , height : "38px" } }, [
h ( "span" , { key : "pulse" , style : { position : "absolute" , top : "50%" , left : "50%" , width : "38px" , height : "38px" , borderRadius : "50%" , border : "1.5px solid rgba(62,207,142,0.5)" , animation : "sbPulse 2.4s ease-out infinite" } }),
h ( "div" , { key : "core" , style : { position : "relative" , width : "38px" , height : "38px" , borderRadius : "50%" , display : "grid" , placeItems : "center" , background : "#121214" , border : "1px solid rgba(62,207,142,0.45)" , boxShadow : "0 0 18px rgba(62,207,142,0.25)" } }, fa ( "fa-lock" , { color : C_GREEN , fontSize : "14px" }))
2025-09-08 16:04:58 -04:00
])
2026-06-23 16:52:30 -04:00
]),
h ( "div" , { key : "peer" , style : { flex : "none" , display : "flex" , flexDirection : "column" , alignItems : "center" , gap : "8px" , width : "74px" } }, [
h ( "div" , { key : "n" , style : { width : "50px" , height : "50px" , borderRadius : "15px" , display : "grid" , placeItems : "center" , background : "rgba(62,207,142,0.1)" , border : "1px solid rgba(62,207,142,0.3)" , animation : "sbNode 3s ease-in-out infinite" , animationDelay : "1.5s" } }, fa ( "fa-user" , { color : C_GREEN , fontSize : "20px" })),
h ( "span" , { key : "l" , style : { fontSize : "11px" , fontWeight : 600 , color : "#9a9aa2" } }, "Peer" )
2025-09-08 16:04:58 -04:00
])
2026-06-23 16:52:30 -04:00
])
2025-09-08 16:04:58 -04:00
])
2026-06-23 16:52:30 -04:00
),
h (
"div" ,
{ key : "badges" , style : { position : "relative" , zIndex : 2 , display : "flex" , flexWrap : "wrap" , gap : "8px" } },
[ "ECDH P-384" , "AES-256-GCM" , "Perfect Forward Secrecy" ]. map (
( label ) => h ( "span" , { key : label , style : { display : "inline-flex" , alignItems : "center" , gap : "6px" , padding : "6px 11px" , borderRadius : "8px" , border : "1px solid rgba(255,255,255,0.07)" , background : "rgba(255,255,255,0.025)" , fontFamily : MONO , fontSize : "11px" , fontWeight : 500 , color : "#9a9aa2" } }, [
h ( "span" , { key : "dot" , style : { width : "5px" , height : "5px" , borderRadius : "50%" , background : C_GREEN } }),
label
])
)
)
]);
const segToggle = atIntro && h ( "div" , { key : "seg" , style : { position : "relative" , display : "flex" , padding : "4px" , borderRadius : "12px" , border : "1px solid rgba(255,255,255,0.07)" , background : "#141416" , marginBottom : "26px" } }, [
h ( "div" , { key : "ind" , style : { position : "absolute" , top : "4px" , bottom : "4px" , left : "4px" , width : "calc(50% - 4px)" , borderRadius : "9px" , background : "rgba(255,255,255,0.07)" , border : "1px solid rgba(255,255,255,0.08)" , transform : isCreate ? "translateX(0%)" : "translateX(100%)" , transition : "transform .26s cubic-bezier(.3,.8,.3,1)" } }),
h ( "button" , { key : "c" , className : "sb-seg-btn" , onClick : () => setMode ( "create" ), style : { position : "relative" , zIndex : 1 , flex : 1 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "8px" , padding : "11px" , border : "none" , background : "transparent" , color : isCreate ? "#f4f4f6" : "#7b7b83" , fontFamily : "inherit" , fontSize : "14px" , fontWeight : 700 , cursor : "pointer" } }, [ fa ( "fa-plus" , { key : "i" }), "Create" ]),
h ( "button" , { key : "j" , className : "sb-seg-btn" , onClick : () => setMode ( "join" ), style : { position : "relative" , zIndex : 1 , flex : 1 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "8px" , padding : "11px" , border : "none" , background : "transparent" , color : ! isCreate ? "#f4f4f6" : "#7b7b83" , fontFamily : "inherit" , fontSize : "14px" , fontWeight : 700 , cursor : "pointer" } }, [ fa ( "fa-link" , { key : "i" }), "Join" ])
]);
const backButton = ( key ) => h ( "button" , { key : key || "back" , className : "sb-soft-btn" , onClick : resetToSelect , style : { display : "inline-flex" , alignItems : "center" , gap : "6px" , marginBottom : "14px" , padding : "6px 11px 6px 8px" , borderRadius : "8px" , border : "1px solid rgba(255,255,255,0.08)" , background : "transparent" , color : "#9a9aa2" , fontFamily : "inherit" , fontSize : "12.5px" , fontWeight : 600 , cursor : "pointer" } }, [ fa ( "fa-chevron-left" , { key : "i" }), "Back" ]);
const credBlock = h ( "div" , { key : "codeblock" , style : { borderRadius : "13px" , border : "1px solid rgba(255,255,255,0.08)" , background : "#141416" , overflow : "hidden" , marginBottom : "16px" } }, [
h ( "div" , { key : "bar" , style : { display : "flex" , alignItems : "center" , gap : "8px" , padding : "9px 12px" , borderBottom : "1px solid rgba(255,255,255,0.06)" , background : "rgba(0,0,0,0.2)" } }, [
h ( "span" , { key : "dot" , style : { width : "7px" , height : "7px" , borderRadius : "50%" , background : accent } }),
h ( "span" , { key : "tag" , style : { fontFamily : MONO , fontSize : "10.5px" , fontWeight : 600 , color : "#8a8a92" } }, ` ${ isCreate ? "offer" : "answer" } \xB7 or copy text` ),
h ( "button" , { key : "copy" , onClick : copyCred , style : { marginLeft : "auto" , padding : "4px 9px" , borderRadius : "6px" , border : `1px solid ${ copied ? "rgba(62,207,142,0.4)" : "rgba(255,255,255,0.1)" } ` , background : copied ? "rgba(62,207,142,0.1)" : "rgba(255,255,255,0.04)" , color : copied ? C_GREEN : "#b3b3ba" , fontFamily : "inherit" , fontSize : "11px" , fontWeight : 600 , cursor : "pointer" , transition : "all .14s" } }, copied ? "Copied" : "Copy" )
]),
// The handshake code is sensitive — keep it blurred until the
// user deliberately reveals it, underscoring that it must be
// shared only over a channel they trust.
h ( "div" , { key : "codewrap" , style : { position : "relative" } }, [
h ( "div" , { key : "code" , className : "sb-sc" , style : { fontFamily : MONO , fontSize : "11px" , lineHeight : 1.55 , color : "#c9ccd8" , wordBreak : "break-all" , padding : "11px 12px" , maxHeight : "72px" , overflowY : "auto" , filter : codeRevealed ? "none" : "blur(6px)" , userSelect : codeRevealed ? "text" : "none" , transition : "filter .2s" } }, credCode ),
! codeRevealed && h ( "button" , { key : "reveal" , onClick : () => setCodeRevealed ( true ), style : { position : "absolute" , inset : 0 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "8px" , border : "none" , background : "rgba(20,20,22,0.25)" , color : "#cfcfd4" , fontFamily : "inherit" , fontSize : "12px" , fontWeight : 600 , cursor : "pointer" } }, [
fa ( "fa-eye" , { key : "i" , fontSize : "15px" }),
"Click to reveal \u2014 keep this code private"
])
])
]);
const showQrButton = qrCodeUrl && h ( "button" , { key : "showqr" , onClick : () => setQrModalOpen ( true ), style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "13px" , padding : "15px 16px" , borderRadius : "14px" , border : `1px solid ${ isCreate ? "rgba(240,137,42,0.3)" : "rgba(62,207,142,0.3)" } ` , background : isCreate ? "rgba(240,137,42,0.06)" : "rgba(62,207,142,0.06)" , color : "inherit" , fontFamily : "inherit" , cursor : "pointer" , textAlign : "left" , marginBottom : "14px" } }, [
h ( "span" , { key : "ic" , style : { flex : "none" , width : "42px" , height : "42px" , borderRadius : "12px" , display : "grid" , placeItems : "center" , background : isCreate ? "rgba(240,137,42,0.12)" : "rgba(62,207,142,0.12)" , border : `1px solid ${ isCreate ? "rgba(240,137,42,0.28)" : "rgba(62,207,142,0.28)" } ` } }, fa ( "fa-qrcode" , { color : accent , fontSize : "18px" })),
h ( "span" , { key : "tx" , style : { flex : 1 } }, [
h ( "span" , { key : "t" , style : { display : "block" , fontSize : "14.5px" , fontWeight : 700 , color : "#f4f4f6" } }, "Show QR code" ),
h ( "span" , { key : "s" , style : { display : "block" , fontSize : "12.5px" , color : "#8a8a92" , marginTop : "1px" } }, `Full-screen \xB7 let your peer scan ${ ( qrFramesTotal || 0 ) > 1 ? ` all ${ qrFramesTotal } frames` : "" } ` )
]),
fa ( "fa-chevron-right" , { color : "#6b6b73" })
]);
let inner ;
if ( showVerification ) {
const verified = bothVerificationsConfirmed ;
const cells = ( verificationCode || "" ). split ( "" ). map (( ch , i ) => h ( "div" , { key : i , style : { flex : 1 , maxWidth : "46px" , aspectRatio : "0.82" , display : "grid" , placeItems : "center" , borderRadius : "10px" , border : "1px solid rgba(62,207,142,0.25)" , background : "rgba(62,207,142,0.05)" , fontFamily : MONO , fontSize : "22px" , fontWeight : 700 , color : C_GREEN } }, ch ));
inner = h ( "div" , { key : "verify" , style : { animation : "sbUp .3s ease" } }, [
! verified && backButton ( "vback" ),
h ( "div" , { key : "head" , style : { display : "flex" , alignItems : "center" , gap : "11px" , marginBottom : "8px" } }, [
h ( "div" , { key : "i" , style : { width : "34px" , height : "34px" , flex : "none" , borderRadius : "10px" , display : "grid" , placeItems : "center" , background : "rgba(62,207,142,0.1)" , border : "1px solid rgba(62,207,142,0.25)" } }, fa ( "fa-shield-alt" , { color : C_GREEN })),
h ( "h2" , { key : "t" , style : { margin : 0 , fontSize : "21px" , fontWeight : 800 , letterSpacing : "-0.4px" , color : "#f4f4f6" } }, "Security verification" )
]),
h ( "p" , { key : "sub" , style : { margin : "0 0 18px" , fontSize : "13.5px" , lineHeight : 1.55 , color : "#8a8a92" } }, "Compare this safety code with your peer over a separate channel (voice / in person), then type it to unlock the chat." ),
h ( "div" , { key : "cells" , style : { display : "flex" , gap : "6px" , justifyContent : "center" , marginBottom : "20px" , flexWrap : "wrap" } }, cells ),
verified ? h ( "div" , { key : "ok" , style : { display : "flex" , flexDirection : "column" , alignItems : "center" , textAlign : "center" , padding : "24px 16px" , borderRadius : "16px" , border : "1px solid rgba(62,207,142,0.25)" , background : "rgba(62,207,142,0.06)" , animation : "sbUp .3s ease" } }, [
h ( "div" , { key : "i" , style : { width : "54px" , height : "54px" , borderRadius : "16px" , display : "grid" , placeItems : "center" , background : "rgba(62,207,142,0.14)" , border : "1px solid rgba(62,207,142,0.35)" , marginBottom : "14px" } }, fa ( "fa-check" , { color : C_GREEN , fontSize : "24px" })),
h ( "div" , { key : "t" , style : { fontSize : "18px" , fontWeight : 800 , color : "#f4f4f6" } }, "Channel verified" ),
h ( "div" , { key : "s" , style : { fontSize : "13.5px" , color : "#8a8a92" , marginTop : "5px" } }, "Both parties confirmed. Opening the secure chat\u2026" )
]) : h ( "div" , { key : "form" }, [
h ( "div" , { key : "lbl" , style : { fontSize : "12.5px" , fontWeight : 600 , color : "#9a9aa2" , marginBottom : "8px" } }, "Enter the verified code" ),
h ( "input" , { key : "in" , value : sasInput , onChange : ( e ) => {
setSasInput ( e . target . value . toUpperCase ());
if ( sasError ) setSasError ( "" );
}, disabled : localVerificationConfirmed , autoFocus : true , autoComplete : "off" , spellCheck : false , placeholder : verificationCode ? "Type code here" : "Waiting for code\u2026" , style : { width : "100%" , textAlign : "center" , letterSpacing : "6px" , borderRadius : "12px" , border : `1px solid ${ sasInput . length ? canConfirm || localVerificationConfirmed ? "rgba(62,207,142,0.5)" : "rgba(255,255,255,0.14)" : "rgba(255,255,255,0.08)" } ` , background : "#141416" , color : "#f4f4f6" , fontFamily : MONO , fontSize : "20px" , fontWeight : 700 , padding : "14px" , outline : "none" , textTransform : "uppercase" , marginBottom : sasError ? "8px" : "16px" } }),
sasError && h ( "p" , { key : "err" , style : { color : "#e5727a" , fontSize : "12.5px" , margin : "0 0 16px" } }, sasError ),
h ( "div" , { key : "status" , style : { display : "flex" , flexDirection : "column" , gap : "8px" , marginBottom : "16px" } }, [
h ( "div" , { key : "you" , style : { display : "flex" , alignItems : "center" , justifyContent : "space-between" , padding : "11px 14px" , borderRadius : "11px" , border : "1px solid rgba(255,255,255,0.06)" , background : "#141416" } }, [
h ( "span" , { key : "l" , style : { fontSize : "13px" , color : "#cfcfd4" , fontWeight : 600 } }, "Your confirmation" ),
h ( "span" , { key : "v" , style : { display : "inline-flex" , alignItems : "center" , gap : "6px" , fontSize : "12.5px" , fontWeight : 600 , color : localVerificationConfirmed ? C_GREEN : "#7b7b83" } }, [ fa ( localVerificationConfirmed ? "fa-check-circle" : "fa-clock" , { key : "i" }), localVerificationConfirmed ? "Confirmed" : "Pending" ])
2025-09-08 16:04:58 -04:00
]),
2026-06-23 16:52:30 -04:00
h ( "div" , { key : "peer" , style : { display : "flex" , alignItems : "center" , justifyContent : "space-between" , padding : "11px 14px" , borderRadius : "11px" , border : "1px solid rgba(255,255,255,0.06)" , background : "#141416" } }, [
h ( "span" , { key : "l" , style : { fontSize : "13px" , color : "#cfcfd4" , fontWeight : 600 } }, "Peer confirmation" ),
h ( "span" , { key : "v" , style : { display : "inline-flex" , alignItems : "center" , gap : "6px" , fontSize : "12.5px" , fontWeight : 600 , color : remoteVerificationConfirmed ? C_GREEN : "#7b7b83" } }, [ fa ( remoteVerificationConfirmed ? "fa-check-circle" : "fa-clock" , { key : "i" }), remoteVerificationConfirmed ? "Confirmed" : "Pending" ])
2025-09-08 16:04:58 -04:00
])
]),
2026-06-23 16:52:30 -04:00
h ( "div" , { key : "btns" , style : { display : "flex" , gap : "10px" } }, [
h ( "button" , { key : "ok" , onClick : handleSasConfirm , disabled : ! canConfirm , style : { flex : 1 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "8px" , padding : "14px" , borderRadius : "13px" , border : "none" , background : canConfirm ? C_GREEN : "rgba(255,255,255,0.05)" , color : canConfirm ? "#08160e" : "#56565e" , fontFamily : "inherit" , fontSize : "14.5px" , fontWeight : 700 , cursor : canConfirm ? "pointer" : "not-allowed" , boxShadow : canConfirm ? "0 8px 24px rgba(62,207,142,0.25)" : "none" } }, [ fa ( localVerificationConfirmed ? "fa-check-circle" : "fa-check" , { key : "i" }), localVerificationConfirmed ? "Confirmed" : "Confirm code" ]),
h ( "button" , { key : "no" , onClick : handleVerificationReject , style : { flex : "none" , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "7px" , padding : "14px 16px" , borderRadius : "13px" , border : "1px solid rgba(229,114,122,0.3)" , background : "transparent" , color : "#e5727a" , fontFamily : "inherit" , fontSize : "13.5px" , fontWeight : 600 , cursor : "pointer" } }, [ fa ( "fa-times" , { key : "i" }), "Don't match" ])
2025-09-08 16:04:58 -04:00
])
])
]);
2026-06-23 16:52:30 -04:00
} else if ( isGenerating ) {
const genSteps = [ "Generating ECDH P-384 key pair" , "Deriving verification code" , "Pinning Perfect Forward Secrecy" ];
inner = h ( "div" , { key : "gen" , style : { animation : "sbUp .28s ease" } }, [
h ( "div" , { key : "head" , style : { display : "flex" , alignItems : "center" , gap : "13px" , marginBottom : "22px" } }, [
h ( "div" , { key : "sp" , style : { width : "44px" , height : "44px" , flex : "none" , display : "grid" , placeItems : "center" } }, fa ( "fa-circle-notch" , { color : C_ORANGE , fontSize : "32px" , animation : "sbSpin 1s linear infinite" })),
h ( "div" , { key : "tx" }, [
h ( "h2" , { key : "t" , style : { margin : 0 , fontSize : "20px" , fontWeight : 800 , letterSpacing : "-0.4px" , color : "#f4f4f6" } }, isCreate ? "Securing your channel" : "Building your answer" ),
h ( "p" , { key : "s" , style : { margin : "3px 0 0" , fontSize : "13px" , color : "#8a8a92" } }, "Forging keys strong enough to resist tampering." )
])
]),
h (
"div" ,
{ key : "steps" , style : { display : "flex" , flexDirection : "column" , borderRadius : "13px" , border : "1px solid rgba(255,255,255,0.07)" , background : "#141416" , overflow : "hidden" } },
genSteps . map (( label , i ) => {
const done = genProgress > i ;
const active = genProgress === i ;
return h ( "div" , { key : i , style : { display : "flex" , alignItems : "center" , gap : "12px" , padding : "13px 15px" , borderTop : i ? "1px solid rgba(255,255,255,0.05)" : "none" , transition : "background .3s" , background : done ? "rgba(62,207,142,0.04)" : "transparent" } }, [
h (
"div" ,
{ key : "d" , style : { flex : "none" , width : "20px" , height : "20px" , borderRadius : "50%" , display : "grid" , placeItems : "center" , background : done ? "rgba(62,207,142,0.12)" : active ? "rgba(240,137,42,0.12)" : "rgba(255,255,255,0.04)" , border : `1px solid ${ done ? "rgba(62,207,142,0.3)" : active ? "rgba(240,137,42,0.3)" : "rgba(255,255,255,0.1)" } ` , transition : "all .3s" } },
done ? fa ( "fa-check" , { color : C_GREEN , fontSize : "11px" }) : h ( "span" , { style : { width : "6px" , height : "6px" , borderRadius : "50%" , background : active ? C_ORANGE : "#56565e" , animation : active ? "sbBlink 1s ease-in-out infinite" : "none" } })
),
h ( "span" , { key : "l" , style : { fontSize : "13.5px" , color : done ? "#cfcfd4" : active ? "#e8e8eb" : "#6b6b73" , transition : "color .3s" } }, label )
]);
})
)
]);
} else if ( isOfferCred || isAnswerCred ) {
inner = h ( "div" , { key : "cred" , style : { animation : "sbUp .3s ease" } }, [
backButton ( "cback" ),
h ( "h2" , { key : "h" , style : { margin : "0 0 6px" , fontSize : "23px" , fontWeight : 800 , letterSpacing : "-0.5px" , color : "#f4f4f6" } }, isCreate ? "Share your invitation" : "Send back your answer" ),
h ( "p" , { key : "p" , style : { margin : "0 0 18px" , fontSize : "14px" , lineHeight : 1.55 , color : "#8a8a92" } }, isCreate ? "Show the QR or send the code to your peer. It is one-time and expires shortly." : "Give this answer to the channel creator so they can finish the handshake." ),
showQrButton ,
credBlock ,
isOfferCred && h ( "div" , { key : "offerextra" , style : { marginTop : "4px" } }, [
h ( "div" , { key : "lbl" , style : { fontSize : "12.5px" , fontWeight : 600 , color : "#9a9aa2" , marginBottom : "8px" } }, "Then receive the answer your peer sends back" ),
h (
"div" ,
{ key : "ta" , style : { borderRadius : "12px" , border : `1px solid ${ hasAnswer ? "rgba(255,255,255,0.18)" : "rgba(255,255,255,0.07)" } ` , background : "#141416" , padding : "11px 14px" , marginBottom : "10px" } },
h ( "textarea" , { value : answerInput , onChange : ( e ) => {
setAnswerInput ( e . target . value );
if ( e . target . value . trim (). length > 0 && typeof markAnswerCreated === "function" ) markAnswerCreated ();
}, rows : 2 , placeholder : "Paste peer's answer code\u2026" , style : { width : "100%" , resize : "none" , border : "none" , outline : "none" , background : "transparent" , color : "#d7d7db" , fontFamily : MONO , fontSize : "12px" , lineHeight : 1.55 , minHeight : "44px" } })
),
h ( "div" , { key : "btns" , style : { display : "flex" , gap : "10px" } }, [
h ( "button" , { key : "scan" , className : "sb-scan-btn" , onClick : () => setShowQRScannerModal ( true ), style : { flex : "none" , display : "inline-flex" , alignItems : "center" , justifyContent : "center" , gap : "8px" , padding : "14px 16px" , borderRadius : "13px" , border : "1px solid rgba(255,255,255,0.1)" , background : "rgba(255,255,255,0.04)" , color : "#cfcfd4" , fontFamily : "inherit" , fontSize : "14px" , fontWeight : 700 , cursor : "pointer" } }, [ fa ( "fa-camera" , { key : "i" }), "Scan" ]),
h ( "button" , { key : "est" , onClick : onConnect , disabled : ! hasAnswer , style : { flex : 1 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "9px" , padding : "14px" , borderRadius : "13px" , border : "none" , background : hasAnswer ? C_ORANGE : "rgba(255,255,255,0.05)" , color : hasAnswer ? "#1a0f04" : "#56565e" , fontFamily : "inherit" , fontSize : "14.5px" , fontWeight : 700 , cursor : hasAnswer ? "pointer" : "not-allowed" , boxShadow : hasAnswer ? "0 8px 24px rgba(240,137,42,0.28)" : "none" } }, "Establish connection" )
])
]),
isAnswerCred && h ( "div" , { key : "answerextra" , style : { marginTop : "4px" , display : "flex" , alignItems : "center" , gap : "10px" , padding : "12px 14px" , borderRadius : "12px" , border : "1px solid rgba(62,207,142,0.18)" , background : "rgba(62,207,142,0.05)" } }, [
fa ( "fa-circle-notch" , { key : "i" , color : C_GREEN , animation : "sbSpin 1.4s linear infinite" }),
h ( "span" , { key : "t" , style : { fontSize : "13px" , color : "#cfcfd4" , fontWeight : 500 } }, "Send this answer to the creator, then wait \u2014 the chat opens once they connect." )
])
]);
} else if ( isCreate ) {
inner = h ( "div" , { key : "introC" , style : { animation : "sbUp .28s ease" } }, [
h ( "h2" , { key : "h" , style : { margin : "0 0 6px" , fontSize : "23px" , fontWeight : 800 , letterSpacing : "-0.5px" , color : "#f4f4f6" } }, "Create a new channel" ),
h ( "p" , { key : "p" , style : { margin : "0 0 22px" , fontSize : "14px" , lineHeight : 1.55 , color : "#8a8a92" } }, "Your device generates the keys and a one-time invitation. Nothing touches a server." ),
h ( "button" , { key : "gen" , className : "sb-gen-btn" , onClick : () => {
requestNotificationPermissionOnInteraction ();
if ( webrtcManagerRef . current ) handleCreateOffer ();
}, style : { width : "100%" , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "9px" , padding : "15px" , borderRadius : "13px" , border : "none" , background : C_ORANGE , color : "#1a0f04" , fontFamily : "inherit" , fontSize : "15px" , fontWeight : 700 , cursor : "pointer" , boxShadow : "0 8px 24px rgba(240,137,42,0.28)" } }, [ fa ( "fa-bolt" , { key : "i" }), "Generate keys & invitation" ])
]);
} else {
inner = h ( "div" , { key : "introJ" , style : { animation : "sbUp .28s ease" } }, [
h ( "h2" , { key : "h" , style : { margin : "0 0 6px" , fontSize : "23px" , fontWeight : 800 , letterSpacing : "-0.5px" , color : "#f4f4f6" } }, "Join a channel" ),
h ( "p" , { key : "p" , style : { margin : "0 0 16px" , fontSize : "14px" , lineHeight : 1.55 , color : "#8a8a92" } }, "Scan your peer's QR with your camera, or paste their invitation code." ),
h ( "button" , { key : "scan" , className : "sb-scan-btn" , onClick : () => {
requestNotificationPermissionOnInteraction ();
setShowQRScannerModal ( true );
}, style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "13px" , padding : "15px 16px" , borderRadius : "14px" , border : "1px solid rgba(62,207,142,0.3)" , background : "rgba(62,207,142,0.06)" , color : "inherit" , fontFamily : "inherit" , cursor : "pointer" , textAlign : "left" , marginBottom : "14px" } }, [
h ( "span" , { key : "ic" , style : { flex : "none" , width : "42px" , height : "42px" , borderRadius : "12px" , display : "grid" , placeItems : "center" , background : "rgba(62,207,142,0.12)" , border : "1px solid rgba(62,207,142,0.28)" } }, fa ( "fa-camera" , { color : C_GREEN , fontSize : "18px" })),
h ( "span" , { key : "tx" , style : { flex : 1 } }, [
h ( "span" , { key : "t" , style : { display : "block" , fontSize : "14.5px" , fontWeight : 700 , color : "#f4f4f6" } }, "Scan QR with camera" ),
h ( "span" , { key : "s" , style : { display : "block" , fontSize : "12.5px" , color : "#8a8a92" , marginTop : "1px" } }, "Fastest \u2014 point at your peer's screen" )
]),
fa ( "fa-chevron-right" , { color : "#6b6b73" })
]),
h ( "div" , { key : "or" , style : { display : "flex" , alignItems : "center" , gap : "12px" , marginBottom : "14px" } }, [
h ( "span" , { key : "a" , style : { flex : 1 , height : "1px" , background : "rgba(255,255,255,0.07)" } }),
h ( "span" , { key : "m" , style : { fontSize : "11px" , fontWeight : 600 , color : "#56565e" , textTransform : "uppercase" , letterSpacing : "0.7px" } }, "or paste code" ),
h ( "span" , { key : "b" , style : { flex : 1 , height : "1px" , background : "rgba(255,255,255,0.07)" } })
]),
h (
"div" ,
{ key : "ta" , style : { borderRadius : "13px" , border : `1px solid ${ hasInvite ? "rgba(255,255,255,0.18)" : "rgba(255,255,255,0.07)" } ` , background : "#141416" , padding : "13px 15px" , marginBottom : "12px" } },
h ( "textarea" , { value : offerInput , onChange : ( e ) => {
setOfferInput ( e . target . value );
if ( e . target . value . trim (). length > 0 && typeof markAnswerCreated === "function" ) markAnswerCreated ();
}, rows : 3 , placeholder : "Paste invitation code here\u2026" , style : { width : "100%" , resize : "none" , border : "none" , outline : "none" , background : "transparent" , color : "#d7d7db" , fontFamily : MONO , fontSize : "12.5px" , lineHeight : 1.6 , minHeight : "66px" } })
),
h ( "button" , { key : "connect" , onClick : () => {
requestNotificationPermissionOnInteraction ();
onCreateAnswer ();
}, disabled : ! hasInvite || connectionStatus === "connecting" , style : { width : "100%" , display : "inline-flex" , alignItems : "center" , justifyContent : "center" , gap : "9px" , padding : "14px" , borderRadius : "13px" , border : "none" , background : hasInvite && connectionStatus !== "connecting" ? C_ORANGE : "rgba(255,255,255,0.05)" , color : hasInvite && connectionStatus !== "connecting" ? "#1a0f04" : "#56565e" , fontFamily : "inherit" , fontSize : "15px" , fontWeight : 700 , cursor : hasInvite && connectionStatus !== "connecting" ? "pointer" : "not-allowed" , boxShadow : hasInvite && connectionStatus !== "connecting" ? "0 8px 24px rgba(240,137,42,0.28)" : "none" } }, connectionStatus === "connecting" ? "Processing\u2026" : "Connect" )
]);
2025-09-08 16:04:58 -04:00
}
2026-06-23 16:52:30 -04:00
const DOWNLOADS = {
mac : { name : "macOS" , format : ".dmg \xB7 Apple Silicon & Intel" , icon : "fab fa-apple" , url : "https://github.com/SecureBitChat/securebit-desktop/releases/download/v0.1.0/SecureBit.Chat_0.1.0_x64.dmg" },
win : { name : "Windows" , format : ".exe \xB7 64-bit installer" , icon : "fab fa-windows" , url : "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe" },
linux : { name : "Linux" , format : ".AppImage" , icon : "fab fa-linux" , url : "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage" }
};
const detectOS = () => {
const ua = ( navigator . userAgent || "" ) + " " + ( navigator . platform || "" );
if ( /Mac|iPhone|iPad|iPod/i . test ( ua ) && ! /Android/i . test ( ua )) return "mac" ;
if ( /Win/i . test ( ua )) return "win" ;
if ( /Linux/i . test ( ua ) && ! /Android/i . test ( ua )) return "linux" ;
return "win" ;
};
const detectedOS = detectOS ();
const otherOS = [ "mac" , "win" , "linux" ]. filter (( k ) => k !== detectedOS );
const dlLink = ( url ) => {
try {
window . open ( url , "_blank" , "noopener" );
} catch ( e ) {
}
};
2026-06-24 15:41:15 -04:00
const platformsMenu = platformsOpen && h ( "div" , { key : "platmenu" , className : "sb-platforms-menu" , style : { position : "absolute" , left : 0 , bottom : "calc(100% + 10px)" , width : "344px" , maxWidth : "100%" , borderRadius : "16px" , border : "1px solid rgba(255,255,255,0.1)" , background : "#161618" , boxShadow : "0 24px 60px rgba(0,0,0,0.55)" , overflow : "hidden" , zIndex : 25 , animation : "sbUp .2s ease" } }, [
2026-06-23 16:52:30 -04:00
h ( "div" , { key : "mh" , style : { display : "flex" , alignItems : "center" , gap : "10px" , padding : "14px 16px" , borderBottom : "1px solid rgba(255,255,255,0.06)" } }, [
h ( "div" , { key : "t" , style : { flex : 1 , lineHeight : 1.2 } }, [
h ( "div" , { key : "a" , style : { fontSize : "14px" , fontWeight : 800 , color : "#f4f4f6" } }, "Download SecureBit" ),
h ( "div" , { key : "b" , style : { fontSize : "11.5px" , color : "#7b7b83" } }, "Free \xB7 open source" )
]),
h ( "span" , { key : "pill" , style : { fontFamily : MONO , fontSize : "10px" , fontWeight : 600 , color : C_GREEN , padding : "3px 8px" , borderRadius : "6px" , background : "rgba(62,207,142,0.1)" , border : "1px solid rgba(62,207,142,0.22)" } }, "You're on Web" )
]),
h (
"div" ,
{ key : "rec" , style : { padding : "12px 12px 6px" } },
h ( "button" , { key : "b" , onClick : () => dlLink ( DOWNLOADS [ detectedOS ]. url ), style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "12px" , padding : "13px 14px" , borderRadius : "12px" , border : "1px solid rgba(240,137,42,0.4)" , background : "rgba(240,137,42,0.08)" , color : "inherit" , fontFamily : "inherit" , cursor : "pointer" , textAlign : "left" } }, [
h ( "span" , { key : "ic" , style : { flex : "none" , display : "grid" , placeItems : "center" , width : "38px" , height : "38px" , borderRadius : "11px" , background : "rgba(240,137,42,0.14)" , border : "1px solid rgba(240,137,42,0.3)" , color : C_ORANGE } }, h ( "i" , { className : DOWNLOADS [ detectedOS ]. icon , style : { fontSize : "17px" } })),
h ( "span" , { key : "tx" , style : { flex : 1 , minWidth : 0 } }, [
h ( "span" , { key : "n" , style : { display : "block" , fontSize : "13.5px" , fontWeight : 700 , color : "#f4f4f6" } }, DOWNLOADS [ detectedOS ]. name ),
h ( "span" , { key : "f" , style : { display : "block" , fontSize : "11px" , color : "#f0b072" , marginTop : "1px" } }, `Recommended for this device \xB7 ${ DOWNLOADS [ detectedOS ]. format } ` )
2025-09-08 16:04:58 -04:00
]),
2026-06-23 16:52:30 -04:00
fa ( "fa-download" , { color : C_ORANGE })
])
),
h (
"div" ,
{ key : "others" , style : { padding : "0 12px 8px" , display : "flex" , flexDirection : "column" , gap : "2px" } },
otherOS . map (( k ) => h ( "button" , { key : k , onClick : () => dlLink ( DOWNLOADS [ k ]. url ), style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "12px" , padding : "11px 14px" , borderRadius : "11px" , border : "none" , background : "transparent" , color : "inherit" , fontFamily : "inherit" , cursor : "pointer" , textAlign : "left" } }, [
h ( "span" , { key : "ic" , style : { flex : "none" , display : "grid" , placeItems : "center" , width : "34px" , height : "34px" , borderRadius : "10px" , background : "rgba(255,255,255,0.04)" , border : "1px solid rgba(255,255,255,0.08)" , color : "#cfcfd4" } }, h ( "i" , { className : DOWNLOADS [ k ]. icon , style : { fontSize : "15px" } })),
h ( "span" , { key : "tx" , style : { flex : 1 , minWidth : 0 } }, [
h ( "span" , { key : "n" , style : { display : "block" , fontSize : "13px" , fontWeight : 600 , color : "#e8e8eb" } }, DOWNLOADS [ k ]. name ),
h ( "span" , { key : "f" , style : { display : "block" , fontSize : "11px" , color : "#7b7b83" , marginTop : "1px" } }, DOWNLOADS [ k ]. format )
2025-09-08 16:04:58 -04:00
]),
2026-06-23 16:52:30 -04:00
fa ( "fa-download" , { color : "#8a8a92" })
]))
),
h ( "div" , { key : "soon" , style : { display : "flex" , alignItems : "center" , gap : "9px" , padding : "12px 16px" , borderTop : "1px solid rgba(255,255,255,0.06)" , background : "rgba(255,255,255,0.015)" } }, [
fa ( "fa-clock" , { key : "i" , color : "#6b6b73" }),
h ( "span" , { key : "t" , style : { fontSize : "11.5px" , lineHeight : 1.45 , color : "#7b7b83" } }, "Mobile (iOS, Android) and browser extensions (Chrome, Firefox, Opera) are coming soon." )
])
]);
2026-06-24 15:41:15 -04:00
const footer = h ( "div" , { key : "footer" , className : "sb-conn-footer" , style : { position : "relative" , marginTop : "30px" , paddingTop : "18px" , borderTop : "1px solid rgba(255,255,255,0.06)" , display : "flex" , alignItems : "center" , justifyContent : "space-between" , gap : "12px" , flexWrap : "wrap" } }, [
2026-06-23 16:52:30 -04:00
h ( "button" , { key : "dl" , onClick : () => setPlatformsOpen (( v ) => ! v ), style : { display : "inline-flex" , alignItems : "center" , gap : "9px" , padding : "8px 13px 8px 9px" , borderRadius : "10px" , border : `1px solid ${ platformsOpen ? "rgba(240,137,42,0.4)" : "rgba(255,255,255,0.08)" } ` , background : platformsOpen ? "rgba(240,137,42,0.06)" : "rgba(255,255,255,0.02)" , color : "inherit" , fontFamily : "inherit" , cursor : "pointer" , transition : "all .15s" } }, [
fa ( "fa-download" , { key : "i" , color : C_ORANGE }),
h ( "span" , { key : "t" , style : { fontSize : "12.5px" , fontWeight : 700 , color : "#e8e8eb" } }, "Download desktop app" ),
fa ( "fa-chevron-down" , { key : "c" , color : "#6b6b73" , style : { fontSize : "11px" , transform : platformsOpen ? "rotate(180deg)" : "rotate(0deg)" , transition : "transform .2s" } })
]),
h ( "button" , { key : "settings" , className : "sb-link" , onClick : () => setShowIceSettings && setShowIceSettings ( true ), style : { display : "inline-flex" , alignItems : "center" , gap : "7px" , background : "none" , border : "none" , color : "#8a8a92" , fontFamily : "inherit" , fontSize : "12.5px" , fontWeight : 600 , cursor : "pointer" } }, [ fa ( "fa-sliders-h" , { key : "i" }), "Advanced settings" ]),
platformsMenu
]);
const settingsOverlay = showIceSettings && typeof window !== "undefined" && window . IceServerSettings ? h ( window . IceServerSettings , {
key : "ice-settings" ,
isOpen : true ,
embedded : true ,
onClose : () => setShowIceSettings ( false ),
initial : {
useCustom : Array . isArray ( customIceServers ) && customIceServers . length > 0 ,
serversText : iceServersText ,
privacyMode : relayOnlyMode ? "relay-only" : "standard" ,
persisted : iceSettingsPersisted
},
hasSaved : iceSettingsPersisted ,
onApply : handleApplyIceSettings ,
onForget : handleForgetIceSettings
}) : null ;
2026-06-26 00:00:13 -04:00
const rightPanel = h ( "div" , { key : "right" , style : compact ? { flex : 1 , minWidth : 0 , width : "100%" , position : "relative" , overflow : "hidden" , display : "flex" , flexDirection : "column" , height : "100%" } : { flex : "0.95 1 460px" , minWidth : "min(100%, 320px)" , position : "relative" , overflow : "hidden" , display : "flex" , flexDirection : "column" , height : "100vh" } }, [
2026-06-23 16:52:30 -04:00
h (
"div" ,
{ key : "scroll" , className : "custom-scrollbar" , style : { flex : 1 , overflowY : "auto" , display : "flex" , flexDirection : "column" , padding : "42px 44px" } },
h ( "div" , { style : { maxWidth : "430px" , width : "100%" , margin : "auto" } }, [
h ( "div" , { key : "kicker" , style : { fontFamily : MONO , fontSize : "11px" , fontWeight : 600 , color : "#6b6b73" , textTransform : "uppercase" , letterSpacing : "1px" , marginBottom : "10px" } }, kicker ),
segToggle ,
inner ,
footer
])
),
settingsOverlay
]);
const qrModal = qrModalOpen && qrCodeUrl && h (
"div" ,
{ key : "qrmodal" , onClick : () => setQrModalOpen ( false ), style : { position : "fixed" , inset : 0 , zIndex : 50 , display : "flex" , alignItems : "center" , justifyContent : "center" , padding : "32px" , background : "rgba(6,6,8,0.82)" , backdropFilter : "blur(10px)" , animation : "sbUp .2s ease" } },
h ( "div" , { onClick : ( e ) => e . stopPropagation (), style : { width : "100%" , maxWidth : "460px" , borderRadius : "22px" , border : "1px solid rgba(255,255,255,0.1)" , background : "#111113" , boxShadow : "0 30px 90px rgba(0,0,0,0.6)" , overflow : "hidden" } }, [
h ( "div" , { key : "head" , style : { display : "flex" , alignItems : "center" , gap : "11px" , padding : "18px 20px" , borderBottom : "1px solid rgba(255,255,255,0.06)" } }, [
h ( "span" , { key : "d" , style : { width : "9px" , height : "9px" , borderRadius : "50%" , background : accent } }),
h ( "div" , { key : "tx" , style : { flex : 1 , lineHeight : 1.2 } }, [
h ( "div" , { key : "t" , style : { fontSize : "15.5px" , fontWeight : 800 , color : "#f4f4f6" } }, isCreate ? "Share your invitation" : "Send back your answer" ),
h ( "div" , { key : "s" , style : { fontSize : "12px" , color : "#7b7b83" } }, ` ${ isCreate ? "offer" : "answer" } \xB7 one-time` )
]),
h ( "button" , { key : "x" , onClick : () => setQrModalOpen ( false ), style : { width : "32px" , height : "32px" , display : "grid" , placeItems : "center" , borderRadius : "9px" , border : "none" , background : "rgba(255,255,255,0.05)" , color : "#9a9aa2" , cursor : "pointer" } }, fa ( "fa-times" ))
]),
h ( "div" , { key : "body" , style : { padding : "22px 24px 24px" } }, [
h (
"div" ,
{ key : "qr" , style : { position : "relative" , width : "100%" , aspectRatio : "1" , borderRadius : "18px" , overflow : "hidden" , background : "#fff" , padding : "18px" , display : "grid" , placeItems : "center" } },
h ( "img" , { src : qrCodeUrl , alt : "QR code" , style : { width : "100%" , height : "100%" , objectFit : "contain" , display : "block" } })
),
h ( "div" , { key : "ctrls" , style : { display : "flex" , flexDirection : "column" , alignItems : "center" , gap : "12px" , marginTop : "18px" } }, [
( qrFramesTotal || 0 ) >= 1 && h ( "div" , { key : "frame" , style : { display : "flex" , alignItems : "center" , gap : "9px" } }, [
h ( "span" , { key : "l" , style : { fontFamily : MONO , fontSize : "12px" , fontWeight : 600 , color : "#9a9aa2" } }, `Frame ${ Math . max ( 1 , qrFrameIndex || 1 ) } / ${ qrFramesTotal || 1 } ` ),
h ( "div" , { key : "dots" , style : { display : "flex" , gap : "5px" } }, Array . from ({ length : qrFramesTotal || 1 }, ( _ , i ) => h ( "span" , { key : i , style : { width : "7px" , height : "7px" , borderRadius : "50%" , background : i + 1 === ( qrFrameIndex || 1 ) ? accent : "rgba(255,255,255,0.14)" , transition : "background .25s" } })))
2025-09-08 16:04:58 -04:00
]),
2026-06-23 16:52:30 -04:00
( qrFramesTotal || 0 ) > 1 && h ( "div" , { key : "nav" , style : { display : "flex" , alignItems : "center" , gap : "6px" } }, [
h ( "button" , { key : "prev" , onClick : prevQrFrame , style : { width : "40px" , height : "36px" , display : "grid" , placeItems : "center" , borderRadius : "10px" , border : "1px solid rgba(255,255,255,0.1)" , background : "rgba(255,255,255,0.04)" , color : "#cfcfd4" , cursor : "pointer" } }, fa ( "fa-chevron-left" )),
h ( "button" , { key : "auto" , onClick : toggleQrManualMode , style : { display : "inline-flex" , alignItems : "center" , gap : "7px" , padding : "9px 18px" , borderRadius : "10px" , border : `1px solid ${ qrManualMode ? "rgba(255,255,255,0.1)" : "rgba(240,137,42,0.45)" } ` , background : qrManualMode ? "rgba(255,255,255,0.04)" : "rgba(240,137,42,0.08)" , color : qrManualMode ? "#9a9aa2" : C_ORANGE , fontFamily : "inherit" , fontSize : "13px" , fontWeight : 600 , cursor : "pointer" } }, qrManualMode ? "Manual" : "Auto" ),
h ( "button" , { key : "next" , onClick : nextQrFrame , style : { width : "40px" , height : "36px" , display : "grid" , placeItems : "center" , borderRadius : "10px" , border : "1px solid rgba(255,255,255,0.1)" , background : "rgba(255,255,255,0.04)" , color : "#cfcfd4" , cursor : "pointer" } }, fa ( "fa-chevron-right" ))
2025-09-27 19:07:17 -04:00
]),
2026-06-23 16:52:30 -04:00
h ( "p" , { key : "hint" , style : { margin : "2px 0 0" , textAlign : "center" , fontSize : "12px" , lineHeight : 1.5 , color : "#6b6b73" } }, ( qrFramesTotal || 0 ) > 1 ? `The handshake is split across ${ qrFramesTotal } frames \u2014 keep this open until your peer captures all of them.` : "Keep this open until your peer captures the code." )
2025-09-08 16:04:58 -04:00
])
])
2026-06-23 16:52:30 -04:00
])
);
const hero = h ( "div" , { key : "hero" , style : { display : "flex" , flexWrap : "wrap" , minHeight : "100vh" , width : "100%" , background : "#0f0f11" , color : "#e8e8eb" } }, [ leftPanel , rightPanel ]);
const uniqueSection = atIntro && h ( UniqueFeatureSlider , { key : "unique-features-slider" });
const partnersSection = atIntro && h ( BecomePartner , { key : "become-partner" });
const roadmapSection = atIntro && h ( Roadmap , { key : "roadmap" });
const communitySection = atIntro && h ( CommunityCTA , { key : "community-cta" });
const keyframeStyle = h ( "style" , { key : "kf" , dangerouslySetInnerHTML : {
2026-06-24 15:41:15 -04:00
__html : "@keyframes sbFlowR{0%{left:4%;opacity:0}12%{opacity:1}88%{opacity:1}100%{left:96%;opacity:0}}@keyframes sbFlowL{0%{left:96%;opacity:0}12%{opacity:1}88%{opacity:1}100%{left:4%;opacity:0}}@keyframes sbPulse{0%,100%{transform:translate(-50%,-50%) scale(1);opacity:.5}50%{transform:translate(-50%,-50%) scale(1.5);opacity:0}}@keyframes sbSpin{to{transform:rotate(360deg)}}@keyframes sbUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes sbNode{0%,100%{box-shadow:0 0 0 0 rgba(62,207,142,0)}50%{box-shadow:0 0 0 6px rgba(62,207,142,.06)}}@keyframes sbScan{0%{top:8%}100%{top:88%}}@keyframes sbBlink{0%,100%{opacity:1}50%{opacity:.35}}"
2026-06-23 16:52:30 -04:00
} });
2026-06-26 00:00:13 -04:00
if ( compact ) {
return h ( "div" , { className : "sb-start" , style : { flex : 1 , minHeight : 0 , width : "100%" , display : "flex" , flexDirection : "column" , background : "#0f0f11" , color : "#e8e8eb" } }, [ keyframeStyle , rightPanel , qrModal ]);
}
2026-06-23 16:52:30 -04:00
return h ( "div" , { className : "sb-start" , style : { width : "100%" } }, [ keyframeStyle , hero , uniqueSection , partnersSection , roadmapSection , communitySection , qrModal ]);
2025-09-08 16:04:58 -04:00
};
2025-09-23 20:01:02 -04:00
var createScrollToBottomFunction = ( chatMessagesRef ) => {
return () => {
if ( chatMessagesRef && chatMessagesRef . current ) {
const scrollAttempt = () => {
if ( chatMessagesRef . current ) {
chatMessagesRef . current . scrollTo ({
top : chatMessagesRef . current . scrollHeight ,
behavior : "smooth"
});
}
};
scrollAttempt ();
setTimeout ( scrollAttempt , 50 );
setTimeout ( scrollAttempt , 150 );
setTimeout ( scrollAttempt , 300 );
requestAnimationFrame (() => {
setTimeout ( scrollAttempt , 100 );
});
}
};
};
2026-06-23 16:52:30 -04:00
var runSecurityReport = async ( webrtcManager ) => {
let securityData = null ;
try {
if ( webrtcManager && window . EnhancedSecureCryptoUtils ) {
securityData = await window . EnhancedSecureCryptoUtils . calculateSecurityLevel ( webrtcManager );
}
} catch ( e ) {
}
if ( ! securityData ) {
alert ( "Security verification in progress\u2026\nPlease wait for real-time cryptographic verification to complete." );
return ;
}
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace" ;
const esc = ( s ) => String ( s ). replace ( /[&<>"]/g , ( c ) => ({ "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ })[ c ]);
const accent = securityData . color === "orange" ? "#f0892a" : securityData . color === "yellow" ? "#e3c84e" : securityData . color === "red" ? "#e5727a" : "#3ecf8e" ;
const accentRGB = securityData . color === "orange" ? "240,137,42" : securityData . color === "yellow" ? "227,200,78" : securityData . color === "red" ? "229,114,122" : "62,207,142" ;
const score = Math . max ( 0 , Math . min ( 100 , Math . round ( securityData . score || 0 )));
const circ = 2 * Math . PI * 56 ;
const dashArray = ` ${ ( circ * Math . min ( 1 , score / 100 )). toFixed ( 1 ) } ${ circ . toFixed ( 1 ) } ` ;
const level = String ( securityData . level || "SECURE" ). toUpperCase ();
const isReal = securityData . isRealData !== false ;
const entries = securityData . verificationResults ? Object . entries ( securityData . verificationResults ) : [];
const passedCount = Number . isFinite ( securityData . passedChecks ) ? securityData . passedChecks : entries . filter (([, r ]) => r && r . passed ). length ;
const totalCount = Number . isFinite ( securityData . totalChecks ) ? securityData . totalChecks : entries . length ;
const verifiedAt = new Date ( securityData . timestamp || Date . now ()). toLocaleTimeString ([], { hour : "2-digit" , minute : "2-digit" , second : "2-digit" , hour12 : false });
const pretty = ( k ) => {
let s = String ( k ). replace ( /^verify/i , "" ). replace ( /([a-z0-9])([A-Z])/g , "$1 $2" ). replace ( /([A-Z]+)([A-Z][a-z])/g , "$1 $2" ). trim ();
s = s . replace ( /\b(ecdh|ecdsa|aes|gcm|hmac|pfs|sas|mitm|asn|dtls|hkdf|spki|oid|p384)\b/gi , ( m ) => m . toUpperCase ());
return s . charAt ( 0 ). toUpperCase () + s . slice ( 1 );
};
const checkIcon = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4 10-11"/></svg>` ;
const xIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#e5727a" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>` ;
const testsHTML = entries . map (([ k , r ], i ) => {
const passed = !! ( r && r . passed );
const desc = r && r . details || ( passed ? "Test passed" : "Test failed or unavailable" );
const bg = passed ? "#161618" : "#121214" ;
const border = passed ? "rgba(62,207,142,0.16)" : "rgba(229,114,122,0.18)" ;
const iconBg = passed ? "rgba(62,207,142,0.12)" : "rgba(229,114,122,0.1)" ;
const iconBorder = passed ? "rgba(62,207,142,0.26)" : "rgba(229,114,122,0.24)" ;
const titleColor = passed ? "#f4f4f6" : "#cfcfd4" ;
return `<div style="display:flex; align-items:flex-start; gap:13px; padding:16px 18px; border-radius:14px; background: ${ bg } ; border:1px solid ${ border } ; animation:svIn .3s cubic-bezier(.2,.7,.3,1) both; animation-delay: ${ ( i * 0.04 ). toFixed ( 2 ) } s;">
<span style="flex:none; width:34px; height:34px; border-radius:9px; display:grid; place-items:center; background: ${ iconBg } ; border:1px solid ${ iconBorder } ;"> ${ passed ? checkIcon : xIcon } </span>
<div style="flex:1; min-width:0;">
<div style="font-size:14.5px; font-weight:700; letter-spacing:-0.2px; color: ${ titleColor } ; margin-bottom:3px;"> ${ esc ( pretty ( k )) } </div>
<div style="font-family: ${ MONO } ; font-size:11.5px; line-height:1.45; color:#8a8a92;"> ${ esc ( desc ) } </div>
</div>
</div>` ;
}). join ( "" );
const modal = document . createElement ( "div" );
modal . id = "sb-security-report" ;
modal . style . cssText = "position:fixed; inset:0; z-index:10000; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(8,8,10,0.62); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); font-family:'Manrope',system-ui,-apple-system,sans-serif; overflow:auto;" ;
modal . innerHTML = `
<style>@keyframes svIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes svPulse{0%,100%{opacity:1}50%{opacity:.4}}</style>
<div style="position:relative; max-width:960px; width:100%; border-radius:20px; background:radial-gradient(1100px 640px at 50% -6%, rgba( ${ accentRGB } ,0.05), transparent 62%), #0f0f11; border:1px solid rgba(255,255,255,0.08); color:#e8e8eb; padding:30px; box-shadow:0 30px 70px rgba(0,0,0,0.55); animation:svIn .3s cubic-bezier(.2,.7,.3,1); max-height:calc(100vh - 48px); overflow:auto;">
<button class="sv-close" type="button" title="Close" style="position:absolute; top:16px; right:16px; width:32px; height:32px; border-radius:9px; display:grid; place-items:center; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.02); color:#8a8a92; cursor:pointer; z-index:2;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
<div style="position:relative; overflow:hidden; border-radius:18px; background:#141416; border:1px solid rgba(255,255,255,0.07); padding:28px 30px; display:flex; align-items:center; gap:30px; flex-wrap:wrap; margin-bottom:20px;">
<div style="position:absolute; top:0; left:0; right:0; height:1px; background:linear-gradient(90deg, transparent, rgba( ${ accentRGB } ,0.6), transparent);"></div>
<div style="position:relative; flex:none; width:128px; height:128px;">
<svg width="128" height="128" viewBox="0 0 128 128" style="transform:rotate(-90deg);">
<circle cx="64" cy="64" r="56" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="9"/>
<circle cx="64" cy="64" r="56" fill="none" stroke=" ${ accent } " stroke-width="9" stroke-linecap="round" stroke-dasharray=" ${ dashArray } "/>
</svg>
<div style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;">
<span style="font-size:30px; font-weight:800; letter-spacing:-1px; color:#f4f4f6; line-height:1;"> ${ score } </span>
<span style="font-family: ${ MONO } ; font-size:10px; font-weight:600; color:#6b6b73; text-transform:uppercase; letter-spacing:1px; margin-top:4px;">/ 100 pts</span>
</div>
</div>
<div style="flex:1; min-width:240px;">
<div style="font-family: ${ MONO } ; font-size:11px; font-weight:600; color:#6b6b73; text-transform:uppercase; letter-spacing:1.6px; margin-bottom:10px;">Real-time security verification</div>
<div style="display:flex; align-items:center; gap:12px; margin-bottom:14px; flex-wrap:wrap;">
<h2 style="margin:0; font-size:26px; font-weight:800; letter-spacing:-0.7px; color:#f4f4f6;">Security level: ${ esc ( level ) } </h2>
<span style="display:inline-flex; align-items:center; gap:8px; padding:6px 12px; border-radius:9px; background:rgba( ${ accentRGB } ,0.12); border:1px solid rgba( ${ accentRGB } ,0.3); font-family: ${ MONO } ; font-size:11px; font-weight:700; color: ${ accent } ; text-transform:uppercase; letter-spacing:0.6px;"><span style="width:7px; height:7px; border-radius:50%; background: ${ accent } ; animation:svPulse 2s ease-in-out infinite;"></span>Active</span>
</div>
<div style="display:flex; gap:28px; flex-wrap:wrap;">
<div><div style="font-family: ${ MONO } ; font-size:10px; font-weight:600; color:#56565e; text-transform:uppercase; letter-spacing:1px; margin-bottom:4px;">Tests passed</div><div style="font-size:15px; font-weight:700; color:#e8e8eb;"><span style="color: ${ accent } ;"> ${ passedCount } </span> / ${ totalCount } </div></div>
<div><div style="font-family: ${ MONO } ; font-size:10px; font-weight:600; color:#56565e; text-transform:uppercase; letter-spacing:1px; margin-bottom:4px;">Verified at</div><div style="font-family: ${ MONO } ; font-size:15px; font-weight:600; color:#e8e8eb;"> ${ esc ( verifiedAt ) } </div></div>
<div><div style="font-family: ${ MONO } ; font-size:10px; font-weight:600; color:#56565e; text-transform:uppercase; letter-spacing:1px; margin-bottom:4px;">Source</div><div style="font-size:15px; font-weight:600; color:#e8e8eb;"> ${ isReal ? "Real cryptographic tests" : "Simulated data" } </div></div>
</div>
</div>
<button class="sv-rerun" type="button" style="flex:none; display:inline-flex; align-items:center; gap:9px; padding:12px 18px; border-radius:11px; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.025); color:#cfcfd4; font-family:'Manrope',sans-serif; font-size:14px; font-weight:700; cursor:pointer; transition:all .2s;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>
Re-run
</button>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(248px, 1fr)); gap:12px;"> ${ testsHTML } </div>
<div style="display:flex; align-items:center; gap:12px; margin-top:18px; padding:16px 20px; border-radius:14px; background:rgba( ${ accentRGB } ,0.06); border:1px solid rgba( ${ accentRGB } ,0.18);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke=" ${ accent } " stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="flex:none;"><path d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"/><path d="M9.2 12.2l2 2 3.6-3.8"/></svg>
<span style="font-size:14px; font-weight:600; color:#e8e8eb;"> ${ isReal ? "Real-time verification using actual cryptographic functions \u2014 no mock data." : "Warning: connection may not be fully established \u2014 values may be simulated." } </span>
</div>
</div>` ;
const onKey = ( e ) => {
if ( e . key === "Escape" ) close ();
};
const close = () => {
if ( modal . parentNode ) modal . remove ();
document . removeEventListener ( "keydown" , onKey );
};
modal . querySelector ( ".sv-close" ). addEventListener ( "click" , close );
modal . addEventListener ( "click" , ( e ) => {
if ( e . target === modal ) close ();
});
document . addEventListener ( "keydown" , onKey );
const rerun = modal . querySelector ( ".sv-rerun" );
rerun . addEventListener ( "mouseenter" , () => {
rerun . style . borderColor = "rgba(240,137,42,0.45)" ;
rerun . style . color = "#f0892a" ;
});
rerun . addEventListener ( "mouseleave" , () => {
rerun . style . borderColor = "rgba(255,255,255,0.1)" ;
rerun . style . color = "#cfcfd4" ;
});
rerun . addEventListener ( "click" , () => {
close ();
runSecurityReport ( webrtcManager );
});
document . body . appendChild ( modal );
};
2026-06-26 00:00:13 -04:00
var SecureBitChatHeader = ({ status , onDisconnect , webrtcManager , title , isOffline , peerPresence , onRenameTitle }) => {
2026-06-23 16:52:30 -04:00
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace" ;
const [ showNetwork , setShowNetwork ] = React . useState ( false );
const [ sec , setSec ] = React . useState ( null );
2026-06-26 00:00:13 -04:00
const [ editingName , setEditingName ] = React . useState ( false );
const [ nameDraft , setNameDraft ] = React . useState ( "" );
2026-06-23 16:52:30 -04:00
React . useEffect (() => {
let alive = true ;
const fetchSec = async () => {
try {
if ( ! webrtcManager ) return ;
let data = null ;
if ( typeof webrtcManager . getRealSecurityLevel === "function" ) data = await webrtcManager . getRealSecurityLevel ();
else if ( typeof webrtcManager . calculateAndReportSecurityLevel === "function" ) data = await webrtcManager . calculateAndReportSecurityLevel ();
else if ( window . EnhancedSecureCryptoUtils ) data = await window . EnhancedSecureCryptoUtils . calculateSecurityLevel ( webrtcManager );
if ( alive && data && data . isRealData !== false ) setSec ( data );
} catch ( e ) {
}
};
fetchSec ();
const onCalc = ( e ) => {
if ( alive && e . detail && e . detail . securityData ) setSec ( e . detail . securityData );
};
document . addEventListener ( "real-security-calculated" , onCalc );
const iv = setInterval ( fetchSec , 15e3 );
return () => {
alive = false ;
clearInterval ( iv );
document . removeEventListener ( "real-security-calculated" , onCalc );
};
}, [ webrtcManager ]);
2026-06-26 00:00:13 -04:00
const onlineConnected = status === "connected" || status === "verified" ;
const dropped = status === "disconnected" || status === "peer_disconnected" ;
const connected = onlineConnected && ! isOffline ;
const connDot = isOffline || dropped ? "#e5727a" : onlineConnected ? "#3ecf8e" : "#e3c84e" ;
const connLabel = isOffline ? "Offline" : onlineConnected ? "Connected" : status === "peer_disconnected" ? "Peer disconnected" : status === "disconnected" ? "Disconnected" : "Connecting\u2026" ;
const connGlow = isOffline || dropped ? "0 0 0 3px rgba(229,114,122,0.16)" : onlineConnected ? "0 0 0 3px rgba(62,207,142,0.16)" : "0 0 0 3px rgba(227,200,78,0.16)" ;
const peerDot = onlineConnected && ! isOffline ? PRESENCE_DOT [ peerPresence ] || "#3ecf8e" : connDot ;
const peerPresenceWord = onlineConnected && ! isOffline && peerPresence ? PRESENCE_WORD [ peerPresence ] || null : null ;
const startRename = () => {
setNameDraft ( title || "" );
setEditingName ( true );
};
const commitRename = () => {
if ( typeof onRenameTitle === "function" ) onRenameTitle ( nameDraft );
setEditingName ( false );
};
const renameKey = ( e ) => {
if ( e . key === "Enter" ) {
e . preventDefault ();
commitRename ();
} else if ( e . key === "Escape" ) {
setEditingName ( false );
}
};
2026-06-23 16:52:30 -04:00
const passed = sec && Number . isFinite ( sec . passedChecks ) ? sec . passedChecks : null ;
const total = sec && Number . isFinite ( sec . totalChecks ) ? sec . totalChecks : null ;
const scoreLabel = passed != null && total ? passed + "/" + total : sec ? sec . score + "%" : "\u2014" ;
const accent = sec ? sec . color === "green" ? "#3ecf8e" : sec . color === "orange" ? "#f0892a" : sec . color === "yellow" ? "#e3c84e" : "#e5727a" : "#3ecf8e" ;
const secBtn = React . createElement ( "div" , {
key : "sec" ,
title : "Run security verification" ,
onClick : () => runSecurityReport ( webrtcManager ),
className : "sb-secpill" ,
style : { display : "flex" , alignItems : "center" , gap : "9px" , padding : "7px 13px" , borderRadius : "9px" , border : "1px solid " + ( showNetwork ? "rgba(255,255,255,0.16)" : "rgba(255,255,255,0.07)" ), background : showNetwork ? "rgba(255,255,255,0.05)" : "rgba(255,255,255,0.02)" , cursor : "pointer" , fontFamily : "inherit" , transition : "all .15s" }
}, [
React . createElement ( "i" , { key : "i" , className : "fas fa-shield-halved" , style : { color : accent , fontSize : "13px" } }),
2026-06-26 00:00:13 -04:00
React . createElement ( "span" , { key : "l" , className : "sb-sec-label" , style : { fontSize : "13px" , fontWeight : 600 , color : "#e8e8eb" } }, sec ? sec . level || "Secure" : "Secure" ),
React . createElement ( "span" , { key : "d" , className : "sb-sec-div" , style : { width : "1px" , height : "13px" , background : "rgba(255,255,255,0.12)" } }),
React . createElement ( "span" , { key : "s" , className : "sb-sec-score" , style : { fontFamily : MONO , fontSize : "11.5px" , fontWeight : 500 , color : "#8a8a92" } }, scoreLabel ),
2026-06-23 16:52:30 -04:00
React . createElement ( "button" , {
key : "c" ,
type : "button" ,
title : "Network & crypto details" ,
onClick : ( e ) => {
e . stopPropagation ();
setShowNetwork (( v ) => ! v );
},
style : { background : "none" , border : "none" , padding : 0 , margin : 0 , cursor : "pointer" , display : "grid" , placeItems : "center" }
}, React . createElement ( "i" , { className : "fas fa-chevron-down" , style : { color : "#6b6b73" , fontSize : "11px" , transform : showNetwork ? "rotate(180deg)" : "rotate(0deg)" , transition : "transform .2s" } }))
]);
2026-06-26 00:00:13 -04:00
const headerResponsiveCss = React . createElement ( "style" , { key : "hdr-css" , dangerouslySetInnerHTML : {
__html : (
// Mobile: leave room for the drawer hamburger and shed non-essential header
// chrome so avatar + name + status + Disconnect fit a narrow screen.
"@media (max-width:768px){.sb-chat-header{padding-left:60px !important;gap:10px !important;}.sb-chat-header .sb-sec-score,.sb-chat-header .sb-sec-label,.sb-chat-header .sb-sec-div{display:none !important;}.sb-chat-header .sb-secpill{padding:8px !important;gap:6px !important;}.sb-chat-header .sb-conn-text{display:none !important;}.sb-chat-header .sb-conn{padding:9px !important;}.sb-chat-header .sb-hdr-sub{display:none !important;}}@media (max-width:480px){.sb-chat-header{padding-right:12px !important;gap:8px !important;}}"
)
} });
2026-06-23 16:52:30 -04:00
const header = React . createElement ( "header" , {
key : "hdr" ,
2026-06-26 00:00:13 -04:00
className : "sb-chat-header" ,
2026-06-23 16:52:30 -04:00
style : { flex : "none" , display : "flex" , alignItems : "center" , justifyContent : "space-between" , gap : "24px" , padding : "0 20px" , height : "64px" , borderBottom : "1px solid rgba(255,255,255,0.06)" , background : "rgba(18,18,20,0.72)" , backdropFilter : "blur(14px)" , WebkitBackdropFilter : "blur(14px)" }
}, [
2026-06-26 00:00:13 -04:00
headerResponsiveCss ,
// The SecureBit brand/logo lives in the left rail; this header identifies the
// ACTIVE conversation — avatar monogram + local label + connection status.
2026-06-23 16:52:30 -04:00
React . createElement ( "div" , { key : "left" , style : { display : "flex" , alignItems : "center" , gap : "12px" , minWidth : 0 } }, [
2026-06-26 00:00:13 -04:00
React . createElement ( "div" , { key : "avatar" , style : { position : "relative" , flex : "none" , width : "36px" , height : "36px" , borderRadius : "10px" , display : "grid" , placeItems : "center" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.09)" , fontSize : "13px" , fontWeight : 700 , letterSpacing : "-0.3px" , color : "#e8e8eb" } }, [
monoInitials ( title || "Chat" ),
React . createElement ( "span" , { key : "dot" , style : { position : "absolute" , right : "-2px" , bottom : "-2px" , width : "11px" , height : "11px" , borderRadius : "50%" , background : peerDot , border : "2px solid #121214" } })
]),
editingName ? React . createElement ( "div" , { key : "edit" , style : { display : "flex" , flexDirection : "column" , gap : "4px" , minWidth : 0 } }, [
React . createElement ( "div" , { key : "row" , style : { display : "flex" , alignItems : "center" , gap : "6px" } }, [
React . createElement ( "input" , { key : "in" , autoFocus : true , value : nameDraft , maxLength : 32 , placeholder : "Name this chat" , onChange : ( e ) => setNameDraft ( e . target . value ), onKeyDown : renameKey , onBlur : commitRename , style : { width : "210px" , padding : "5px 10px" , borderRadius : "8px" , border : "1px solid rgba(240,137,42,0.55)" , background : "#0f0f11" , color : "#f4f4f6" , fontFamily : "inherit" , fontSize : "14px" , fontWeight : 700 , outline : "none" } }),
React . createElement ( "button" , { key : "ok" , onMouseDown : ( e ) => e . preventDefault (), onClick : commitRename , title : "Save" , style : { flex : "none" , width : "28px" , height : "28px" , borderRadius : "8px" , display : "grid" , placeItems : "center" , border : "none" , background : "#f0892a" , color : "#1a0f04" , cursor : "pointer" } }, React . createElement ( "i" , { className : "fas fa-check" , style : { fontSize : "12px" } }))
]),
React . createElement ( "div" , { key : "hint" , style : { fontSize : "11px" , color : "#56565e" } }, "Local label \xB7 stored only on this device" )
]) : React . createElement ( "div" , { key : "txt" , style : { lineHeight : 1.2 , minWidth : 0 } }, [
React . createElement ( "div" , { key : "r1" , style : { display : "flex" , alignItems : "center" , gap : "7px" } }, [
React . createElement ( "span" , { key : "n" , style : { fontSize : "15px" , fontWeight : 800 , letterSpacing : "-0.3px" , color : "#f4f4f6" , whiteSpace : "nowrap" , overflow : "hidden" , textOverflow : "ellipsis" } }, title || "Secure chat" ),
React . createElement ( "button" , { key : "edit" , onClick : startRename , title : "Rename chat (local only)" , style : { flex : "none" , width : "24px" , height : "24px" , borderRadius : "7px" , display : "grid" , placeItems : "center" , border : "none" , background : "transparent" , color : "#56565e" , cursor : "pointer" } }, React . createElement ( "i" , { className : "fas fa-pen" , style : { fontSize : "11px" } }))
2026-06-23 16:52:30 -04:00
]),
2026-06-26 00:00:13 -04:00
React . createElement ( "div" , { key : "r2" , className : "sb-hdr-sub" , style : { fontSize : "11px" , color : "#6b6b73" , fontWeight : 500 , whiteSpace : "nowrap" , overflow : "hidden" , textOverflow : "ellipsis" } }, isOffline ? "No network \xB7 reconnecting" : peerPresenceWord || ( onlineConnected ? "P2P \xB7 end-to-end encrypted" : status === "peer_disconnected" ? "Peer disconnected" : status === "disconnected" ? "Disconnected" : "Connecting\u2026" ))
2026-06-23 16:52:30 -04:00
])
]),
secBtn ,
2026-06-26 00:00:13 -04:00
React . createElement ( "div" , { key : "right" , className : "sb-hdr-right" , style : { display : "flex" , alignItems : "center" , gap : "9px" } }, [
React . createElement ( "div" , { key : "conn" , className : "sb-conn" , style : { display : "flex" , alignItems : "center" , gap : "8px" , padding : "8px 13px" , borderRadius : "9px" , border : "1px solid rgba(255,255,255,0.07)" , background : "rgba(255,255,255,0.02)" } }, [
React . createElement ( "span" , { key : "dot" , style : { flex : "none" , width : "7px" , height : "7px" , borderRadius : "50%" , background : connDot , boxShadow : connGlow } }),
React . createElement ( "span" , { key : "t" , className : "sb-conn-text" , style : { fontSize : "13px" , fontWeight : 600 , color : "#cfcfd4" } }, connLabel )
2026-06-23 16:52:30 -04:00
]),
React . createElement ( "button" , { key : "dc" , onClick : onDisconnect , className : "sb-disconnect" , style : { display : "flex" , alignItems : "center" , gap : "7px" , padding : "8px 14px" , borderRadius : "9px" , border : "1px solid rgba(255,255,255,0.08)" , background : "transparent" , color : "#9a9aa2" , fontFamily : "inherit" , fontSize : "13px" , fontWeight : 600 , cursor : "pointer" , transition : "all .15s" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-power-off" , style : { fontSize : "12px" } }),
React . createElement ( "span" , { key : "t" , className : "sb-hide-sm" }, "Disconnect" )
])
])
]);
const netPanel = showNetwork && React . createElement ( "div" , {
key : "net" ,
style : { flex : "none" , padding : "13px 20px" , borderBottom : "1px solid rgba(255,255,255,0.06)" , background : "rgba(18,18,20,0.72)" , backdropFilter : "blur(14px)" , WebkitBackdropFilter : "blur(14px)" }
}, React . createElement (
"div" ,
{ style : { maxWidth : "1000px" , margin : "0 auto" , display : "grid" , gridTemplateColumns : "repeat(auto-fit,minmax(140px,1fr))" , gap : "14px" , fontFamily : MONO } },
[
[ "Transport" , "WebRTC \xB7 DTLS" ],
[ "Cipher" , "AES-256-GCM" ],
[ "Key exchange" , "ECDH P-384" ],
[ "Security" , scoreLabel + ( sec ? " \xB7 " + sec . score + "%" : "" )]
]. map (([ k , v ], i ) => React . createElement ( "div" , { key : "nf" + i }, [
React . createElement ( "div" , { key : "k" , style : { fontSize : "10px" , color : "#6b6b73" , textTransform : "uppercase" , letterSpacing : "0.6px" , marginBottom : "4px" } }, k ),
React . createElement ( "div" , { key : "v" , style : { fontSize : "12.5px" , color : i === 3 ? accent : "#cfcfd4" , fontWeight : 500 } }, v )
]))
));
return React . createElement ( "div" , { style : { flex : "none" } }, [ header , netPanel ]);
};
2025-09-08 16:04:58 -04:00
var EnhancedChatInterface = ({
2026-06-26 00:00:13 -04:00
title ,
isOffline ,
peerPresence ,
onRenameTitle ,
2025-09-08 16:04:58 -04:00
messages ,
messageInput ,
setMessageInput ,
onSendMessage ,
onDisconnect ,
keyFingerprint ,
isVerified ,
chatMessagesRef ,
scrollToBottom ,
2026-05-26 22:40:36 -04:00
webrtcManager ,
2026-06-23 16:52:30 -04:00
status ,
2026-05-26 22:40:36 -04:00
pendingIncomingFiles = [],
2026-06-18 21:15:43 -04:00
onIncomingDecision ,
// Secure chat extras
codeMode ,
setCodeMode ,
viewOnceMode ,
setViewOnceMode ,
2026-06-19 02:58:03 -04:00
viewOnceTtl ,
setViewOnceTtl ,
2026-06-18 21:15:43 -04:00
disappearTtl ,
setDisappearTtl ,
nowTick ,
onUnsendMessage ,
2026-06-19 02:58:03 -04:00
onMessageExpire
2025-09-08 16:04:58 -04:00
}) => {
const [ showScrollButton , setShowScrollButton ] = React . useState ( false );
const [ showFileTransfer , setShowFileTransfer ] = React . useState ( false );
2026-06-23 16:52:30 -04:00
const [ fileSendMode , setFileSendMode ] = React . useState ( false );
const [ showTimer , setShowTimer ] = React . useState ( false );
const [ showOnce , setShowOnce ] = React . useState ( false );
const [ showHandshake , setShowHandshake ] = React . useState ( false );
const taRef = React . useRef ( null );
React . useEffect (() => {
const el = taRef . current ;
if ( ! el || codeMode ) return ;
el . style . height = "auto" ;
el . style . height = Math . min ( el . scrollHeight , 240 ) + "px" ;
}, [ messageInput , codeMode ]);
2026-05-26 22:40:36 -04:00
React . useEffect (() => {
if ( pendingIncomingFiles . length > 0 ) {
setShowFileTransfer ( true );
}
}, [ pendingIncomingFiles . length ]);
2025-09-08 16:04:58 -04:00
React . useEffect (() => {
if ( chatMessagesRef . current && messages . length > 0 ) {
const { scrollTop , scrollHeight , clientHeight } = chatMessagesRef . current ;
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100 ;
if ( isNearBottom ) {
const smoothScroll = () => {
if ( chatMessagesRef . current ) {
chatMessagesRef . current . scrollTo ({
top : chatMessagesRef . current . scrollHeight ,
behavior : "smooth"
});
}
};
smoothScroll ();
setTimeout ( smoothScroll , 50 );
setTimeout ( smoothScroll , 150 );
}
}
}, [ messages , chatMessagesRef ]);
const handleScroll = () => {
if ( chatMessagesRef . current ) {
const { scrollTop , scrollHeight , clientHeight } = chatMessagesRef . current ;
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100 ;
setShowScrollButton ( ! isNearBottom );
}
};
const handleScrollToBottom = () => {
2025-09-23 20:01:02 -04:00
if ( typeof scrollToBottom === "function" ) {
scrollToBottom ();
setShowScrollButton ( false );
2026-06-23 16:52:30 -04:00
} else if ( chatMessagesRef . current ) {
chatMessagesRef . current . scrollTo ({ top : chatMessagesRef . current . scrollHeight , behavior : "smooth" });
2025-09-23 20:01:02 -04:00
setShowScrollButton ( false );
}
2025-09-08 16:04:58 -04:00
};
2025-10-14 22:51:48 -04:00
const handleKeyPress = ( e ) => {
2026-06-23 16:52:30 -04:00
if ( e . key !== "Enter" ) return ;
if ( codeMode ) {
if ( e . metaKey || e . ctrlKey ) {
e . preventDefault ();
onSendMessage ();
}
} else if ( ! e . shiftKey ) {
2025-10-14 22:51:48 -04:00
e . preventDefault ();
2025-09-08 16:04:58 -04:00
onSendMessage ();
}
};
const isFileTransferReady = () => {
if ( ! webrtcManager ) return false ;
const connected = webrtcManager . isConnected ? webrtcManager . isConnected () : false ;
const verified = webrtcManager . isVerified || false ;
const hasDataChannel = webrtcManager . dataChannel && webrtcManager . dataChannel . readyState === "open" ;
return connected && verified && hasDataChannel ;
};
2026-06-23 16:52:30 -04:00
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace" ;
const fmtShort = ( s ) => {
if ( ! s ) return "" ;
if ( s >= 86400 && s % 86400 === 0 ) return s / 86400 + "d" ;
if ( s >= 3600 && s % 3600 === 0 ) return s / 3600 + "h" ;
if ( s >= 60 ) return Math . round ( s / 60 ) + "m" ;
return s + "s" ;
};
const chipStyle = ( active ) => ({
display : "flex" ,
alignItems : "center" ,
gap : "6px" ,
padding : "7px 11px" ,
borderRadius : "8px" ,
border : "1px solid " + ( active ? "rgba(255,255,255,0.18)" : "rgba(255,255,255,0.07)" ),
background : active ? "rgba(255,255,255,0.06)" : "transparent" ,
color : active ? "#fff" : "#9a9aa2" ,
fontFamily : "inherit" ,
fontSize : "12.5px" ,
fontWeight : 600 ,
cursor : "pointer" ,
transition : "all .15s"
});
const optStyle = ( sel ) => ({
padding : "6px 12px" ,
borderRadius : "8px" ,
border : "1px solid " + ( sel ? "rgba(255,255,255,0.22)" : "rgba(255,255,255,0.07)" ),
background : sel ? "rgba(255,255,255,0.07)" : "transparent" ,
color : sel ? "#fff" : "#8a8a92" ,
fontFamily : MONO ,
fontSize : "12px" ,
fontWeight : 500 ,
cursor : "pointer" ,
transition : "all .14s"
});
const timerDefs = [
{ label : "Off" , v : 0 },
{ label : "5s" , v : 5 },
{ label : "30s" , v : 30 },
{ label : "1m" , v : 60 },
{ label : "1h" , v : 3600 },
{ label : "24h" , v : 86400 }
];
const onceDefs = [
{ label : "Off" , v : 0 },
{ label : "5s" , v : 5 },
{ label : "10s" , v : 10 },
{ label : "30s" , v : 30 },
{ label : "1m" , v : 60 }
];
const onceSelected = viewOnceMode ? viewOnceTtl : 0 ;
const pickTimer = ( v ) => {
setDisappearTtl ( v );
setShowTimer ( false );
};
const pickOnce = ( v ) => {
if ( v === 0 ) setViewOnceMode ( false );
else {
setViewOnceTtl ( v );
setViewOnceMode ( true );
}
setShowOnce ( false );
};
const hasText = !! ( messageInput && messageInput . trim ());
const fmtT = ( ts ) => {
try {
return new Date ( ts ). toLocaleTimeString ( "en-GB" , { hour : "2-digit" , minute : "2-digit" , second : "2-digit" });
} catch ( e ) {
return "" ;
}
};
const systemMessages = messages . filter (( m ) => m . type === "system" && typeof m . message === "string" && m . message . trim ());
const chatMessages = messages . filter (( m ) => m . type !== "system" );
const handshakeCard = ( isVerified || systemMessages . length > 0 ) && React . createElement ( "div" , {
key : "handshake" ,
style : { border : "1px solid rgba(255,255,255,0.07)" , borderRadius : "12px" , background : "#161618" , overflow : "hidden" }
}, [
React . createElement ( "button" , {
key : "hs-btn" ,
onClick : () => setShowHandshake (( v ) => ! v ),
style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "13px" , padding : "14px 16px" , background : "transparent" , border : "none" , color : "inherit" , cursor : "pointer" , textAlign : "left" , fontFamily : "inherit" }
}, [
2025-09-08 16:04:58 -04:00
React . createElement (
"div" ,
2026-06-23 16:52:30 -04:00
{ key : "ic" , style : { flex : "none" , width : "30px" , height : "30px" , display : "grid" , placeItems : "center" } },
React . createElement ( "i" , { className : "fas fa-check" , style : { color : "#3ecf8e" , fontSize : "16px" } })
2025-09-08 16:04:58 -04:00
),
2026-06-23 16:52:30 -04:00
React . createElement ( "div" , { key : "tx" , style : { flex : 1 , minWidth : 0 } }, [
React . createElement ( "div" , { key : "t1" , style : { fontSize : "13.5px" , fontWeight : 600 , color : "#e8e8eb" , whiteSpace : "nowrap" , overflow : "hidden" , textOverflow : "ellipsis" } }, "Secure channel established" ),
React . createElement ( "div" , { key : "t2" , style : { fontSize : "12px" , color : "#7b7b83" , marginTop : "1px" , whiteSpace : "nowrap" , overflow : "hidden" , textOverflow : "ellipsis" } }, "Verified \xB7 Perfect Forward Secrecy" + ( systemMessages . length ? " \xB7 " + systemMessages . length + ( systemMessages . length === 1 ? " event" : " events" ) : "" ))
]),
React . createElement ( "i" , { key : "chev" , className : "fas fa-chevron-down" , style : { flex : "none" , color : "#6b6b73" , fontSize : "13px" , transform : showHandshake ? "rotate(180deg)" : "rotate(0deg)" , transition : "transform .2s" } })
]),
showHandshake && React . createElement ( "div" , { key : "hs-body" , style : { padding : "2px 16px 14px 59px" } }, [
systemMessages . length > 0 && React . createElement (
2025-09-08 16:04:58 -04:00
"div" ,
2026-06-23 16:52:30 -04:00
{ key : "steps" , className : "sb-scroll" , style : { marginBottom : "12px" , maxHeight : "220px" , overflowY : "auto" , paddingRight : "6px" } },
systemMessages . map (( m , i ) => React . createElement ( "div" , { key : "s" + i , style : { display : "flex" , gap : "11px" , padding : "6px 0" , borderTop : i === 0 ? "none" : "1px solid rgba(255,255,255,0.04)" } }, [
React . createElement ( "span" , { key : "d" , style : { flex : "none" , width : "5px" , height : "5px" , borderRadius : "50%" , background : "#3ecf8e" , marginTop : "7px" , opacity : 0.6 } }),
React . createElement ( "span" , { key : "t" , style : { flex : 1 , fontSize : "12.5px" , color : "#9a9aa2" , lineHeight : 1.5 , wordBreak : "break-word" } }, String ( m . message || "" ). trim ()),
React . createElement ( "span" , { key : "tm" , style : { flex : "none" , fontFamily : MONO , fontSize : "10.5px" , color : "#56565e" } }, fmtT ( m . timestamp ))
]))
2025-09-08 16:04:58 -04:00
),
2026-06-23 16:52:30 -04:00
keyFingerprint && React . createElement ( "div" , { key : "sn" , style : { display : "flex" , alignItems : "center" , gap : "9px" , padding : "10px 12px" , borderRadius : "9px" , background : "rgba(255,255,255,0.025)" , border : "1px solid rgba(255,255,255,0.06)" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-lock" , style : { color : "#8a8a92" , fontSize : "12px" } }),
React . createElement ( "span" , { key : "l" , style : { fontSize : "11.5px" , color : "#8a8a92" } }, "Safety number" ),
React . createElement ( "span" , { key : "v" , style : { fontFamily : MONO , fontSize : "12px" , color : "#cfcfd4" , letterSpacing : "0.8px" , fontWeight : 500 , wordBreak : "break-all" } }, keyFingerprint )
])
])
]);
const emptyState = React . createElement (
"div" ,
{ key : "empty" , style : { display : "flex" , alignItems : "center" , justifyContent : "center" , flex : 1 , minHeight : "40vh" } },
React . createElement ( "div" , { style : { textAlign : "center" , maxWidth : "420px" } }, [
React . createElement ( "img" , { key : "ic" , src : "/logo/securebit-mark.svg" , alt : "SecureBit" , style : { width : "60px" , height : "60px" , objectFit : "contain" , display : "block" , margin : "0 auto 16px" } }),
React . createElement ( "h3" , { key : "t" , style : { fontSize : "17px" , fontWeight : 700 , color : "#e8e8eb" , margin : "0 0 6px" } }, "Secure channel is ready" ),
React . createElement ( "p" , { key : "p" , style : { fontSize : "13px" , color : "#7b7b83" , margin : 0 } }, "Every message is end-to-end encrypted on your device before it leaves." )
])
);
const messagesArea = React . createElement ( "main" , {
key : "main" ,
ref : chatMessagesRef ,
onScroll : handleScroll ,
className : "sb-scroll" ,
style : { flex : 1 , overflowY : "auto" , padding : "20px 20px 22px" }
}, React . createElement (
"div" ,
{ style : { width : "100%" , maxWidth : "1000px" , margin : "0 auto" , display : "flex" , flexDirection : "column" , gap : "16px" , minHeight : "100%" } },
chatMessages . length === 0 ? [ handshakeCard , emptyState ] : [ handshakeCard ]. concat ( chatMessages . map (( msg ) => React . createElement ( EnhancedChatMessage , {
key : msg . id ,
message : msg . message ,
type : msg . type ,
timestamp : msg . timestamp ,
mid : msg . mid ,
status : msg . status ,
viewOnce : msg . viewOnce ,
viewOnceTtl : msg . viewOnceTtl ,
expiresAt : msg . expiresAt ,
expired : msg . expired ,
nowTick ,
canUnsend : typeof onUnsendMessage === "function" ,
onUnsend : onUnsendMessage ,
onExpire : () => onMessageExpire && onMessageExpire ( msg . id )
})))
));
const timerRow = showTimer && React . createElement (
"div" ,
{ key : "timer-row" , style : { display : "flex" , flexWrap : "wrap" , alignItems : "center" , gap : "8px" , padding : "10px 12px" , marginBottom : "10px" , borderRadius : "11px" , border : "1px solid rgba(255,255,255,0.07)" , background : "#161618" } },
[ React . createElement ( "span" , { key : "lbl" , style : { fontSize : "12px" , color : "#8a8a92" , fontWeight : 600 , marginRight : "4px" } }, "Disappear after" )]. concat (
timerDefs . map (( d ) => React . createElement ( "button" , { key : "td" + d . v , onClick : () => pickTimer ( d . v ), style : optStyle ( disappearTtl === d . v ) }, d . label ))
)
);
const onceRow = showOnce && React . createElement (
"div" ,
{ key : "once-row" , style : { display : "flex" , flexWrap : "wrap" , alignItems : "center" , gap : "8px" , padding : "10px 12px" , marginBottom : "10px" , borderRadius : "11px" , border : "1px solid rgba(255,255,255,0.07)" , background : "#161618" } },
[ React . createElement ( "span" , { key : "lbl" , style : { fontSize : "12px" , color : "#8a8a92" , fontWeight : 600 , marginRight : "4px" } }, "Visible for" )]. concat (
onceDefs . map (( d ) => React . createElement ( "button" , { key : "od" + d . v , onClick : () => pickOnce ( d . v ), style : optStyle ( onceSelected === d . v ) }, d . label ))
)
);
const filePanel = showFileTransfer && React . createElement (
"div" ,
{ key : "file-panel" , style : { marginBottom : "10px" } },
React . createElement ( window . FileTransferComponent || (() => React . createElement ( "div" , { style : { padding : "16px" , textAlign : "center" , color : "#e5727a" } }, "FileTransferComponent not loaded" )), {
webrtcManager ,
isConnected : isFileTransferReady (),
pendingIncomingFiles ,
onIncomingDecision ,
showDropzone : fileSendMode
})
);
const chipsRow = React . createElement ( "div" , { key : "chips" , style : { display : "flex" , alignItems : "center" , justifyContent : "space-between" , flexWrap : "wrap" , gap : "8px" , marginBottom : "10px" } }, [
React . createElement ( "button" , { key : "files" , onClick : () => {
if ( showFileTransfer && fileSendMode ) {
setShowFileTransfer ( false );
setFileSendMode ( false );
} else {
setShowFileTransfer ( true );
setFileSendMode ( true );
}
}, className : "sb-chip" , style : chipStyle ( showFileTransfer && fileSendMode ) }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-paperclip" , style : { fontSize : "13px" } }),
showFileTransfer && fileSendMode ? "Hide files" : "Send files"
]),
React . createElement ( "div" , { key : "right" , style : { display : "flex" , alignItems : "center" , gap : "6px" , flexWrap : "wrap" } }, [
React . createElement ( "button" , { key : "code" , onClick : () => setCodeMode (( v ) => ! v ), className : "sb-chip" , style : chipStyle ( codeMode ) }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-code" , style : { fontSize : "13px" } }),
"Code"
]),
React . createElement ( "button" , { key : "once" , onClick : () => {
setShowOnce (( v ) => ! v );
setShowTimer ( false );
}, className : "sb-chip" , style : chipStyle ( showOnce || viewOnceMode ) }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-eye-slash" , style : { fontSize : "13px" } }),
viewOnceMode ? "View once \xB7 " + fmtShort ( viewOnceTtl ) : "View once"
]),
React . createElement ( "button" , { key : "timer" , onClick : () => {
setShowTimer (( v ) => ! v );
setShowOnce ( false );
}, className : "sb-chip" , style : chipStyle ( showTimer || disappearTtl > 0 ) }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-stopwatch" , style : { fontSize : "13px" } }),
disappearTtl > 0 ? "Timer \xB7 " + fmtShort ( disappearTtl ) : "Timer"
])
])
]);
const codeStrip = codeMode && React . createElement ( "div" , { key : "code-strip" , style : { display : "flex" , alignItems : "center" , gap : "8px" , padding : "8px 14px" , border : "1px solid rgba(255,255,255,0.08)" , borderBottom : "none" , borderRadius : "14px 14px 0 0" , background : "#161618" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-code" , style : { color : "#8a8a92" , fontSize : "12px" } }),
React . createElement ( "span" , { key : "s" , style : { fontSize : "11.5px" , fontWeight : 600 , color : "#8a8a92" } }, "Code snippet \xB7 formatting preserved \xB7 \u2318\u21B5 to send" ),
React . createElement ( "button" , { key : "c" , onClick : () => setCodeMode ( false ), className : "sb-link" , style : { marginLeft : "auto" , background : "none" , border : "none" , color : "#6b6b73" , cursor : "pointer" , fontSize : "11.5px" , fontFamily : "inherit" , fontWeight : 600 } }, "Close" )
]);
const inputRow = React . createElement ( "div" , {
key : "input" ,
style : { display : "flex" , alignItems : "flex-end" , gap : "11px" , padding : "11px 11px 11px 16px" , border : "1px solid " + ( hasText ? "rgba(255,255,255,0.18)" : "rgba(255,255,255,0.08)" ), background : "#161618" , borderRadius : codeMode ? "0 0 14px 14px" : "14px" , transition : "border .15s" }
}, [
React . createElement ( "div" , { key : "ta-wrap" , style : { flex : 1 , minWidth : 0 } }, [
React . createElement ( "textarea" , {
key : "ta" ,
value : messageInput ,
ref : taRef ,
onChange : ( e ) => setMessageInput ( e . target . value ),
onKeyDown : handleKeyPress ,
rows : 1 ,
maxLength : 2e3 ,
placeholder : codeMode ? "Paste or write code\u2026" : "Type an encrypted message\u2026" ,
className : "sb-textarea" ,
style : { width : "100%" , minHeight : codeMode ? "120px" : "22px" , maxHeight : "240px" , resize : "none" , border : "none" , outline : "none" , background : "transparent" , color : "#e8e8eb" , fontFamily : codeMode ? MONO : "inherit" , fontSize : codeMode ? "13px" : "14.5px" , lineHeight : 1.55 , padding : "6px 0" }
}),
React . createElement ( "div" , { key : "foot" , style : { display : "flex" , alignItems : "center" , gap : "12px" , marginTop : "3px" } }, [
React . createElement ( "span" , { key : "enc" , style : { display : "inline-flex" , alignItems : "center" , gap : "5px" , fontSize : "11px" , color : "#56565e" } }, [
React . createElement ( "i" , { key : "i" , className : "fas fa-lock" , style : { color : "#3ecf8e" , fontSize : "10px" } }),
"Encrypted on your device"
]),
React . createElement ( "span" , { key : "cnt" , style : { fontFamily : MONO , fontSize : "10.5px" , color : "#56565e" , marginLeft : "auto" } }, ( messageInput ? messageInput . length : 0 ) + "/2000" )
])
]),
React . createElement ( "button" , {
key : "send" ,
onClick : onSendMessage ,
disabled : ! hasText ,
title : "Send" ,
className : "sb-send" ,
style : { flex : "none" , width : "44px" , height : "44px" , borderRadius : "11px" , border : "none" , display : "grid" , placeItems : "center" , cursor : hasText ? "pointer" : "default" , background : hasText ? "#f0892a" : "rgba(255,255,255,0.05)" , color : hasText ? "#1a0f04" : "#56565e" , transition : "all .15s" }
}, React . createElement ( "i" , { className : "fas fa-paper-plane" , style : { fontSize : "15px" } }))
]);
const composer = React . createElement (
"footer" ,
{ key : "composer" , style : { flex : "none" , padding : "12px 20px 18px" , background : "#0f0f11" , borderTop : "1px solid rgba(255,255,255,0.05)" } },
React . createElement ( "div" , { style : { maxWidth : "1000px" , margin : "0 auto" } }, [
timerRow ,
onceRow ,
filePanel ,
chipsRow ,
codeStrip ,
inputRow
])
2025-09-08 16:04:58 -04:00
);
2026-06-23 16:52:30 -04:00
const scrollBtn = showScrollButton && React . createElement ( "button" , {
key : "scrollbtn" ,
onClick : handleScrollToBottom ,
style : { position : "fixed" , right : "24px" , bottom : "150px" , width : "44px" , height : "44px" , borderRadius : "50%" , border : "1px solid rgba(255,255,255,0.1)" , background : "#26262b" , color : "#cfcfd4" , display : "grid" , placeItems : "center" , cursor : "pointer" , zIndex : 50 , boxShadow : "0 6px 20px rgba(0,0,0,0.4)" }
}, React . createElement ( "i" , { className : "fas fa-arrow-down" , style : { fontSize : "15px" } }));
const chatHeader = React . createElement ( SecureBitChatHeader , {
key : "chat-header" ,
status ,
onDisconnect ,
2026-06-26 00:00:13 -04:00
webrtcManager ,
title ,
isOffline ,
peerPresence ,
onRenameTitle
2026-06-23 16:52:30 -04:00
});
return React . createElement ( "div" , {
className : "chat-container" ,
style : { display : "flex" , flexDirection : "column" , height : "100vh" , background : "#0f0f11" , color : "#e8e8eb" }
}, [ chatHeader , messagesArea , scrollBtn , composer ]);
2025-09-08 16:04:58 -04:00
};
2026-06-26 00:00:13 -04:00
var buildSessionMessage = ( message , type , opts = {}) => ({
message ,
type ,
id : Date . now () + Math . random (),
timestamp : typeof opts . timestamp === "number" ? opts . timestamp : Date . now (),
mid : opts . mid ,
status : opts . status ,
viewOnce : opts . viewOnce === true ,
viewOnceTtl : typeof opts . viewOnceTtl === "number" ? opts . viewOnceTtl : 15 ,
expiresAt : typeof opts . expiresAt === "number" ? opts . expiresAt : void 0
});
var SB_SVG = {
chevL : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>' ,
chevR : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>' ,
plus : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>' ,
users : '<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 17.5V19"/><circle cx="10" cy="8" r="3.2"/><path d="M20 19v-1.5a3.5 3.5 0 0 0-2.6-3.4"/><path d="M15.5 5.3a3.2 3.2 0 0 1 0 5.4"/></svg>' ,
burger : '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg>'
};
var SessionsSidebar = ({ chats , collapsed , drawerOpen , onToggleCollapse , onSelect , onNewChat , onRename , onCloseDrawer , myStatus , onSetStatus }) => {
const h = React . createElement ;
const [ editingId , setEditingId ] = React . useState ( null );
const [ draft , setDraft ] = React . useState ( "" );
const [ presenceOpen , setPresenceOpen ] = React . useState ( false );
const startEdit = ( c ) => ( e ) => {
e . stopPropagation ();
setEditingId ( c . id );
setDraft ( c . name );
};
const commitEdit = () => {
if ( editingId ) {
onRename ( editingId , draft );
setEditingId ( null );
}
};
const editKey = ( e ) => {
if ( e . key === "Enter" ) {
e . preventDefault ();
commitEdit ();
} else if ( e . key === "Escape" ) {
setEditingId ( null );
}
};
const renameInput = ( extra = {}) => h ( "input" , {
autoFocus : true ,
value : draft ,
onChange : ( e ) => setDraft ( e . target . value ),
onKeyDown : editKey ,
onBlur : commitEdit ,
onClick : ( e ) => e . stopPropagation (),
style : Object . assign ({ width : "100%" , background : "rgba(255,255,255,0.06)" , border : "1px solid rgba(240,137,42,0.5)" , borderRadius : "6px" , color : "#f4f4f6" , fontFamily : "inherit" , fontSize : "14px" , fontWeight : 700 , padding : "2px 6px" , outline : "none" }, extra )
});
const icon = ( svg , style ) => h ( "span" , { style : Object . assign ({ display : "grid" , placeItems : "center" }, style || {}), dangerouslySetInnerHTML : { __html : svg } });
const avatar = ( c , size , ring ) => h ( "div" , {
style : { position : "relative" , flex : "none" , width : size + "px" , height : size + "px" , borderRadius : ( size >= 44 ? 12 : 11 ) + "px" , display : "grid" , placeItems : "center" , background : c . active ? "rgba(255,255,255,0.06)" : "rgba(255,255,255,0.035)" , border : "1px solid rgba(255,255,255," + ( c . active ? "0.14" : "0.07" ) + ")" , fontSize : "13px" , fontWeight : 700 , letterSpacing : "-0.3px" , color : c . active ? "#f4f4f6" : "#9a9aa2" }
}, [ c . mono , h ( "span" , { key : "dot" , style : { position : "absolute" , right : "-2px" , bottom : "-2px" , width : "11px" , height : "11px" , borderRadius : "50%" , background : c . dot , border : "2px solid " + ring } })]);
const expandedRow = ( c ) => h ( "div" , {
key : c . id ,
onClick : () => onSelect ( c . id ),
style : { position : "relative" , display : "flex" , alignItems : "center" , gap : "12px" , padding : "11px 12px" , marginBottom : "4px" , borderRadius : "11px" , background : c . active ? "#161618" : "transparent" , border : "1px solid " + ( c . active ? "rgba(255,255,255,0.08)" : "transparent" ), cursor : "pointer" }
}, [
c . active && h ( "span" , { key : "bar" , style : { position : "absolute" , left : 0 , top : "12px" , bottom : "12px" , width : "3px" , borderRadius : "0 3px 3px 0" , background : "#f0892a" } }),
avatar ( c , 38 , c . active ? "#161618" : "#0c0c0e" ),
h ( "div" , { key : "body" , style : { flex : 1 , minWidth : 0 } }, [
h ( "div" , { key : "top" , style : { display : "flex" , alignItems : "center" , gap : "7px" } }, [
editingId === c . id ? renameInput () : h ( "span" , {
key : "name" ,
onDoubleClick : startEdit ( c ),
title : "Double-click to rename" ,
style : { flex : 1 , minWidth : 0 , fontSize : "14px" , fontWeight : c . active ? 700 : 600 , letterSpacing : "-0.2px" , color : c . active ? "#f4f4f6" : "#cfcfd4" , whiteSpace : "nowrap" , overflow : "hidden" , textOverflow : "ellipsis" }
}, c . name ),
c . unread && editingId !== c . id && h ( "span" , { key : "u" , style : { flex : "none" , minWidth : "18px" , height : "18px" , padding : "0 5px" , borderRadius : "9px" , display : "grid" , placeItems : "center" , background : "#f0892a" , color : "#1a0f04" , fontFamily : "'JetBrains Mono',monospace" , fontSize : "10px" , fontWeight : 700 } }, c . unread )
]),
h ( "div" , { key : "prev" , style : { fontSize : "12px" , color : c . active ? "#8a8a92" : "#6b6b73" , whiteSpace : "nowrap" , overflow : "hidden" , textOverflow : "ellipsis" } }, c . preview )
])
]);
const dockItem = ( c ) => h ( "div" , { key : c . id , style : { position : "relative" } }, [
c . active && h ( "span" , { key : "bar" , style : { position : "absolute" , left : "-13px" , top : "9px" , bottom : "9px" , width : "3px" , borderRadius : "0 3px 3px 0" , background : "#f0892a" } }),
h ( "div" , {
key : "tile" ,
onClick : () => onSelect ( c . id ),
title : c . name ,
style : { position : "relative" , width : "44px" , height : "44px" , borderRadius : "12px" , display : "grid" , placeItems : "center" , cursor : "pointer" , background : c . active ? "rgba(255,255,255,0.06)" : "rgba(255,255,255,0.03)" , border : "1px solid rgba(255,255,255," + ( c . active ? "0.14" : "0.07" ) + ")" , fontSize : "13px" , fontWeight : 700 , letterSpacing : "-0.3px" , color : c . active ? "#f4f4f6" : "#9a9aa2" }
}, [
c . mono ,
h ( "span" , { key : "dot" , style : { position : "absolute" , right : "-2px" , bottom : "-2px" , width : "11px" , height : "11px" , borderRadius : "50%" , background : c . dot , border : "2.5px solid #0c0c0e" } }),
c . unread && h ( "span" , { key : "u" , style : { position : "absolute" , left : "-5px" , top : "-5px" , minWidth : "17px" , height : "17px" , padding : "0 4px" , borderRadius : "9px" , display : "grid" , placeItems : "center" , background : "#f0892a" , color : "#1a0f04" , fontFamily : "'JetBrains Mono',monospace" , fontSize : "9.5px" , fontWeight : 700 , border : "2px solid #0c0c0e" } }, c . unread )
])
]);
const brandMark = ( size ) => h (
"div" ,
{ style : { width : size + "px" , height : size + "px" , flex : "none" , display : "grid" , placeItems : "center" } },
h ( "img" , { src : "/logo/securebit-mark.svg" , alt : "SecureBit" , style : { width : "100%" , height : "100%" , objectFit : "contain" , display : "block" } })
);
const collapseBtn = ( svg , title ) => h ( "button" , { onClick : onToggleCollapse , title , style : { width : "30px" , height : "30px" , borderRadius : "8px" , display : "grid" , placeItems : "center" , border : "1px solid rgba(255,255,255,0.07)" , background : "transparent" , color : "#8a8a92" , cursor : "pointer" }, dangerouslySetInnerHTML : { __html : svg } });
const myMeta = MY_STATUS_OPTIONS . find (( o ) => o . key === myStatus ) || MY_STATUS_OPTIONS [ 0 ];
const PRES_SVG = {
user : '<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20v-1.5a4 4 0 0 0-4-4h-4a4 4 0 0 0-4 4V20"/><circle cx="12" cy="8" r="3.6"/></svg>' ,
check : '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#f0892a" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4 10-11"/></svg>' ,
chevUp : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#6b6b73" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6"/></svg>' ,
lock : '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 11V7a5 5 0 0 1 10 0v4"/><rect x="4.5" y="11" width="15" height="9" rx="2.2"/></svg>'
};
const presenceMenu = ( pos ) => presenceOpen ? h ( "div" , {
key : "pmenu" ,
style : Object . assign ({ position : "absolute" , zIndex : 30 , borderRadius : "14px" , background : "#161618" , border : "1px solid rgba(255,255,255,0.1)" , boxShadow : "0 16px 40px rgba(0,0,0,0.55)" , padding : "6px" }, pos )
}, [
h ( "div" , { key : "h" , style : { padding : "9px 10px 7px" , fontFamily : "'JetBrains Mono',monospace" , fontSize : "10px" , fontWeight : 600 , color : "#56565e" , textTransform : "uppercase" , letterSpacing : "1.2px" } }, "Set your status" ),
... MY_STATUS_OPTIONS . map (( o ) => h ( "button" , {
key : o . key ,
onClick : () => {
onSetStatus ( o . key );
setPresenceOpen ( false );
},
style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "11px" , padding : "9px 10px" , borderRadius : "9px" , border : "none" , background : "transparent" , cursor : "pointer" , textAlign : "left" }
}, [
h ( "span" , { key : "d" , style : { flex : "none" , width : "10px" , height : "10px" , borderRadius : "50%" , background : o . dot } }),
h ( "span" , { key : "t" , style : { flex : 1 , minWidth : 0 } }, [
h ( "span" , { key : "w" , style : { display : "block" , fontSize : "13.5px" , fontWeight : 600 , color : "#e8e8eb" } }, o . word ),
h ( "span" , { key : "de" , style : { display : "block" , fontSize : "11.5px" , color : "#6b6b73" } }, o . desc )
]),
o . key === myStatus && h ( "span" , { key : "c" , style : { flex : "none" , display : "grid" , placeItems : "center" }, dangerouslySetInnerHTML : { __html : PRES_SVG . check } })
])),
h ( "div" , { key : "note" , style : { display : "flex" , alignItems : "flex-start" , gap : "8px" , margin : "6px 6px 4px" , padding : "9px 10px" , borderRadius : "9px" , background : "rgba(62,207,142,0.06)" , border : "1px solid rgba(62,207,142,0.16)" } }, [
h ( "span" , { key : "i" , style : { flex : "none" , marginTop : "1px" , display : "grid" }, dangerouslySetInnerHTML : { __html : PRES_SVG . lock } }),
h ( "span" , { key : "t" , style : { fontSize : "11px" , lineHeight : 1.45 , color : "#8a8a92" } }, "Sent end-to-end to connected peers only \u2014 never stored on a server." )
])
]) : null ;
const presencePanelExpanded = h ( "div" , { key : "you" , style : { flex : "none" , position : "relative" , marginTop : "10px" , borderTop : "1px solid rgba(255,255,255,0.06)" , padding : "10px 12px 12px" } }, [
presenceMenu ({ left : "12px" , right : "12px" , bottom : "64px" }),
h ( "button" , { key : "btn" , onClick : () => setPresenceOpen (( v ) => ! v ), style : { width : "100%" , display : "flex" , alignItems : "center" , gap : "11px" , padding : "7px 8px" , borderRadius : "11px" , border : "1px solid rgba(255,255,255,0.06)" , background : "rgba(255,255,255,0.02)" , cursor : "pointer" } }, [
h ( "div" , { key : "av" , style : { position : "relative" , flex : "none" , width : "36px" , height : "36px" , borderRadius : "10px" , display : "grid" , placeItems : "center" , background : "rgba(240,137,42,0.12)" , border : "1px solid rgba(240,137,42,0.24)" , color : "#f0892a" } }, [
h ( "span" , { key : "i" , style : { display : "grid" }, dangerouslySetInnerHTML : { __html : PRES_SVG . user } }),
h ( "span" , { key : "dot" , style : { position : "absolute" , right : "-2px" , bottom : "-2px" , width : "11px" , height : "11px" , borderRadius : "50%" , background : myMeta . dot , border : "2px solid #0c0c0e" } })
]),
h ( "div" , { key : "tx" , style : { flex : 1 , minWidth : 0 , textAlign : "left" } }, [
h ( "div" , { key : "y" , style : { fontSize : "13.5px" , fontWeight : 700 , color : "#f4f4f6" } }, "You" ),
h ( "div" , { key : "w" , style : { fontSize : "12px" , color : "#8a8a92" } }, myMeta . word )
]),
h ( "span" , { key : "ch" , style : { display : "grid" , placeItems : "center" }, dangerouslySetInnerHTML : { __html : PRES_SVG . chevUp } })
])
]);
const presencePanelCollapsed = h ( "div" , { key : "you" , style : { flex : "none" , position : "relative" , display : "flex" , flexDirection : "column" , alignItems : "center" , padding : "0 0 13px" } }, [
presenceMenu ({ left : "60px" , bottom : "8px" , width : "248px" }),
h ( "button" , { key : "btn" , onClick : () => setPresenceOpen (( v ) => ! v ), title : "Your status \u2014 " + myMeta . word , style : { position : "relative" , width : "44px" , height : "44px" , borderRadius : "12px" , display : "grid" , placeItems : "center" , cursor : "pointer" , background : "rgba(240,137,42,0.12)" , border : "1px solid rgba(240,137,42,0.24)" , color : "#f0892a" } }, [
h ( "span" , { key : "i" , style : { display : "grid" }, dangerouslySetInnerHTML : { __html : PRES_SVG . user } }),
h ( "span" , { key : "dot" , style : { position : "absolute" , right : "-2px" , bottom : "-2px" , width : "12px" , height : "12px" , borderRadius : "50%" , background : myMeta . dot , border : "2.5px solid #0c0c0e" } })
])
]);
const expandedInner = [
h ( "div" , { key : "head" , style : { flex : "none" , display : "flex" , alignItems : "center" , justifyContent : "space-between" , padding : "0 12px 0 16px" , height : "64px" , borderBottom : "1px solid rgba(255,255,255,0.06)" } }, [
h ( "div" , { key : "brand" , style : { display : "flex" , alignItems : "center" , gap : "10px" } }, [ brandMark ( 30 ), h ( "span" , { key : "t" , style : { fontSize : "15px" , fontWeight : 800 , letterSpacing : "-0.3px" , color : "#f4f4f6" } }, "SecureBit" )]),
collapseBtn ( SB_SVG . chevL , "Collapse" )
]),
h ( "div" , { key : "label" , style : { flex : "none" , display : "flex" , alignItems : "center" , justifyContent : "space-between" , padding : "16px 16px 9px" } }, [
h ( "span" , { key : "l" , style : { fontFamily : "'JetBrains Mono',monospace" , fontSize : "10px" , fontWeight : 600 , color : "#56565e" , textTransform : "uppercase" , letterSpacing : "1.3px" } }, "Chats" ),
h ( "span" , { key : "c" , style : { fontFamily : "'JetBrains Mono',monospace" , fontSize : "10px" , fontWeight : 600 , color : "#6b6b73" } }, String ( chats . length ))
]),
h ( "div" , { key : "list" , className : "msc-scroll" , style : { flex : 1 , overflowY : "auto" , padding : "0 10px" } }, [
... chats . map ( expandedRow ),
h ( "div" , { key : "gh" , style : { marginTop : "14px" , padding : "0 2px 6px" } }, h ( "span" , { style : { fontFamily : "'JetBrains Mono',monospace" , fontSize : "10px" , fontWeight : 600 , color : "#56565e" , textTransform : "uppercase" , letterSpacing : "1.3px" } }, "Group chats" )),
h ( "div" , { key : "gph" , title : "Coming in v6.0" , style : { display : "flex" , alignItems : "center" , gap : "12px" , padding : "11px 12px" , borderRadius : "11px" , background : "transparent" , border : "1px dashed rgba(255,255,255,0.09)" , cursor : "not-allowed" } }, [
h ( "div" , { key : "i" , style : { flex : "none" , width : "38px" , height : "38px" , borderRadius : "11px" , display : "grid" , placeItems : "center" , background : "rgba(255,255,255,0.025)" , border : "1px solid rgba(255,255,255,0.06)" , color : "#56565e" }, dangerouslySetInnerHTML : { __html : SB_SVG . users } }),
h ( "div" , { key : "b" , style : { flex : 1 , minWidth : 0 } }, [
h ( "div" , { key : "t" , style : { fontSize : "14px" , fontWeight : 600 , color : "#8a8a92" } }, "Group chats" ),
h ( "div" , { key : "s" , style : { fontSize : "11.5px" , color : "#56565e" } }, "Up to 8 peers \xB7 P2P mesh" )
]),
h ( "span" , { key : "soon" , style : { flex : "none" , padding : "4px 9px" , borderRadius : "7px" , background : "rgba(240,137,42,0.1)" , border : "1px solid rgba(240,137,42,0.24)" , fontFamily : "'JetBrains Mono',monospace" , fontSize : "9.5px" , fontWeight : 700 , color : "#f0892a" , textTransform : "uppercase" , letterSpacing : "0.8px" } }, "Soon" )
])
]),
h ( "div" , { key : "new" , style : { flex : "none" , padding : "12px" } }, h ( "button" , {
onClick : onNewChat ,
style : { width : "100%" , display : "inline-flex" , alignItems : "center" , justifyContent : "center" , gap : "9px" , padding : "12px" , borderRadius : "11px" , border : "none" , background : "#f0892a" , color : "#1a0f04" , fontFamily : "inherit" , fontSize : "14px" , fontWeight : 700 , cursor : "pointer" , boxShadow : "0 8px 24px rgba(240,137,42,0.28)" }
}, [ icon ( SB_SVG . plus , { key : "p" }), "New chat" ])),
presencePanelExpanded
];
const collapsedInner = [
h ( "div" , { key : "head" , style : { flex : "none" , display : "flex" , flexDirection : "column" , alignItems : "center" , gap : "10px" , padding : "13px 0" , width : "100%" , borderBottom : "1px solid rgba(255,255,255,0.06)" } }, [ brandMark ( 32 ), collapseBtn ( SB_SVG . chevR , "Expand" )]),
h ( "div" , { key : "list" , className : "msc-scroll" , style : { flex : 1 , overflowY : "auto" , display : "flex" , flexDirection : "column" , alignItems : "center" , gap : "10px" , padding : "14px 0" , width : "100%" } }, [
... chats . map ( dockItem ),
h ( "div" , { key : "sep" , style : { width : "30px" , height : "1px" , background : "rgba(255,255,255,0.07)" , margin : "2px 0" } }),
h ( "div" , { key : "gph" , title : "Group chats \u2014 coming in v6.0" , style : { position : "relative" , width : "44px" , height : "44px" , borderRadius : "12px" , display : "grid" , placeItems : "center" , cursor : "not-allowed" , background : "transparent" , border : "1px dashed rgba(255,255,255,0.1)" , color : "#56565e" }, dangerouslySetInnerHTML : { __html : SB_SVG . users } })
]),
h ( "div" , { key : "new" , style : { flex : "none" , padding : "13px 0" } }, h ( "button" , {
onClick : onNewChat ,
title : "New chat" ,
style : { width : "44px" , height : "44px" , borderRadius : "12px" , display : "grid" , placeItems : "center" , border : "none" , background : "#f0892a" , color : "#1a0f04" , cursor : "pointer" , boxShadow : "0 8px 24px rgba(240,137,42,0.28)" },
dangerouslySetInnerHTML : { __html : SB_SVG . plus }
})),
presencePanelCollapsed
];
const railWidth = collapsed ? "72px" : "292px" ;
const railStyle = { flex : "none" , width : railWidth , display : "flex" , flexDirection : "column" , alignItems : collapsed ? "center" : "stretch" , background : "#0c0c0e" , borderRight : "1px solid rgba(255,255,255,0.06)" };
const inner = collapsed ? collapsedInner : expandedInner ;
return h ( React . Fragment , null , [
// Responsive behaviour (inline styles can't express media queries).
h ( "style" , { key : "css" , dangerouslySetInnerHTML : { __html : "@media (max-width:1023px){.sb-rail{display:none !important;}.sb-burger{display:grid !important;}}@media (min-width:1024px){.sb-drawer-overlay{display:none !important;}}" } }),
// Desktop rail
h ( "aside" , { key : "rail" , className : "sb-rail" , style : railStyle }, inner ),
// Mobile drawer overlay
h ( "div" , {
key : "drawer" ,
className : "sb-drawer-overlay" ,
onClick : onCloseDrawer ,
style : { position : "fixed" , inset : 0 , zIndex : 60 , background : "rgba(6,6,8,0.6)" , backdropFilter : "blur(4px)" , WebkitBackdropFilter : "blur(4px)" , display : drawerOpen ? "block" : "none" }
}, h ( "aside" , { onClick : ( e ) => e . stopPropagation (), style : { position : "absolute" , left : 0 , top : 0 , bottom : 0 , width : "292px" , display : "flex" , flexDirection : "column" , background : "#0c0c0e" , borderRight : "1px solid rgba(255,255,255,0.06)" , boxShadow : "0 0 60px rgba(0,0,0,0.6)" } }, expandedInner ))
]);
};
2025-09-08 16:04:58 -04:00
var EnhancedSecureP2PChat = () => {
2026-06-26 00:00:13 -04:00
const [ sessionsState , dispatch ] = React . useReducer ( sessionsReducer , void 0 , createInitialState );
const activeSessionId = sessionsState . activeSessionId ;
const activeIdRef = React . useRef ( null );
activeIdRef . current = activeSessionId ;
const active = activeSessionId ? sessionsState . sessions [ activeSessionId ] : null ;
const EMPTY_ARR = React . useRef ([]). current ;
const managersRef = React . useRef ( /* @__PURE__ */ new Map ());
const integrationsRef = React . useRef ( /* @__PURE__ */ new Map ());
const queuesRef = React . useRef ( /* @__PURE__ */ new Map ());
const dispatchActive = React . useCallback (( build ) => {
const id = activeIdRef . current ;
if ( ! id ) return ;
dispatch ( build ( id ));
}, []);
const messages = active ? active . messages : EMPTY_ARR ;
const setMessages = React . useCallback (( updaterOrArr ) => {
const id = activeIdRef . current ;
if ( ! id ) return ;
if ( typeof updaterOrArr === "function" ) dispatch ({ type : SESSION_ACTIONS . SET_MESSAGES , id , updater : updaterOrArr });
else dispatch ({ type : SESSION_ACTIONS . SET_MESSAGES , id , messages : updaterOrArr });
}, []);
const connectionStatus = active ? active . status : "disconnected" ;
const setConnectionStatus = React . useCallback (( status ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_STATUS , id , status })), [ dispatchActive ]);
const keyFingerprint = active ? active . keyFingerprint : "" ;
const setKeyFingerprint = React . useCallback (( fingerprint ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_FINGERPRINT , id , fingerprint })), [ dispatchActive ]);
const verificationCode = active ? active . verificationCode : "" ;
const setVerificationCode = React . useCallback (( code ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_VERIFICATION , id , code })), [ dispatchActive ]);
const isVerified = active ? active . sas . isVerified : false ;
const setIsVerified = React . useCallback (( v ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { isVerified : !! v } })), [ dispatchActive ]);
const localVerificationConfirmed = active ? active . sas . localConfirmed : false ;
const setLocalVerificationConfirmed = React . useCallback (( v ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { localConfirmed : !! v } })), [ dispatchActive ]);
const remoteVerificationConfirmed = active ? active . sas . remoteConfirmed : false ;
const setRemoteVerificationConfirmed = React . useCallback (( v ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { remoteConfirmed : !! v } })), [ dispatchActive ]);
const bothVerificationsConfirmed = active ? active . sas . bothConfirmed : false ;
const setBothVerificationsConfirmed = React . useCallback (( v ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { bothConfirmed : !! v } })), [ dispatchActive ]);
const pendingIncomingFiles = active ? active . pendingIncomingFiles : EMPTY_ARR ;
const setPendingIncomingFiles = React . useCallback (( updaterOrArr ) => {
const id = activeIdRef . current ;
if ( ! id ) return ;
if ( typeof updaterOrArr === "function" ) dispatch ({ type : SESSION_ACTIONS . SET_PENDING_FILES , id , updater : updaterOrArr });
else dispatch ({ type : SESSION_ACTIONS . SET_PENDING_FILES , id , files : updaterOrArr });
}, []);
const setupField = ( name , fallback ) => active ? active . setup [ name ] : fallback ;
const setSetupField = ( name ) => React . useCallback (( value ) => dispatchActive (( id ) => ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { [ name ] : value } })), [ dispatchActive ]);
const offerData = setupField ( "offerData" , "" );
const setOfferData = setSetupField ( "offerData" );
const answerData = setupField ( "answerData" , "" );
const setAnswerData = setSetupField ( "answerData" );
const offerInput = setupField ( "offerInput" , "" );
const setOfferInput = setSetupField ( "offerInput" );
const answerInput = setupField ( "answerInput" , "" );
const setAnswerInput = setSetupField ( "answerInput" );
const showOfferStep = setupField ( "showOfferStep" , false );
const setShowOfferStep = setSetupField ( "showOfferStep" );
const showAnswerStep = setupField ( "showAnswerStep" , false );
const setShowAnswerStep = setSetupField ( "showAnswerStep" );
const showVerification = setupField ( "showVerification" , false );
const setShowVerification = setSetupField ( "showVerification" );
const showQRCode = setupField ( "showQRCode" , false );
const setShowQRCode = setSetupField ( "showQRCode" );
const qrCodeUrl = setupField ( "qrCodeUrl" , "" );
const setQrCodeUrl = setSetupField ( "qrCodeUrl" );
const isGeneratingKeys = setupField ( "isGeneratingKeys" , false );
const setIsGeneratingKeys = setSetupField ( "isGeneratingKeys" );
const webrtcManagerRef = React . useMemo (() => ({
get current () {
return managersRef . current . get ( activeIdRef . current ) || null ;
},
set current ( v ) {
const id = activeIdRef . current ;
if ( ! id ) return ;
if ( v ) managersRef . current . set ( id , v );
else managersRef . current . delete ( id );
}
}), []);
const notificationIntegrationRef = React . useMemo (() => ({
get current () {
return integrationsRef . current . get ( activeIdRef . current ) || null ;
},
set current ( v ) {
const id = activeIdRef . current ;
if ( ! id ) return ;
if ( v ) integrationsRef . current . set ( id , v );
else integrationsRef . current . delete ( id );
}
}), []);
const [ myStatus , setMyStatusState ] = React . useState (() => {
try {
return localStorage . getItem ( "securebit_my_status" ) || "available" ;
} catch {
return "available" ;
}
});
const myStatusRef = React . useRef ( myStatus );
myStatusRef . current = myStatus ;
const wirePresence = ( s ) => s === "invisible" ? "offline" : s ;
const sendPresenceTo = React . useCallback (( mgr , s ) => {
if ( ! mgr || typeof mgr . sendMessage !== "function" ) return ;
try {
if ( mgr . isConnected && mgr . isConnected ()) {
const p = mgr . sendMessage ( JSON . stringify ({ type : "presence" , status : wirePresence ( s ) }));
if ( p && typeof p . catch === "function" ) p . catch (() => {
});
}
} catch ( _ ) {
}
}, []);
const setMyStatus = React . useCallback (( key ) => {
setMyStatusState ( key );
try {
localStorage . setItem ( "securebit_my_status" , key );
} catch {
}
for ( const mgr of managersRef . current . values ()) sendPresenceTo ( mgr , key );
}, [ sendPresenceTo ]);
2026-06-18 21:15:43 -04:00
const [ codeMode , setCodeMode ] = React . useState ( false );
const [ viewOnceMode , setViewOnceMode ] = React . useState ( false );
2026-06-19 02:58:03 -04:00
const [ viewOnceTtl , setViewOnceTtl ] = React . useState ( 15 );
2026-06-18 21:15:43 -04:00
const [ disappearTtl , setDisappearTtl ] = React . useState ( 0 );
const [ nowTick , setNowTick ] = React . useState (() => Date . now ());
2026-06-23 16:52:30 -04:00
const [ isOffline , setIsOffline ] = React . useState ( typeof navigator !== "undefined" && navigator . onLine === false );
const offlineRef = React . useRef ( isOffline );
React . useEffect (() => {
offlineRef . current = isOffline ;
}, [ isOffline ]);
React . useEffect (() => {
const goOffline = () => setIsOffline ( true );
const goOnline = () => setIsOffline ( false );
window . addEventListener ( "offline" , goOffline );
window . addEventListener ( "online" , goOnline );
return () => {
window . removeEventListener ( "offline" , goOffline );
window . removeEventListener ( "online" , goOnline );
};
}, []);
2026-05-17 14:48:52 -04:00
const [ relayOnlyMode , setRelayOnlyMode ] = React . useState (() => {
try {
return localStorage . getItem ( "securebit_relay_only_mode" ) === "true" ;
} catch {
return false ;
}
});
2026-06-15 16:05:31 -04:00
const [ customIceServers , setCustomIceServers ] = React . useState ( null );
const [ iceServersText , setIceServersText ] = React . useState ( "" );
const [ iceSettingsPersisted , setIceSettingsPersisted ] = React . useState ( false );
const [ showIceSettings , setShowIceSettings ] = React . useState ( false );
2026-06-15 15:39:13 -04:00
React . useEffect (() => {
let cancelled = false ;
loadIceSettings (). then (( saved ) => {
if ( cancelled || ! saved ) return ;
if ( Array . isArray ( saved . servers ) && saved . servers . length > 0 ) {
setCustomIceServers ( saved . servers );
setIceServersText ( JSON . stringify ( saved . servers , null , 2 ));
}
if ( saved . privacyMode === "relay-only" ) {
setRelayOnlyMode ( true );
}
setIceSettingsPersisted ( true );
}). catch (() => {
});
return () => {
cancelled = true ;
};
}, []);
2026-06-15 16:05:31 -04:00
React . useEffect (() => {
const open = () => setShowIceSettings ( true );
window . addEventListener ( "securebit:open-network-settings" , open );
return () => window . removeEventListener ( "securebit:open-network-settings" , open );
}, []);
const handleApplyIceSettings = React . useCallback (( next , persist ) => {
2026-06-15 15:39:13 -04:00
const servers = next . useCustom && Array . isArray ( next . servers ) ? next . servers : null ;
setCustomIceServers ( servers && servers . length ? servers : null );
setIceServersText ( next . serversText || "" );
setRelayOnlyMode ( next . privacyMode === "relay-only" );
2026-06-15 16:05:31 -04:00
setShowIceSettings ( false );
2026-06-15 15:39:13 -04:00
if ( persist ) {
setIceSettingsPersisted ( true );
saveIceSettings ({ servers : servers || [], privacyMode : next . privacyMode }). catch (() => {
});
2026-06-15 16:05:31 -04:00
} else if ( iceSettingsPersisted ) {
2026-06-15 15:39:13 -04:00
setIceSettingsPersisted ( false );
clearIceSettings (). catch (() => {
});
}
2026-06-15 16:05:31 -04:00
}, [ iceSettingsPersisted ]);
const handleForgetIceSettings = React . useCallback ( async () => {
2026-06-15 15:39:13 -04:00
await clearIceSettings (). catch (() => {
});
setIceSettingsPersisted ( false );
setCustomIceServers ( null );
setIceServersText ( "" );
}, []);
2025-09-08 16:04:58 -04:00
const [ messageInput , setMessageInput ] = React . useState ( "" );
2025-09-23 20:01:02 -04:00
const [ showQRScanner , setShowQRScanner ] = React . useState ( false );
const [ showQRScannerModal , setShowQRScannerModal ] = React . useState ( false );
2025-09-08 16:04:58 -04:00
const [ securityLevel , setSecurityLevel ] = React . useState ( null );
2025-10-14 22:51:48 -04:00
const [ sessionTimeLeft , setSessionTimeLeft ] = React . useState ( 0 );
2025-09-08 16:04:58 -04:00
const [ pendingSession , setPendingSession ] = React . useState ( null );
2025-09-23 20:01:02 -04:00
const [ connectionState , setConnectionState ] = React . useState ({
status : "disconnected" ,
hasActiveAnswer : false ,
answerCreatedAt : null ,
isUserInitiatedDisconnect : false
});
const updateConnectionState = ( newState , options = {}) => {
const { preserveAnswer = false , isUserAction = false } = options ;
setConnectionState (( prev ) => ({
... prev ,
... newState ,
isUserInitiatedDisconnect : isUserAction ,
hasActiveAnswer : preserveAnswer ? prev . hasActiveAnswer : false ,
answerCreatedAt : preserveAnswer ? prev . answerCreatedAt : null
}));
};
const shouldPreserveAnswerData = () => {
2026-05-19 09:49:22 -04:00
const hasAnswerData = !! answerData || answerInput && typeof answerInput === "string" && answerInput . trim (). length > 0 ;
const hasAnswerQR = qrCodeUrl && typeof qrCodeUrl === "string" && qrCodeUrl . trim (). length > 0 ;
const shouldPreserve = connectionState . hasActiveAnswer && ! connectionState . isUserInitiatedDisconnect || hasAnswerData && ! connectionState . isUserInitiatedDisconnect || hasAnswerQR && ! connectionState . isUserInitiatedDisconnect ;
2025-09-23 20:01:02 -04:00
return shouldPreserve ;
};
2025-10-02 01:43:32 -04:00
const markAnswerCreated = () => {
2025-09-23 20:01:02 -04:00
updateConnectionState ({
hasActiveAnswer : true ,
answerCreatedAt : Date . now ()
});
};
2025-09-08 16:04:58 -04:00
React . useEffect (() => {
2026-05-19 09:49:22 -04:00
return installDebugWindowHooks ({
targetWindow : window ,
webrtcManagerRef ,
onClearData : handleClearData
});
2025-09-23 20:01:02 -04:00
}, []);
2026-06-18 20:37:50 -04:00
const addMessageWithAutoScroll = React . useCallback (( message , type , opts = {}) => {
2025-09-08 16:04:58 -04:00
const newMessage = {
message ,
type ,
id : Date . now () + Math . random (),
2026-06-23 16:52:30 -04:00
timestamp : typeof opts . timestamp === "number" ? opts . timestamp : Date . now (),
2026-06-18 20:37:50 -04:00
mid : opts . mid ,
2026-06-23 16:52:30 -04:00
status : opts . status ,
// WhatsApp-style: sending | sent | delivered | failed
2026-06-18 20:37:50 -04:00
viewOnce : opts . viewOnce === true ,
2026-06-19 02:58:03 -04:00
viewOnceTtl : typeof opts . viewOnceTtl === "number" ? opts . viewOnceTtl : 15 ,
2026-06-18 20:37:50 -04:00
expiresAt : typeof opts . expiresAt === "number" ? opts . expiresAt : void 0
2025-09-08 16:04:58 -04:00
};
setMessages (( prev ) => {
const updated = [... prev , newMessage ];
setTimeout (() => {
if ( chatMessagesRef ? . current ) {
const container = chatMessagesRef . current ;
try {
const { scrollTop , scrollHeight , clientHeight } = container ;
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100 ;
if ( isNearBottom || prev . length === 0 ) {
requestAnimationFrame (() => {
if ( container && container . scrollTo ) {
container . scrollTo ({
top : container . scrollHeight ,
behavior : "smooth"
});
}
});
}
} catch ( error ) {
console . warn ( "Scroll error:" , error );
container . scrollTop = container . scrollHeight ;
}
}
}, 50 );
return updated ;
});
}, []);
2026-06-23 16:52:30 -04:00
const updateMessageStatus = React . useCallback (( mid , status ) => {
if ( ! mid ) return ;
setMessages (( prev ) => prev . map (( m ) => String ( m . mid ) === String ( mid ) && m . type === "sent" ? { ... m , status } : m ));
}, []);
const flushOfflineQueues = React . useCallback (() => {
2026-06-26 00:00:13 -04:00
for ( const [ id , q ] of queuesRef . current . entries ()) {
const mgr = managersRef . current . get ( id );
const out = q . outgoing ;
q . outgoing = [];
for ( const item of out ) {
const send = mgr ? . sendMessage ? .( item . outText , item . meta );
if ( send && typeof send . then === "function" ) {
send . then (() => dispatch ({ type : SESSION_ACTIONS . UPDATE_MESSAGE_STATUS , id , mid : item . mid , status : "delivered" })). catch (() => dispatch ({ type : SESSION_ACTIONS . UPDATE_MESSAGE_STATUS , id , mid : item . mid , status : "failed" }));
}
}
const inc = q . incoming ;
q . incoming = [];
if ( inc . length > 0 ) {
dispatch ({ type : SESSION_ACTIONS . ADD_MESSAGE , id , message : buildSessionMessage (
`Connection restored \u2014 ${ inc . length } message ${ inc . length === 1 ? "" : "s" } received while you were offline.` ,
"notice"
) });
}
const viewing = id === activeIdRef . current && ( typeof document === "undefined" || document . visibilityState === "visible" );
for ( const item of inc ) {
dispatch ({ type : SESSION_ACTIONS . ADD_MESSAGE , id , message : buildSessionMessage ( item . message , item . type , item . opts ) });
if ( item . opts && item . opts . mid && item . type === "received" ) {
if ( viewing ) {
try {
mgr ? . sendDeliveryReceipt ? .( item . opts . mid );
} catch ( _ ) {
}
} else if ( q . pendingReadAcks ) q . pendingReadAcks . push ( item . opts . mid );
2026-06-23 16:52:30 -04:00
}
}
}
2026-06-26 00:00:13 -04:00
}, []);
2026-06-23 16:52:30 -04:00
React . useEffect (() => {
if ( isOffline ) return ;
flushOfflineQueues ();
}, [ isOffline , flushOfflineQueues ]);
2025-09-08 16:04:58 -04:00
const updateSecurityLevel = React . useCallback ( async () => {
if ( window . isUpdatingSecurity ) {
return ;
}
window . isUpdatingSecurity = true ;
try {
2026-05-19 09:49:22 -04:00
if ( webrtcManagerRef . current ) {
2025-09-23 20:01:02 -04:00
setSecurityLevel ({
level : "MAXIMUM" ,
score : 100 ,
color : "green" ,
details : "All security features enabled by default" ,
passedChecks : 10 ,
totalChecks : 10 ,
isRealData : true
});
2025-09-08 16:04:58 -04:00
if ( window . DEBUG_MODE ) {
2026-05-19 09:49:22 -04:00
const currentLevel = webrtcManagerRef . current . ecdhKeyPair && webrtcManagerRef . current . ecdsaKeyPair ? await webrtcManagerRef . current . calculateSecurityLevel () : {
2025-09-23 20:01:02 -04:00
level : "MAXIMUM" ,
score : 100 ,
sessionType : "premium" ,
passedChecks : 10 ,
totalChecks : 10
};
2025-09-08 16:04:58 -04:00
}
}
} catch ( error ) {
console . error ( "Failed to update security level:" , error );
setSecurityLevel ({
level : "ERROR" ,
score : 0 ,
color : "red" ,
details : "Verification failed"
});
} finally {
setTimeout (() => {
window . isUpdatingSecurity = false ;
}, 2e3 );
}
}, []);
const chatMessagesRef = React . useRef ( null );
2025-09-23 20:01:02 -04:00
const scrollToBottom = createScrollToBottomFunction ( chatMessagesRef );
2026-05-17 14:48:52 -04:00
React . useEffect (() => {
try {
localStorage . setItem ( "securebit_relay_only_mode" , String ( relayOnlyMode ));
} catch {
}
2026-05-19 09:49:22 -04:00
if ( webrtcManagerRef . current ? . _config ? . webrtc ) {
webrtcManagerRef . current . _setRelayOnlyMode ( relayOnlyMode );
2026-05-17 14:48:52 -04:00
}
}, [ relayOnlyMode ]);
2025-09-23 20:01:02 -04:00
React . useEffect (() => {
if ( messages . length > 0 && chatMessagesRef . current ) {
scrollToBottom ();
setTimeout ( scrollToBottom , 50 );
setTimeout ( scrollToBottom , 150 );
2025-09-08 16:04:58 -04:00
}
2025-09-23 20:01:02 -04:00
}, [ messages ]);
2026-06-26 00:00:13 -04:00
const anyExpiring = sessionsState . order . some (( id ) => ( sessionsState . sessions [ id ] ? . messages || []). some (( m ) => typeof m . expiresAt === "number" ));
const sessionsStateRef = React . useRef ( sessionsState );
sessionsStateRef . current = sessionsState ;
2026-06-18 20:37:50 -04:00
React . useEffect (() => {
2026-06-26 00:00:13 -04:00
if ( ! anyExpiring ) return ;
const expireFn = ( prev ) => {
const now = Date . now ();
let changed = false ;
const next = prev . map (( m ) => {
if ( typeof m . expiresAt === "number" && m . expiresAt <= now && ! m . expired ) {
changed = true ;
return { ... m , expired : true , message : "" , expiresAt : void 0 };
}
return m ;
});
return changed ? next : prev ;
};
2026-06-18 20:37:50 -04:00
const interval = setInterval (() => {
const now = Date . now ();
setNowTick ( now );
2026-06-26 00:00:13 -04:00
const st = sessionsStateRef . current ;
for ( const id of st . order ) {
const msgs = st . sessions [ id ] ? . messages || [];
if ( msgs . some (( m ) => typeof m . expiresAt === "number" && m . expiresAt <= now && ! m . expired )) {
dispatch ({ type : SESSION_ACTIONS . SET_MESSAGES , id , updater : expireFn });
}
}
2026-06-18 20:37:50 -04:00
}, 1e3 );
return () => clearInterval ( interval );
2026-06-26 00:00:13 -04:00
}, [ anyExpiring ]);
const createSession = ( opts = {}) => {
const role = opts . role || "offer" ;
const entry = createSessionEntry ({ role });
const id = entry . id ;
dispatch ({ type : SESSION_ACTIONS . CREATE_SESSION , entry , activate : opts . activate !== false });
queuesRef . current . set ( id , { incoming : [], outgoing : [], pendingReadAcks : [] });
const setMessages2 = ( u ) => {
if ( typeof u === "function" ) dispatch ({ type : SESSION_ACTIONS . SET_MESSAGES , id , updater : u });
else dispatch ({ type : SESSION_ACTIONS . SET_MESSAGES , id , messages : u });
};
const addMessageWithAutoScroll2 = ( message , type , opts2 = {}) => {
dispatch ({ type : SESSION_ACTIONS . ADD_MESSAGE , id , message : buildSessionMessage ( message , type , opts2 ) });
if ( type === "received" && id !== activeIdRef . current ) {
dispatch ({ type : SESSION_ACTIONS . INCREMENT_UNREAD , id });
}
};
const updateMessageStatus2 = ( mid , status ) => {
if ( mid ) dispatch ({ type : SESSION_ACTIONS . UPDATE_MESSAGE_STATUS , id , mid , status });
};
const setConnectionStatus2 = ( status ) => dispatch ({ type : SESSION_ACTIONS . SET_STATUS , id , status });
const setKeyFingerprint2 = ( fingerprint ) => dispatch ({ type : SESSION_ACTIONS . SET_FINGERPRINT , id , fingerprint });
const setVerificationCode2 = ( code ) => dispatch ({ type : SESSION_ACTIONS . SET_VERIFICATION , id , code });
const setIsVerified2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { isVerified : !! v } });
const setLocalVerificationConfirmed2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { localConfirmed : !! v } });
const setRemoteVerificationConfirmed2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { remoteConfirmed : !! v } });
const setBothVerificationsConfirmed2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . SET_SAS , id , sas : { bothConfirmed : !! v } });
const setShowVerification2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { showVerification : !! v } });
const setShowOfferStep2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { showOfferStep : !! v } });
const setShowAnswerStep2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { showAnswerStep : !! v } });
const setShowQRCode2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { showQRCode : !! v } });
const setQrCodeUrl2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { qrCodeUrl : v } });
const setOfferData2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { offerData : v } });
const setAnswerData2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { answerData : v } });
const setOfferInput2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { offerInput : v } });
const setAnswerInput2 = ( v ) => dispatch ({ type : SESSION_ACTIONS . PATCH_SETUP , id , patch : { answerInput : v } });
const setPendingIncomingFiles2 = ( u ) => {
if ( typeof u === "function" ) dispatch ({ type : SESSION_ACTIONS . SET_PENDING_FILES , id , updater : u });
else dispatch ({ type : SESSION_ACTIONS . SET_PENDING_FILES , id , files : u });
};
const sessionQueues = () => queuesRef . current . get ( id ) || { incoming : [], outgoing : [] };
2026-06-18 20:37:50 -04:00
const handleMessage = ( message , type , meta ) => {
2025-09-08 16:04:58 -04:00
if ( typeof message === "string" && message . trim (). startsWith ( "{" )) {
try {
const parsedMessage = JSON . parse ( message );
2026-06-26 00:00:13 -04:00
if ( parsedMessage . type === "presence" ) {
const st = parsedMessage . data && parsedMessage . data . status || parsedMessage . status ;
if ( st ) dispatch ({ type : SESSION_ACTIONS . SET_PEER_PRESENCE , id , presence : st });
return ;
}
2025-09-08 16:04:58 -04:00
const blockedTypes = [
"file_transfer_start" ,
"file_transfer_response" ,
"file_chunk" ,
"chunk_confirmation" ,
"file_transfer_complete" ,
"file_transfer_error" ,
"heartbeat" ,
"verification" ,
"verification_response" ,
"verification_confirmed" ,
"verification_both_confirmed" ,
"peer_disconnect" ,
"key_rotation_signal" ,
"key_rotation_ready" ,
2026-06-23 16:52:30 -04:00
"security_upgrade" ,
"message_delete" ,
"message_receipt"
2025-09-08 16:04:58 -04:00
];
if ( parsedMessage . type && blockedTypes . includes ( parsedMessage . type )) {
2025-10-02 01:43:32 -04:00
console . log ( `Blocked system/file message from chat: ${ parsedMessage . type } ` );
2025-09-08 16:04:58 -04:00
return ;
}
2026-06-26 00:00:13 -04:00
if ( parsedMessage . type === "message" && typeof parsedMessage . data === "string" ) {
message = parsedMessage . data ;
if ( parsedMessage . meta && typeof parsedMessage . meta === "object" ) meta = parsedMessage . meta ;
}
2025-09-08 16:04:58 -04:00
} catch ( parseError ) {
}
}
2026-06-26 00:00:13 -04:00
const opts2 = {};
2026-06-18 20:37:50 -04:00
if ( meta && typeof meta === "object" ) {
2026-06-26 00:00:13 -04:00
if ( typeof meta . mid === "string" ) opts2 . mid = meta . mid ;
2026-06-19 02:58:03 -04:00
if ( meta . once === true ) {
2026-06-26 00:00:13 -04:00
opts2 . viewOnce = true ;
opts2 . viewOnceTtl = Number . isFinite ( meta . onceTtl ) ? meta . onceTtl : 15 ;
2026-06-19 02:58:03 -04:00
}
2026-06-18 20:37:50 -04:00
if ( Number . isFinite ( meta . ttl ) && meta . ttl > 0 ) {
2026-06-26 00:00:13 -04:00
opts2 . expiresAt = Date . now () + meta . ttl * 1e3 ;
2026-06-18 20:37:50 -04:00
}
2026-06-26 00:00:13 -04:00
if ( Number . isFinite ( meta . ts )) opts2 . timestamp = meta . ts ;
2026-06-23 16:52:30 -04:00
}
if ( offlineRef . current && type === "received" ) {
2026-06-26 00:00:13 -04:00
sessionQueues (). incoming . push ({ message , type , opts : opts2 });
2026-06-23 16:52:30 -04:00
return ;
2026-06-18 20:37:50 -04:00
}
2026-06-26 00:00:13 -04:00
addMessageWithAutoScroll2 ( message , type , opts2 );
if ( opts2 . mid && type === "received" ) {
const beingViewed = id === activeIdRef . current && ( typeof document === "undefined" || document . visibilityState === "visible" );
if ( beingViewed ) {
try {
manager ? . sendDeliveryReceipt ? .( opts2 . mid );
} catch ( _ ) {
}
} else {
const q = sessionQueues ();
if ( q . pendingReadAcks ) q . pendingReadAcks . push ( opts2 . mid );
2026-06-23 16:52:30 -04:00
}
}
2025-09-08 16:04:58 -04:00
};
const handleStatusChange = ( status ) => {
2026-06-26 00:00:13 -04:00
setConnectionStatus2 ( status );
2025-09-08 16:04:58 -04:00
if ( status === "connected" ) {
document . dispatchEvent ( new CustomEvent ( "new-connection" ));
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} else if ( status === "verifying" ) {
2026-06-26 00:00:13 -04:00
setShowVerification2 ( true );
2025-09-08 16:04:58 -04:00
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} else if ( status === "verified" ) {
2026-06-26 00:00:13 -04:00
setIsVerified2 ( true );
setShowVerification2 ( false );
setBothVerificationsConfirmed2 ( true );
setConnectionStatus2 ( "connected" );
2025-09-23 20:01:02 -04:00
setTimeout (() => {
2026-06-26 00:00:13 -04:00
setIsVerified2 ( true );
2025-09-23 20:01:02 -04:00
}, 0 );
2026-06-26 00:00:13 -04:00
try {
const s = myStatusRef . current === "invisible" ? "offline" : myStatusRef . current ;
setTimeout (() => {
try {
const p = manager . sendMessage ? .( JSON . stringify ({ type : "presence" , status : s }));
if ( p && typeof p . catch === "function" ) p . catch (() => {
});
} catch ( _ ) {
}
}, 400 );
} catch ( _ ) {
}
2025-09-08 16:04:58 -04:00
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} else if ( status === "connecting" ) {
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} else if ( status === "disconnected" ) {
2026-06-26 00:00:13 -04:00
setConnectionStatus2 ( "disconnected" );
setIsVerified2 ( false );
setShowVerification2 ( false );
setLocalVerificationConfirmed2 ( false );
setRemoteVerificationConfirmed2 ( false );
setBothVerificationsConfirmed2 ( false );
if ( id === activeIdRef . current ) document . dispatchEvent ( new CustomEvent ( "disconnected" ));
2025-09-08 16:04:58 -04:00
} else if ( status === "peer_disconnected" ) {
2026-06-26 00:00:13 -04:00
if ( id === activeIdRef . current ) {
setSessionTimeLeft ( 0 );
document . dispatchEvent ( new CustomEvent ( "peer-disconnect" ));
}
setConnectionStatus2 ( "peer_disconnected" );
setIsVerified2 ( false );
setShowVerification2 ( false );
setLocalVerificationConfirmed2 ( false );
setRemoteVerificationConfirmed2 ( false );
setBothVerificationsConfirmed2 ( false );
2025-09-08 16:04:58 -04:00
}
};
const handleKeyExchange = ( fingerprint ) => {
if ( fingerprint === "" ) {
2026-06-26 00:00:13 -04:00
setKeyFingerprint2 ( "" );
2025-09-08 16:04:58 -04:00
} else {
2026-06-26 00:00:13 -04:00
setKeyFingerprint2 ( fingerprint );
2025-09-08 16:04:58 -04:00
}
};
const handleVerificationRequired = ( code ) => {
if ( code === "" ) {
2026-06-26 00:00:13 -04:00
setVerificationCode2 ( "" );
setShowVerification2 ( false );
2025-09-08 16:04:58 -04:00
} else {
2026-06-26 00:00:13 -04:00
setVerificationCode2 ( code );
setShowVerification2 ( true );
2025-09-08 16:04:58 -04:00
}
};
const handleVerificationStateChange = ( state ) => {
2026-06-26 00:00:13 -04:00
setLocalVerificationConfirmed2 ( state . localConfirmed );
setRemoteVerificationConfirmed2 ( state . remoteConfirmed );
setBothVerificationsConfirmed2 ( state . bothConfirmed );
2025-09-08 16:04:58 -04:00
};
const handleAnswerError = ( errorType , errorMessage ) => {
if ( errorType === "replay_attack" ) {
2025-09-23 20:01:02 -04:00
setSessionTimeLeft ( 0 );
2025-09-08 16:04:58 -04:00
setPendingSession ( null );
2026-06-26 00:00:13 -04:00
addMessageWithAutoScroll2 ( "\u{1F4A1} Data is outdated. Please create a new invitation or use a current response code." , "system" );
2025-09-08 16:04:58 -04:00
if ( typeof console . clear === "function" ) {
console . clear ();
}
} else if ( errorType === "security_violation" ) {
2025-09-23 20:01:02 -04:00
setSessionTimeLeft ( 0 );
2025-09-08 16:04:58 -04:00
setPendingSession ( null );
2026-06-26 00:00:13 -04:00
addMessageWithAutoScroll2 ( ` Security breach: ${ errorMessage } ` , "system" );
2025-09-08 16:04:58 -04:00
if ( typeof console . clear === "function" ) {
console . clear ();
}
}
};
if ( typeof console . clear === "function" ) {
console . clear ();
}
2026-06-26 00:00:13 -04:00
const manager = new EnhancedSecureWebRTCManager (
2025-09-08 16:04:58 -04:00
handleMessage ,
handleStatusChange ,
handleKeyExchange ,
handleVerificationRequired ,
handleAnswerError ,
2026-05-17 14:48:52 -04:00
handleVerificationStateChange ,
{
webrtc : {
relayOnly : relayOnlyMode ,
2026-06-15 15:39:13 -04:00
// Priority: user's custom servers > operator override > built-in defaults.
2026-06-15 16:05:31 -04:00
iceServers : Array . isArray ( customIceServers ) && customIceServers . length ? customIceServers : Array . isArray ( window . SECUREBIT_ICE_SERVERS ) ? window . SECUREBIT_ICE_SERVERS : void 0
2026-05-17 14:48:52 -04:00
}
}
2025-09-08 16:04:58 -04:00
);
2026-06-26 00:00:13 -04:00
managersRef . current . set ( id , manager );
manager . onMessageDelete = ( mid ) => {
2026-06-18 20:37:50 -04:00
if ( ! mid ) return ;
2026-06-26 00:00:13 -04:00
setMessages2 (( prev ) => prev . filter (( m ) => String ( m . mid ) !== String ( mid )));
2026-06-18 20:37:50 -04:00
};
2026-06-26 00:00:13 -04:00
manager . onMessageDelivered = ( mid ) => {
updateMessageStatus2 ( mid , "read" );
2026-06-23 16:52:30 -04:00
};
2026-06-26 00:00:13 -04:00
if ( typeof Notification !== "undefined" && Notification && Notification . permission === "granted" && window . NotificationIntegration && ! integrationsRef . current . get ( id )) {
2025-10-15 19:58:28 -04:00
try {
2026-06-26 00:00:13 -04:00
const integration = new window . NotificationIntegration ( manager );
2025-10-15 19:58:28 -04:00
integration . init (). then (() => {
2026-06-26 00:00:13 -04:00
integrationsRef . current . set ( id , integration );
2025-10-15 19:58:28 -04:00
}). catch (( error ) => {
});
} catch ( error ) {
}
}
2026-06-26 00:00:13 -04:00
handleMessage ( " SecureBit.chat Enhanced Security Edition v4.10.0 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks." , "system" );
manager . setFileTransferCallbacks (
// Progress callback
( progress ) => {
console . log ( "File progress:" , progress );
},
// File received callback — auto-save to disk, no button press needed.
( fileData ) => {
const sizeMb = Math . max ( 1 , Math . round (( fileData . fileSize || 0 ) / ( 1024 * 1024 )));
const saveToDisk = async () => {
const url = await fileData . getObjectURL ();
const a = document . createElement ( "a" );
a . href = url ;
a . download = fileData . fileName || "file" ;
document . body . appendChild ( a );
a . click ();
a . remove ();
setTimeout (() => fileData . revokeObjectURL ( url ), 15e3 );
};
saveToDisk (). then (() => {
addMessageWithAutoScroll2 ( `File received & saved: ${ fileData . fileName } ( ${ sizeMb } MB)` , "system" );
}). catch (( e ) => {
console . error ( "Auto-save failed:" , e );
addMessageWithAutoScroll2 ( `File received: ${ fileData . fileName } ( ${ sizeMb } MB). Open the file panel to download it.` , "system" );
});
},
// Error callback
( error ) => {
console . error ( "File transfer error:" , error );
if ( error . includes ( "Connection not ready" )) {
addMessageWithAutoScroll2 ( ` File transfer error: connection not ready. Try again later.` , "system" );
} else if ( error . includes ( "File too large" )) {
addMessageWithAutoScroll2 ( ` File is too big. Maximum size: 100 MB` , "system" );
} else {
addMessageWithAutoScroll2 ( ` File transfer error: ${ error } ` , "system" );
}
},
// Incoming file request callback — receiver must explicitly accept before any data is sent
( fileRequest ) => {
setPendingIncomingFiles2 (( prev ) => {
if ( prev . some (( f ) => f . fileId === fileRequest . fileId )) return prev ;
return [... prev , fileRequest ];
});
}
);
return id ;
};
const createSessionRef = React . useRef ( createSession );
createSessionRef . current = createSession ;
const destroyingRef = React . useRef ( /* @__PURE__ */ new Set ());
const destroySession = React . useCallback (( id ) => {
if ( ! id || destroyingRef . current . has ( id )) return ;
destroyingRef . current . add ( id );
try {
const mgr = managersRef . current . get ( id );
if ( mgr ) {
try {
mgr . disconnect ();
} catch ( _ ) {
}
managersRef . current . delete ( id );
}
const integ = integrationsRef . current . get ( id );
if ( integ ) {
try {
integ . cleanup ? .();
} catch ( _ ) {
}
integrationsRef . current . delete ( id );
}
queuesRef . current . delete ( id );
dispatch ({ type : SESSION_ACTIONS . REMOVE_SESSION , id });
} finally {
destroyingRef . current . delete ( id );
}
}, []);
React . useEffect (() => {
if ( sessionsState . order . length === 0 ) createSessionRef . current ({ role : "offer" });
}, [ sessionsState . order . length ]);
const [ sidebarCollapsed , setSidebarCollapsed ] = React . useState (() => {
try {
return localStorage . getItem ( "securebit_sidebar_collapsed" ) === "true" ;
} catch {
return false ;
}
});
React . useEffect (() => {
try {
localStorage . setItem ( "securebit_sidebar_collapsed" , String ( sidebarCollapsed ));
} catch {
}
}, [ sidebarCollapsed ]);
const [ sidebarDrawerOpen , setSidebarDrawerOpen ] = React . useState ( false );
const handleSelectSession = React . useCallback (( id ) => {
dispatch ({ type : SESSION_ACTIONS . SET_ACTIVE , id });
dispatch ({ type : SESSION_ACTIONS . CLEAR_UNREAD , id });
setSidebarDrawerOpen ( false );
}, []);
const handleNewChat = React . useCallback (() => {
createSessionRef . current ({ role : "offer" });
setSidebarDrawerOpen ( false );
}, []);
const handleRenameSession = React . useCallback (( id , label ) => {
dispatch ({ type : SESSION_ACTIONS . RENAME , id , label });
}, []);
const flushReadAcks = React . useCallback (( id ) => {
if ( ! id ) return ;
const q = queuesRef . current . get ( id );
const mgr = managersRef . current . get ( id );
if ( ! q || ! mgr || ! q . pendingReadAcks || q . pendingReadAcks . length === 0 ) return ;
const acks = q . pendingReadAcks ;
q . pendingReadAcks = [];
for ( const mid of acks ) {
try {
mgr . sendDeliveryReceipt ? .( mid );
} catch ( _ ) {
}
}
}, []);
React . useEffect (() => {
if ( ! activeSessionId ) return ;
dispatch ({ type : SESSION_ACTIONS . CLEAR_UNREAD , id : activeSessionId });
if ( typeof document === "undefined" || document . visibilityState === "visible" ) flushReadAcks ( activeSessionId );
}, [ activeSessionId , flushReadAcks ]);
const didInitRef = React . useRef ( false );
React . useEffect (() => {
if ( didInitRef . current ) return ;
didInitRef . current = true ;
let isTabSwitching = false ;
let tabSwitchTimeout = null ;
2025-09-08 16:04:58 -04:00
const handleBeforeUnload = ( event ) => {
if ( event . type === "beforeunload" && ! isTabSwitching ) {
2026-06-26 00:00:13 -04:00
for ( const mgr of managersRef . current . values ()) {
2025-09-08 16:04:58 -04:00
try {
2026-06-26 00:00:13 -04:00
if ( mgr . isConnected && mgr . isConnected ()) {
try {
mgr . sendSystemMessage ({ type : "peer_disconnect" , reason : "user_disconnect" , timestamp : Date . now () });
} catch ( _ ) {
}
setTimeout (() => {
try {
mgr . disconnect ();
} catch ( _ ) {
}
}, 100 );
} else {
mgr . disconnect ();
2025-09-08 16:04:58 -04:00
}
2026-06-26 00:00:13 -04:00
} catch ( _ ) {
}
2025-09-08 16:04:58 -04:00
}
} else if ( isTabSwitching ) {
event . preventDefault ();
event . returnValue = "" ;
}
};
const handleVisibilityChange = () => {
if ( document . visibilityState === "hidden" ) {
isTabSwitching = true ;
2026-06-26 00:00:13 -04:00
if ( tabSwitchTimeout ) clearTimeout ( tabSwitchTimeout );
2025-09-08 16:04:58 -04:00
tabSwitchTimeout = setTimeout (() => {
isTabSwitching = false ;
}, 5e3 );
} else if ( document . visibilityState === "visible" ) {
isTabSwitching = false ;
if ( tabSwitchTimeout ) {
clearTimeout ( tabSwitchTimeout );
tabSwitchTimeout = null ;
}
2026-06-26 00:00:13 -04:00
flushReadAcks ( activeIdRef . current );
2025-09-08 16:04:58 -04:00
}
};
2026-06-26 00:00:13 -04:00
window . addEventListener ( "beforeunload" , handleBeforeUnload );
2025-09-08 16:04:58 -04:00
document . addEventListener ( "visibilitychange" , handleVisibilityChange );
return () => {
window . removeEventListener ( "beforeunload" , handleBeforeUnload );
document . removeEventListener ( "visibilitychange" , handleVisibilityChange );
if ( tabSwitchTimeout ) {
clearTimeout ( tabSwitchTimeout );
tabSwitchTimeout = null ;
}
2026-06-26 00:00:13 -04:00
for ( const mgr of managersRef . current . values ()) {
try {
mgr . disconnect ();
} catch ( _ ) {
}
2025-09-08 16:04:58 -04:00
}
2026-06-26 00:00:13 -04:00
managersRef . current . clear ();
for ( const integ of integrationsRef . current . values ()) {
try {
integ . cleanup ? .();
} catch ( _ ) {
}
}
integrationsRef . current . clear ();
queuesRef . current . clear ();
2025-09-08 16:04:58 -04:00
};
}, []);
2025-09-23 20:01:02 -04:00
const compressOfferData = ( offerData2 ) => {
try {
const offer = typeof offerData2 === "string" ? JSON . parse ( offerData2 ) : offerData2 ;
const minimalOffer = {
type : offer . type ,
version : offer . version ,
timestamp : offer . timestamp ,
sessionId : offer . sessionId ,
connectionId : offer . connectionId ,
verificationCode : offer . verificationCode ,
salt : offer . salt ,
// Use only key fingerprints instead of full keys
keyFingerprints : offer . keyFingerprints ,
// Add a reference to get full data
fullDataAvailable : true ,
compressionLevel : "minimal"
};
return JSON . stringify ( minimalOffer );
} catch ( error ) {
console . error ( "Error compressing offer data:" , error );
return offerData2 ;
}
};
const createQRReference = ( offerData2 ) => {
try {
const referenceId = `offer_ ${ Date . now () } _ ${ Math . random (). toString ( 36 ). substr ( 2 , 9 ) } ` ;
localStorage . setItem ( `qr_offer_ ${ referenceId } ` , JSON . stringify ( offerData2 ));
const qrReference = {
type : "secure_offer_reference" ,
referenceId ,
timestamp : Date . now (),
message : "Scan this QR code and use the reference ID to get full offer data"
};
return JSON . stringify ( qrReference );
} catch ( error ) {
console . error ( "Error creating QR reference:" , error );
return null ;
}
};
const createTemplateOffer = ( offer ) => {
const templateOffer = {
type : "enhanced_secure_offer_template" ,
version : "4.0" ,
sessionId : offer . sessionId ,
connectionId : offer . connectionId ,
verificationCode : offer . verificationCode ,
timestamp : offer . timestamp ,
// Avoid bulky fields (SDP, raw keys); keep only fingerprints and essentials
keyFingerprints : offer . keyFingerprints ,
// Keep concise auth hints (omit large nonces)
authChallenge : offer ? . authChallenge ? . challenge ,
// Optionally include a compact capability hint if small
capabilities : Array . isArray ( offer . capabilities ) && offer . capabilities . length <= 5 ? offer . capabilities : void 0
};
return templateOffer ;
};
const MAX_QR_LEN = 800 ;
2025-10-05 06:21:14 -04:00
const BIN_MAX_QR_LEN = 400 ;
2025-09-27 19:07:17 -04:00
const [ qrFramesTotal , setQrFramesTotal ] = React . useState ( 0 );
const [ qrFrameIndex , setQrFrameIndex ] = React . useState ( 0 );
const [ qrManualMode , setQrManualMode ] = React . useState ( false );
2025-09-23 20:01:02 -04:00
const qrAnimationRef = React . useRef ({ timer : null , chunks : [], idx : 0 , active : false });
2026-06-26 00:00:13 -04:00
React . useEffect (() => () => {
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
qrAnimationRef . current . timer = null ;
}
} catch {
}
}, [ activeSessionId ]);
2025-09-23 20:01:02 -04:00
const stopQrAnimation = () => {
try {
if ( qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks : [], idx : 0 , active : false };
setQrFrameIndex ( 0 );
setQrFramesTotal ( 0 );
2025-09-27 19:07:17 -04:00
setQrManualMode ( false );
};
2025-10-05 06:21:14 -04:00
const renderCurrent = async () => {
const { chunks , idx } = qrAnimationRef . current || {};
if ( ! chunks || ! chunks . length ) return ;
const current = chunks [ idx % chunks . length ];
try {
const isDesktop = typeof window !== "undefined" && ( window . innerWidth || 0 ) >= 1024 ;
const QR_SIZE = isDesktop ? 720 : 512 ;
const url = await ( window . generateQRCode ? window . generateQRCode ( current , { errorCorrectionLevel : "M" , margin : 2 , size : QR_SIZE }) : Promise . resolve ( "" ));
if ( url ) setQrCodeUrl ( url );
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Animated QR render error (current):" , e );
2025-10-05 06:21:14 -04:00
}
setQrFrameIndex (( qrAnimationRef . current ? . idx || 0 ) % ( qrAnimationRef . current ? . chunks ? . length || 1 ) + 1 );
};
const renderAndAdvance = async () => {
await renderCurrent ();
const len = qrAnimationRef . current ? . chunks ? . length || 0 ;
if ( len > 0 ) {
const nextIdx = (( qrAnimationRef . current ? . idx || 0 ) + 1 ) % len ;
qrAnimationRef . current . idx = nextIdx ;
setQrFrameIndex ( nextIdx + 1 );
}
};
2025-09-27 19:07:17 -04:00
const toggleQrManualMode = () => {
const newManualMode = ! qrManualMode ;
setQrManualMode ( newManualMode );
if ( newManualMode ) {
if ( qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
qrAnimationRef . current . timer = null ;
}
console . log ( "QR Manual mode enabled - auto-scroll stopped" );
} else {
2025-10-05 06:21:14 -04:00
if ( qrAnimationRef . current . chunks . length > 1 ) {
const intervalMs = 3e3 ;
qrAnimationRef . current . active = true ;
clearInterval ( qrAnimationRef . current . timer );
qrAnimationRef . current . timer = setInterval ( renderAndAdvance , intervalMs );
2025-09-27 19:07:17 -04:00
}
console . log ( "QR Manual mode disabled - auto-scroll resumed" );
}
};
2025-10-05 06:21:14 -04:00
const nextQrFrame = async () => {
2025-09-27 19:07:17 -04:00
console . log ( "\u{1F3AE} nextQrFrame called, qrFramesTotal:" , qrFramesTotal , "qrAnimationRef.current:" , qrAnimationRef . current );
if ( qrAnimationRef . current . chunks . length > 1 ) {
const nextIdx = ( qrAnimationRef . current . idx + 1 ) % qrAnimationRef . current . chunks . length ;
qrAnimationRef . current . idx = nextIdx ;
setQrFrameIndex ( nextIdx + 1 );
console . log ( "\u{1F3AE} Next frame index:" , nextIdx + 1 );
2025-10-05 06:21:14 -04:00
try {
clearInterval ( qrAnimationRef . current . timer );
} catch {
}
qrAnimationRef . current . timer = null ;
await renderCurrent ();
if ( ! qrManualMode && qrAnimationRef . current . chunks . length > 1 ) {
const intervalMs = 3e3 ;
qrAnimationRef . current . active = true ;
qrAnimationRef . current . timer = setInterval ( renderAndAdvance , intervalMs );
} else {
qrAnimationRef . current . active = false ;
}
2025-09-27 19:07:17 -04:00
} else {
console . log ( "\u{1F3AE} No multiple frames to navigate" );
}
};
2025-10-05 06:21:14 -04:00
const prevQrFrame = async () => {
2025-09-27 19:07:17 -04:00
console . log ( "\u{1F3AE} prevQrFrame called, qrFramesTotal:" , qrFramesTotal , "qrAnimationRef.current:" , qrAnimationRef . current );
if ( qrAnimationRef . current . chunks . length > 1 ) {
const prevIdx = ( qrAnimationRef . current . idx - 1 + qrAnimationRef . current . chunks . length ) % qrAnimationRef . current . chunks . length ;
qrAnimationRef . current . idx = prevIdx ;
setQrFrameIndex ( prevIdx + 1 );
console . log ( "\u{1F3AE} Previous frame index:" , prevIdx + 1 );
2025-10-05 06:21:14 -04:00
try {
clearInterval ( qrAnimationRef . current . timer );
} catch {
}
qrAnimationRef . current . timer = null ;
await renderCurrent ();
if ( ! qrManualMode && qrAnimationRef . current . chunks . length > 1 ) {
const intervalMs = 3e3 ;
qrAnimationRef . current . active = true ;
qrAnimationRef . current . timer = setInterval ( renderAndAdvance , intervalMs );
} else {
qrAnimationRef . current . active = false ;
}
2025-09-27 19:07:17 -04:00
} else {
console . log ( "\u{1F3AE} No multiple frames to navigate" );
}
2025-09-23 20:01:02 -04:00
};
const qrChunksBufferRef = React . useRef ({ id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] });
2025-10-05 06:21:14 -04:00
const generateQRCode = async ( data ) => {
2025-09-23 20:01:02 -04:00
try {
const originalSize = typeof data === "string" ? data . length : JSON . stringify ( data ). length ;
const isDesktop = typeof window !== "undefined" && ( window . innerWidth || 0 ) >= 1024 ;
const QR_SIZE = isDesktop ? 720 : 512 ;
2025-10-07 23:58:54 -04:00
if ( typeof window . generateBinaryQRCodeFromObject === "function" ) {
try {
const obj = typeof data === "string" ? JSON . parse ( data ) : data ;
const qrDataUrl = await window . generateBinaryQRCodeFromObject ( obj , { errorCorrectionLevel : "M" , size : QR_SIZE , margin : 2 });
if ( qrDataUrl ) {
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks : [], idx : 0 , active : false };
setQrFrameIndex ( 0 );
setQrFramesTotal ( 0 );
setQrManualMode ( false );
setQrCodeUrl ( qrDataUrl );
setQrFramesTotal ( 1 );
setQrFrameIndex ( 1 );
return ;
}
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Binary QR generation failed, falling back to compressed:" , e ? . message || e );
2025-10-07 23:58:54 -04:00
}
}
if ( typeof window . generateCompressedQRCode === "function" ) {
try {
const payload2 = typeof data === "string" ? data : JSON . stringify ( data );
const qrDataUrl = await window . generateCompressedQRCode ( payload2 , { errorCorrectionLevel : "M" , size : QR_SIZE , margin : 2 });
if ( qrDataUrl ) {
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks : [], idx : 0 , active : false };
setQrFrameIndex ( 0 );
setQrFramesTotal ( 0 );
setQrManualMode ( false );
setQrCodeUrl ( qrDataUrl );
setQrFramesTotal ( 1 );
setQrFrameIndex ( 1 );
return ;
}
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Compressed QR generation failed, falling back to plain:" , e ? . message || e );
2025-10-07 23:58:54 -04:00
}
}
const payload = typeof data === "string" ? data : JSON . stringify ( data );
2025-09-23 20:01:02 -04:00
if ( payload . length <= MAX_QR_LEN ) {
if ( ! window . generateQRCode ) throw new Error ( "QR code generator unavailable" );
2025-10-05 06:21:14 -04:00
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks : [], idx : 0 , active : false };
setQrFrameIndex ( 0 );
setQrFramesTotal ( 0 );
setQrManualMode ( false );
2025-09-23 20:01:02 -04:00
const qrDataUrl = await window . generateQRCode ( payload , { errorCorrectionLevel : "M" , size : QR_SIZE , margin : 2 });
setQrCodeUrl ( qrDataUrl );
setQrFramesTotal ( 1 );
setQrFrameIndex ( 1 );
return ;
}
2025-10-05 06:21:14 -04:00
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks : [], idx : 0 , active : false };
setQrFrameIndex ( 0 );
setQrFramesTotal ( 0 );
setQrManualMode ( false );
2025-09-23 20:01:02 -04:00
const id = `raw_ ${ Date . now () } _ ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` ;
2025-09-27 19:07:17 -04:00
const TARGET_CHUNKS = 10 ;
const FRAME_MAX = Math . max ( 200 , Math . floor ( payload . length / TARGET_CHUNKS ));
2025-09-23 20:01:02 -04:00
const total = Math . ceil ( payload . length / FRAME_MAX );
const rawChunks = [];
for ( let i = 0 ; i < total ; i ++ ) {
const seq = i + 1 ;
const part = payload . slice ( i * FRAME_MAX , ( i + 1 ) * FRAME_MAX );
rawChunks . push ( JSON . stringify ({ hdr : { v : 1 , id , seq , total , rt : "raw" }, body : part }));
}
if ( ! window . generateQRCode ) throw new Error ( "QR code generator unavailable" );
if ( rawChunks . length === 1 ) {
const url = await window . generateQRCode ( rawChunks [ 0 ], { errorCorrectionLevel : "M" , margin : 2 , size : QR_SIZE });
setQrCodeUrl ( url );
setQrFramesTotal ( 1 );
setQrFrameIndex ( 1 );
return ;
}
qrAnimationRef . current . chunks = rawChunks ;
qrAnimationRef . current . idx = 0 ;
qrAnimationRef . current . active = true ;
setQrFramesTotal ( rawChunks . length );
setQrFrameIndex ( 1 );
const EC_OPTS = { errorCorrectionLevel : "M" , margin : 2 , size : QR_SIZE };
2025-10-05 06:21:14 -04:00
await renderNext ();
2025-09-27 19:07:17 -04:00
if ( ! qrManualMode ) {
const intervalMs = 4e3 ;
2025-10-05 06:21:14 -04:00
qrAnimationRef . current . active = true ;
qrAnimationRef . current . timer = setInterval ( renderAndAdvance , intervalMs );
2025-09-27 19:07:17 -04:00
}
2025-09-23 20:01:02 -04:00
return ;
} catch ( error ) {
console . error ( "QR code generation failed:" , error );
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : ` QR code generation failed: ${ error . message } ` ,
2025-09-23 20:01:02 -04:00
type : "error"
}]);
}
};
const reconstructFromTemplate = ( templateData ) => {
const fullOffer = {
type : "enhanced_secure_offer" ,
version : templateData . version ,
timestamp : templateData . timestamp ,
sessionId : templateData . sessionId ,
connectionId : templateData . connectionId ,
verificationCode : templateData . verificationCode ,
salt : templateData . salt ,
sdp : templateData . sdp ,
keyFingerprints : templateData . keyFingerprints ,
capabilities : templateData . capabilities ,
// Reconstruct ECDH key object
ecdhPublicKey : {
keyType : "ECDH" ,
keyData : templateData . ecdhKeyData ,
timestamp : templateData . timestamp - 1e3 ,
// Approximate
version : templateData . version ,
signature : templateData . ecdhSignature
},
// Reconstruct ECDSA key object
ecdsaPublicKey : {
keyType : "ECDSA" ,
keyData : templateData . ecdsaKeyData ,
timestamp : templateData . timestamp - 999 ,
// Approximate
version : templateData . version ,
signature : templateData . ecdsaSignature
},
// Reconstruct auth challenge
authChallenge : {
challenge : templateData . authChallenge ,
timestamp : templateData . timestamp ,
nonce : templateData . authNonce ,
version : templateData . version
},
// Generate security level (can be recalculated)
securityLevel : {
level : "CRITICAL" ,
score : 20 ,
color : "red" ,
verificationResults : {
encryption : { passed : false , details : "Encryption not working" , points : 0 },
keyExchange : { passed : true , details : "Simple key exchange verified" , points : 15 },
messageIntegrity : { passed : false , details : "Message integrity failed" , points : 0 },
rateLimiting : { passed : true , details : "Rate limiting active" , points : 5 },
ecdsa : { passed : false , details : "Enhanced session required - feature not available" , points : 0 },
metadataProtection : { passed : false , details : "Enhanced session required - feature not available" , points : 0 },
pfs : { passed : false , details : "Enhanced session required - feature not available" , points : 0 },
nestedEncryption : { passed : false , details : "Enhanced session required - feature not available" , points : 0 },
packetPadding : { passed : false , details : "Enhanced session required - feature not available" , points : 0 },
advancedFeatures : { passed : false , details : "Premium session required - feature not available" , points : 0 }
},
timestamp : templateData . timestamp ,
details : "Real verification: 20/100 security checks passed (2/4 available)" ,
isRealData : true ,
passedChecks : 2 ,
totalChecks : 4 ,
sessionType : "demo" ,
maxPossibleScore : 50
}
};
return fullOffer ;
};
const handleQRScan = async ( scannedData ) => {
try {
2025-10-07 23:58:54 -04:00
console . log ( "QR Code scanned:" , scannedData . substring ( 0 , 100 ) + "..." );
console . log ( "Current buffer state:" , qrChunksBufferRef . current );
if ( scannedData . startsWith ( "SB1:bin:" ) || qrChunksBufferRef . current && qrChunksBufferRef . current . id ) {
console . log ( "Binary chunk detected:" , scannedData . substring ( 0 , 50 ) + "..." );
if ( ! qrChunksBufferRef . current . id ) {
console . log ( "Initializing buffer for binary chunks" );
qrChunksBufferRef . current = {
id : `bin_ ${ Date . now () } ` ,
total : 4 ,
// We expect 4 chunks
seen : /* @__PURE__ */ new Set (),
items : [],
lastUpdateMs : Date . now ()
};
}
const chunkHash = scannedData . substring ( 0 , 50 );
if ( qrChunksBufferRef . current . seen . has ( chunkHash )) {
console . log ( `Chunk already scanned, ignoring...` );
return Promise . resolve ( false );
}
qrChunksBufferRef . current . seen . add ( chunkHash );
qrChunksBufferRef . current . items . push ( scannedData );
qrChunksBufferRef . current . lastUpdateMs = Date . now ();
try {
const uniqueCount = qrChunksBufferRef . current . seen . size ;
document . dispatchEvent ( new CustomEvent ( "qr-scan-progress" , {
detail : {
id : qrChunksBufferRef . current . id ,
seq : uniqueCount ,
total : qrChunksBufferRef . current . total
}
}));
setQrFramesTotal ( qrChunksBufferRef . current . total );
setQrFrameIndex ( uniqueCount );
} catch {
}
const isComplete = qrChunksBufferRef . current . seen . size >= qrChunksBufferRef . current . total ;
console . log ( `Chunks collected: ${ qrChunksBufferRef . current . seen . size } / ${ qrChunksBufferRef . current . total } , complete: ${ isComplete } ` );
if ( ! isComplete ) {
console . log ( `Scanned chunk ${ qrChunksBufferRef . current . seen . size } / ${ qrChunksBufferRef . current . total } , waiting for more...` );
return Promise . resolve ( false );
}
try {
const fullBinaryData = qrChunksBufferRef . current . items . join ( "" );
if ( showOfferStep ) {
setAnswerInput ( fullBinaryData );
} else {
setOfferInput ( fullBinaryData );
}
setMessages (( prev ) => [... prev , {
message : "All binary chunks captured. Payload reconstructed." ,
type : "success"
}]);
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
setShowQRScannerModal ( false );
return Promise . resolve ( true );
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Binary chunks reconstruction failed:" , e );
2025-10-07 23:58:54 -04:00
return Promise . resolve ( false );
}
}
if ( scannedData . length > 100 && ! scannedData . startsWith ( "{" ) && ! scannedData . startsWith ( "[" )) {
console . log ( "Detected potential binary chunk (long non-JSON string):" , scannedData . substring ( 0 , 50 ) + "..." );
if ( ! qrChunksBufferRef . current . id ) {
console . log ( "Initializing buffer for potential binary chunks" );
qrChunksBufferRef . current = {
id : `bin_ ${ Date . now () } ` ,
total : 4 ,
// We expect 4 chunks
seen : /* @__PURE__ */ new Set (),
items : [],
lastUpdateMs : Date . now ()
};
}
const chunkHash = scannedData . substring ( 0 , 50 );
if ( qrChunksBufferRef . current . seen . has ( chunkHash )) {
console . log ( `Chunk already scanned, ignoring...` );
return Promise . resolve ( false );
}
qrChunksBufferRef . current . seen . add ( chunkHash );
qrChunksBufferRef . current . items . push ( scannedData );
qrChunksBufferRef . current . lastUpdateMs = Date . now ();
try {
const uniqueCount = qrChunksBufferRef . current . seen . size ;
document . dispatchEvent ( new CustomEvent ( "qr-scan-progress" , {
detail : {
id : qrChunksBufferRef . current . id ,
seq : uniqueCount ,
total : qrChunksBufferRef . current . total
}
}));
setQrFramesTotal ( qrChunksBufferRef . current . total );
setQrFrameIndex ( uniqueCount );
} catch {
}
const isComplete = qrChunksBufferRef . current . seen . size >= qrChunksBufferRef . current . total ;
console . log ( `Chunks collected: ${ qrChunksBufferRef . current . seen . size } / ${ qrChunksBufferRef . current . total } , complete: ${ isComplete } ` );
if ( ! isComplete ) {
console . log ( `Scanned chunk ${ qrChunksBufferRef . current . seen . size } / ${ qrChunksBufferRef . current . total } , waiting for more...` );
return Promise . resolve ( false );
}
try {
const fullBinaryData = qrChunksBufferRef . current . items . join ( "" );
if ( showOfferStep ) {
setAnswerInput ( fullBinaryData );
} else {
setOfferInput ( fullBinaryData );
}
setMessages (( prev ) => [... prev , {
message : "All binary chunks captured. Payload reconstructed." ,
type : "success"
}]);
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
setShowQRScannerModal ( false );
return Promise . resolve ( true );
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Binary chunks reconstruction failed:" , e );
2025-10-07 23:58:54 -04:00
return Promise . resolve ( false );
}
}
2025-10-05 06:21:14 -04:00
let parsedData ;
if ( typeof window . decodeAnyPayload === "function" ) {
const any = window . decodeAnyPayload ( scannedData );
if ( typeof any === "string" ) {
parsedData = JSON . parse ( any );
} else {
parsedData = any ;
}
} else {
const maybeDecompressed = typeof window . decompressIfNeeded === "function" ? window . decompressIfNeeded ( scannedData ) : scannedData ;
parsedData = JSON . parse ( maybeDecompressed );
}
2025-10-07 23:58:54 -04:00
console . log ( "Decoded data:" , parsedData );
2025-09-23 20:01:02 -04:00
if ( parsedData . hdr && parsedData . body ) {
const { hdr } = parsedData ;
if ( ! qrChunksBufferRef . current . id || qrChunksBufferRef . current . id !== hdr . id ) {
qrChunksBufferRef . current = { id : hdr . id , total : hdr . total || 1 , seen : /* @__PURE__ */ new Set (), items : [], lastUpdateMs : Date . now () };
try {
document . dispatchEvent ( new CustomEvent ( "qr-scan-progress" , { detail : { id : hdr . id , seq : 0 , total : hdr . total || 1 } }));
} catch {
}
}
if ( ! qrChunksBufferRef . current . seen . has ( hdr . seq )) {
qrChunksBufferRef . current . seen . add ( hdr . seq );
qrChunksBufferRef . current . items . push ( scannedData );
qrChunksBufferRef . current . lastUpdateMs = Date . now ();
}
try {
const uniqueCount = qrChunksBufferRef . current . seen . size ;
document . dispatchEvent ( new CustomEvent ( "qr-scan-progress" , { detail : { id : hdr . id , seq : uniqueCount , total : qrChunksBufferRef . current . total || hdr . total || 0 } }));
} catch {
}
const isComplete = qrChunksBufferRef . current . seen . size >= ( qrChunksBufferRef . current . total || 1 );
if ( ! isComplete ) {
return Promise . resolve ( false );
}
if ( hdr . rt === "raw" ) {
try {
const parts = qrChunksBufferRef . current . items . map (( s ) => JSON . parse ( s )). sort (( a , b ) => ( a . hdr . seq || 0 ) - ( b . hdr . seq || 0 )). map (( p ) => p . body || "" );
const fullText = parts . join ( "" );
const payloadObj = JSON . parse ( fullText );
if ( showOfferStep ) {
setAnswerInput ( JSON . stringify ( payloadObj , null , 2 ));
} else {
setOfferInput ( JSON . stringify ( payloadObj , null , 2 ));
}
2025-10-02 01:43:32 -04:00
setMessages (( prev ) => [... prev , { message : "All frames captured. RAW payload reconstructed." , type : "success" }]);
2025-09-23 20:01:02 -04:00
try {
document . dispatchEvent ( new CustomEvent ( "qr-scan-complete" , { detail : { id : hdr . id } }));
} catch {
}
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
setShowQRScannerModal ( false );
return Promise . resolve ( true );
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "RAW multi-frame reconstruction failed:" , e );
2025-09-23 20:01:02 -04:00
return Promise . resolve ( false );
}
2025-10-05 06:21:14 -04:00
} else if ( hdr . rt === "bin" ) {
try {
const parts = qrChunksBufferRef . current . items . map (( s ) => JSON . parse ( s )). sort (( a , b ) => ( a . hdr . seq || 0 ) - ( b . hdr . seq || 0 )). map (( p ) => p . body || "" );
const fullText = parts . join ( "" );
let payloadObj ;
if ( typeof window . decodeAnyPayload === "function" ) {
const any = window . decodeAnyPayload ( fullText );
payloadObj = typeof any === "string" ? JSON . parse ( any ) : any ;
} else {
payloadObj = JSON . parse ( fullText );
}
if ( showOfferStep ) {
setAnswerInput ( JSON . stringify ( payloadObj , null , 2 ));
} else {
setOfferInput ( JSON . stringify ( payloadObj , null , 2 ));
}
setMessages (( prev ) => [... prev , { message : "All frames captured. BIN payload reconstructed." , type : "success" }]);
try {
document . dispatchEvent ( new CustomEvent ( "qr-scan-complete" , { detail : { id : hdr . id } }));
} catch {
}
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
setShowQRScannerModal ( false );
return Promise . resolve ( true );
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "BIN multi-frame reconstruction failed:" , e );
2025-10-05 06:21:14 -04:00
return Promise . resolve ( false );
}
2025-09-23 20:01:02 -04:00
} else if ( window . receiveAndProcess ) {
try {
const results = await window . receiveAndProcess ( qrChunksBufferRef . current . items );
if ( results . length > 0 ) {
const { payloadObj } = results [ 0 ];
if ( showOfferStep ) {
setAnswerInput ( JSON . stringify ( payloadObj , null , 2 ));
} else {
setOfferInput ( JSON . stringify ( payloadObj , null , 2 ));
}
2025-10-02 01:43:32 -04:00
setMessages (( prev ) => [... prev , { message : "All frames captured. COSE payload reconstructed." , type : "success" }]);
2025-09-23 20:01:02 -04:00
try {
document . dispatchEvent ( new CustomEvent ( "qr-scan-complete" , { detail : { id : hdr . id } }));
} catch {
}
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
setShowQRScannerModal ( false );
return Promise . resolve ( true );
}
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "COSE multi-chunk processing failed:" , e );
2025-09-23 20:01:02 -04:00
}
return Promise . resolve ( false );
} else {
return Promise . resolve ( false );
}
}
if ( parsedData . type === "enhanced_secure_offer_template" ) {
console . log ( "QR scan: Template-based offer detected, reconstructing..." );
const fullOffer = reconstructFromTemplate ( parsedData );
if ( showOfferStep ) {
setAnswerInput ( JSON . stringify ( fullOffer , null , 2 ));
console . log ( "\u{1F4F1} Template data populated to answerInput (waiting for response mode)" );
} else {
setOfferInput ( JSON . stringify ( fullOffer , null , 2 ));
console . log ( "\u{1F4F1} Template data populated to offerInput (paste invitation mode)" );
}
setMessages (( prev ) => [... prev , {
message : "\u{1F4F1} QR code scanned successfully! Full offer reconstructed from template." ,
type : "success"
}]);
setShowQRScannerModal ( false );
return true ;
} else if ( parsedData . type === "secure_offer_reference" && parsedData . referenceId ) {
const fullOfferData = localStorage . getItem ( `qr_offer_ ${ parsedData . referenceId } ` );
if ( fullOfferData ) {
const fullOffer = JSON . parse ( fullOfferData );
if ( showOfferStep ) {
setAnswerInput ( JSON . stringify ( fullOffer , null , 2 ));
} else {
setOfferInput ( JSON . stringify ( fullOffer , null , 2 ));
}
setMessages (( prev ) => [... prev , {
message : "\u{1F4F1} QR code scanned successfully! Full offer data retrieved." ,
type : "success"
}]);
setShowQRScannerModal ( false );
return true ;
} else {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "QR code reference found but full data not available. Please use copy/paste." ,
2025-09-23 20:01:02 -04:00
type : "error"
}]);
return false ;
}
} else {
2025-10-05 06:21:14 -04:00
if ( ! parsedData . sdp && parsedData . type === "enhanced_secure_offer" ) {
2025-09-23 20:01:02 -04:00
setMessages (( prev ) => [... prev , {
2025-10-05 06:21:14 -04:00
message : "Compressed QR may omit SDP for brevity. Use copy/paste if connection fails." ,
2025-09-23 20:01:02 -04:00
type : "warning"
}]);
}
if ( showOfferStep ) {
console . log ( "QR scan: Populating answerInput with:" , parsedData );
setAnswerInput ( JSON . stringify ( parsedData , null , 2 ));
} else {
console . log ( "QR scan: Populating offerInput with:" , parsedData );
setOfferInput ( JSON . stringify ( parsedData , null , 2 ));
}
setMessages (( prev ) => [... prev , {
message : "\u{1F4F1} QR code scanned successfully!" ,
type : "success"
}]);
setShowQRScannerModal ( false );
return true ;
}
} catch ( error ) {
if ( showOfferStep ) {
setAnswerInput ( scannedData );
} else {
setOfferInput ( scannedData );
}
setMessages (( prev ) => [... prev , {
message : "\u{1F4F1} QR code scanned successfully!" ,
type : "success"
}]);
setShowQRScannerModal ( false );
return true ;
}
2025-09-08 16:04:58 -04:00
};
const handleCreateOffer = async () => {
try {
2026-05-19 09:49:22 -04:00
setIsGeneratingKeys ( true );
2025-09-08 16:04:58 -04:00
setOfferData ( "" );
setShowOfferStep ( false );
2025-09-23 20:01:02 -04:00
setShowQRCode ( false );
setQrCodeUrl ( "" );
2026-05-19 09:49:22 -04:00
const offer = await webrtcManagerRef . current . createSecureOffer ();
2025-09-08 16:04:58 -04:00
setOfferData ( offer );
setShowOfferStep ( true );
2025-09-23 20:01:02 -04:00
const offerString = typeof offer === "object" ? JSON . stringify ( offer ) : offer ;
2025-10-05 06:21:14 -04:00
try {
if ( typeof window . encodeBinaryToPrefixed === "function" ) {
const bin = window . encodeBinaryToPrefixed ( offerString );
2025-10-07 23:58:54 -04:00
const TARGET_CHUNKS = 4 ;
2025-10-08 01:24:04 -04:00
let total = TARGET_CHUNKS ;
let FRAME_MAX = Math . max ( 200 , Math . ceil ( bin . length / TARGET_CHUNKS ));
2025-10-05 06:21:14 -04:00
if ( FRAME_MAX <= 0 ) FRAME_MAX = 200 ;
2025-10-08 01:24:04 -04:00
if ( bin . length <= FRAME_MAX ) {
total = 1 ;
FRAME_MAX = bin . length ;
} else {
FRAME_MAX = Math . ceil ( bin . length / TARGET_CHUNKS );
total = TARGET_CHUNKS ;
2025-10-05 06:21:14 -04:00
}
2025-10-07 23:58:54 -04:00
const id = `bin_ ${ Date . now () } _ ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` ;
2025-10-05 06:21:14 -04:00
const chunks = [];
for ( let i = 0 ; i < total ; i ++ ) {
const seq = i + 1 ;
const part = bin . slice ( i * FRAME_MAX , ( i + 1 ) * FRAME_MAX );
2025-10-07 23:58:54 -04:00
chunks . push ( part );
2025-10-05 06:21:14 -04:00
}
const isDesktop = typeof window !== "undefined" && ( window . innerWidth || 0 ) >= 1024 ;
const QR_SIZE = isDesktop ? 720 : 512 ;
if ( window . generateQRCode && chunks . length > 0 ) {
const firstUrl = await window . generateQRCode ( chunks [ 0 ], { errorCorrectionLevel : "M" , size : QR_SIZE , margin : 2 });
if ( firstUrl ) setQrCodeUrl ( firstUrl );
}
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks , idx : 0 , active : true };
setQrFramesTotal ( chunks . length );
setQrFrameIndex ( 1 );
setQrManualMode ( false );
const intervalMs = 3e3 ;
qrAnimationRef . current . timer = setInterval ( renderAndAdvance , intervalMs );
try {
setShowQRCode ( true );
} catch {
}
2025-10-07 23:58:54 -04:00
} else {
await generateQRCode ( offer );
try {
setShowQRCode ( true );
} catch {
}
2025-10-05 06:21:14 -04:00
}
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Offer QR generation failed:" , e );
2025-10-05 06:21:14 -04:00
}
2025-09-08 16:04:58 -04:00
const existingMessages = messages . filter (
( m ) => m . type === "system" && ( m . message . includes ( "Secure invitation created" ) || m . message . includes ( "Send the encrypted code" ))
);
if ( existingMessages . length === 0 ) {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "Secure invitation created and encrypted!" ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
setMessages (( prev ) => [... prev , {
2026-06-26 00:00:13 -04:00
message : "Send the invitation code to your interlocutor via a secure channel (voice call, SMS, etc.)." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
}
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} catch ( error ) {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : `Error creating invitation: ${ error . message } ` ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
2025-10-19 15:23:02 -04:00
} finally {
2026-05-19 09:49:22 -04:00
setIsGeneratingKeys ( false );
2025-09-08 16:04:58 -04:00
}
};
const handleCreateAnswer = async () => {
try {
if ( ! offerInput . trim ()) {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "You need to insert the invitation code from your interlocutor." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
return ;
}
try {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "Processing the secure invitation..." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
let offer ;
try {
2025-10-05 06:21:14 -04:00
if ( typeof window . decodeAnyPayload === "function" ) {
const any = window . decodeAnyPayload ( offerInput . trim ());
offer = typeof any === "string" ? JSON . parse ( any ) : any ;
} else {
const rawText = typeof window . decompressIfNeeded === "function" ? window . decompressIfNeeded ( offerInput . trim ()) : offerInput . trim ();
offer = JSON . parse ( rawText );
}
2025-09-08 16:04:58 -04:00
} catch ( parseError ) {
throw new Error ( `Invalid invitation format: ${ parseError . message } ` );
}
if ( ! offer || typeof offer !== "object" ) {
throw new Error ( "The invitation must be an object" );
}
2025-09-23 20:01:02 -04:00
const isValidOfferType = offer . t === "offer" || offer . type === "enhanced_secure_offer" ;
if ( ! isValidOfferType ) {
throw new Error ( "Invalid invitation type. Expected offer or enhanced_secure_offer" );
2025-09-08 16:04:58 -04:00
}
2026-05-19 09:49:22 -04:00
const answer = await webrtcManagerRef . current . createSecureAnswer ( offer );
2025-09-08 16:04:58 -04:00
setAnswerData ( answer );
setShowAnswerStep ( true );
2025-09-27 19:07:17 -04:00
const answerString = typeof answer === "object" ? JSON . stringify ( answer ) : answer ;
2025-10-05 06:21:14 -04:00
try {
if ( typeof window . encodeBinaryToPrefixed === "function" ) {
const bin = window . encodeBinaryToPrefixed ( answerString );
2025-10-07 23:58:54 -04:00
const TARGET_CHUNKS = 4 ;
2025-10-08 01:24:04 -04:00
let total = TARGET_CHUNKS ;
let FRAME_MAX = Math . max ( 200 , Math . ceil ( bin . length / TARGET_CHUNKS ));
2025-10-05 06:21:14 -04:00
if ( FRAME_MAX <= 0 ) FRAME_MAX = 200 ;
2025-10-08 01:24:04 -04:00
if ( bin . length <= FRAME_MAX ) {
total = 1 ;
FRAME_MAX = bin . length ;
} else {
FRAME_MAX = Math . ceil ( bin . length / TARGET_CHUNKS );
total = TARGET_CHUNKS ;
2025-10-05 06:21:14 -04:00
}
2025-10-07 23:58:54 -04:00
const id = `ans_ ${ Date . now () } _ ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` ;
2025-10-05 06:21:14 -04:00
const chunks = [];
for ( let i = 0 ; i < total ; i ++ ) {
const seq = i + 1 ;
const part = bin . slice ( i * FRAME_MAX , ( i + 1 ) * FRAME_MAX );
2025-10-07 23:58:54 -04:00
chunks . push ( part );
2025-10-05 06:21:14 -04:00
}
const isDesktop = typeof window !== "undefined" && ( window . innerWidth || 0 ) >= 1024 ;
const QR_SIZE = isDesktop ? 720 : 512 ;
if ( window . generateQRCode && chunks . length > 0 ) {
const firstUrl = await window . generateQRCode ( chunks [ 0 ], { errorCorrectionLevel : "M" , size : QR_SIZE , margin : 2 });
if ( firstUrl ) setQrCodeUrl ( firstUrl );
}
try {
if ( qrAnimationRef . current && qrAnimationRef . current . timer ) {
clearInterval ( qrAnimationRef . current . timer );
}
} catch {
}
qrAnimationRef . current = { timer : null , chunks , idx : 0 , active : true };
setQrFramesTotal ( chunks . length );
setQrFrameIndex ( 1 );
setQrManualMode ( false );
const intervalMs = 3e3 ;
qrAnimationRef . current . timer = setInterval ( renderAndAdvance , intervalMs );
try {
setShowQRCode ( true );
} catch {
}
} else {
2025-10-07 23:58:54 -04:00
await generateQRCode ( answer );
2025-10-05 06:21:14 -04:00
try {
setShowQRCode ( true );
} catch {
}
}
2025-10-14 22:51:48 -04:00
} catch ( e ) {
console . warn ( "Answer QR generation failed:" , e );
2025-10-05 06:21:14 -04:00
}
2026-05-19 09:49:22 -04:00
if ( typeof markAnswerCreated === "function" ) {
markAnswerCreated ();
2025-10-02 01:43:32 -04:00
}
2025-09-08 16:04:58 -04:00
const existingResponseMessages = messages . filter (
( m ) => m . type === "system" && ( m . message . includes ( "Secure response created" ) || m . message . includes ( "Send the response" ))
);
if ( existingResponseMessages . length === 0 ) {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "Secure response created!" ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "Send the response code to the initiator via a secure channel or let them scan the QR code below." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
}
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} catch ( error ) {
console . error ( "Error in handleCreateAnswer:" , error );
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : `Error processing the invitation: ${ error . message } ` ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
}
} catch ( error ) {
console . error ( "Error in handleCreateAnswer:" , error );
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : `Invitation processing error: ${ error . message } ` ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
}
};
const handleConnect = async () => {
try {
if ( ! answerInput . trim ()) {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "You need to insert the response code from your interlocutor." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
return ;
}
try {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "Processing the secure response..." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
let answer ;
try {
2025-10-05 06:21:14 -04:00
if ( typeof window . decodeAnyPayload === "function" ) {
const anyAnswer = window . decodeAnyPayload ( answerInput . trim ());
answer = typeof anyAnswer === "string" ? JSON . parse ( anyAnswer ) : anyAnswer ;
} else {
const rawText = typeof window . decompressIfNeeded === "function" ? window . decompressIfNeeded ( answerInput . trim ()) : answerInput . trim ();
answer = JSON . parse ( rawText );
}
2025-09-08 16:04:58 -04:00
} catch ( parseError ) {
throw new Error ( `Invalid response format: ${ parseError . message } ` );
}
if ( ! answer || typeof answer !== "object" ) {
throw new Error ( "The response must be an object" );
}
2025-09-23 20:01:02 -04:00
const answerType = answer . t || answer . type ;
if ( ! answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer" ) {
throw new Error ( "Invalid response type. Expected answer or enhanced_secure_answer" );
2025-09-08 16:04:58 -04:00
}
2026-05-19 09:49:22 -04:00
await webrtcManagerRef . current . handleSecureAnswer ( answer );
2025-09-23 20:01:02 -04:00
if ( pendingSession ) {
setPendingSession ( null );
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : `All security features enabled by default` ,
2025-09-23 20:01:02 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
2025-09-08 16:04:58 -04:00
}
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : "Finalizing the secure connection..." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
if ( ! window . isUpdatingSecurity ) {
updateSecurityLevel (). catch ( console . error );
}
} catch ( error ) {
2025-09-23 20:01:02 -04:00
console . error ( "Error in handleConnect inner try:" , error );
let errorMessage = "Connection setup error" ;
if ( error . message . includes ( "CRITICAL SECURITY FAILURE" )) {
if ( error . message . includes ( "ECDH public key structure" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Invalid response code - missing or corrupted cryptographic key. Please check the code and try again." ;
2025-09-23 20:01:02 -04:00
} else if ( error . message . includes ( "ECDSA public key structure" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Invalid response code - missing signature verification key. Please check the code and try again." ;
2025-09-23 20:01:02 -04:00
} else {
2025-10-02 01:43:32 -04:00
errorMessage = "Security validation failed - possible attack detected" ;
2025-09-23 20:01:02 -04:00
}
} else if ( error . message . includes ( "too old" ) || error . message . includes ( "replay" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Response data is outdated - please use a fresh invitation" ;
2025-09-23 20:01:02 -04:00
} else if ( error . message . includes ( "MITM" ) || error . message . includes ( "signature" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Security breach detected - connection rejected" ;
2025-09-23 20:01:02 -04:00
} else if ( error . message . includes ( "Invalid" ) || error . message . includes ( "format" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Invalid response format - please check the code" ;
2025-09-23 20:01:02 -04:00
} else {
2025-10-02 01:43:32 -04:00
errorMessage = ` ${ error . message } ` ;
2025-09-23 20:01:02 -04:00
}
2025-09-08 16:04:58 -04:00
setMessages (( prev ) => [... prev , {
2025-09-23 20:01:02 -04:00
message : errorMessage ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
2025-09-23 20:01:02 -04:00
timestamp : Date . now (),
showRetryButton : true
2025-09-08 16:04:58 -04:00
}]);
2025-09-23 20:01:02 -04:00
if ( ! error . message . includes ( "too old" ) && ! error . message . includes ( "replay" )) {
2025-09-08 16:04:58 -04:00
setPendingSession ( null );
2025-09-23 20:01:02 -04:00
setSessionTimeLeft ( 0 );
2025-09-08 16:04:58 -04:00
}
2025-09-23 20:01:02 -04:00
setConnectionStatus ( "failed" );
2025-09-08 16:04:58 -04:00
}
} catch ( error ) {
2025-09-23 20:01:02 -04:00
console . error ( "Error in handleConnect outer try:" , error );
let errorMessage = "Connection setup error" ;
if ( error . message . includes ( "CRITICAL SECURITY FAILURE" )) {
if ( error . message . includes ( "ECDH public key structure" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Invalid response code - missing or corrupted cryptographic key. Please check the code and try again." ;
2025-09-23 20:01:02 -04:00
} else if ( error . message . includes ( "ECDSA public key structure" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Invalid response code - missing signature verification key. Please check the code and try again." ;
2025-09-23 20:01:02 -04:00
} else {
2025-10-02 01:43:32 -04:00
errorMessage = "Security validation failed - possible attack detected" ;
2025-09-23 20:01:02 -04:00
}
} else if ( error . message . includes ( "too old" ) || error . message . includes ( "replay" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Response data is outdated - please use a fresh invitation" ;
2025-09-23 20:01:02 -04:00
} else if ( error . message . includes ( "MITM" ) || error . message . includes ( "signature" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Security breach detected - connection rejected" ;
2025-09-23 20:01:02 -04:00
} else if ( error . message . includes ( "Invalid" ) || error . message . includes ( "format" )) {
2025-10-02 01:43:32 -04:00
errorMessage = "Invalid response format - please check the code" ;
2025-09-23 20:01:02 -04:00
} else {
2025-10-02 01:43:32 -04:00
errorMessage = ` ${ error . message } ` ;
2025-09-23 20:01:02 -04:00
}
2025-09-08 16:04:58 -04:00
setMessages (( prev ) => [... prev , {
2025-09-23 20:01:02 -04:00
message : errorMessage ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
2025-09-23 20:01:02 -04:00
timestamp : Date . now (),
showRetryButton : true
2025-09-08 16:04:58 -04:00
}]);
2025-09-23 20:01:02 -04:00
if ( ! error . message . includes ( "too old" ) && ! error . message . includes ( "replay" )) {
2025-09-08 16:04:58 -04:00
setPendingSession ( null );
2025-09-23 20:01:02 -04:00
setSessionTimeLeft ( 0 );
2025-09-08 16:04:58 -04:00
}
2025-09-23 20:01:02 -04:00
setConnectionStatus ( "failed" );
2025-09-08 16:04:58 -04:00
}
};
2026-05-17 14:48:52 -04:00
const handleVerifyConnection = async ( userCode , isValid = true ) => {
2025-09-08 16:04:58 -04:00
if ( isValid ) {
2026-05-19 09:49:22 -04:00
webrtcManagerRef . current . confirmVerification ( userCode );
2025-09-08 16:04:58 -04:00
setLocalVerificationConfirmed ( true );
2025-10-15 19:58:28 -04:00
try {
2026-05-19 09:49:22 -04:00
if ( window . NotificationIntegration && webrtcManagerRef . current && ! notificationIntegrationRef . current ) {
const integration = new window . NotificationIntegration ( webrtcManagerRef . current );
2025-10-15 19:58:28 -04:00
await integration . init ();
notificationIntegrationRef . current = integration ;
const status = integration . getStatus ();
if ( status . permission === "granted" ) {
setMessages (( prev ) => [... prev , {
message : "\u2713 Notifications enabled - you will receive alerts when the tab is inactive" ,
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
} else {
setMessages (( prev ) => [... prev , {
message : "\u2139 Notifications disabled - you can enable them using the button on the main page" ,
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
}
} else if ( notificationIntegrationRef . current ) {
} else {
}
} catch ( error ) {
console . warn ( "Failed to initialize notifications:" , error );
}
2025-09-08 16:04:58 -04:00
} else {
setMessages (( prev ) => [... prev , {
2025-10-02 01:43:32 -04:00
message : " Verification rejected. The connection is unsafe! Session reset.." ,
2025-09-08 16:04:58 -04:00
type : "system" ,
id : Date . now (),
timestamp : Date . now ()
}]);
setLocalVerificationConfirmed ( false );
setRemoteVerificationConfirmed ( false );
setBothVerificationsConfirmed ( false );
setShowVerification ( false );
setVerificationCode ( "" );
setConnectionStatus ( "disconnected" );
2025-10-14 22:51:48 -04:00
setOfferData ( "" );
setAnswerData ( "" );
2025-09-08 16:04:58 -04:00
setOfferInput ( "" );
setAnswerInput ( "" );
setShowOfferStep ( false );
setShowAnswerStep ( false );
setKeyFingerprint ( "" );
setSecurityLevel ( null );
setIsVerified ( false );
setMessages ([]);
setSessionTimeLeft ( 0 );
setPendingSession ( null );
document . dispatchEvent ( new CustomEvent ( "disconnected" ));
handleDisconnect ();
}
};
const handleSendMessage = async () => {
if ( ! messageInput . trim ()) {
return ;
}
2026-05-19 09:49:22 -04:00
if ( ! webrtcManagerRef . current ) {
2025-09-08 16:04:58 -04:00
return ;
}
2026-06-23 16:52:30 -04:00
const baseTextEarly = messageInput . trim ();
const midEarly = `m_ ${ Date . now (). toString ( 36 ) } _ ${ Math . random (). toString ( 36 ). slice ( 2 , 8 ) } ` ;
const offlineNow = isOffline || typeof navigator !== "undefined" && navigator . onLine === false || window . pwaOfflineManager && window . pwaOfflineManager . isOnline === false ;
if ( offlineNow ) {
const outTextOff = codeMode ? "```\n" + baseTextEarly + "\n```" : baseTextEarly ;
const tsOff = Date . now ();
const metaOff = { mid : midEarly , ts : tsOff };
if ( viewOnceMode ) {
metaOff . once = true ;
metaOff . onceTtl = viewOnceTtl ;
}
if ( disappearTtl > 0 ) metaOff . ttl = disappearTtl ;
const echoOpts = { mid : midEarly , status : "sent" , timestamp : tsOff };
if ( disappearTtl > 0 ) echoOpts . expiresAt = tsOff + disappearTtl * 1e3 ;
addMessageWithAutoScroll ( outTextOff , "sent" , echoOpts );
2026-06-26 00:00:13 -04:00
const q = queuesRef . current . get ( activeIdRef . current );
if ( q ) q . outgoing . push ({ outText : outTextOff , meta : metaOff , mid : midEarly });
2026-06-23 16:52:30 -04:00
setMessageInput ( "" );
if ( codeMode ) setCodeMode ( false );
if ( viewOnceMode ) setViewOnceMode ( false );
return ;
}
2026-06-26 00:00:13 -04:00
if ( ! webrtcManagerRef . current . isConnected ()) {
return ;
}
2025-09-08 16:04:58 -04:00
try {
2026-06-23 16:52:30 -04:00
const baseText = baseTextEarly ;
2026-06-18 21:15:43 -04:00
const outText = codeMode ? "```\n" + baseText + "\n```" : baseText ;
2026-06-23 16:52:30 -04:00
const mid = midEarly ;
const meta = { mid , ts : Date . now () };
2026-06-19 02:58:03 -04:00
if ( viewOnceMode ) {
meta . once = true ;
meta . onceTtl = viewOnceTtl ;
}
2026-06-18 21:15:43 -04:00
if ( disappearTtl > 0 ) meta . ttl = disappearTtl ;
2026-06-23 16:52:30 -04:00
const localOpts = { mid , status : "sending" };
2026-06-18 21:15:43 -04:00
if ( disappearTtl > 0 ) localOpts . expiresAt = Date . now () + disappearTtl * 1e3 ;
2026-06-18 20:37:50 -04:00
addMessageWithAutoScroll ( outText , "sent" , localOpts );
2026-06-23 16:52:30 -04:00
try {
await webrtcManagerRef . current . sendMessage ( outText , meta );
2026-06-26 00:00:13 -04:00
updateMessageStatus ( mid , "delivered" );
2026-06-23 16:52:30 -04:00
} catch ( sendErr ) {
updateMessageStatus ( mid , "failed" );
throw sendErr ;
}
2025-09-08 16:04:58 -04:00
setMessageInput ( "" );
2026-06-18 21:15:43 -04:00
if ( codeMode ) setCodeMode ( false );
if ( viewOnceMode ) setViewOnceMode ( false );
2025-09-08 16:04:58 -04:00
} catch ( error ) {
const msg = String ( error ? . message || error );
if ( ! /queued for sending|Data channel not ready/i . test ( msg )) {
2025-10-02 01:43:32 -04:00
addMessageWithAutoScroll ( `Sending error: ${ msg } ` , "system" );
2025-09-08 16:04:58 -04:00
}
}
};
2026-06-18 20:37:50 -04:00
const handleUnsendMessage = React . useCallback (( mid ) => {
if ( ! mid ) return ;
setMessages (( prev ) => prev . filter (( m ) => String ( m . mid ) !== String ( mid )));
try {
webrtcManagerRef . current ? . sendMessageDelete ? .( String ( mid ));
} catch ( _ ) {
}
}, []);
const handleMessageExpire = React . useCallback (( id ) => {
2026-06-23 16:52:30 -04:00
setMessages (( prev ) => prev . map (( m ) => m . id === id ? { ... m , expired : true , message : "" , expiresAt : void 0 } : m ));
2026-06-18 20:37:50 -04:00
}, []);
2025-09-08 16:04:58 -04:00
const handleClearData = () => {
setOfferData ( "" );
setAnswerData ( "" );
setOfferInput ( "" );
setAnswerInput ( "" );
setShowOfferStep ( false );
2026-05-19 09:49:22 -04:00
setIsGeneratingKeys ( false );
2025-09-27 19:07:17 -04:00
if ( ! shouldPreserveAnswerData ()) {
setShowAnswerStep ( false );
}
2025-09-08 16:04:58 -04:00
setShowVerification ( false );
2025-09-23 20:01:02 -04:00
setShowQRCode ( false );
setShowQRScanner ( false );
setShowQRScannerModal ( false );
2025-10-07 23:58:54 -04:00
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
2025-09-27 19:07:17 -04:00
if ( ! shouldPreserveAnswerData ()) {
setQrCodeUrl ( "" );
}
2025-09-08 16:04:58 -04:00
setVerificationCode ( "" );
setIsVerified ( false );
setKeyFingerprint ( "" );
setSecurityLevel ( null );
setConnectionStatus ( "disconnected" );
setMessages ([]);
setMessageInput ( "" );
setLocalVerificationConfirmed ( false );
setRemoteVerificationConfirmed ( false );
setBothVerificationsConfirmed ( false );
2025-10-02 01:43:32 -04:00
if ( typeof console . clear === "function" ) {
console . clear ();
}
2025-09-23 20:01:02 -04:00
setSessionTimeLeft ( 0 );
2025-09-08 16:04:58 -04:00
setPendingSession ( null );
document . dispatchEvent ( new CustomEvent ( "peer-disconnect" ));
};
2026-05-26 22:40:36 -04:00
const handleIncomingDecision = React . useCallback ( async ( fileId , accepted ) => {
try {
if ( accepted ) {
await webrtcManagerRef . current ? . acceptIncomingFile ( fileId );
} else {
await webrtcManagerRef . current ? . rejectIncomingFile ( fileId );
}
} finally {
setPendingIncomingFiles (( prev ) => prev . filter (( f ) => f . fileId !== fileId ));
}
}, []);
2025-09-08 16:04:58 -04:00
const handleDisconnect = () => {
2025-10-14 22:51:48 -04:00
try {
2026-06-26 00:00:13 -04:00
const id = activeIdRef . current ;
2025-09-23 20:01:02 -04:00
setSessionTimeLeft ( 0 );
2025-10-14 22:51:48 -04:00
document . dispatchEvent ( new CustomEvent ( "peer-disconnect" ));
document . dispatchEvent ( new CustomEvent ( "disconnected" ));
document . dispatchEvent ( new CustomEvent ( "session-cleanup" , {
2026-06-26 00:00:13 -04:00
detail : { timestamp : Date . now (), reason : "manual_disconnect" }
2025-10-14 22:51:48 -04:00
}));
2026-06-26 00:00:13 -04:00
destroySession ( id );
if ( typeof console . clear === "function" ) console . clear ();
2025-10-14 22:51:48 -04:00
} catch ( error ) {
console . error ( "Error during disconnect:" , error );
}
2025-09-08 16:04:58 -04:00
};
const handleSessionActivated = ( session ) => {
let message ;
if ( session . type === "demo" ) {
2025-10-02 01:43:32 -04:00
message = ` Demo session activated for 6 minutes. You can create invitations!` ;
2025-09-08 16:04:58 -04:00
} else {
2025-10-02 01:43:32 -04:00
message = ` All security features enabled by default. You can create invitations!` ;
2025-09-08 16:04:58 -04:00
}
addMessageWithAutoScroll ( message , "system" );
};
React . useEffect (() => {
if ( connectionStatus === "connected" && isVerified ) {
2025-10-02 01:43:32 -04:00
addMessageWithAutoScroll ( " Secure connection successfully established and verified! You can now communicate safely with full protection against MITM attacks and Perfect Forward Secrecy.." , "system" );
2025-09-08 16:04:58 -04:00
}
}, [ connectionStatus , isVerified ]);
2025-09-23 20:01:02 -04:00
const isConnectedAndVerified = ( connectionStatus === "connected" || connectionStatus === "verified" ) && isVerified ;
2026-06-23 16:52:30 -04:00
React . useEffect (() => {
document . body . classList . toggle ( "sb-in-chat" , isConnectedAndVerified );
return () => document . body . classList . remove ( "sb-in-chat" );
}, [ isConnectedAndVerified ]);
2025-09-08 16:04:58 -04:00
React . useEffect (() => {
2025-09-23 20:01:02 -04:00
if ( isConnectedAndVerified && pendingSession && connectionStatus !== "failed" ) {
setPendingSession ( null );
setSessionTimeLeft ( 0 );
2025-10-02 01:43:32 -04:00
addMessageWithAutoScroll ( " All security features enabled by default" , "system" );
2025-09-08 16:04:58 -04:00
}
2025-09-23 20:01:02 -04:00
}, [ isConnectedAndVerified , pendingSession , connectionStatus ]);
2025-10-07 23:58:54 -04:00
React . useEffect (() => {
if ( showQRScannerModal && window . Html5Qrcode ) {
const html5Qrcode = new window . Html5Qrcode ( "qr-reader" );
const config = {
fps : 10
// Убираем qrbox чтобы использовать всю область
};
let isScanning = true ;
html5Qrcode . start (
{ facingMode : "environment" },
// Use back camera
config ,
( decodedText , decodedResult ) => {
if ( ! isScanning ) {
console . log ( "Scanner stopped, ignoring scan" );
return ;
}
console . log ( "QR Code scanned:" , decodedText );
console . log ( "Current buffer state:" , qrChunksBufferRef . current );
handleQRScan ( decodedText ). then (( success ) => {
console . log ( "QR scan result:" , success );
if ( success ) {
console . log ( "Closing scanner and modal" );
isScanning = false ;
try {
console . log ( "Stopping scanner..." );
html5Qrcode . stop (). then (() => {
console . log ( "Scanner stopped, clearing..." );
html5Qrcode . clear ();
setShowQRScannerModal ( false );
}). catch (( err ) => {
console . log ( "Error stopping scanner:" , err );
try {
html5Qrcode . clear ();
} catch ( clearErr ) {
console . log ( "Error clearing scanner:" , clearErr );
}
setShowQRScannerModal ( false );
});
} catch ( err ) {
console . log ( "Error in scanner cleanup:" , err );
setShowQRScannerModal ( false );
}
} else {
console . log ( "Continuing to scan for more chunks..." );
}
}). catch (( error ) => {
console . error ( "QR scan processing error:" , error );
});
},
( error ) => {
if ( isScanning ) {
console . log ( "QR scan error (ignored):" , error );
}
}
). catch (( err ) => {
console . error ( "QR Scanner start error:" , err );
setShowQRScannerModal ( false );
});
return () => {
isScanning = false ;
try {
html5Qrcode . stop (). then (() => {
html5Qrcode . clear ();
}). catch (( err ) => {
console . log ( "Scanner already stopped or error stopping:" , err );
try {
html5Qrcode . clear ();
} catch ( clearErr ) {
console . log ( "Error clearing scanner in cleanup:" , clearErr );
}
});
} catch ( err ) {
console . log ( "Error in cleanup:" , err );
try {
html5Qrcode . clear ();
} catch ( clearErr ) {
console . log ( "Error clearing scanner in cleanup:" , clearErr );
}
}
};
}
}, [ showQRScannerModal ]);
2026-06-26 00:00:13 -04:00
const sessionChats = decorateSessions ( sessionsState );
const showSidebar = sessionsState . order . length > 1 || sessionsState . order . some (( id ) => {
const s = sessionsState . sessions [ id ];
return s && s . sas && s . sas . isVerified ;
});
2025-09-08 16:04:58 -04:00
return React . createElement ( "div" , {
2026-06-26 00:00:13 -04:00
className : "minimal-bg" ,
// With the rail visible the app is a fixed-height shell (rail + column
// fill the viewport, design-style). Otherwise it's the scrollable landing.
// flexDirection:'row' is explicit — the .minimal-bg class forces
// flex-direction:column, which would otherwise stack the rail ABOVE the chat.
style : showSidebar ? { display : "flex" , flexDirection : "row" , height : "100vh" , width : "100%" , overflow : "hidden" } : { minHeight : "100vh" }
2025-09-08 16:04:58 -04:00
}, [
2026-06-26 00:00:13 -04:00
showSidebar && React . createElement ( SessionsSidebar , {
key : "sessions-sidebar" ,
chats : sessionChats ,
collapsed : sidebarCollapsed ,
drawerOpen : sidebarDrawerOpen ,
onToggleCollapse : () => setSidebarCollapsed (( v ) => ! v ),
onSelect : handleSelectSession ,
onNewChat : handleNewChat ,
onRename : handleRenameSession ,
onCloseDrawer : () => setSidebarDrawerOpen ( false ),
myStatus ,
onSetStatus : setMyStatus
2025-09-08 16:04:58 -04:00
}),
2026-06-26 00:00:13 -04:00
// Mobile-only hamburger that opens the drawer (hidden on desktop via CSS).
showSidebar && React . createElement ( "button" , {
key : "sb-burger" ,
className : "sb-burger" ,
onClick : () => setSidebarDrawerOpen ( true ),
style : { display : "none" , position : "fixed" , top : "13px" , left : "13px" , zIndex : 55 , width : "38px" , height : "38px" , borderRadius : "10px" , placeItems : "center" , border : "1px solid rgba(255,255,255,0.1)" , background : "rgba(18,18,20,0.9)" , color : "#cfcfd4" , cursor : "pointer" },
dangerouslySetInnerHTML : { __html : SB_SVG . burger }
}),
React . createElement ( "div" , {
key : "app-column" ,
className : showSidebar ? "minimal-bg" : "minimal-bg min-h-screen" ,
style : showSidebar ? { flex : 1 , minWidth : 0 , height : "100vh" , overflow : "hidden" , display : "flex" , flexDirection : "column" } : {}
}, [
// Advanced network settings now render inside the connection
// screen's right panel (see EnhancedConnectionSetup), matching
// the design's slide-up-within-the-right-column behavior.
// The verified chat renders its own in-chat header (SecureBit Chat
// design); the shared header is shown only on the landing/setup view.
! isConnectedAndVerified && ! showSidebar && window . EnhancedMinimalHeader && React . createElement ( window . EnhancedMinimalHeader , {
key : "header" ,
status : connectionStatus ,
fingerprint : keyFingerprint ,
verificationCode ,
onDisconnect : handleDisconnect ,
isConnected : isConnectedAndVerified ,
securityLevel ,
// sessionManager removed - all features enabled by default
webrtcManager : webrtcManagerRef . current
}),
React . createElement (
"main" ,
{
key : "main"
},
/* @__PURE__ */ (() => {
return isConnectedAndVerified ;
})() ? (() => {
return React . createElement ( EnhancedChatInterface , {
title : active ? active . peerLabel : "" ,
isOffline ,
peerPresence : active ? active . peerPresence : null ,
onRenameTitle : ( label ) => {
if ( activeSessionId ) dispatch ({ type : SESSION_ACTIONS . RENAME , id : activeSessionId , label });
},
messages ,
messageInput ,
setMessageInput ,
onSendMessage : handleSendMessage ,
onDisconnect : handleDisconnect ,
keyFingerprint ,
isVerified ,
chatMessagesRef ,
scrollToBottom ,
webrtcManager : webrtcManagerRef . current ,
status : connectionStatus ,
pendingIncomingFiles ,
onIncomingDecision : handleIncomingDecision ,
// Secure chat extras
codeMode ,
setCodeMode ,
viewOnceMode ,
setViewOnceMode ,
viewOnceTtl ,
setViewOnceTtl ,
disappearTtl ,
setDisappearTtl ,
nowTick ,
onUnsendMessage : handleUnsendMessage ,
onMessageExpire : handleMessageExpire
});
})() : React . createElement ( EnhancedConnectionSetup , {
onCreateOffer : handleCreateOffer ,
onCreateAnswer : handleCreateAnswer ,
onConnect : handleConnect ,
onClearData : handleClearData ,
onVerifyConnection : handleVerifyConnection ,
connectionStatus ,
offerData ,
answerData ,
offerInput ,
setOfferInput ,
answerInput ,
setAnswerInput ,
showOfferStep ,
showAnswerStep ,
verificationCode ,
showVerification ,
showQRCode ,
qrCodeUrl ,
showQRScanner ,
setShowQRCode ,
setShowQRScanner ,
setShowQRScannerModal ,
2025-09-23 20:01:02 -04:00
messages ,
2026-06-26 00:00:13 -04:00
localVerificationConfirmed ,
remoteVerificationConfirmed ,
bothVerificationsConfirmed ,
// QR control props
qrFramesTotal ,
qrFrameIndex ,
qrManualMode ,
toggleQrManualMode ,
nextQrFrame ,
prevQrFrame ,
// PAKE passwords removed - using SAS verification instead
markAnswerCreated ,
notificationIntegrationRef ,
isGeneratingKeys ,
setIsGeneratingKeys ,
handleCreateOffer ,
relayOnlyMode ,
setRelayOnlyMode ,
webrtcManagerRef ,
showIceSettings ,
setShowIceSettings ,
iceServersText ,
iceSettingsPersisted ,
customIceServers ,
handleApplyIceSettings ,
handleForgetIceSettings ,
// Render only the create/connect card inside the chat column
// (an additional session), instead of the full landing.
compact : showSidebar
})
),
// QR Scanner Modal — camera scan (design import: "Start Secure" / Camera scan modal)
showQRScannerModal && (() => {
const closeScanner = () => {
setShowQRScannerModal ( false );
qrChunksBufferRef . current = { id : null , total : 0 , seen : /* @__PURE__ */ new Set (), items : [] };
};
const buf = qrChunksBufferRef . current ;
const hasParts = !! ( buf && buf . id && buf . total > 1 );
const framesText = hasParts ? `Scanning frames\u2026 ${ buf . seen . size } / ${ buf . total } ` : "Scanning\u2026" ;
const corner = ( k , st ) => React . createElement ( "span" , {
key : k ,
style : Object . assign ({ position : "absolute" , width : "34px" , height : "34px" , zIndex : 3 }, st )
2025-09-23 20:01:02 -04:00
});
2026-06-26 00:00:13 -04:00
return React . createElement ( "div" , {
key : "qr-scanner-modal" ,
onClick : closeScanner ,
style : { position : "fixed" , inset : 0 , zIndex : 50 , display : "flex" , alignItems : "center" , justifyContent : "center" , padding : "32px" , background : "rgba(6,6,8,0.82)" , backdropFilter : "blur(10px)" , WebkitBackdropFilter : "blur(10px)" , animation : "sbUp .2s ease" }
2025-10-07 23:58:54 -04:00
}, [
2026-06-24 15:41:15 -04:00
React . createElement ( "div" , {
2026-06-26 00:00:13 -04:00
key : "scanner-container" ,
onClick : ( e ) => e . stopPropagation (),
style : { width : "100%" , maxWidth : "420px" , borderRadius : "22px" , border : "1px solid rgba(255,255,255,0.1)" , background : "#111113" , boxShadow : "0 30px 90px rgba(0,0,0,0.6)" , overflow : "hidden" }
2025-10-07 23:58:54 -04:00
}, [
2026-06-26 00:00:13 -04:00
// Header
2025-10-07 23:58:54 -04:00
React . createElement ( "div" , {
2026-06-26 00:00:13 -04:00
key : "scanner-header" ,
style : { display : "flex" , alignItems : "center" , gap : "11px" , padding : "18px 20px" , borderBottom : "1px solid rgba(255,255,255,0.06)" }
2025-10-07 23:58:54 -04:00
}, [
2026-06-26 00:00:13 -04:00
React . createElement ( "span" , {
key : "scanner-icon" ,
style : { display : "flex" },
dangerouslySetInnerHTML : { __html : '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8.5V6.5A2.5 2.5 0 0 1 4.5 4h2M17.5 4h2A2.5 2.5 0 0 1 22 6.5v2M22 15.5v2a2.5 2.5 0 0 1-2.5 2.5h-2M6.5 20h-2A2.5 2.5 0 0 1 2 17.5v-2"/><circle cx="12" cy="12" r="3.2"/></svg>' }
}),
2025-10-07 23:58:54 -04:00
React . createElement ( "div" , {
2026-06-26 00:00:13 -04:00
key : "scanner-titles" ,
style : { flex : 1 , lineHeight : 1.2 }
}, [
React . createElement ( "div" , {
key : "scanner-title" ,
style : { fontSize : "15.5px" , fontWeight : 800 , color : "#f4f4f6" }
}, "Scan QR code" ),
React . createElement ( "div" , {
key : "scanner-hint" ,
style : { fontSize : "12px" , color : "#7b7b83" }
}, "Point your camera at their QR" )
]),
React . createElement ( "button" , {
key : "close-btn" ,
onClick : closeScanner ,
style : { width : "32px" , height : "32px" , display : "grid" , placeItems : "center" , borderRadius : "9px" , border : "none" , background : "rgba(255,255,255,0.05)" , color : "#9a9aa2" , cursor : "pointer" }
}, React . createElement ( "i" , { className : "fas fa-times" }))
2026-06-24 15:41:15 -04:00
]),
2026-06-26 00:00:13 -04:00
// Body
2026-06-24 15:41:15 -04:00
React . createElement ( "div" , {
2026-06-26 00:00:13 -04:00
key : "scanner-body" ,
style : { padding : "22px 24px 24px" }
2026-06-24 15:41:15 -04:00
}, [
React . createElement ( "div" , {
2026-06-26 00:00:13 -04:00
key : "viewfinder" ,
style : { position : "relative" , aspectRatio : "1" , borderRadius : "18px" , overflow : "hidden" , background : "#000" , border : "1px solid rgba(255,255,255,0.1)" }
2026-06-24 15:41:15 -04:00
}, [
2026-06-26 00:00:13 -04:00
React . createElement ( "div" , {
key : "vf-bg" ,
style : { position : "absolute" , inset : 0 , background : "radial-gradient(circle at 50% 45%, #1a1a1f, #000)" }
}),
// Camera video is injected here by Html5Qrcode
React . createElement ( "div" , {
key : "qr-reader" ,
id : "qr-reader" ,
style : { position : "absolute" , inset : 0 , zIndex : 1 }
2026-06-24 15:41:15 -04:00
}),
2026-06-26 00:00:13 -04:00
corner ( "c-tl" , { top : "18px" , left : "18px" , borderTop : "2.5px solid #3ecf8e" , borderLeft : "2.5px solid #3ecf8e" , borderRadius : "8px 0 0 0" }),
corner ( "c-tr" , { top : "18px" , right : "18px" , borderTop : "2.5px solid #3ecf8e" , borderRight : "2.5px solid #3ecf8e" , borderRadius : "0 8px 0 0" }),
corner ( "c-bl" , { bottom : "18px" , left : "18px" , borderBottom : "2.5px solid #3ecf8e" , borderLeft : "2.5px solid #3ecf8e" , borderRadius : "0 0 0 8px" }),
corner ( "c-br" , { bottom : "18px" , right : "18px" , borderBottom : "2.5px solid #3ecf8e" , borderRight : "2.5px solid #3ecf8e" , borderRadius : "0 0 8px 0" }),
2026-06-24 15:41:15 -04:00
React . createElement ( "span" , {
2026-06-26 00:00:13 -04:00
key : "scan-line" ,
style : { position : "absolute" , left : "18px" , right : "18px" , height : "2.5px" , zIndex : 2 , background : "linear-gradient(90deg, transparent, #3ecf8e, transparent)" , boxShadow : "0 0 16px #3ecf8e" , animation : "sbScan 1.5s ease-in-out infinite alternate" }
}),
React . createElement ( "div" , {
key : "scan-status" ,
style : { position : "absolute" , bottom : 0 , left : 0 , right : 0 , zIndex : 3 , display : "flex" , alignItems : "center" , justifyContent : "center" , gap : "8px" , padding : "14px" , background : "linear-gradient(transparent, rgba(0,0,0,0.6))" }
}, [
React . createElement ( "span" , {
key : "spinner" ,
style : { display : "flex" , animation : "sbSpin 1.4s linear infinite" },
dangerouslySetInnerHTML : { __html : '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.2-8.6"/></svg>' }
}),
React . createElement ( "span" , {
key : "scan-frames" ,
style : { fontSize : "12.5px" , fontWeight : 600 , color : "#cfcfd4" }
}, framesText )
])
]),
React . createElement ( "p" , {
key : "scanner-note" ,
style : { margin : "16px 0 0" , textAlign : "center" , fontSize : "12px" , lineHeight : 1.5 , color : "#6b6b73" }
}, "Hold steady until all parts are captured. Camera access is local \u2014 nothing is uploaded." )
])
2025-10-07 23:58:54 -04:00
])
2026-06-26 00:00:13 -04:00
]);
})()
])
// end app-column
2025-09-08 16:04:58 -04:00
]);
};
2025-12-29 10:51:07 -04:00
var UpdateCheckerWrapper = ({ children }) => {
if ( typeof window !== "undefined" && window . UpdateChecker ) {
return React . createElement ( window . UpdateChecker , {
debug : false
}, children );
}
return children ;
};
2025-09-08 16:04:58 -04:00
function initializeApp () {
if ( window . EnhancedSecureCryptoUtils && window . EnhancedSecureWebRTCManager ) {
2025-12-29 10:51:07 -04:00
const AppWithUpdateChecker = React . createElement (
UpdateCheckerWrapper ,
null ,
React . createElement ( EnhancedSecureP2PChat )
);
ReactDOM . render ( AppWithUpdateChecker , document . getElementById ( "root" ));
2025-09-08 16:04:58 -04:00
} else {
2025-10-02 01:43:32 -04:00
console . error ( "\u041C\u043E\u0434\u0443\u043B\u0438 \u043D\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u044B:" , {
2025-09-08 16:04:58 -04:00
hasCrypto : !! window . EnhancedSecureCryptoUtils ,
hasWebRTC : !! window . EnhancedSecureWebRTCManager
});
}
}
2025-09-23 20:01:02 -04:00
if ( typeof window !== "undefined" ) {
window . addEventListener ( "unhandledrejection" , ( event ) => {
2025-10-02 01:43:32 -04:00
console . error ( "Unhandled promise rejection:" , event . reason );
2025-09-23 20:01:02 -04:00
event . preventDefault ();
});
window . addEventListener ( "error" , ( event ) => {
2025-10-02 01:43:32 -04:00
console . error ( "Global error:" , event . error );
2025-09-23 20:01:02 -04:00
event . preventDefault ();
});
if ( ! window . initializeApp ) {
window . initializeApp = initializeApp ;
}
2025-09-08 16:04:58 -04:00
}
2025-12-29 10:51:07 -04:00
if ( window . EnhancedSecureCryptoUtils && window . EnhancedSecureWebRTCManager ) {
const UpdateCheckerWrapper2 = ({ children }) => {
if ( typeof window !== "undefined" && window . UpdateChecker ) {
return React . createElement ( window . UpdateChecker , {
debug : false
}, children );
}
return children ;
};
const AppWithUpdateChecker = React . createElement (
UpdateCheckerWrapper2 ,
null ,
React . createElement ( EnhancedSecureP2PChat )
);
ReactDOM . render ( AppWithUpdateChecker , document . getElementById ( "root" ));
} else {
ReactDOM . render ( React . createElement ( EnhancedSecureP2PChat ), document . getElementById ( "root" ));
}
2025-09-08 16:04:58 -04:00
//# sourceMappingURL=app.js.map