release: v4.8.20 secure chat tools — completed, fixed and polished
Completes the messaging controls from v4.8.14 and fixes the bug that made them appear broken for recipients. Fixed: - Per-message metadata was silently dropped for recipients. NotificationIntegration wrapped onMessage and deliverMessageToUI with 2-arg shims that called the originals without the 3rd argument (meta); with notifications enabled, view-once, disappearing timers and unsend all failed on the receiving side. Both wrappers now forward all arguments. Added tests/notification-meta-forwarding.test.mjs. - Chat would not open after SAS: composer props were threaded into the wrong component (EnhancedConnectionSetup vs EnhancedChatInterface) -> ReferenceError nowTick on the verified re-render. Props moved to the chat component. Changed: - Code blocks: lightweight dependency-free syntax highlighting via React nodes (no innerHTML/remote scripts); code mode expands the input; copy auto-clears the clipboard after ~30s. - View-once: configurable visible-after-open time (5s/15s/30s/1m) via meta.onceTtl. - Disappearing timer: duration picker (Off/30s/5m/1h) instead of click-cycling. - Composer toolbar moved next to "Send files"; borderless buttons, brand-orange active state; pickers open upward and are mobile-friendly. - Sender bubble background lightened to rgba(249,115,22,0.05). Removed: - Panic wipe button (disconnect already wipes keys and clears session state). Transport unchanged: per-message metadata travels inside the encrypted envelope, whitelisted/bounded by _sanitizeMessageMeta. Full suite: 19 files, all passing. Docs (README, CHANGELOG) updated; version bumped to 4.8.20.
This commit is contained in:
Vendored
+276
-134
@@ -204,6 +204,128 @@ var parseMessageSegments = (text) => {
|
||||
if (last < text.length) segments.push({ kind: "text", content: text.slice(last) });
|
||||
return segments.some((s) => s.kind === "code") ? segments : null;
|
||||
};
|
||||
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;
|
||||
};
|
||||
var CodeBlock = ({ code, lang }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const handleCopy = async () => {
|
||||
@@ -214,13 +336,13 @@ var CodeBlock = ({ code, lang }) => {
|
||||
}
|
||||
};
|
||||
return React.createElement("div", {
|
||||
className: "my-1 rounded-lg overflow-hidden border border-gray-600/40",
|
||||
style: { backgroundColor: "#1b1c1b" }
|
||||
className: "my-1 rounded-lg overflow-hidden",
|
||||
style: { backgroundColor: "#1b1c1b", border: "0 solid #e5e7eb" }
|
||||
}, [
|
||||
React.createElement("div", {
|
||||
key: "hdr",
|
||||
className: "flex items-center justify-between px-3 py-1.5 border-b border-gray-600/30",
|
||||
style: { backgroundColor: "#222322" }
|
||||
className: "flex items-center justify-between px-3 py-1.5",
|
||||
style: { backgroundColor: "#222322", border: "0 solid #e5e7eb" }
|
||||
}, [
|
||||
React.createElement("span", {
|
||||
key: "lang",
|
||||
@@ -243,7 +365,7 @@ var CodeBlock = ({ code, lang }) => {
|
||||
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 }
|
||||
}, React.createElement("code", null, code))
|
||||
}, React.createElement("code", null, highlightCode(code)))
|
||||
]);
|
||||
};
|
||||
var MessageBody = ({ text }) => {
|
||||
@@ -266,62 +388,93 @@ var MessageBody = ({ text }) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
var ChatToolbar = ({ codeMode, setCodeMode, viewOnceMode, setViewOnceMode, disappearTtl, setDisappearTtl, onPanicWipe }) => {
|
||||
const ttlCycle = [0, 30, 300, 3600];
|
||||
const ttlLabel = (s) => s === 0 ? "Off" : s >= 3600 ? `${Math.round(s / 3600)}h` : s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`;
|
||||
const cycleTtl = () => {
|
||||
const i = ttlCycle.indexOf(disappearTtl);
|
||||
setDisappearTtl(ttlCycle[(i + 1) % ttlCycle.length] || 0);
|
||||
};
|
||||
const pill = (key, { active, activeClass, icon, label, title, onClick }) => React.createElement("button", {
|
||||
var ChatToolbar = ({ codeMode, setCodeMode, viewOnceMode, setViewOnceMode, viewOnceTtl, setViewOnceTtl, disappearTtl, setDisappearTtl }) => {
|
||||
const [openMenu, setOpenMenu] = React.useState(null);
|
||||
const fmt = (s) => s >= 3600 ? `${Math.round(s / 3600)}h` : s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`;
|
||||
const btnClass = (active) => `inline-flex items-center gap-2 h-9 px-3 rounded-lg text-xs font-medium transition-colors duration-150 ${active ? "accent-orange bg-orange-500/10" : "text-gray-400 hover:text-gray-200 hover:bg-gray-700/40"}`;
|
||||
const pickerItem = (opt, current, onPick) => React.createElement("button", {
|
||||
key: String(opt.value),
|
||||
type: "button",
|
||||
onClick: () => {
|
||||
onPick(opt.value);
|
||||
setOpenMenu(null);
|
||||
},
|
||||
// Comfortable tap target + readable size on mobile.
|
||||
className: `w-full text-left px-4 py-3 sm:py-2.5 text-sm flex items-center gap-3 transition-colors ${current === opt.value ? "accent-orange bg-orange-500/10" : "text-gray-200 hover:bg-gray-700/50 active:bg-gray-700/60"}`
|
||||
}, [
|
||||
React.createElement("i", { key: "i", className: `${opt.icon || "far fa-clock"} w-4 text-center` }),
|
||||
React.createElement("span", { key: "l" }, opt.label)
|
||||
]);
|
||||
const picker = (options, current, onPick) => React.createElement("div", {
|
||||
className: "absolute right-0 z-50 min-w-[180px] max-w-[78vw] rounded-xl shadow-2xl overflow-hidden",
|
||||
style: { backgroundColor: "#1f201f", border: "0 solid #e5e7eb", bottom: "100%", marginBottom: "8px" }
|
||||
}, options.map((opt) => pickerItem(opt, current, onPick)));
|
||||
const labelBtn = (key, { active, icon, label, title, onClick }) => React.createElement("button", {
|
||||
key,
|
||||
type: "button",
|
||||
onClick,
|
||||
title,
|
||||
className: `inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs border transition-all duration-200 ${active ? activeClass : "text-gray-400 border-gray-600/50 hover:text-gray-200 hover:border-gray-500"}`
|
||||
"aria-pressed": !!active,
|
||||
className: btnClass(active)
|
||||
}, [
|
||||
React.createElement("i", { key: "i", className: icon }),
|
||||
label ? React.createElement("span", { key: "l" }, label) : null
|
||||
React.createElement("i", { key: "i", className: `${icon} text-[13px]` }),
|
||||
React.createElement("span", { key: "l", className: "leading-none" }, label)
|
||||
]);
|
||||
return React.createElement("div", {
|
||||
className: "flex items-center flex-wrap gap-2 pb-3"
|
||||
}, [
|
||||
pill("code", {
|
||||
return React.createElement("div", { className: "flex items-center gap-1" }, [
|
||||
// Invisible backdrop closes any open picker on outside click.
|
||||
openMenu && React.createElement("div", {
|
||||
key: "backdrop",
|
||||
className: "fixed inset-0 z-40",
|
||||
onClick: () => setOpenMenu(null)
|
||||
}),
|
||||
// Code — toggles code mode (expands the input box).
|
||||
labelBtn("code", {
|
||||
active: codeMode,
|
||||
activeClass: "text-green-400 border-green-500/40 bg-green-500/10",
|
||||
icon: "fas fa-code",
|
||||
label: "Code",
|
||||
title: "Send as a code block (with copy button)",
|
||||
title: "Send as a code block (expands the input)",
|
||||
onClick: () => setCodeMode((v) => !v)
|
||||
}),
|
||||
pill("once", {
|
||||
active: viewOnceMode,
|
||||
activeClass: "text-orange-400 border-orange-500/40 bg-orange-500/10",
|
||||
icon: "fas fa-eye-slash",
|
||||
label: "View once",
|
||||
title: "Recipient can read it once, then it is deleted (cooperative \u2014 not screenshot-proof)",
|
||||
onClick: () => setViewOnceMode((v) => !v)
|
||||
}),
|
||||
pill("ttl", {
|
||||
active: disappearTtl > 0,
|
||||
activeClass: "text-blue-400 border-blue-500/40 bg-blue-500/10",
|
||||
icon: "fas fa-stopwatch",
|
||||
label: `Timer: ${ttlLabel(disappearTtl)}`,
|
||||
title: "Disappearing messages \u2014 auto-delete on both sides",
|
||||
onClick: cycleTtl
|
||||
}),
|
||||
React.createElement("div", { key: "spacer", className: "flex-1" }),
|
||||
pill("panic", {
|
||||
active: true,
|
||||
activeClass: "text-red-400 border-red-500/40 bg-red-500/10 hover:bg-red-500/20",
|
||||
icon: "fas fa-fire-extinguisher",
|
||||
label: "Panic",
|
||||
title: "Wipe this conversation and keys, and disconnect",
|
||||
onClick: () => {
|
||||
const ok = typeof window !== "undefined" && window.confirm ? window.confirm("Panic wipe: delete all messages, wipe keys and disconnect now?") : true;
|
||||
if (ok && typeof onPanicWipe === "function") onPanicWipe();
|
||||
}
|
||||
})
|
||||
// View once — pick how long it stays after the peer opens it.
|
||||
React.createElement("div", { key: "once", className: "relative" }, [
|
||||
labelBtn("once-btn", {
|
||||
active: viewOnceMode,
|
||||
icon: "fas fa-eye-slash",
|
||||
label: viewOnceMode ? `Once \xB7 ${fmt(viewOnceTtl)}` : "View once",
|
||||
title: "View once \u2014 vanishes after the peer reads it",
|
||||
onClick: () => setOpenMenu(openMenu === "once" ? null : "once")
|
||||
}),
|
||||
openMenu === "once" && picker([
|
||||
{ value: 0, label: "Off", icon: "fas fa-ban" },
|
||||
{ value: 5, label: "5s after reading" },
|
||||
{ value: 15, label: "15s after reading" },
|
||||
{ value: 30, label: "30s after reading" },
|
||||
{ value: 60, label: "1m after reading" }
|
||||
], viewOnceMode ? viewOnceTtl : 0, (v) => {
|
||||
if (v === 0) {
|
||||
setViewOnceMode(false);
|
||||
} else {
|
||||
setViewOnceTtl(v);
|
||||
setViewOnceMode(true);
|
||||
}
|
||||
})
|
||||
]),
|
||||
// Timer — pick the disappearing duration.
|
||||
React.createElement("div", { key: "timer", className: "relative" }, [
|
||||
labelBtn("timer-btn", {
|
||||
active: disappearTtl > 0,
|
||||
icon: "fas fa-stopwatch",
|
||||
label: disappearTtl > 0 ? `Timer \xB7 ${fmt(disappearTtl)}` : "Timer",
|
||||
title: "Disappearing message \u2014 deletes on both sides",
|
||||
onClick: () => setOpenMenu(openMenu === "timer" ? null : "timer")
|
||||
}),
|
||||
openMenu === "timer" && picker([
|
||||
{ value: 0, label: "Off", icon: "fas fa-ban" },
|
||||
{ value: 30, label: "30 seconds" },
|
||||
{ value: 300, label: "5 minutes" },
|
||||
{ value: 3600, label: "1 hour" }
|
||||
], disappearTtl, (v) => setDisappearTtl(v))
|
||||
])
|
||||
]);
|
||||
};
|
||||
var EnhancedCopyButton = ({ text, className = "", children }) => {
|
||||
@@ -536,7 +689,7 @@ var VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed,
|
||||
])
|
||||
]);
|
||||
};
|
||||
var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt, nowTick, canUnsend, onUnsend, onExpire }) => {
|
||||
var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, viewOnceTtl, expiresAt, nowTick, canUnsend, onUnsend, onExpire }) => {
|
||||
const [revealed, setRevealed] = React.useState(false);
|
||||
const revealTimerRef = React.useRef(null);
|
||||
const formatTime = (ts) => {
|
||||
@@ -553,7 +706,7 @@ var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt,
|
||||
switch (type) {
|
||||
case "sent":
|
||||
return {
|
||||
container: "ml-auto bg-orange-500/15 border-orange-500/20 text-primary",
|
||||
container: "ml-auto bg-orange-500/5 border-orange-500/20 text-primary",
|
||||
icon: "fas fa-lock accent-orange",
|
||||
label: "Encrypted"
|
||||
};
|
||||
@@ -583,9 +736,10 @@ var EnhancedChatMessage = ({ message, type, timestamp, mid, viewOnce, expiresAt,
|
||||
const handleReveal = () => {
|
||||
if (revealed) return;
|
||||
setRevealed(true);
|
||||
const ms = Math.max(1, typeof viewOnceTtl === "number" ? viewOnceTtl : 15) * 1e3;
|
||||
revealTimerRef.current = setTimeout(() => {
|
||||
onExpire && onExpire();
|
||||
}, 12e3);
|
||||
}, ms);
|
||||
};
|
||||
let body;
|
||||
if (isViewOnce && !revealed) {
|
||||
@@ -1531,12 +1685,13 @@ var EnhancedChatInterface = ({
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
nowTick,
|
||||
onUnsendMessage,
|
||||
onMessageExpire,
|
||||
onPanicWipe
|
||||
onMessageExpire
|
||||
}) => {
|
||||
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
||||
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
|
||||
@@ -1687,6 +1842,7 @@ var EnhancedChatInterface = ({
|
||||
timestamp: msg.timestamp,
|
||||
mid: msg.mid,
|
||||
viewOnce: msg.viewOnce,
|
||||
viewOnceTtl: msg.viewOnceTtl,
|
||||
expiresAt: msg.expiresAt,
|
||||
nowTick,
|
||||
canUnsend: typeof onUnsendMessage === "function",
|
||||
@@ -1726,36 +1882,53 @@ var EnhancedChatInterface = ({
|
||||
"div",
|
||||
{ className: "max-w-4xl mx-auto px-4" },
|
||||
[
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: () => setShowFileTransfer(!showFileTransfer),
|
||||
className: `flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4 ${showFileTransfer ? "mb-4" : ""}`
|
||||
},
|
||||
[
|
||||
React.createElement(
|
||||
"svg",
|
||||
{
|
||||
className: `w-4 h-4 mr-2 transform transition-transform ${showFileTransfer ? "rotate-180" : ""}`,
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
viewBox: "0 0 24 24"
|
||||
},
|
||||
showFileTransfer ? React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M5 15l7-7 7 7"
|
||||
}) : React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
})
|
||||
),
|
||||
showFileTransfer ? "Hide file transfer" : "Send files"
|
||||
]
|
||||
),
|
||||
React.createElement("div", {
|
||||
key: "composer-bar",
|
||||
className: `flex items-center justify-between flex-wrap gap-2 ${showFileTransfer ? "mb-4" : ""}`
|
||||
}, [
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
key: "send-files-toggle",
|
||||
onClick: () => setShowFileTransfer(!showFileTransfer),
|
||||
className: "flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4"
|
||||
},
|
||||
[
|
||||
React.createElement(
|
||||
"svg",
|
||||
{
|
||||
className: `w-4 h-4 mr-2 transform transition-transform ${showFileTransfer ? "rotate-180" : ""}`,
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
viewBox: "0 0 24 24"
|
||||
},
|
||||
showFileTransfer ? React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M5 15l7-7 7 7"
|
||||
}) : React.createElement("path", {
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeWidth: 2,
|
||||
d: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
})
|
||||
),
|
||||
showFileTransfer ? "Hide file transfer" : "Send files"
|
||||
]
|
||||
),
|
||||
React.createElement(ChatToolbar, {
|
||||
key: "toolbar",
|
||||
codeMode,
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl
|
||||
})
|
||||
]),
|
||||
showFileTransfer && React.createElement(window.FileTransferComponent || (() => React.createElement("div", {
|
||||
className: "p-4 text-center text-red-400"
|
||||
}, "FileTransferComponent not loaded")), {
|
||||
@@ -1774,16 +1947,6 @@ var EnhancedChatInterface = ({
|
||||
"div",
|
||||
{ className: "max-w-4xl mx-auto p-4" },
|
||||
[
|
||||
React.createElement(ChatToolbar, {
|
||||
key: "toolbar",
|
||||
codeMode,
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
onPanicWipe
|
||||
}),
|
||||
React.createElement(
|
||||
"div",
|
||||
{ key: "inputrow", className: "flex items-stretch space-x-3" },
|
||||
@@ -1796,11 +1959,11 @@ var EnhancedChatInterface = ({
|
||||
value: messageInput,
|
||||
onChange: (e) => setMessageInput(e.target.value),
|
||||
onKeyDown: handleKeyPress,
|
||||
placeholder: "Enter message to encrypt...",
|
||||
rows: 2,
|
||||
placeholder: codeMode ? "Paste or type code\u2026" : "Enter message to encrypt...",
|
||||
rows: codeMode ? 8 : 2,
|
||||
maxLength: 2e3,
|
||||
style: { backgroundColor: "#272827" },
|
||||
className: "w-full p-3 border border-gray-600 rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
||||
style: codeMode ? { backgroundColor: "#1b1c1b", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" } : { backgroundColor: "#272827" },
|
||||
className: `w-full p-3 border rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:outline-none transition-all custom-scrollbar text-sm ${codeMode ? "border-orange-500/40 focus:border-orange-500/60" : "border-gray-600 focus:border-green-500/40"}`
|
||||
}),
|
||||
React.createElement(
|
||||
"div",
|
||||
@@ -1842,6 +2005,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
const [messages, setMessages] = React.useState([]);
|
||||
const [codeMode, setCodeMode] = React.useState(false);
|
||||
const [viewOnceMode, setViewOnceMode] = React.useState(false);
|
||||
const [viewOnceTtl, setViewOnceTtl] = React.useState(15);
|
||||
const [disappearTtl, setDisappearTtl] = React.useState(0);
|
||||
const [nowTick, setNowTick] = React.useState(() => Date.now());
|
||||
const [connectionStatus, setConnectionStatus] = React.useState("disconnected");
|
||||
@@ -1970,6 +2134,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
timestamp: Date.now(),
|
||||
mid: opts.mid,
|
||||
viewOnce: opts.viewOnce === true,
|
||||
viewOnceTtl: typeof opts.viewOnceTtl === "number" ? opts.viewOnceTtl : 15,
|
||||
expiresAt: typeof opts.expiresAt === "number" ? opts.expiresAt : void 0
|
||||
};
|
||||
setMessages((prev) => {
|
||||
@@ -2106,7 +2271,10 @@ var EnhancedSecureP2PChat = () => {
|
||||
const opts = {};
|
||||
if (meta && typeof meta === "object") {
|
||||
if (typeof meta.mid === "string") opts.mid = meta.mid;
|
||||
if (meta.once === true) opts.viewOnce = true;
|
||||
if (meta.once === true) {
|
||||
opts.viewOnce = true;
|
||||
opts.viewOnceTtl = Number.isFinite(meta.onceTtl) ? meta.onceTtl : 15;
|
||||
}
|
||||
if (Number.isFinite(meta.ttl) && meta.ttl > 0) {
|
||||
opts.expiresAt = Date.now() + meta.ttl * 1e3;
|
||||
}
|
||||
@@ -2270,7 +2438,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.8.15 - 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");
|
||||
handleMessage(" SecureBit.chat Enhanced Security Edition v4.8.20 - 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");
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (event.type === "beforeunload" && !isTabSwitching) {
|
||||
if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
|
||||
@@ -3470,7 +3638,10 @@ var EnhancedSecureP2PChat = () => {
|
||||
const outText = codeMode ? "```\n" + baseText + "\n```" : baseText;
|
||||
const mid = `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const meta = { mid };
|
||||
if (viewOnceMode) meta.once = true;
|
||||
if (viewOnceMode) {
|
||||
meta.once = true;
|
||||
meta.onceTtl = viewOnceTtl;
|
||||
}
|
||||
if (disappearTtl > 0) meta.ttl = disappearTtl;
|
||||
const localOpts = { mid };
|
||||
if (disappearTtl > 0) localOpts.expiresAt = Date.now() + disappearTtl * 1e3;
|
||||
@@ -3497,36 +3668,6 @@ var EnhancedSecureP2PChat = () => {
|
||||
const handleMessageExpire = React.useCallback((id) => {
|
||||
setMessages((prev) => prev.filter((m) => m.id !== id));
|
||||
}, []);
|
||||
const handlePanicWipe = React.useCallback(() => {
|
||||
setMessages([]);
|
||||
try {
|
||||
const mgr = webrtcManagerRef.current;
|
||||
if (mgr) {
|
||||
if (typeof mgr._secureWipeKeys === "function") {
|
||||
try {
|
||||
mgr._secureWipeKeys();
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
if (typeof mgr.disconnect === "function") {
|
||||
try {
|
||||
mgr.disconnect();
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
}
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent("disconnected"));
|
||||
} catch (_) {
|
||||
}
|
||||
setConnectionStatus("disconnected");
|
||||
setIsVerified(false);
|
||||
setKeyFingerprint("");
|
||||
setSecurityLevel(null);
|
||||
setMessageInput("");
|
||||
}, []);
|
||||
const handleClearData = () => {
|
||||
setOfferData("");
|
||||
setAnswerData("");
|
||||
@@ -3788,12 +3929,13 @@ var EnhancedSecureP2PChat = () => {
|
||||
setCodeMode,
|
||||
viewOnceMode,
|
||||
setViewOnceMode,
|
||||
viewOnceTtl,
|
||||
setViewOnceTtl,
|
||||
disappearTtl,
|
||||
setDisappearTtl,
|
||||
nowTick,
|
||||
onUnsendMessage: handleUnsendMessage,
|
||||
onMessageExpire: handleMessageExpire,
|
||||
onPanicWipe: handlePanicWipe
|
||||
onMessageExpire: handleMessageExpire
|
||||
});
|
||||
})() : React.createElement(EnhancedConnectionSetup, {
|
||||
onCreateOffer: handleCreateOffer,
|
||||
|
||||
Reference in New Issue
Block a user