κ-store: 12 shell blobs (aggregate 5061bc7afbef)
Browse files- .gitattributes +1 -0
- b/040b54338f22e6abbc19071ec1b97487717982c8ddc7fa3500d7c46269e93344 +0 -0
- b/076005621416e21a26486060b3f618d08f594fb0fea18a84375e68ba4256bebc +0 -0
- b/11d9ba0d372a50f7402c01cf9b2934fdb0b34a33425d308ec130e8e277de5260 +0 -0
- b/1f7746684dce60648c3d5ffbd4d289f862222ab4eb82c14c5f07239d1b47f5c6 +16 -0
- b/38689d2013d1f64086bf523e77d557616cd702511c322e60ef15a5ebb325b6ad +127 -0
- b/3946cf9e0a3bfb709b722ab949131b69f5670cbb75221f1c28701877ecdd4ec6 +36 -0
- b/4e8a770de63abc7d655780dad449eda292e54e62f96b7252ae978bbca40f1586 +256 -0
- b/6bbf54f41980838ea8cf1c08123b7aa6e9820eeb58da964cd50e875b2893faef +36 -0
- b/8bd23f0acbaf00093b9daedeac08c9513ccbe0a8647b91eb104bfd27175682a8 +30 -0
- b/9e602f8270c163a794c8e1b9d455569a2069c15e2f7db955ded05659b326567b +196 -0
- b/9f62d9e0480be19a114542104a51a4e69da89a49c2960d75910ed0b57ea41c55 +141 -0
- b/a0c782c6ebee1f7d91088089ea3a39942725459947f8882be8534cdb9f3d4029 +0 -0
- b/be803875bc9cdbbc411c84e5f8a407189d775d7d9fc3e72f0a28093f37414933 +380 -0
- b/c741295f6742e247afb4eabc8b1f7423a4171c56f8bc933aa7a696e8ef97756d +0 -0
- b/d80d218db7c66a0414c0c02204d0e0398249b2ec619f903d02d97781fef92102 +36 -0
- b/e811350a91c04a5999d552279aca78f833ec7946ae929e0af42575fb62c061c0 +0 -0
- b/ec9aea0c39ca06aa2001b22aef511d449b8e64c2c2b095ffedf46e2492f03b19 +0 -0
- b/f1ff491ce68b55a5a5d68a825e6a78dcb0ff53edd426f99c2b095573e93acd01 +0 -0
- b/f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51 +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
b/f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51 filter=lfs diff=lfs merge=lfs -text
|
b/040b54338f22e6abbc19071ec1b97487717982c8ddc7fa3500d7c46269e93344
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/076005621416e21a26486060b3f618d08f594fb0fea18a84375e68ba4256bebc
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/11d9ba0d372a50f7402c01cf9b2934fdb0b34a33425d308ec130e8e277de5260
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/1f7746684dce60648c3d5ffbd4d289f862222ab4eb82c14c5f07239d1b47f5c6
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// messenger-skin.mjs — bakes the SLACK re-tone as the messenger default (aubergine chrome + a floating,
|
| 2 |
+
// inset content pane; see the data-skin="slack" block in messenger-skins.css) and applies the hi-DPI /
|
| 3 |
+
// high-FPS perf layer. No picker / no toggle. Additive + guarded. Escape hatch: set localStorage
|
| 4 |
+
// "holo.skin" to graphite | warm | contrast | light (or "" to remove) to override the default.
|
| 5 |
+
|
| 6 |
+
(function () {
|
| 7 |
+
if (!document.querySelector("link[data-holo-skins]")) {
|
| 8 |
+
const l = document.createElement("link"); l.rel = "stylesheet"; l.href = "/apps/holo-messenger/messenger-skins.css"; l.setAttribute("data-holo-skins", "1"); document.head.appendChild(l);
|
| 9 |
+
}
|
| 10 |
+
let skin = "slack";
|
| 11 |
+
try { const s = localStorage.getItem("holo.skin"); if (s) skin = s; } catch {}
|
| 12 |
+
if (skin) document.documentElement.setAttribute("data-skin", skin); else document.documentElement.removeAttribute("data-skin");
|
| 13 |
+
// remove any lingering picker + its persisted flag from earlier ?skins=1 sessions
|
| 14 |
+
try { localStorage.removeItem("holo.skins"); } catch {}
|
| 15 |
+
try { const p = document.querySelector(".holo-skin-pick"); if (p) p.remove(); } catch {}
|
| 16 |
+
})();
|
b/38689d2013d1f64086bf523e77d557616cd702511c322e60ef15a5ebb325b6ad
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-m1-boot.mjs — M1: INSTANT SERVERLESS BOOT. One self-verifying link boots the WHOLE messenger.
|
| 2 |
+
//
|
| 3 |
+
// A single web link that, opened in a COLD browser, brings up the entire Holo Messenger — shell + your
|
| 4 |
+
// identity + Q — with ZERO origin servers in the delivery path (every shell byte is a content-addressed κ
|
| 5 |
+
// object, served from cache/OPFS on repeat opens). The descriptor travels INSIDE the link (b64url JSON in
|
| 6 |
+
// the URL fragment), exactly like holo-chat-link.mjs / holo-together.mjs: a bare browser verifies it LOCALLY
|
| 7 |
+
// (Law L5: re-derive the κ), then boots. The κ is SHA-256 of the canonical PUBLIC fields — the shareable,
|
| 8 |
+
// tamper-evident id. Any private pairing bearer is EXCLUDED from the κ (whoever holds the link may adopt it).
|
| 9 |
+
//
|
| 10 |
+
// This commits to the SHELL MANIFEST (the exact asset set the service worker precaches), so a tampered link
|
| 11 |
+
// that points the boot at a different/injected asset set breaks integrity and is refused. Laws: L2 canonical,
|
| 12 |
+
// L5 verify-by-re-derivation. Additive + fail-soft: if anything here throws, app.html's normal boot proceeds.
|
| 13 |
+
|
| 14 |
+
export const BOOT_LINK_VERSION = 1;
|
| 15 |
+
const ENTRY_PATH = "/apps/holo-messenger/app.html";
|
| 16 |
+
|
| 17 |
+
// THE SHELL — the minimal asset set required to paint the messenger and reach interactive, and the single
|
| 18 |
+
// source of truth the service worker precaches (messenger-sw.js mirrors these basenames; the M1 witness
|
| 19 |
+
// asserts the two never drift). Q's brain, the bridges, and all media are DELIBERATELY absent: they are
|
| 20 |
+
// deferred (never on the first-paint critical path), so the shell stays tiny and cache-resident.
|
| 21 |
+
export const SHELL_MANIFEST = [
|
| 22 |
+
"/apps/holo-messenger/app.html",
|
| 23 |
+
"/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js",
|
| 24 |
+
"/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css",
|
| 25 |
+
"/apps/holo-messenger/holo-messenger-app.mjs",
|
| 26 |
+
"/apps/holo-messenger/messenger-shadcn-ui.mjs",
|
| 27 |
+
"/apps/holo-messenger/holo-messenger-weave.mjs",
|
| 28 |
+
"/apps/holo-messenger/messenger-skin.mjs",
|
| 29 |
+
"/apps/holo-messenger/messenger-skins.css",
|
| 30 |
+
"/apps/holo-messenger/messenger-sw-register.mjs",
|
| 31 |
+
"/apps/holo-messenger/holo-m1-boot.mjs",
|
| 32 |
+
"/apps/holo-messenger/mail/holo-mail-attach.mjs",
|
| 33 |
+
"/apps/holo-messenger/_vendor/wallpaper-default.jpg",
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
const _te = new TextEncoder();
|
| 37 |
+
async function _sha256hex(s) { const h = await (globalThis.crypto || crypto).subtle.digest("SHA-256", _te.encode(s)); return [...new Uint8Array(h)].map((x) => x.toString(16).padStart(2, "0")).join(""); }
|
| 38 |
+
function _b64urlEncode(obj) { const b = btoa(unescape(encodeURIComponent(JSON.stringify(obj)))); return b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); }
|
| 39 |
+
function _b64urlDecode(str) { const b = String(str).replace(/-/g, "+").replace(/_/g, "/"); return JSON.parse(decodeURIComponent(escape(atob(b)))); }
|
| 40 |
+
|
| 41 |
+
// The shell manifest (shell-manifest.json, minted by _build/gen-shell-manifest.mjs) is the content hash of
|
| 42 |
+
// every shell BYTE: [{ path, kappa=SHA-256(bytes) }]. In a browser we fetch it; in Node the caller passes it.
|
| 43 |
+
export async function loadShellManifest() {
|
| 44 |
+
try {
|
| 45 |
+
if (typeof fetch === "function" && typeof location !== "undefined") {
|
| 46 |
+
const r = await fetch("/apps/holo-messenger/shell-manifest.json", { cache: "no-store" });
|
| 47 |
+
if (r && r.ok) return await r.json();
|
| 48 |
+
}
|
| 49 |
+
} catch {}
|
| 50 |
+
return null;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// The ONE hash the boot link commits: SHA-256 over the SORTED per-asset content κs. Any browser re-derives it
|
| 54 |
+
// from shell-manifest.json → thereby verifies EVERY shell byte (Law L5). Recomputed from `assets`, never trusting
|
| 55 |
+
// the manifest's self-declared `aggregate`, so a forged manifest fails. Empty (unverifiable) → no commitment.
|
| 56 |
+
export async function shellAggregate(manifest) {
|
| 57 |
+
const m = manifest || (await loadShellManifest());
|
| 58 |
+
if (!m || !Array.isArray(m.assets) || !m.assets.length) return "";
|
| 59 |
+
return _sha256hex(m.assets.map((a) => String(a.kappa)).sort().join(","));
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// canonical PUBLIC form — any private `pair` bearer is EXCLUDED so the κ is the shareable id. Commits to the
|
| 63 |
+
// entry, the shell set, the display identity (so a tampered "this is <who>'s messenger" card breaks — anti-phish).
|
| 64 |
+
function _canon(d) {
|
| 65 |
+
return [
|
| 66 |
+
"v" + (d.v | 0), d.kind || "boot", d.app || "holo-messenger", d.entry || ENTRY_PATH,
|
| 67 |
+
d.shell || "", d.name || "", d.truename || "", d.op || "",
|
| 68 |
+
String(d.created || 0), String(d.expires || 0),
|
| 69 |
+
].join("␟");
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Build the one-link door for the whole messenger. `op`/`name`/`truename` are PUBLIC display identity (the
|
| 73 |
+
// landing shows "resume <name>'s messenger"); `pair` is an optional bearer pairing payload (device-roam) and
|
| 74 |
+
// is NOT part of the κ. Opening the link still auto-enrolls a sovereign identity unless a pair bearer is honored.
|
| 75 |
+
export async function makeBootLink({ op = "", name = "", truename = "", pair = null, ttlSeconds = 30 * 24 * 3600, origin = null, nowMs = null, aggregate = null, manifest = null } = {}) {
|
| 76 |
+
const now = nowMs || Date.now();
|
| 77 |
+
const shell = aggregate || (await shellAggregate(manifest)); // commit the shell BYTES (aggregate content κ)
|
| 78 |
+
const d = {
|
| 79 |
+
v: BOOT_LINK_VERSION, kind: "boot", app: "holo-messenger", entry: ENTRY_PATH,
|
| 80 |
+
shell,
|
| 81 |
+
op: String(op || "").slice(0, 96), name: String(name || "").slice(0, 60), truename: String(truename || "").slice(0, 60),
|
| 82 |
+
created: now, expires: now + ttlSeconds * 1000,
|
| 83 |
+
};
|
| 84 |
+
if (pair) d.pair = pair; // bearer pairing payload — NOT part of the κ
|
| 85 |
+
d.kappa = await _sha256hex(_canon(d));
|
| 86 |
+
const payload = _b64urlEncode(d);
|
| 87 |
+
const org = origin || (typeof location !== "undefined" ? location.origin : "");
|
| 88 |
+
return { kappa: d.kappa, payload, descriptor: d,
|
| 89 |
+
https: `${org}${ENTRY_PATH}#m1=${payload}`, // opens in ANY cold browser
|
| 90 |
+
holo: `holo://os${ENTRY_PATH}#m1=${payload}` }; // in-shell full surface
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// parse a boot link (URL, holo:// form, `#m1=<payload>`, or bare payload) → { ok, descriptor, integrity, expired }.
|
| 94 |
+
export async function parseBootLink(input, { nowMs = null, aggregate = null, manifest = null } = {}) {
|
| 95 |
+
let payload = String(input || "");
|
| 96 |
+
if (payload.includes("#")) payload = payload.split("#").pop();
|
| 97 |
+
payload = payload.replace(/^m1=/, "");
|
| 98 |
+
let d; try { d = _b64urlDecode(payload); } catch { return { ok: false, error: "unreadable boot link" }; }
|
| 99 |
+
if (!d || d.v !== BOOT_LINK_VERSION || d.kind !== "boot" || d.app !== "holo-messenger" || !d.kappa) return { ok: false, error: "not a valid messenger boot link" };
|
| 100 |
+
const integrity = (await _sha256hex(_canon(d))) === d.kappa; // Law L5: re-derive; tamper → false
|
| 101 |
+
const current = aggregate != null ? aggregate : (await shellAggregate(manifest));// THIS build's shell-byte aggregate
|
| 102 |
+
const shellMatches = !!current && d.shell === current; // link's shell BYTES must match (unverifiable → false)
|
| 103 |
+
const expired = !!(d.expires && (nowMs || Date.now()) > d.expires);
|
| 104 |
+
return { ok: true, descriptor: d, integrity, shellMatches, expired, aggregate: current };
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// convenience: verify + return the boot descriptor only if it is safe to act on (integrity + shell + fresh).
|
| 108 |
+
export async function resolveBootLink(input, opts = {}) {
|
| 109 |
+
const p = await parseBootLink(input, opts);
|
| 110 |
+
if (!p.ok) return { ok: false, reason: p.error };
|
| 111 |
+
if (!p.integrity) return { ok: false, reason: "tampered" };
|
| 112 |
+
if (!p.shellMatches) return { ok: false, reason: "shell-mismatch" };
|
| 113 |
+
if (p.expired) return { ok: false, reason: "expired" };
|
| 114 |
+
return { ok: true, descriptor: p.descriptor, pair: p.descriptor.pair || null };
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
export function describe(d) {
|
| 118 |
+
const who = (d && (d.name || d.truename)) || "someone";
|
| 119 |
+
return { headline: `Resume ${who}'s messenger`, cta: "Open" };
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export function installM1Boot() {
|
| 123 |
+
if (typeof window === "undefined") return false;
|
| 124 |
+
window.HoloM1Boot = Object.assign(window.HoloM1Boot || {}, { version: BOOT_LINK_VERSION, SHELL_MANIFEST, makeBootLink, parseBootLink, resolveBootLink, describe, loadShellManifest, shellAggregate });
|
| 125 |
+
return true;
|
| 126 |
+
}
|
| 127 |
+
try { installM1Boot(); } catch {}
|
b/3946cf9e0a3bfb709b722ab949131b69f5670cbb75221f1c28701877ecdd4ec6
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en"><head>
|
| 3 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 4 |
+
<title>Holo Messenger</title>
|
| 5 |
+
<link rel="stylesheet" href="/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css?v=ec9aea0c39ca">
|
| 6 |
+
<style>html,body{height:100%;margin:0;background:#0b141a;color-scheme:dark}#root{height:100vh}</style>
|
| 7 |
+
<link rel="preload" as="image" href="/apps/holo-messenger/_vendor/wallpaper-default.jpg" fetchpriority="high">
|
| 8 |
+
</head><body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module">
|
| 11 |
+
// ?v=N - bump on each deploy so the browser can never serve a stale cached bundle (the cause of a frozen/old build).
|
| 12 |
+
import "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js?v=c741295f6742"; // → window.HoloMessengerUI
|
| 13 |
+
// Opt-in: render the WhatsApp UX from STREAMING shadcn κ-components instead of the chatscope bundle.
|
| 14 |
+
// Additive — only takes over window.HoloMessengerUI.mount when ?ui=shadcn (or localStorage["holo.ui.shadcn"]="1");
|
| 15 |
+
// the default boot stays the untouched chatscope UI. Runs before boot() so it is fed the same live model.
|
| 16 |
+
import "/apps/holo-messenger/messenger-shadcn-ui.mjs";
|
| 17 |
+
import { boot } from "/apps/holo-messenger/holo-messenger-app.mjs?v=040b54338f22";
|
| 18 |
+
boot(document.getElementById("root")).catch(e => {
|
| 19 |
+
document.body.innerHTML = "<pre style='color:#f88;padding:16px;white-space:pre-wrap'>boot error: " + (e && e.stack || e) + "</pre>";
|
| 20 |
+
});
|
| 21 |
+
// Intelligent Inbox (holo-mail) - additive, feature-flagged OFF by default. Dynamic import + catch so it
|
| 22 |
+
// can NEVER block or break the messenger boot. Enable per-operator: localStorage["holo.mail.enabled"]="1".
|
| 23 |
+
import("/apps/holo-messenger/mail/holo-mail-attach.mjs").catch(() => {});
|
| 24 |
+
// HoloChat, woven into THIS messenger — one canonical surface, no separate overlay. Cards render natively
|
| 25 |
+
// in the bubbles; the composer "+" opens the unified tray (apps · Spaces · money · identity · prove).
|
| 26 |
+
// Additive + catch-guarded so it can never block boot. Enable: localStorage["holo.chat.enabled"]="1" or ?chat=1.
|
| 27 |
+
import("/apps/holo-messenger/holo-messenger-weave.mjs").catch(() => {});
|
| 28 |
+
// Colour re-tone picker (Default · Graphite · Warm · Contrast · Light) — colours only, all features
|
| 29 |
+
// intact. Gated: shows only with ?skins=1 or localStorage["holo.skins"]="1". Additive + catch-guarded.
|
| 30 |
+
import("/apps/holo-messenger/messenger-skin.mjs").catch(() => {});
|
| 31 |
+
// Asset cache (service worker) — repeat opens serve wallpaper/logos/bundle/css/js NETWORK-FREE. Caches
|
| 32 |
+
// only own-origin static assets (stale-while-revalidate); all dynamic/API/bridge/media traffic passes
|
| 33 |
+
// through untouched. Additive + catch-guarded. Escape hatch: open with ?nosw=1 to unregister.
|
| 34 |
+
import("/apps/holo-messenger/messenger-sw-register.mjs").catch(() => {});
|
| 35 |
+
</script>
|
| 36 |
+
</body></html>
|
b/4e8a770de63abc7d655780dad449eda292e54e62f96b7252ae978bbca40f1586
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* messenger-skins.css — OPTIONAL colour re-tones, applied ONLY when <html> carries data-skin="…".
|
| 2 |
+
* Default (no attribute) is untouched, so this can never disturb the shipping look. Colours only — no
|
| 3 |
+
* layout — safe and fully reversible.
|
| 4 |
+
*
|
| 5 |
+
* The theme drives ~half its colour from --wa-* vars on .holo-wa-root and hardcodes the other half to the
|
| 6 |
+
* same values. This shared block re-points the major hardcoded surfaces back onto the vars; each skin then
|
| 7 |
+
* only remaps the vars and the whole surface re-tones consistently. */
|
| 8 |
+
|
| 9 |
+
html[data-skin] .holo-wa-root { background: var(--wa-bg) !important; color: var(--wa-ink); }
|
| 10 |
+
html[data-skin] .holo-wa-root .cs-main-container,
|
| 11 |
+
html[data-skin] .holo-wa-root .cs-chat-container,
|
| 12 |
+
html[data-skin] .holo-wa-root .cs-message-list { background: transparent; }
|
| 13 |
+
html[data-skin] .holo-wa-root .cs-sidebar,
|
| 14 |
+
html[data-skin] .holo-wa-root .holo-rail,
|
| 15 |
+
html[data-skin] .holo-wa-root .cs-conversation-list { background: var(--wa-side) !important; }
|
| 16 |
+
html[data-skin] .holo-wa-root .cs-conversation-header,
|
| 17 |
+
html[data-skin] .holo-wa-root .holo-list-title,
|
| 18 |
+
html[data-skin] .holo-wa-root .cs-message-input,
|
| 19 |
+
html[data-skin] .holo-wa-root .holo-composer { background: var(--wa-head) !important; }
|
| 20 |
+
html[data-skin] .holo-wa-root .cs-search,
|
| 21 |
+
html[data-skin] .holo-wa-root .holo-wp-search,
|
| 22 |
+
html[data-skin] .holo-wa-root .cs-message-input__content-editor-wrapper,
|
| 23 |
+
html[data-skin] .holo-wa-root .cs-message-input__content-editor { background: color-mix(in srgb, var(--wa-head) 78%, var(--wa-bg)) !important; color: var(--wa-ink) !important; }
|
| 24 |
+
html[data-skin] .holo-wa-root .cs-conversation:hover,
|
| 25 |
+
html[data-skin] .holo-wa-root .cs-conversation.cs-conversation--active { background: var(--wa-head) !important; }
|
| 26 |
+
html[data-skin] .holo-wa-root .cs-conversation__name,
|
| 27 |
+
html[data-skin] .holo-wa-root .cs-conversation-header__user-name { color: var(--wa-ink) !important; }
|
| 28 |
+
html[data-skin] .holo-wa-root .cs-conversation__info,
|
| 29 |
+
html[data-skin] .holo-wa-root .cs-conversation__last-sender,
|
| 30 |
+
html[data-skin] .holo-wa-root .cs-conversation-header__info { color: var(--wa-dim) !important; }
|
| 31 |
+
html[data-skin] .holo-wa-root .cs-message--incoming .cs-message__content { background: var(--wa-in) !important; color: var(--wa-ink) !important; }
|
| 32 |
+
html[data-skin] .holo-wa-root .cs-message--outgoing .cs-message__content { background: var(--wa-out) !important; }
|
| 33 |
+
html[data-skin] .holo-wa-root .cs-message__content { color: var(--wa-ink); }
|
| 34 |
+
html[data-skin] .holo-wa-root .holo-rail-btn.on,
|
| 35 |
+
html[data-skin] .holo-wa-root .holo-rail-net.on,
|
| 36 |
+
html[data-skin] .holo-wa-root .holo-catchup-pill { color: var(--wa-accent) !important; }
|
| 37 |
+
html[data-skin] .holo-wa-root .holo-c-send,
|
| 38 |
+
html[data-skin] .holo-wa-root .cs-button--send { background: var(--wa-accent) !important; }
|
| 39 |
+
|
| 40 |
+
/* ── cooler / graphite ── */
|
| 41 |
+
html[data-skin="graphite"] .holo-wa-root { --wa-bg:#0d1117; --wa-side:#12171e; --wa-head:#1c232c; --wa-in:#1c232c; --wa-out:#1f4a55; --wa-ink:#e6eaf0; --wa-dim:#8b96a3; --wa-accent:#4ab3d4; --wa-line:#222a34; }
|
| 42 |
+
/* ── warmer ── */
|
| 43 |
+
html[data-skin="warm"] .holo-wa-root { --wa-bg:#14100c; --wa-side:#1b1611; --wa-head:#271f18; --wa-in:#271f18; --wa-out:#4d3f1f; --wa-ink:#efe8df; --wa-dim:#a89a88; --wa-accent:#e0a53c; --wa-line:#2c241b; }
|
| 44 |
+
/* ── higher contrast ── */
|
| 45 |
+
html[data-skin="contrast"] .holo-wa-root { --wa-bg:#000000; --wa-side:#0a0a0a; --wa-head:#181818; --wa-in:#1d1d1d; --wa-out:#00805e; --wa-ink:#ffffff; --wa-dim:#a6adb4; --wa-accent:#00e08a; --wa-line:#2a2a2a; }
|
| 46 |
+
/* ── light mode (experimental: some deep-hardcoded text may stay dark) ── */
|
| 47 |
+
html[data-skin="light"] .holo-wa-root { --wa-bg:#d7dce1; --wa-side:#ffffff; --wa-head:#f0f2f5; --wa-in:#ffffff; --wa-out:#d9fdd3; --wa-ink:#111b21; --wa-dim:#54656f; --wa-accent:#008069; --wa-line:#e4e8ec; }
|
| 48 |
+
html[data-skin="light"] .holo-wa-root .cs-message__content { color: #111b21 !important; }
|
| 49 |
+
html[data-skin="light"] .holo-wa-root .cs-message-list { background: var(--wa-bg) !important; background-image: none !important; }
|
| 50 |
+
html[data-skin="light"] .holo-wa-root .holo-wp-tile { background-image: none !important; }
|
| 51 |
+
|
| 52 |
+
/* ────────────────────────────────────────────────────────────────────────────────────────────────
|
| 53 |
+
* HI-DPI + HIGH-FPS layer (always on, colour-independent). The honest "8K/retina" story: a DOM/CSS UI
|
| 54 |
+
* is ALREADY resolution-independent — text and the SVG icons are vectors, so on a retina/4K/8K panel it
|
| 55 |
+
* renders at the display's native device pixels with zero extra work (no bitmaps to upscale). What we
|
| 56 |
+
* actually tune here is smoothness + latency:
|
| 57 |
+
* • content-visibility skips rendering/layout of off-screen chat rows → a huge list scrolls at the
|
| 58 |
+
* display's full refresh (60/120Hz) and first paint is near-instant (browser does O(visible) work).
|
| 59 |
+
* • the scrollers are promoted to their own GPU layer so scrolling never touches the main thread.
|
| 60 |
+
* • crisp text rasterisation + high-quality image scaling for hi-DPI. */
|
| 61 |
+
.holo-wa-root { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
|
| 62 |
+
.holo-wa-root .cs-conversation { content-visibility: auto; contain-intrinsic-size: auto 72px; }
|
| 63 |
+
.holo-wa-root .cs-message { content-visibility: auto; contain-intrinsic-size: auto 44px; }
|
| 64 |
+
.holo-wa-root .cs-conversation-list,
|
| 65 |
+
.holo-wa-root .cs-message-list .scrollbar-container,
|
| 66 |
+
.holo-wa-root .cs-message-list__scroll-wrapper { will-change: scroll-position; transform: translateZ(0); backface-visibility: hidden; overflow-anchor: auto; }
|
| 67 |
+
.holo-wa-root .cs-avatar img, .holo-wa-root .holo-av-real, .holo-wa-root .holo-media-el img { image-rendering: auto; }
|
| 68 |
+
.holo-wa-root .holo-media-el img, .holo-wa-root .cs-message img { image-rendering: -webkit-optimize-contrast; }
|
| 69 |
+
@media (min-resolution: 1.5dppx) { .holo-wa-root { text-rendering: geometricPrecision; } }
|
| 70 |
+
|
| 71 |
+
/* floating skin picker */
|
| 72 |
+
.holo-skin-pick { position: fixed; right: 16px; bottom: 86px; z-index: 90; display: flex; gap: 4px; align-items: center; background: #0b141aee; border: 1px solid #2a3942; border-radius: 999px; padding: 5px; backdrop-filter: blur(10px); font: 12.5px ui-sans-serif, system-ui, sans-serif; box-shadow: 0 8px 30px #0007; }
|
| 73 |
+
.holo-skin-pick button { background: transparent; border: 0; color: #aebac1; border-radius: 999px; padding: 5px 11px; cursor: pointer; }
|
| 74 |
+
.holo-skin-pick button:hover { background: #ffffff14; color: #e9edef; }
|
| 75 |
+
.holo-skin-pick button.on { background: #00a884; color: #0b141a; font-weight: 600; }
|
| 76 |
+
|
| 77 |
+
/* instant wallpaper */
|
| 78 |
+
/* An inline low-res earth paints INSTANTLY (zero network — it ships in this CSS) as the chat-pane
|
| 79 |
+
* backdrop; the full 2560px wallpaper is preloaded (see app.html) and covers it the moment it decodes.
|
| 80 |
+
* So the pane is never empty and there is no visible load. */
|
| 81 |
+
html[data-skin] .holo-wa-root .cs-chat-container { background: var(--wa-bg) url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDABQODxIPDRQSERIXFhQYHzMhHxwcHz8tLyUzSkFOTUlBSEZSXHZkUldvWEZIZoxob3p9hIWET2ORm4+AmnaBhH//2wBDARYXFx8bHzwhITx/VEhUf39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3//wAARCAAgADADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCAR+1SLHVaPUYm2Zyp/jyOB9KE1AbzgjPXaRgAf41pdisjQSL2/SphDx0/Sq8epW64zknuBV6O+s3XKzJ9CcGk5tFciZGIT6fpT1h+YZx+VTC5tuomT880yTUdNgI8ycF89EXJqPavoP2aW5ya28f8bMT9cVIsVqDyCfq1VPtPIOwgfWpDcKFyvzH8q1ujKzL8bWynAiT8RmpGmTsiAfSs0XR/5aKOem3mnrdkcBGz+FF0Go6aW6ZiEwF/6ZjH61XNvdH/AJZnn3qc3rFdrQMwqP7bcKP3cSj2C0tB3Z//2Q==") center center / cover no-repeat !important; }
|
| 82 |
+
html[data-skin] .holo-wa-root .cs-message-list { transition: background-image .25s ease; }
|
| 83 |
+
|
| 84 |
+
/* ────────────────────────────────────────────────────────────────────────────────────────────────
|
| 85 |
+
* SLACK skin — data-skin="slack". Unlike the colour-only re-tones above, this one also does LAYOUT: the
|
| 86 |
+
* Slack "window envelope". An aubergine chrome (far-left rail + channel sidebar) wraps a content pane that
|
| 87 |
+
* floats as a rounded island inset by an EVEN margin on ALL FOUR sides — the darker chrome shows through
|
| 88 |
+
* every gap (top, right, bottom, AND a left gutter between sidebar and pane), with rounded corners + a soft
|
| 89 |
+
* shadow for depth. Placed at end-of-file so source order beats the base skin block AND the wallpaper rule
|
| 90 |
+
* above (equal specificity → later wins); every rule carries `.holo-wa-root` to match the base depth.
|
| 91 |
+
*
|
| 92 |
+
* Two coupling facts make the float robust: (1) the composer is a position:absolute overlay on .holo-main
|
| 93 |
+
* whose `left` is CSS `34%` — the SAME 34% as the sidebar's flex-basis — so re-anchoring it to
|
| 94 |
+
* `calc(34% + gap)` keeps it glued to the gapped pane at every window width, no JS. (2) the bundle gives the
|
| 95 |
+
* pane height:100%, which ignores margin and overflows; `height:auto` + flex:1 1 auto lets the even margin
|
| 96 |
+
* actually shrink it. ALL geometry lives in the ≥900px media query so the narrow/mobile single-column layout
|
| 97 |
+
* (sidebar collapses, composer full-bleed) is left exactly as the bundle intends. Colours + margins only,
|
| 98 |
+
* fully reversible, every feature intact. */
|
| 99 |
+
html[data-skin="slack"] .holo-wa-root {
|
| 100 |
+
--wa-bg:#150E1A; --wa-side:#2A1530; --wa-head:#1B1B1F; --wa-in:#26262B; --wa-out:#3A2140;
|
| 101 |
+
--wa-ink:#E9E5ED; --wa-dim:#A99DB4; --wa-accent:#C6A6E6; --wa-line:#ffffff12;
|
| 102 |
+
--slk-pane:#1B1B1F; --slk-hi:#5C2D66; --slk-hover:#3A2140; --slk-gap:10px;
|
| 103 |
+
background: var(--wa-bg) !important;
|
| 104 |
+
}
|
| 105 |
+
/* chrome behind the floating pane (the colour seen in the inset margin/gutter) */
|
| 106 |
+
html[data-skin="slack"] .holo-wa-root.holo-wa-root,
|
| 107 |
+
html[data-skin="slack"] .holo-wa-root .cs-main-container { background: var(--wa-bg) !important; }
|
| 108 |
+
/* the WebGPU wallpaper backdrop is off in Slack — the pane is a flat surface */
|
| 109 |
+
html[data-skin="slack"] .holo-wa-root .holo-backdrop { display: none !important; }
|
| 110 |
+
/* far-left workspace rail: darkest aubergine */
|
| 111 |
+
html[data-skin="slack"] .holo-wa-root .holo-rail { background:#1C0E22 !important; border-right:1px solid #ffffff10; }
|
| 112 |
+
html[data-skin="slack"] .holo-wa-root .holo-rail-btn.on,
|
| 113 |
+
html[data-skin="slack"] .holo-wa-root .holo-rail-net.on { color: var(--wa-accent) !important; }
|
| 114 |
+
/* cohesive aubergine sidebar (title/chips/catch-up all share the sidebar surface) */
|
| 115 |
+
html[data-skin="slack"] .holo-wa-root .holo-list-head,
|
| 116 |
+
html[data-skin="slack"] .holo-wa-root .holo-list-title,
|
| 117 |
+
html[data-skin="slack"] .holo-wa-root .holo-chips,
|
| 118 |
+
html[data-skin="slack"] .holo-wa-root .holo-catchup { background: var(--wa-side) !important; }
|
| 119 |
+
html[data-skin="slack"] .holo-wa-root .cs-search { background:#1F0F24 !important; border:1px solid #ffffff12 !important; color:var(--wa-ink) !important; }
|
| 120 |
+
/* conversation rows = inset rounded pills (Slack). Inset applied to EVERY row so hover/active only swap the
|
| 121 |
+
* background — no layout jump. Active carries the highlight + white text. */
|
| 122 |
+
html[data-skin="slack"] .holo-wa-root .cs-conversation { margin:1px 8px !important; border-radius:8px !important; }
|
| 123 |
+
html[data-skin="slack"] .holo-wa-root .cs-conversation:hover { background: var(--slk-hover) !important; }
|
| 124 |
+
html[data-skin="slack"] .holo-wa-root .cs-conversation.cs-conversation--active { background: var(--slk-hi) !important; color:#fff !important; }
|
| 125 |
+
html[data-skin="slack"] .holo-wa-root .cs-conversation.cs-conversation--active * { color:#fff !important; }
|
| 126 |
+
/* pane + its inner surfaces (colours are width-independent; the float geometry is gated below) */
|
| 127 |
+
html[data-skin="slack"] .holo-wa-root .cs-chat-container { background: var(--slk-pane) !important; }
|
| 128 |
+
html[data-skin="slack"] .holo-wa-root .cs-message-list { background: var(--slk-pane) !important; background-image: none !important; }
|
| 129 |
+
html[data-skin="slack"] .holo-wa-root .cs-conversation-header { background: var(--slk-pane) !important; border-bottom: 1px solid #ffffff12; }
|
| 130 |
+
html[data-skin="slack"] .holo-wa-root .holo-composer { background: var(--slk-pane) !important; border-top: 1px solid #ffffff12; }
|
| 131 |
+
html[data-skin="slack"] .holo-wa-root .holo-c-input { background:#26262B !important; border:1px solid #ffffff14 !important; border-radius:8px !important; color:var(--wa-ink) !important; }
|
| 132 |
+
|
| 133 |
+
/* ── the floating envelope (desktop only; narrow layout keeps the bundle's single-column defaults) ── */
|
| 134 |
+
@media (min-width: 900px) {
|
| 135 |
+
html[data-skin="slack"] .holo-wa-root .cs-chat-container {
|
| 136 |
+
flex: 1 1 auto !important; min-width: 0; height: auto !important; align-self: stretch;
|
| 137 |
+
margin: var(--slk-gap) !important; /* even inset on all four sides */
|
| 138 |
+
border-radius: 12px; overflow: hidden;
|
| 139 |
+
box-shadow: 0 0 0 1px #00000055, 0 14px 38px #00000085;
|
| 140 |
+
}
|
| 141 |
+
/* glue the composer to the gapped pane: left tracks the sidebar's 34%, plus the gutter */
|
| 142 |
+
html[data-skin="slack"] .holo-wa-root .holo-composer {
|
| 143 |
+
left: calc(34% + var(--slk-gap)) !important; right: var(--slk-gap) !important;
|
| 144 |
+
bottom: var(--slk-gap) !important; width: auto !important; border-radius: 0 0 12px 12px;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* ── THEATRE MODE — full-screen wallpaper + glass, under the Slack skin ────────────────────────────
|
| 149 |
+
* Theatre (`.theater`, toggle ⌘/Ctrl+\) collapses the rail + sidebar and floats ONE conversation over a
|
| 150 |
+
* FULL-BLEED wallpaper as centred glass capsules (see whatsapp-theme.css "Stage 6"). Three things make it
|
| 151 |
+
* immersive + fast here:
|
| 152 |
+
* 1. WALLPAPER edge-to-edge — a static image on the root (the whole viewport), NOT the WebGPU `.holo-backdrop`
|
| 153 |
+
* canvas (which we turn OFF: it was rendering at opacity:0, all cost no pixels). The image is the one
|
| 154 |
+
* app.html already preloads, so it paints with zero extra latency; every layer above it is transparent.
|
| 155 |
+
* 2. LIGHT + CLEAR — the heavy radial vignette + hue-tint is replaced by a soft top/bottom legibility scrim
|
| 156 |
+
* only, so the middle of the wallpaper stays bright and open.
|
| 157 |
+
* 3. STUNNING GLASS, CHEAP — real backdrop-blur is limited to the TWO capsules (header + composer); message
|
| 158 |
+
* bubbles are plain translucent fills (no per-bubble blur) so a long thread never janks input.
|
| 159 |
+
* Selector depth `…"slack"] .holo-wa-root.theater .X` = (0,4,1) beats the base slack rules (0,3,1) and
|
| 160 |
+
* theatre's own (0,3,0); transforms are left untouched so theatre's centring + auto-quiet fade keep animating. */
|
| 161 |
+
html[data-skin="slack"] .holo-wa-root.theater {
|
| 162 |
+
/* FOLLOW the operator's chosen wallpaper (--wp-img, set from localStorage holo-messenger/wallpaper-src),
|
| 163 |
+
falling back to the preloaded default when unset. `none` (the "plain" mode) is honoured → dark. */
|
| 164 |
+
background: #0b0f14 var(--wp-img, url("/apps/holo-messenger/_vendor/wallpaper-default.jpg")) center center / cover no-repeat !important;
|
| 165 |
+
}
|
| 166 |
+
html[data-skin="slack"] .holo-wa-root.theater .holo-backdrop { display: none !important; } /* WebGPU canvas off → lower latency */
|
| 167 |
+
html[data-skin="slack"] .holo-wa-root.theater .cs-chat-container {
|
| 168 |
+
background: transparent !important; margin: 0 !important; border-radius: 0 !important;
|
| 169 |
+
overflow: visible !important; box-shadow: none !important; /* full-bleed, no slack box */
|
| 170 |
+
}
|
| 171 |
+
/* transparent reading column, aligned to the SAME 812px as the chrome so bubbles line up with the composer */
|
| 172 |
+
html[data-skin="slack"] .holo-wa-root.theater .cs-message-list {
|
| 173 |
+
background: transparent !important;
|
| 174 |
+
padding-left: max(20px, calc((100% - 812px) / 2)) !important;
|
| 175 |
+
padding-right: max(20px, calc((100% - 812px) / 2)) !important;
|
| 176 |
+
}
|
| 177 |
+
/* clear + light: only a soft top/bottom scrim for legibility; the wallpaper stays bright in the middle */
|
| 178 |
+
html[data-skin="slack"] .holo-wa-root.theater .holo-main::before {
|
| 179 |
+
background: linear-gradient(to bottom, rgba(0,0,0,.30) 0%, transparent 16%, transparent 80%, rgba(0,0,0,.42) 100%) !important;
|
| 180 |
+
}
|
| 181 |
+
/* stunning glass capsules — vivid backdrop-blur, crisp edge, top highlight (only these two get real blur) */
|
| 182 |
+
html[data-skin="slack"] .holo-wa-root.theater .cs-conversation-header,
|
| 183 |
+
html[data-skin="slack"] .holo-wa-root.theater .holo-composer {
|
| 184 |
+
background: rgba(22,24,32,.44) !important;
|
| 185 |
+
-webkit-backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
|
| 186 |
+
backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
|
| 187 |
+
border: 1px solid rgba(255,255,255,.16) !important;
|
| 188 |
+
box-shadow: 0 14px 40px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.16) !important;
|
| 189 |
+
}
|
| 190 |
+
html[data-skin="slack"] .holo-wa-root.theater .holo-composer {
|
| 191 |
+
left: 50% !important; right: auto !important; width: min(calc(100% - 40px), 812px) !important;
|
| 192 |
+
bottom: 18px !important; border-radius: 18px !important;
|
| 193 |
+
}
|
| 194 |
+
/* bubbles: light translucent fills (no blur = no jank on long threads) */
|
| 195 |
+
html[data-skin="slack"] .holo-wa-root.theater .cs-message--incoming .cs-message__content { background: rgba(28,28,36,.62) !important; }
|
| 196 |
+
html[data-skin="slack"] .holo-wa-root.theater .cs-message--outgoing .cs-message__content { background: rgba(58,33,64,.60) !important; }
|
| 197 |
+
|
| 198 |
+
/* ── FRAME-COLLAPSED — the immersive focus view (chats folded into the rail) ───────────────────────
|
| 199 |
+
* "Collapse chats into the rail" (`.holo-main.frame-collapsed`) folds the conversation list away and lets
|
| 200 |
+
* ONE conversation take the whole frame. The slack skin otherwise left it broken: the composer stayed pinned
|
| 201 |
+
* to `left:34%` (the vanished sidebar edge) so it floated stranded in the middle. Here we make it the SAME
|
| 202 |
+
* immersive treatment as theatre — full-screen wallpaper + glass capsules — but KEEP the rail as a clean
|
| 203 |
+
* glass nav (that's the whole point of this mode), and centre the header/composer/reading-column in the frame
|
| 204 |
+
* to the RIGHT of the rail so everything lines up.
|
| 205 |
+
* · `:has(.holo-main.frame-collapsed)` puts the wallpaper on the ROOT (so it sits behind the rail too).
|
| 206 |
+
* · `:not(.theater)` so theatre (which hides the rail) keeps full ownership when both happen to be on.
|
| 207 |
+
* · Only 3 blur layers total (rail + header + composer); bubbles are plain translucent — stays responsive. */
|
| 208 |
+
html[data-skin="slack"] .holo-wa-root:has(.holo-main.frame-collapsed):not(.theater) {
|
| 209 |
+
/* same as theatre: follow the operator's --wp-img, fall back to the preloaded default */
|
| 210 |
+
background: #0b0f14 var(--wp-img, url("/apps/holo-messenger/_vendor/wallpaper-default.jpg")) center center / cover no-repeat !important;
|
| 211 |
+
}
|
| 212 |
+
html[data-skin="slack"] .holo-wa-root:has(.holo-main.frame-collapsed):not(.theater) .holo-backdrop { display: none !important; }
|
| 213 |
+
/* the left nav bar stays — a clean SOLID strip; the wallpaper covers the entire screen EXCEPT this rail */
|
| 214 |
+
html[data-skin="slack"] .holo-wa-root:has(.holo-main.frame-collapsed):not(.theater) .holo-rail {
|
| 215 |
+
background: #17111D !important;
|
| 216 |
+
border-right: 1px solid rgba(255,255,255,.08) !important;
|
| 217 |
+
}
|
| 218 |
+
/* transparent full-bleed frame + reading column centred in the space right of the rail */
|
| 219 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-chat-container {
|
| 220 |
+
background: transparent !important; margin: 0 !important; border-radius: 0 !important; overflow: visible !important; box-shadow: none !important;
|
| 221 |
+
}
|
| 222 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-message-list {
|
| 223 |
+
background: transparent !important;
|
| 224 |
+
padding-left: max(20px, calc((100% - 812px) / 2)) !important;
|
| 225 |
+
padding-right: max(20px, calc((100% - 812px) / 2)) !important;
|
| 226 |
+
}
|
| 227 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed::before {
|
| 228 |
+
content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
|
| 229 |
+
background: linear-gradient(to bottom, rgba(0,0,0,.30) 0%, transparent 16%, transparent 80%, rgba(0,0,0,.42) 100%);
|
| 230 |
+
}
|
| 231 |
+
/* glass header + composer, centred in the frame (the rail is the nav, the frame is everything right of it) */
|
| 232 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-conversation-header,
|
| 233 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .holo-composer {
|
| 234 |
+
background: rgba(22,24,32,.44) !important;
|
| 235 |
+
-webkit-backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
|
| 236 |
+
backdrop-filter: blur(30px) saturate(1.4) brightness(1.06) !important;
|
| 237 |
+
border: 1px solid rgba(255,255,255,.16) !important;
|
| 238 |
+
box-shadow: 0 14px 40px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.16) !important;
|
| 239 |
+
}
|
| 240 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-conversation-header {
|
| 241 |
+
margin: 14px auto 0 !important; max-width: 812px !important; width: calc(100% - 40px) !important; border-radius: 16px !important;
|
| 242 |
+
}
|
| 243 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .holo-composer {
|
| 244 |
+
left: 50% !important; right: auto !important; transform: translateX(-50%) !important;
|
| 245 |
+
width: min(calc(100% - 40px), 812px) !important; bottom: 18px !important; border-radius: 18px !important;
|
| 246 |
+
}
|
| 247 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-message--incoming .cs-message__content { background: rgba(28,28,36,.62) !important; }
|
| 248 |
+
html[data-skin="slack"] .holo-wa-root:not(.theater) .holo-main.frame-collapsed .cs-message--outgoing .cs-message__content { background: rgba(58,33,64,.60) !important; }
|
| 249 |
+
|
| 250 |
+
/* ── BASE-LAYER fix: composer alignment in frame-collapsed, for ANY skin ───────────────────────────
|
| 251 |
+
* Skin-agnostic (this file is loaded regardless of the active skin). The bundle pins the composer to
|
| 252 |
+
* `left:34%` — the sidebar's flex-basis — but frame-collapsed folds the sidebar to 0, so on every NON-slack
|
| 253 |
+
* skin (graphite/warm/contrast/light/none) the composer floated stranded in the middle. Align it to the
|
| 254 |
+
* full-bleed chat (left:0 = the rail's right edge; right:0 is already the base). `:not(.theater)` yields to
|
| 255 |
+
* theatre's own centred composer; slack's higher-specificity immersive rule above wins for the slack skin. */
|
| 256 |
+
.holo-wa-root:not(.theater) .holo-main.frame-collapsed .holo-composer { left: 0 !important; }
|
b/6bbf54f41980838ea8cf1c08123b7aa6e9820eeb58da964cd50e875b2893faef
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en"><head>
|
| 3 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 4 |
+
<title>Holo Messenger</title>
|
| 5 |
+
<link rel="stylesheet" href="/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css?v=a0c782c6ebee">
|
| 6 |
+
<style>html,body{height:100%;margin:0;background:#0b141a;color-scheme:dark}#root{height:100vh}</style>
|
| 7 |
+
<link rel="preload" as="image" href="/apps/holo-messenger/_vendor/wallpaper-default.jpg" fetchpriority="high">
|
| 8 |
+
</head><body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module">
|
| 11 |
+
// ?v=N - bump on each deploy so the browser can never serve a stale cached bundle (the cause of a frozen/old build).
|
| 12 |
+
import "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js?v=11d9ba0d372a"; // → window.HoloMessengerUI
|
| 13 |
+
// Opt-in: render the WhatsApp UX from STREAMING shadcn κ-components instead of the chatscope bundle.
|
| 14 |
+
// Additive — only takes over window.HoloMessengerUI.mount when ?ui=shadcn (or localStorage["holo.ui.shadcn"]="1");
|
| 15 |
+
// the default boot stays the untouched chatscope UI. Runs before boot() so it is fed the same live model.
|
| 16 |
+
import "/apps/holo-messenger/messenger-shadcn-ui.mjs";
|
| 17 |
+
import { boot } from "/apps/holo-messenger/holo-messenger-app.mjs?v=076005621416";
|
| 18 |
+
boot(document.getElementById("root")).catch(e => {
|
| 19 |
+
document.body.innerHTML = "<pre style='color:#f88;padding:16px;white-space:pre-wrap'>boot error: " + (e && e.stack || e) + "</pre>";
|
| 20 |
+
});
|
| 21 |
+
// Intelligent Inbox (holo-mail) - additive, feature-flagged OFF by default. Dynamic import + catch so it
|
| 22 |
+
// can NEVER block or break the messenger boot. Enable per-operator: localStorage["holo.mail.enabled"]="1".
|
| 23 |
+
import("/apps/holo-messenger/mail/holo-mail-attach.mjs").catch(() => {});
|
| 24 |
+
// HoloChat, woven into THIS messenger — one canonical surface, no separate overlay. Cards render natively
|
| 25 |
+
// in the bubbles; the composer "+" opens the unified tray (apps · Spaces · money · identity · prove).
|
| 26 |
+
// Additive + catch-guarded so it can never block boot. Enable: localStorage["holo.chat.enabled"]="1" or ?chat=1.
|
| 27 |
+
import("/apps/holo-messenger/holo-messenger-weave.mjs").catch(() => {});
|
| 28 |
+
// Colour re-tone picker (Default · Graphite · Warm · Contrast · Light) — colours only, all features
|
| 29 |
+
// intact. Gated: shows only with ?skins=1 or localStorage["holo.skins"]="1". Additive + catch-guarded.
|
| 30 |
+
import("/apps/holo-messenger/messenger-skin.mjs").catch(() => {});
|
| 31 |
+
// Asset cache (service worker) — repeat opens serve wallpaper/logos/bundle/css/js NETWORK-FREE. Caches
|
| 32 |
+
// only own-origin static assets (stale-while-revalidate); all dynamic/API/bridge/media traffic passes
|
| 33 |
+
// through untouched. Additive + catch-guarded. Escape hatch: open with ?nosw=1 to unregister.
|
| 34 |
+
import("/apps/holo-messenger/messenger-sw-register.mjs").catch(() => {});
|
| 35 |
+
</script>
|
| 36 |
+
</body></html>
|
b/8bd23f0acbaf00093b9daedeac08c9513ccbe0a8647b91eb104bfd27175682a8
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// messenger-sw-register.mjs — eagerly register the ONE messenger service worker (holo-sw.js: Reach push +
|
| 2 |
+
// M1 shell cache) on boot, so repeat opens are network-free even before push permission is granted. Additive
|
| 3 |
+
// + catch-guarded so it can never block boot. Escape hatch: open with ?nosw=1 to unregister it.
|
| 4 |
+
//
|
| 5 |
+
// holo-sw.js is ALSO registered by the Reach push flow (holo-push.mjs) — same script, same {type:module}, same
|
| 6 |
+
// dir scope, so the two calls collapse to one registration (idempotent). We register here too because caching
|
| 7 |
+
// must not wait on push. We also retire any stale competing worker (the old messenger-sw.js) at this scope.
|
| 8 |
+
|
| 9 |
+
(async () => {
|
| 10 |
+
if (!("serviceWorker" in navigator)) return;
|
| 11 |
+
try {
|
| 12 |
+
const u = new URL(location.href);
|
| 13 |
+
if (u.searchParams.get("nosw") === "1") {
|
| 14 |
+
for (const r of await navigator.serviceWorker.getRegistrations()) {
|
| 15 |
+
if ((r.scope || "").includes("/apps/holo-messenger/")) await r.unregister();
|
| 16 |
+
}
|
| 17 |
+
console.log("[msgr-sw] unregistered (nosw=1)");
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
// retire the old competing cache-worker if a previous build left it controlling this scope.
|
| 21 |
+
try {
|
| 22 |
+
for (const r of await navigator.serviceWorker.getRegistrations()) {
|
| 23 |
+
const s = (r.active && r.active.scriptURL) || (r.installing && r.installing.scriptURL) || (r.waiting && r.waiting.scriptURL) || "";
|
| 24 |
+
if (s.includes("/messenger-sw.js")) { await r.unregister(); console.log("[msgr-sw] retired stale messenger-sw.js"); }
|
| 25 |
+
}
|
| 26 |
+
} catch {}
|
| 27 |
+
await navigator.serviceWorker.register("/apps/holo-messenger/holo-sw.js", { type: "module", scope: "/apps/holo-messenger/" });
|
| 28 |
+
console.log("[msgr-sw] one worker active (push + shell cache) — repeat opens serve the shell network-free");
|
| 29 |
+
} catch (e) { /* never block boot */ }
|
| 30 |
+
})();
|
b/9e602f8270c163a794c8e1b9d455569a2069c15e2f7db955ded05659b326567b
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-mail-attach.mjs - the in-app entry. Loaded additively by app.html; mounts the Intelligent Inbox
|
| 2 |
+
// (holo-mail-ui) over the REAL engine (email-bridge provider + on-device Q + holo-strand) as a full-screen
|
| 3 |
+
// overlay. When no mailbox is linked yet, it shows the one-tap ONBOARDING first. FEATURE-FLAGGED OFF by
|
| 4 |
+
// default: with the flag off nothing is added to the page, so it can't affect the messenger's paint.
|
| 5 |
+
// flag on: localStorage.setItem("holo.mail.enabled","1") - or open with ?mail=1
|
| 6 |
+
// manual: window.HoloMailOpen() (always available)
|
| 7 |
+
|
| 8 |
+
import { makeMailProvider } from "/apps/holo-messenger/mail/holo-mail-provider.mjs";
|
| 9 |
+
import { makeMultiProvider } from "/apps/holo-messenger/mail/holo-mail-multi.mjs";
|
| 10 |
+
import { attachMailEngine } from "/apps/holo-messenger/mail/holo-mail-engine.mjs";
|
| 11 |
+
import { mountMailUI } from "/apps/holo-messenger/mail/holo-mail-ui.mjs";
|
| 12 |
+
import { makeOnboarding } from "/apps/holo-messenger/mail/holo-mail-onboard.mjs";
|
| 13 |
+
import { mountOnboard } from "/apps/holo-messenger/mail/holo-mail-onboard-ui.mjs";
|
| 14 |
+
import { makeMailHealth } from "/apps/holo-messenger/mail/holo-mail-health.mjs";
|
| 15 |
+
|
| 16 |
+
const BASE = "http://127.0.0.1:8793"; // the primary bridge (account #1). More = more ports.
|
| 17 |
+
const SUPERVISOR = "http://127.0.0.1:8795"; // email-supervisor: spawns/owns one bridge per mailbox
|
| 18 |
+
const ACCOUNTS_KEY = "holo.mail.accounts";
|
| 19 |
+
// Each account is its own email-bridge instance on its own port (isolated state dir), spawned by the
|
| 20 |
+
// supervisor. The list is cached in localStorage so buildProvider() stays synchronous; the supervisor is the
|
| 21 |
+
// source of truth and we refresh from it whenever the inbox opens. The provider is ALWAYS the unified
|
| 22 |
+
// multi-provider, so the engine/UI work over one or many mailboxes unchanged.
|
| 23 |
+
const DEFAULT_ACCOUNTS = [{ id: "default", label: "", base: BASE }];
|
| 24 |
+
function loadAccounts() {
|
| 25 |
+
try { const a = JSON.parse(localStorage.getItem(ACCOUNTS_KEY) || "null"); if (Array.isArray(a) && a.length) return a; } catch {}
|
| 26 |
+
return DEFAULT_ACCOUNTS.slice();
|
| 27 |
+
}
|
| 28 |
+
function saveAccounts(list) { try { localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(list)); } catch {} }
|
| 29 |
+
const primaryBase = () => (loadAccounts()[0] || { base: BASE }).base;
|
| 30 |
+
const flagOn = () => { try { return localStorage.getItem("holo.mail.enabled") === "1" || /(?:^|[?&])mail=1(?:&|$)/.test(location.search); } catch { return false; } };
|
| 31 |
+
|
| 32 |
+
function buildProvider() {
|
| 33 |
+
return makeMultiProvider({ accounts: loadAccounts().map((a) => ({ id: a.id, label: a.label, provider: makeMailProvider({ base: a.base }) })) });
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// the supervisor's control API (loopback). Absent in the web preview / no-host dev - every call fails soft.
|
| 37 |
+
function supervisor(base = SUPERVISOR) {
|
| 38 |
+
const j = async (p, init) => { const r = await fetch(base + p, init); if (!r.ok) throw new Error(`${p} → ${r.status}`); return r.json(); };
|
| 39 |
+
return {
|
| 40 |
+
reachable: async () => { try { await j("/health"); return true; } catch { return false; } },
|
| 41 |
+
accounts: () => j("/accounts"),
|
| 42 |
+
add: (label) => j("/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ label }) }),
|
| 43 |
+
remove: (id) => j("/accounts/" + encodeURIComponent(id), { method: "DELETE" }),
|
| 44 |
+
};
|
| 45 |
+
}
|
| 46 |
+
// pull the authoritative account list from the supervisor into the local cache (so buildProvider sees it).
|
| 47 |
+
async function syncAccountsFromSupervisor() {
|
| 48 |
+
try {
|
| 49 |
+
const rows = await supervisor().accounts();
|
| 50 |
+
if (Array.isArray(rows) && rows.length) saveAccounts(rows.map((r) => ({ id: r.id, label: r.label || "", base: r.base })));
|
| 51 |
+
return rows;
|
| 52 |
+
} catch { return null; } // supervisor down → keep whatever's cached (single default account still works)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Account management surface. add() asks the supervisor to spawn a fresh bridge, then walks the user through
|
| 56 |
+
// linking THAT mailbox; it then joins the unified inbox. Fallback (no supervisor): register a manual port.
|
| 57 |
+
if (typeof window !== "undefined") window.HoloMailAccounts = {
|
| 58 |
+
list: () => loadAccounts(),
|
| 59 |
+
refresh: () => syncAccountsFromSupervisor(),
|
| 60 |
+
async add({ label = "", port, base } = {}) {
|
| 61 |
+
if (await supervisor().reachable()) {
|
| 62 |
+
const acct = await supervisor().add(label); // spawns the bridge on a fresh port
|
| 63 |
+
await syncAccountsFromSupervisor();
|
| 64 |
+
await addAccountFlow(acct.base); // link the new mailbox → unified inbox
|
| 65 |
+
return loadAccounts();
|
| 66 |
+
}
|
| 67 |
+
// no supervisor: accept a manually-started bridge (EMAIL_BRIDGE_PORT=<port> node email-bridge.mjs)
|
| 68 |
+
const b = base || (port ? `http://127.0.0.1:${port}` : null);
|
| 69 |
+
if (!b) throw new Error("no supervisor - pass a port of a manually-started bridge");
|
| 70 |
+
const list = loadAccounts();
|
| 71 |
+
if (!list.some((a) => a.base === b)) { list.push({ id: "acct" + (list.length + 1), label, base: b }); saveAccounts(list); }
|
| 72 |
+
await addAccountFlow(b);
|
| 73 |
+
return loadAccounts();
|
| 74 |
+
},
|
| 75 |
+
async remove(id) {
|
| 76 |
+
try { if (await supervisor().reachable()) await supervisor().remove(id); } catch {}
|
| 77 |
+
const list = loadAccounts().filter((a) => a.id !== id); saveAccounts(list.length ? list : DEFAULT_ACCOUNTS.slice());
|
| 78 |
+
if (overlay && overlay.style.display !== "none") openMail(true);
|
| 79 |
+
return loadAccounts();
|
| 80 |
+
},
|
| 81 |
+
reset() { try { localStorage.removeItem(ACCOUNTS_KEY); } catch {} return loadAccounts(); },
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
// drive onboarding for a specific (freshly-spawned, unlinked) mailbox, then return to the unified inbox.
|
| 85 |
+
async function addAccountFlow(targetBase) {
|
| 86 |
+
if (!overlay) await openMail();
|
| 87 |
+
showOnboarding(buildProvider(), targetBase);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// a thin client over the loopback bridge (creds pass straight through, never stored here).
|
| 91 |
+
function bridgeClient(base) {
|
| 92 |
+
const j = async (p, init) => { const r = await fetch(base + p, init); if (!r.ok) throw new Error(`${p} → ${r.status}`); return r.json(); };
|
| 93 |
+
return {
|
| 94 |
+
status: () => j("/status"),
|
| 95 |
+
login: (body) => j("/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }),
|
| 96 |
+
};
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// build the inbox UI's `source` over the live provider corpus (filled by the bridge SSE stream).
|
| 100 |
+
function buildSource(provider) {
|
| 101 |
+
return {
|
| 102 |
+
async ids() {
|
| 103 |
+
let list = []; try { list = await provider.listThreads(); } catch {}
|
| 104 |
+
const ids = (list || []).map((s) => s.jid).filter(Boolean);
|
| 105 |
+
if (ids.length) return ids;
|
| 106 |
+
const seen = new Set(); provider.allMessages().forEach((m) => seen.add(m.threadId)); return [...seen];
|
| 107 |
+
},
|
| 108 |
+
meta(jid) {
|
| 109 |
+
const msgs = provider.threadMessages(jid);
|
| 110 |
+
const inbound = msgs.find((m) => !m.fromMe) || msgs[0] || {};
|
| 111 |
+
const last = msgs[msgs.length - 1] || {};
|
| 112 |
+
return { who: inbound.fromName || inbound.from || jid, subj: last.subject || inbound.subject || "(no subject)",
|
| 113 |
+
account: provider.accountLabel ? provider.accountLabel(jid) : null,
|
| 114 |
+
msgs: msgs.map((m) => ({ fromMe: m.fromMe, fromName: m.fromName || m.from, from: m.from, date: m.date, text: m.text })) };
|
| 115 |
+
},
|
| 116 |
+
};
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
let overlay = null, host = null, banner = null, ui = null, health = null;
|
| 120 |
+
|
| 121 |
+
const clearHost = () => { host.innerHTML = ""; host.className = ""; };
|
| 122 |
+
|
| 123 |
+
function showBanner(state) {
|
| 124 |
+
if (!banner) return;
|
| 125 |
+
if (state && state.needsReconnect) {
|
| 126 |
+
banner.innerHTML = `<span>${state.message || "Reconnect needed."}</span><button class="hm-reconnect">Reconnect</button>`;
|
| 127 |
+
banner.style.display = "flex";
|
| 128 |
+
banner.querySelector(".hm-reconnect").onclick = () => { banner.style.display = "none"; showOnboarding(buildProvider()); };
|
| 129 |
+
} else { banner.style.display = "none"; }
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
function showOnboarding(provider, targetBase = primaryBase()) {
|
| 133 |
+
if (health) { health.stop(); health = null; }
|
| 134 |
+
clearHost();
|
| 135 |
+
const onboarding = makeOnboarding({ bridge: bridgeClient(targetBase) });
|
| 136 |
+
mountOnboard(host, { onboarding, onLinked: () => showInbox(buildProvider()) }); // rebuild → include newly-linked account
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
async function showInbox(provider) {
|
| 140 |
+
clearHost();
|
| 141 |
+
const engine = await attachMailEngine({ base: primaryBase(), provider }); // real Q + window.HoloStrand
|
| 142 |
+
try { await provider.connect(); } catch {} // live SSE stream (fans out to all accounts)
|
| 143 |
+
await new Promise((r) => setTimeout(r, 600)); // let first messages land
|
| 144 |
+
ui = mountMailUI(host, { engine, source: buildSource(provider), foot: "On-device Q · sovereign κ",
|
| 145 |
+
onSend: (jid, text) => { try { provider.sendReply({ chat: jid, text }); } catch {} } });
|
| 146 |
+
// keep the mailbox healthy: a dropped/expired sign-in surfaces a reconnect banner instead of a dead inbox.
|
| 147 |
+
health = makeMailHealth({ bridge: bridgeClient(primaryBase()) });
|
| 148 |
+
health.start(showBanner);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
export async function openMail(rebuild = false) {
|
| 152 |
+
if (overlay && rebuild) { closeMail(); overlay.remove(); overlay = null; } // re-render over a changed account set
|
| 153 |
+
if (overlay) { overlay.style.display = "block"; return; }
|
| 154 |
+
overlay = document.createElement("div");
|
| 155 |
+
overlay.id = "holo-mail-overlay";
|
| 156 |
+
overlay.style.cssText = "position:fixed;inset:0;z-index:2147482000;background:#0b0d10";
|
| 157 |
+
banner = document.createElement("div");
|
| 158 |
+
banner.style.cssText = "display:none;position:absolute;top:0;left:0;right:0;z-index:2147483001;align-items:center;justify-content:center;gap:12px;padding:9px 14px;background:#e6462e;color:#fff;font:600 13px system-ui";
|
| 159 |
+
host = document.createElement("div"); host.style.cssText = "position:absolute;inset:0";
|
| 160 |
+
const closer = document.createElement("button"); closer.textContent = "✕"; closer.title = "Close (Esc)";
|
| 161 |
+
closer.style.cssText = "position:fixed;top:10px;right:14px;z-index:2147483002;border:0;background:#0006;color:#fff;width:30px;height:30px;border-radius:50%;cursor:pointer;font-size:15px";
|
| 162 |
+
closer.onclick = closeMail;
|
| 163 |
+
const reBtnCss = "border:0;background:#fff;color:#e6462e;border-radius:7px;padding:4px 12px;font:inherit;font-weight:700;cursor:pointer";
|
| 164 |
+
const style = document.createElement("style"); style.textContent = `.hm-reconnect{${reBtnCss}}`; overlay.appendChild(style);
|
| 165 |
+
overlay.append(host, banner, closer); document.body.appendChild(overlay);
|
| 166 |
+
document.addEventListener("keydown", escClose);
|
| 167 |
+
|
| 168 |
+
// refresh the account list from the supervisor (source of truth), then build the unified multi-provider.
|
| 169 |
+
await syncAccountsFromSupervisor();
|
| 170 |
+
const provider = buildProvider();
|
| 171 |
+
let st = null; try { st = await bridgeClient(primaryBase()).status(); } catch {}
|
| 172 |
+
if (st && st.linked) return showInbox(provider);
|
| 173 |
+
showOnboarding(provider);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
export function closeMail() { if (health) { health.stop(); health = null; } if (overlay) overlay.style.display = "none"; }
|
| 177 |
+
function escClose(e) { if (e.key === "Escape" && overlay && overlay.style.display !== "none" && !document.querySelector(".hm-scrim.on")) closeMail(); }
|
| 178 |
+
|
| 179 |
+
function addLauncher() {
|
| 180 |
+
if (document.getElementById("holo-mail-launch")) return;
|
| 181 |
+
const b = document.createElement("button");
|
| 182 |
+
b.id = "holo-mail-launch"; b.title = "Holo Mail"; b.textContent = "✦";
|
| 183 |
+
b.style.cssText = "position:fixed;bottom:18px;right:18px;z-index:2147481000;width:44px;height:44px;border-radius:50%;border:0;cursor:pointer;color:#fff;font-size:18px;background:linear-gradient(135deg,#3b6ef5,#8a4fd6);box-shadow:0 6px 20px rgba(0,0,0,.35)";
|
| 184 |
+
b.onclick = openMail; document.body.appendChild(b);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (typeof window !== "undefined") {
|
| 188 |
+
window.HoloMailOpen = openMail;
|
| 189 |
+
window.HoloMailClose = closeMail;
|
| 190 |
+
if (flagOn()) {
|
| 191 |
+
if (document.readyState !== "loading") addLauncher();
|
| 192 |
+
else document.addEventListener("DOMContentLoaded", addLauncher);
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
export default { openMail, closeMail };
|
b/9f62d9e0480be19a114542104a51a4e69da89a49c2960d75910ed0b57ea41c55
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-messenger-weave.mjs — weave HoloChat INTO the real messenger (additive, no monolith edits).
|
| 2 |
+
//
|
| 3 |
+
// Why: there were two messengers — the beautiful one, and a plainer HoloChat surface behind "#". This
|
| 4 |
+
// dissolves the second into the first, so apps/money/identity/proofs live in the messenger you know.
|
| 5 |
+
// How: the real UI is a closed React/chatscope app exposing only low-level globals, so we weave at the
|
| 6 |
+
// DOM seam (the pattern the app itself uses to turn pay/watch links into cards): a body-level observer
|
| 7 |
+
// upgrades card-text bubbles into native cards, and the composer attach "+" opens the unified App Tray.
|
| 8 |
+
// What: in ANY conversation, a message whose text is a HoloChat card renders as that card; tapping "+"
|
| 9 |
+
// opens apps · Spaces · money · identity · prove, and a pick sends into the current conversation.
|
| 10 |
+
|
| 11 |
+
import "/apps/holo-messenger/holo-app-tray.mjs"; // installs window.HoloApps (+ HoloOpen)
|
| 12 |
+
|
| 13 |
+
const CONTENT = ".cs-message__content";
|
| 14 |
+
const EDITOR = ".cs-message-input__content-editor";
|
| 15 |
+
const SEND = ".cs-button--send";
|
| 16 |
+
const ATTACH = ".cs-button--attachment";
|
| 17 |
+
|
| 18 |
+
// drop a card into whatever conversation is open by POPULATING the real composer (reliable), then the user
|
| 19 |
+
// presses Send — exactly like attaching in WhatsApp. Set the contenteditable's text + dispatch a native
|
| 20 |
+
// `input` event so chatscope's React onInput registers it and enables Send; place the cursor at the end.
|
| 21 |
+
function composerChannel() {
|
| 22 |
+
return {
|
| 23 |
+
send(text) {
|
| 24 |
+
const ed = document.querySelector(EDITOR); if (!ed) return;
|
| 25 |
+
ed.focus();
|
| 26 |
+
ed.textContent = String(text);
|
| 27 |
+
ed.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, inputType: "insertText", data: String(text) }));
|
| 28 |
+
try { const r = document.createRange(); r.selectNodeContents(ed); r.collapse(false); const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); } catch {}
|
| 29 |
+
},
|
| 30 |
+
};
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// upgrade one message-content node: if its text is a HoloChat card, replace it with the rendered card.
|
| 34 |
+
function upgrade(node) {
|
| 35 |
+
try {
|
| 36 |
+
if (!node || node.dataset.holoCard) return;
|
| 37 |
+
const A = window.HoloApps; if (!A || !A.isCard) return;
|
| 38 |
+
const text = (node.textContent || "").trim();
|
| 39 |
+
if (!A.isCard(text)) return;
|
| 40 |
+
node.dataset.holoCard = "1";
|
| 41 |
+
node.textContent = "";
|
| 42 |
+
node.appendChild(A.renderCard(text, composerChannel()));
|
| 43 |
+
} catch {}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// the composer attach "+" opens the unified tray (the modern "attach anything": apps, money, id, files).
|
| 47 |
+
function hookAttach(btn) {
|
| 48 |
+
if (!btn || btn.dataset.holoAttach) return;
|
| 49 |
+
btn.dataset.holoAttach = "1";
|
| 50 |
+
btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (window.HoloApps) window.HoloApps.pick(composerChannel()); }, true);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// a native "Spaces" rail at the top of the chat list — the familiar status/Communities strip. Placed in
|
| 54 |
+
// the app's own .holo-convlist-wrap (a sibling of React's <ul>, so it survives re-renders); tap → open.
|
| 55 |
+
const WRAP = ".holo-convlist-wrap";
|
| 56 |
+
// one clean word per Space — no truncation, no ellipsis.
|
| 57 |
+
const SPACE_SHORT = { "org.hologram.holospace.Web3": "Web3", "org.hologram.holospace.CreatorStudio": "Studio", "org.hologram.holospace.DevCockpit": "Dev", "org.hologram.holospace.AILab": "AI", "org.hologram.holospace.TrustCenter": "Trust", "org.hologram.holospace.EmulationArcade": "Arcade" };
|
| 58 |
+
async function mountSpaces() {
|
| 59 |
+
try {
|
| 60 |
+
if (document.getElementById("holo-spaces")) return;
|
| 61 |
+
const list = document.querySelector(".cs-conversation-list"); if (!list) return;
|
| 62 |
+
const wrap = document.querySelector(WRAP) || list.parentElement; if (!wrap) return;
|
| 63 |
+
const A = window.HoloApps; if (!A || !A.catalog) return;
|
| 64 |
+
const spaces = (await A.catalog()).filter((x) => x.kind === "space");
|
| 65 |
+
if (!spaces.length || document.getElementById("holo-spaces")) return;
|
| 66 |
+
const open0 = (() => { try { return localStorage.getItem("holo.spaces.open") === "1"; } catch { return false; } })();
|
| 67 |
+
const sec = document.createElement("div"); sec.id = "holo-spaces";
|
| 68 |
+
sec.style.cssText = "flex:0 0 auto;border-bottom:1px solid #ffffff0d;background:transparent;";
|
| 69 |
+
const head = document.createElement("button"); head.type = "button"; // one quiet line by default; tap to reveal
|
| 70 |
+
head.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;border:0;background:transparent;color:#e9edef;cursor:pointer;padding:11px 16px;font:600 13px system-ui;opacity:.6;transition:opacity .15s ease;";
|
| 71 |
+
head.innerHTML = `<span style="font-size:15px;">✨</span><span style="flex:1;text-align:left;">Spaces</span><span class="hsp-chev" style="font-size:17px;line-height:1;opacity:.7;transition:transform .18s ease;">›</span>`;
|
| 72 |
+
head.onmouseenter = () => { head.style.opacity = "1"; }; head.onmouseleave = () => { head.style.opacity = ".6"; };
|
| 73 |
+
const rail = document.createElement("div"); rail.style.cssText = "display:" + (open0 ? "flex" : "none") + ";gap:18px;overflow-x:auto;padding:2px 16px 12px;scrollbar-width:none;";
|
| 74 |
+
for (const s of spaces) {
|
| 75 |
+
const nm = SPACE_SHORT[s.id] || s.name;
|
| 76 |
+
const b = document.createElement("button"); b.type = "button"; b.title = s.tagline || s.name;
|
| 77 |
+
b.style.cssText = "flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:6px;border:0;background:transparent;color:#e9edef;cursor:pointer;";
|
| 78 |
+
b.innerHTML = `<span class="hsp-av" style="width:40px;height:40px;border-radius:50%;display:grid;place-items:center;font-size:19px;background:${s.accent}12;box-shadow:inset 0 0 0 1px ${s.accent}44;transition:transform .15s ease,box-shadow .15s ease;">${s.glyph}</span>` +
|
| 79 |
+
`<span style="font-size:13px;font-weight:500;opacity:.62;">${nm}</span>`;
|
| 80 |
+
const av = b.querySelector(".hsp-av");
|
| 81 |
+
b.onmouseenter = () => { av.style.transform = "scale(1.08)"; av.style.boxShadow = "inset 0 0 0 1.5px " + s.accent + "aa"; b.lastChild.style.opacity = "1"; };
|
| 82 |
+
b.onmouseleave = () => { av.style.transform = "scale(1)"; av.style.boxShadow = "inset 0 0 0 1px " + s.accent + "44"; b.lastChild.style.opacity = ".62"; };
|
| 83 |
+
b.onclick = () => (window.HoloOpen ? window.HoloOpen(s) : A.openSpace(s));
|
| 84 |
+
rail.appendChild(b);
|
| 85 |
+
}
|
| 86 |
+
const chev = head.querySelector(".hsp-chev"); chev.style.transform = open0 ? "rotate(90deg)" : "rotate(0deg)";
|
| 87 |
+
head.onclick = () => {
|
| 88 |
+
const opening = rail.style.display === "none";
|
| 89 |
+
rail.style.display = opening ? "flex" : "none";
|
| 90 |
+
chev.style.transform = opening ? "rotate(90deg)" : "rotate(0deg)";
|
| 91 |
+
try { localStorage.setItem("holo.spaces.open", opening ? "1" : "0"); } catch {}
|
| 92 |
+
};
|
| 93 |
+
sec.append(head, rail); wrap.insertBefore(sec, list);
|
| 94 |
+
} catch {}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function scan(root) {
|
| 98 |
+
(root || document).querySelectorAll?.(CONTENT).forEach(upgrade);
|
| 99 |
+
(root || document).querySelectorAll?.(ATTACH).forEach(hookAttach);
|
| 100 |
+
mountSpaces();
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// a light, reversible calm: soften the loudest native strip (the "need you" banner) and hide the rail's
|
| 104 |
+
// scrollbar. Conservative — no functional element is hidden; remove #holo-calm to revert entirely.
|
| 105 |
+
function injectCalm() {
|
| 106 |
+
if (document.getElementById("holo-calm")) return;
|
| 107 |
+
const st = document.createElement("style"); st.id = "holo-calm";
|
| 108 |
+
st.textContent = [
|
| 109 |
+
".holo-catchup{opacity:.8;}", // soften the loud "need you" banner
|
| 110 |
+
".holo-chip:not(.on){opacity:.62;font-weight:500;}", // quiet the inactive filters (all still work)
|
| 111 |
+
".holo-chip{transition:opacity .15s ease;} .holo-chip:hover{opacity:1;}",
|
| 112 |
+
"#holo-spaces::-webkit-scrollbar{display:none;height:0;}",
|
| 113 |
+
].join("");
|
| 114 |
+
(document.head || document.documentElement).appendChild(st);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
let _obs = null;
|
| 118 |
+
function start() {
|
| 119 |
+
if (_obs || typeof MutationObserver === "undefined") return;
|
| 120 |
+
injectCalm();
|
| 121 |
+
scan(document);
|
| 122 |
+
_obs = new MutationObserver((muts) => {
|
| 123 |
+
for (const m of muts) for (const n of m.addedNodes) {
|
| 124 |
+
if (n.nodeType !== 1) continue;
|
| 125 |
+
if (n.matches?.(CONTENT)) upgrade(n);
|
| 126 |
+
if (n.matches?.(ATTACH)) hookAttach(n);
|
| 127 |
+
n.querySelectorAll?.(CONTENT).forEach(upgrade);
|
| 128 |
+
n.querySelectorAll?.(ATTACH).forEach(hookAttach);
|
| 129 |
+
if (n.matches?.(".cs-conversation-list") || n.querySelector?.(".cs-conversation-list")) mountSpaces();
|
| 130 |
+
}
|
| 131 |
+
});
|
| 132 |
+
_obs.observe(document.body, { childList: true, subtree: true });
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
export function installWeave() { if (typeof document !== "undefined") { if (document.body) start(); else document.addEventListener("DOMContentLoaded", start); } return true; }
|
| 136 |
+
|
| 137 |
+
if (typeof window !== "undefined") {
|
| 138 |
+
const flag = () => { try { return localStorage.getItem("holo.chat.enabled") === "1" || /(?:^|[?&])chat=1(?:&|$)/.test(location.search); } catch { return false; } };
|
| 139 |
+
if (flag()) installWeave();
|
| 140 |
+
}
|
| 141 |
+
export default { installWeave };
|
b/a0c782c6ebee1f7d91088089ea3a39942725459947f8882be8534cdb9f3d4029
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/be803875bc9cdbbc411c84e5f8a407189d775d7d9fc3e72f0a28093f37414933
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// messenger-shadcn-ui.mjs — DROP-IN mount(el, model) rendering the Holo Messenger UX from STREAMING
|
| 2 |
+
// shadcn κ-components, faithful to the real app (network app-rail · Q sidebar · triage · rich rows ·
|
| 3 |
+
// Q chat pane · earth wallpaper · real SVG line icons), same contract as the chatscope bundle.
|
| 4 |
+
//
|
| 5 |
+
// ADDITIVE + SAFE: only takes over window.HoloMessengerUI.mount on opt-in (?ui=shadcn or
|
| 6 |
+
// localStorage["holo.ui.shadcn"]="1"). Default boot stays the untouched chatscope UI.
|
| 7 |
+
//
|
| 8 |
+
// PERF: memoized rows · composer owns its draft · windowed thread (≤WINDOW rows in DOM; 1M chat stays tiny).
|
| 9 |
+
// PASS 1 = faithful shell. Rich message content + action surface are Pass 2/3.
|
| 10 |
+
|
| 11 |
+
const WANT = (() => {
|
| 12 |
+
try { const u = new URL(location.href);
|
| 13 |
+
if (u.searchParams.get("ui") === "shadcn") return true;
|
| 14 |
+
if (u.searchParams.get("ui") === "chatscope") return false;
|
| 15 |
+
return localStorage.getItem("holo.ui.shadcn") === "1";
|
| 16 |
+
} catch { return false; }
|
| 17 |
+
})();
|
| 18 |
+
|
| 19 |
+
const WINDOW = 80;
|
| 20 |
+
const BRANDS = "/usr/share/brands/";
|
| 21 |
+
const WP = "/apps/holo-messenger/_vendor/wallpaper-default.jpg"; // the earth photo (default)
|
| 22 |
+
const NET_SLUG = { whatsapp: "whatsapp", telegram: "telegram", signal: "signal", imessage: "imessage", instagram: "instagram", messenger: "messenger", discord: "discord", slack: "slack", gmessages: "googlemessages", googlemessages: "googlemessages", x: "x", twitter: "x", linkedin: "linkedin", gmail: "gmail", email: "gmail", internal: null };
|
| 23 |
+
|
| 24 |
+
const CSS = `
|
| 25 |
+
.wa{height:100dvh;display:grid;grid-template-columns:64px minmax(300px,.46fr) minmax(0,1fr);background:#0b141a;color:#e9edef;
|
| 26 |
+
font:14.5px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
| 27 |
+
.wa *{box-sizing:border-box}
|
| 28 |
+
.wa svg{display:block}
|
| 29 |
+
.rail{background:#111b21;border-right:1px solid #0a1015;display:flex;flex-direction:column;align-items:center;gap:7px;padding:12px 0}
|
| 30 |
+
.rb{position:relative;width:46px;height:46px;border-radius:15px;display:grid;place-items:center;cursor:pointer;color:#aebac1;transition:background .12s,color .12s;background:transparent;border:0}
|
| 31 |
+
.rb:hover{background:#202c33;color:#e9edef}.rb.on{background:#2a3942;color:#00a884}
|
| 32 |
+
.rb img{width:28px;height:28px;border-radius:8px;object-fit:cover}
|
| 33 |
+
.rb .dot{position:absolute;right:5px;bottom:5px;width:10px;height:10px;border-radius:50%;background:#00d95f;border:2px solid #111b21}
|
| 34 |
+
.rb .bdg{position:absolute;top:-3px;right:-3px;min-width:19px;height:19px;border-radius:10px;background:#00a884;color:#0b141a;font-size:11px;font-weight:700;display:grid;place-items:center;padding:0 4px;border:2px solid #111b21}
|
| 35 |
+
.rsep{width:28px;height:1px;background:#22303a;margin:3px 0}
|
| 36 |
+
.rgrow{flex:1 1 auto}
|
| 37 |
+
.rav{width:40px;height:40px;border-radius:50%;overflow:hidden;cursor:pointer;background:radial-gradient(circle at 35% 30%,#7cf0c8,#2b9e7a 60%,#0b3b2e);margin-top:2px}
|
| 38 |
+
.side{background:#111b21;border-right:1px solid #222d34;display:flex;flex-direction:column;min-width:0}
|
| 39 |
+
.stitle{display:flex;align-items:center;gap:6px;padding:15px 14px 10px}
|
| 40 |
+
.stitle .t{font-size:23px;font-weight:600;letter-spacing:-.4px;flex:1 1 auto}
|
| 41 |
+
.search{padding:4px 12px 8px;position:relative}
|
| 42 |
+
.search .si{position:absolute;left:26px;top:50%;transform:translateY(-50%);color:#8696a0;pointer-events:none;z-index:1;display:grid}
|
| 43 |
+
.filt{display:flex;align-items:center;gap:8px;padding:2px 14px 8px}
|
| 44 |
+
.chip{display:inline-flex;align-items:center;gap:6px;background:#202c33;color:#8696a0;border:0;border-radius:999px;padding:5px 14px;font-size:13px;cursor:pointer}
|
| 45 |
+
.chip.on{background:#0b3b2e;color:#00d95f}
|
| 46 |
+
.caret{background:#202c33;color:#8696a0;border:0;border-radius:999px;width:30px;height:28px;display:grid;place-items:center;cursor:pointer;font-size:11px}
|
| 47 |
+
.triage{margin:2px 12px 8px;display:flex;align-items:center;gap:9px;background:linear-gradient(90deg,#0e2a24,#122a1e);color:#7cf0c8;border:1px solid #123c30;border-radius:14px;padding:9px 14px;font-size:13.5px;cursor:pointer}
|
| 48 |
+
.triage .sk{color:#00d95f;display:grid;flex:0 0 auto}
|
| 49 |
+
.chats{flex:1 1 auto;overflow-y:auto;overflow-x:hidden}
|
| 50 |
+
.chats::-webkit-scrollbar{width:6px}.chats::-webkit-scrollbar-thumb{background:#374248;border-radius:6px}
|
| 51 |
+
.row{display:flex;gap:13px;align-items:center;padding:9px 14px;cursor:pointer;position:relative}
|
| 52 |
+
.row:after{content:"";position:absolute;left:78px;right:0;bottom:0;height:1px;background:#1c262d}
|
| 53 |
+
.row:hover{background:#202c33}.row.on{background:#2a3942}.row.on:after{background:transparent}
|
| 54 |
+
.av{position:relative;flex:0 0 auto;border-radius:50%;overflow:hidden}
|
| 55 |
+
.av img{width:100%;height:100%;object-fit:cover;display:block}
|
| 56 |
+
.row .b{min-width:0;flex:1 1 auto}
|
| 57 |
+
.row .n{color:#e9edef;font-size:16px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:6px}
|
| 58 |
+
.row .p{color:#8696a0;font-size:13.5px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:3px}
|
| 59 |
+
.row .rc{color:#53bdeb}
|
| 60 |
+
.row .m{margin-left:auto;text-align:right;display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:0 0 auto}
|
| 61 |
+
.row .t{color:#8696a0;font-size:12px}.row.unread .t{color:#00d95f;font-weight:500}
|
| 62 |
+
.row .pin{color:#f15c6d;font-size:13px;transform:rotate(45deg)}
|
| 63 |
+
.netdot{width:16px;height:16px;border-radius:4px;object-fit:cover;flex:0 0 auto}
|
| 64 |
+
.main{position:relative;display:flex;flex-direction:column;min-width:0;background:#0b141a}
|
| 65 |
+
.wp{position:absolute;inset:0;background:#0b141a center/cover no-repeat;z-index:0}
|
| 66 |
+
.wp:after{content:"";position:absolute;inset:0;background:linear-gradient(rgba(11,20,26,.35),rgba(11,20,26,.5))}
|
| 67 |
+
.mh,.thread,.composer,.e2ewrap{position:relative;z-index:1}
|
| 68 |
+
.mh{height:60px;flex:0 0 auto;background:#202c33;display:flex;align-items:center;gap:14px;padding:0 16px}
|
| 69 |
+
.mh .who .n{font-size:16px;color:#e9edef;font-weight:500}
|
| 70 |
+
.mh .who .s{font-size:13px;color:#00a884;margin-top:1px}
|
| 71 |
+
.mh .sp{margin-left:auto}.mh .icons{display:flex;gap:2px}
|
| 72 |
+
.ico{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;color:#aebac1;cursor:pointer;transition:background .12s;background:transparent;border:0}
|
| 73 |
+
.ico:hover{background:#ffffff14;color:#e9edef}
|
| 74 |
+
.ico.sk{color:#00d95f}
|
| 75 |
+
.e2ewrap{display:flex;justify-content:center;padding:14px 0 2px}
|
| 76 |
+
.e2e{background:#182229;color:#ffd279;font-size:12.5px;padding:6px 14px;border-radius:8px;box-shadow:0 1px .5px #0003}
|
| 77 |
+
.thread{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;padding:10px 8% 14px;display:flex;flex-direction:column;gap:3px}
|
| 78 |
+
.thread::-webkit-scrollbar{width:6px}.thread::-webkit-scrollbar-thumb{background:#374248cc;border-radius:6px}
|
| 79 |
+
.more{align-self:center;color:#8696a0;font-size:12px;padding:8px;opacity:.85}
|
| 80 |
+
.day{align-self:center;background:#182229;color:#8696a0;font-size:12.5px;padding:5px 12px;border-radius:8px;margin:8px 0 6px;box-shadow:0 1px .5px #0003}
|
| 81 |
+
.bub{max-width:65%;padding:6px 9px 8px 10px;border-radius:8px;font-size:14.2px;line-height:1.42;box-shadow:0 1px .5px #0003;word-wrap:break-word;white-space:pre-wrap}
|
| 82 |
+
.in{align-self:flex-start;background:#202c33;border-top-left-radius:0}
|
| 83 |
+
.out{align-self:flex-end;background:#005c4b;border-top-right-radius:0}
|
| 84 |
+
.grp{margin-top:8px}
|
| 85 |
+
.bub .tt{font-size:11px;color:#ffffff8a;float:right;margin:6px 0 -3px 12px;position:relative;top:4px}
|
| 86 |
+
.bub .tt .rc{color:#53bdeb}
|
| 87 |
+
.empty{margin:auto;color:#8696a0;text-align:center;background:#182229;padding:8px 16px;border-radius:8px}
|
| 88 |
+
.composer{flex:0 0 auto;background:#202c33;display:flex;align-items:center;gap:6px;padding:8px 14px}
|
| 89 |
+
.composer .grow{flex:1 1 auto}
|
| 90 |
+
.send{width:44px;height:44px;border-radius:50%;background:#00a884;color:#0b141a;border:0;display:grid;place-items:center;cursor:pointer;flex:0 0 auto}
|
| 91 |
+
/* media */
|
| 92 |
+
.m-img{display:block;max-width:320px;max-height:340px;border-radius:6px;cursor:pointer;object-fit:cover}
|
| 93 |
+
.m-vid{position:relative;max-width:320px;border-radius:6px;overflow:hidden;cursor:pointer}
|
| 94 |
+
.m-vid img{display:block;width:100%;border-radius:6px}
|
| 95 |
+
.m-play{position:absolute;inset:0;display:grid;place-items:center}
|
| 96 |
+
.m-play span{width:48px;height:48px;border-radius:50%;background:#0009;color:#fff;display:grid;place-items:center;font-size:20px}
|
| 97 |
+
.m-file{display:flex;align-items:center;gap:10px;background:#ffffff0f;border-radius:8px;padding:9px 11px;min-width:200px;cursor:pointer}
|
| 98 |
+
.m-file .fi{width:38px;height:38px;border-radius:8px;background:#00a88433;color:#7cf0c8;display:grid;place-items:center;font-size:16px;flex:0 0 auto}
|
| 99 |
+
.m-file .fn{font-size:13.5px;color:#e9edef;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 100 |
+
.m-file .fs{font-size:12px;color:#8696a0;margin-top:2px}
|
| 101 |
+
.m-aud{display:flex;align-items:center;gap:10px;min-width:220px;padding:2px 0}
|
| 102 |
+
.m-aud audio{height:34px}
|
| 103 |
+
.lb{position:fixed;inset:0;background:#000c;z-index:60;display:grid;place-items:center;cursor:zoom-out}
|
| 104 |
+
.lb img,.lb video{max-width:90vw;max-height:90vh;border-radius:8px}
|
| 105 |
+
.lb .x{position:absolute;top:18px;right:22px;color:#fff;font-size:28px;cursor:pointer;background:0;border:0}
|
| 106 |
+
/* attach tray + pay sheet */
|
| 107 |
+
.tray{position:absolute;bottom:66px;left:14px;background:#233138;border-radius:12px;box-shadow:0 10px 34px #0009;padding:6px;z-index:6;min-width:236px}
|
| 108 |
+
.tray button{display:flex;align-items:center;gap:11px;width:100%;background:0;border:0;color:#e9edef;font-size:14.5px;padding:10px 12px;border-radius:8px;cursor:pointer;text-align:left}
|
| 109 |
+
.tray button:hover{background:#2a3942}.tray button:disabled{opacity:.5}
|
| 110 |
+
.tray .sep{height:1px;background:#2a3942;margin:4px 6px}
|
| 111 |
+
.paywrap{position:fixed;inset:0;background:#000a;z-index:60;display:grid;place-items:center}
|
| 112 |
+
.pay{background:#111b21;border-radius:14px;padding:20px;width:346px;max-width:90vw;box-shadow:0 20px 60px #000a}
|
| 113 |
+
.pay h3{margin:0 0 4px;font-size:17px}.pay .psub{color:#8696a0;font-size:13px;margin-bottom:14px}
|
| 114 |
+
.pay .seg{display:flex;background:#202c33;border-radius:10px;padding:3px;margin-bottom:14px}
|
| 115 |
+
.pay .seg button{flex:1;background:0;border:0;color:#8696a0;padding:8px;border-radius:8px;cursor:pointer;font-size:14px}
|
| 116 |
+
.pay .seg button.on{background:#00a884;color:#0b141a;font-weight:600}
|
| 117 |
+
.pay .amt{width:100%;background:#202c33;border:0;border-radius:10px;color:#e9edef;font-size:26px;text-align:center;padding:12px;margin-bottom:10px;outline:0}
|
| 118 |
+
.pay .chips{display:flex;gap:8px;margin-bottom:12px}
|
| 119 |
+
.pay .chips button{flex:1;background:#202c33;border:0;color:#e9edef;border-radius:8px;padding:9px;cursor:pointer}
|
| 120 |
+
.pay .chips button.on{background:#0b3b2e;color:#00d95f}
|
| 121 |
+
.pay .memo{width:100%;background:#202c33;border:0;border-radius:10px;color:#e9edef;padding:10px 12px;margin-bottom:12px;font-size:14px;outline:0}
|
| 122 |
+
.pay .err{color:#f15c6d;font-size:13px;margin-bottom:8px;text-align:center}
|
| 123 |
+
.pay .go{width:100%;background:#00a884;color:#0b141a;border:0;border-radius:10px;padding:12px;font-weight:600;cursor:pointer;font-size:15px}
|
| 124 |
+
.pay .go:disabled{opacity:.5;cursor:default}
|
| 125 |
+
.pay .cancel{width:100%;background:0;color:#8696a0;border:0;padding:10px;margin-top:6px;cursor:pointer}
|
| 126 |
+
`;
|
| 127 |
+
|
| 128 |
+
async function ensureImportmap() {
|
| 129 |
+
if (document.querySelector('script[type="importmap"][data-holo-ui]')) return;
|
| 130 |
+
const map = await (await fetch("/apps/ui/vendor/importmap.json")).json();
|
| 131 |
+
const imports = {}; for (const [k, v] of Object.entries(map.imports || map)) imports[k] = typeof v === "string" ? v.replace(/^\.\//, "/apps/ui/") : v;
|
| 132 |
+
const s = document.createElement("script"); s.type = "importmap"; s.setAttribute("data-holo-ui", "1"); s.textContent = JSON.stringify({ imports }); document.head.appendChild(s);
|
| 133 |
+
}
|
| 134 |
+
let _deps = null;
|
| 135 |
+
async function deps() {
|
| 136 |
+
if (_deps) return _deps;
|
| 137 |
+
await ensureImportmap();
|
| 138 |
+
const React = await import("react");
|
| 139 |
+
const { createRoot } = await import("react-dom/client");
|
| 140 |
+
const reg = await (await fetch("/apps/ui/registry/index.json")).json();
|
| 141 |
+
const K = Object.fromEntries(reg.components.map((c) => [c.name, c.holo]));
|
| 142 |
+
const [button, badge, avatar, input] = await Promise.all(["button", "badge", "avatar", "input"].map((n) => import(K[n])));
|
| 143 |
+
return (_deps = { React, createRoot, C: { button, badge, avatar, input } });
|
| 144 |
+
}
|
| 145 |
+
if (!document.getElementById("wa-shadcn-css")) { const st = document.createElement("style"); st.id = "wa-shadcn-css"; st.textContent = CSS; document.head.appendChild(st); }
|
| 146 |
+
|
| 147 |
+
const initials = (s) => (String(s || "?").trim().split(/\s+/).map((w) => w[0]).join("").slice(0, 2) || "?").toUpperCase();
|
| 148 |
+
const hue = (s) => { let h = 0; s = String(s || ""); for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return `hsl(${h % 360} 40% 46%)`; };
|
| 149 |
+
const isOut = (m) => !!(m && (m.mine || m.out || m.fromMe || m.dir === "out" || m.from === "me" || m.self));
|
| 150 |
+
const threadOf = (model, id) => { try { if (id && model.thread) return model.thread(id) || []; } catch {} return (model.threads && model.threads[id]) || []; };
|
| 151 |
+
const rcMark = (m) => m.status === "read" || m.status === "delivered" ? "✓✓" : m.status === "sent" ? "✓" : "";
|
| 152 |
+
const slugOf = (net) => { const k = String(net || "").toLowerCase(); return NET_SLUG[k] === undefined ? k : NET_SLUG[k]; };
|
| 153 |
+
const mimeKind = (mime, kind) => { const k = String(kind || "").toLowerCase(); if (["image", "video", "audio", "voice"].includes(k)) return k === "voice" ? "audio" : k; const t = String(mime || "").split("/")[0]; if (t === "image" || t === "video" || t === "audio") return t; return "file"; };
|
| 154 |
+
const humanSize = (n) => { n = +n || 0; if (!n) return ""; const u = ["B", "KB", "MB", "GB"]; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return (n < 10 && i ? n.toFixed(1) : Math.round(n)) + " " + u[i]; };
|
| 155 |
+
const fileGlyph = (mime, name) => { const m = String(mime || ""), e = String(name || "").split(".").pop().toLowerCase(); if (m.includes("pdf") || e === "pdf") return "📕"; if (["zip", "rar", "7z"].includes(e)) return "🗜"; if (["doc", "docx"].includes(e)) return "📘"; if (["xls", "xlsx", "csv"].includes(e)) return "📗"; return "📄"; };
|
| 156 |
+
|
| 157 |
+
function buildApp({ React, C }) {
|
| 158 |
+
const { createElement: h, memo, useState, useRef, useEffect, useCallback, useLayoutEffect } = React;
|
| 159 |
+
const Avatar = C.avatar.Avatar, AvatarImage = C.avatar.AvatarImage, AvatarFallback = C.avatar.AvatarFallback;
|
| 160 |
+
const Button = C.button.Button, Input = C.input.Input, Badge = C.badge.Badge;
|
| 161 |
+
|
| 162 |
+
// exact SVG line icons (from the real app's IC set)
|
| 163 |
+
const svg = (kids, s = 22, w = 1.8) => h("svg", { viewBox: "0 0 24 24", width: s, height: s, fill: "none", stroke: "currentColor", strokeWidth: w, strokeLinecap: "round", strokeLinejoin: "round" }, ...kids);
|
| 164 |
+
const P = (d, e) => h("path", { d, ...e });
|
| 165 |
+
const CIR = (cx, cy, r, e) => h("circle", { cx, cy, r, ...e });
|
| 166 |
+
const RC = (x, y, width, height, rx, e) => h("rect", { x, y, width, height, rx, ...e });
|
| 167 |
+
const fillC = { fill: "currentColor", stroke: "none" };
|
| 168 |
+
const IC = {
|
| 169 |
+
chats: () => svg([P("M20 11.4A7.6 7.6 0 0 1 8.9 18.2L4.5 19.4l1.2-4.2A7.6 7.6 0 1 1 20 11.4Z")]),
|
| 170 |
+
search: (s) => svg([CIR(11, 11, 7), P("m20 20-3.6-3.6")], s || 19),
|
| 171 |
+
phone: () => svg([P("M6.6 4H4.5A1.5 1.5 0 0 0 3 5.6 16 16 0 0 0 18.4 21a1.5 1.5 0 0 0 1.6-1.5v-2.1a1.5 1.5 0 0 0-1.2-1.45l-2.2-.45a1.5 1.5 0 0 0-1.5.6l-.5.7a12 12 0 0 1-5.1-5.1l.7-.5a1.5 1.5 0 0 0 .6-1.5l-.45-2.2A1.5 1.5 0 0 0 6.6 4Z")]),
|
| 172 |
+
video: () => svg([RC(3, 6.5, 12, 11, 2.5), P("m15 10.5 5.5-3v9l-5.5-3Z")]),
|
| 173 |
+
menu: () => svg([CIR(12, 5, 1.35, fillC), CIR(12, 12, 1.35, fillC), CIR(12, 19, 1.35, fillC)]),
|
| 174 |
+
newchat: () => svg([P("M13.5 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.5"), P("M18.4 3.6a2 2 0 0 1 2.8 2.8L13 14.6l-3.4.9.9-3.4 7.9-8.5Z")]),
|
| 175 |
+
emoji: () => svg([CIR(12, 12, 8.5), P("M8.8 14.2a4.2 4.2 0 0 0 6.4 0"), CIR(9, 10, 1, fillC), CIR(15, 10, 1, fillC)]),
|
| 176 |
+
plus: () => svg([P("M12 5.5v13M5.5 12h13")]),
|
| 177 |
+
mic: () => svg([RC(9, 3, 6, 11, 3), P("M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21M8.5 21h7")]),
|
| 178 |
+
send: () => svg([P("M4.5 12 20.5 4.5 13 20.5l-2.8-6.4L4.5 12Z")]),
|
| 179 |
+
spark: (s) => svg([P("M12 3.2l1.7 4.6 4.6 1.7-4.6 1.7L12 15.8l-1.7-4.6L5.7 9.5l4.6-1.7L12 3.2Z"), P("M5.5 15.5l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8.8-2Z")], s || 20),
|
| 180 |
+
sun: () => svg([CIR(12, 12, 4), P("M12 2v2M12 20v2M4 12H2M22 12h-2M5.6 5.6 4.2 4.2M19.8 19.8l-1.4-1.4M18.4 5.6l1.4-1.4M4.2 19.8l1.4-1.4")]),
|
| 181 |
+
plusThin: () => svg([P("M12 5.5v13M5.5 12h13")], 26, 1.6),
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
const Av = (name, size, bg, src) => h("div", { className: "av", style: { height: size + "px", width: size + "px" } },
|
| 185 |
+
h(Avatar, { style: { height: "100%", width: "100%", background: bg || hue(name), display: "grid", placeItems: "center" } },
|
| 186 |
+
src ? h(AvatarImage, { src, alt: "", style: { height: "100%", width: "100%", objectFit: "cover" } }) : null,
|
| 187 |
+
h(AvatarFallback, { style: { background: bg || hue(name), color: "#fff", fontSize: (size * 0.36) + "px", fontWeight: 500, height: "100%", width: "100%", display: "grid", placeItems: "center" } }, initials(name))));
|
| 188 |
+
const Ico = (icon, onClick, title, cls) => h("button", { className: "ico" + (cls ? " " + cls : ""), onClick, title }, icon);
|
| 189 |
+
const netLogo = (net) => { const s = slugOf(net); return s ? h("img", { className: "netdot", src: BRANDS + s + ".svg", alt: "", onError: (e) => { e.currentTarget.style.display = "none"; } }) : null; };
|
| 190 |
+
|
| 191 |
+
// rich media in a bubble — image/video/audio/file, with lazy bridge-media resolution
|
| 192 |
+
function MediaView({ media, model, onMedia }) {
|
| 193 |
+
const [url, setUrl] = useState(media.url || media.thumb || "");
|
| 194 |
+
const bucket = mimeKind(media.mime, media.kind);
|
| 195 |
+
useEffect(() => { let alive = true;
|
| 196 |
+
if (!media.url && (media.lazy || media.pending || media.ref) && model.resolveBridgeMedia) {
|
| 197 |
+
Promise.resolve(model.resolveBridgeMedia(media)).then((u) => { if (alive && u) setUrl(typeof u === "string" ? u : (u.url || u.src || "")); }).catch(() => {});
|
| 198 |
+
} return () => { alive = false; }; }, []);
|
| 199 |
+
if (bucket === "image") return url ? h("img", { className: "m-img", src: url, alt: "", loading: "lazy", onClick: () => onMedia({ kind: "image", url }) }) : h("div", { className: "m-file" }, "🖼 Photo");
|
| 200 |
+
if (bucket === "video") return h("div", { className: "m-vid", onClick: () => onMedia({ kind: "video", url }) }, h("img", { src: media.thumb || url, alt: "" }), h("div", { className: "m-play" }, h("span", {}, "▶")));
|
| 201 |
+
if (bucket === "audio") return h("div", { className: "m-aud" }, url ? h("audio", { controls: true, src: url }) : h("span", {}, "🎤 Voice message"));
|
| 202 |
+
return h("div", { className: "m-file", onClick: () => url && window.open(url, "_blank") }, h("div", { className: "fi" }, fileGlyph(media.mime, media.filename)), h("div", { style: { minWidth: 0 } }, h("div", { className: "fn" }, media.filename || "File"), media.size ? h("div", { className: "fs" }, humanSize(media.size)) : null));
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
const Rail = memo(function Rail({ nets, active, totalUnread, onPick }) {
|
| 206 |
+
return h("nav", { className: "rail" },
|
| 207 |
+
h("button", { className: "rb" + (active === "All" ? " on" : ""), title: "All chats", onClick: () => onPick("All") }, IC.chats(), totalUnread ? h("span", { className: "bdg" }, totalUnread > 99 ? "99+" : totalUnread) : null),
|
| 208 |
+
nets.length ? h("div", { className: "rsep" }) : null,
|
| 209 |
+
nets.map((n) => h("button", { key: n.id, className: "rb" + (active === n.id ? " on" : ""), title: n.label, onClick: () => onPick(n.id) },
|
| 210 |
+
(() => { const s = slugOf(n.id); return s ? h("img", { src: BRANDS + s + ".svg", alt: n.label, onError: (e) => { const t = document.createTextNode((n.label || "?")[0].toUpperCase()); try { e.currentTarget.replaceWith(t); } catch {} } }) : (n.label || "?")[0].toUpperCase(); })(),
|
| 211 |
+
h("span", { className: "dot" }), n.unread ? h("span", { className: "bdg" }, n.unread > 99 ? "99+" : n.unread) : null)),
|
| 212 |
+
h("button", { className: "rb", title: "Add network", onClick: () => onPick("+") }, IC.plusThin()),
|
| 213 |
+
h("div", { className: "rgrow" }),
|
| 214 |
+
h("button", { className: "rb", title: "Theme" }, IC.sun()),
|
| 215 |
+
h("div", { className: "rav", title: "Your profile" }));
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
const Row = memo(function Row({ c, on, preview, receipt, onSelect }) {
|
| 219 |
+
return h("div", { className: "row" + (on ? " on" : "") + (c.unread ? " unread" : ""), onClick: () => onSelect(c.id) },
|
| 220 |
+
Av(c.name, 49, c.isQ ? "#12b886" : hue(c.name), c.avatar),
|
| 221 |
+
h("div", { className: "b" },
|
| 222 |
+
h("div", { className: "n" }, c.name || c.id || "Chat", c.network ? netLogo(c.network) : null),
|
| 223 |
+
h("div", { className: "p" }, receipt ? h("span", { className: "rc" }, receipt + " ") : null, preview)),
|
| 224 |
+
h("div", { className: "m" },
|
| 225 |
+
h("div", { className: "t" }, c.time || c.ts || ""),
|
| 226 |
+
c.pinned ? h("span", { className: "pin" }, "📌") : null,
|
| 227 |
+
c.unread ? h(Badge, { style: { background: "#00a884", color: "#0b141a", borderRadius: "999px", height: "20px", minWidth: "20px", justifyContent: "center", padding: "0 6px", fontSize: "12px", fontWeight: 600 } }, String(c.unread)) : null));
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
function Thread({ model, activeId, onMedia }) {
|
| 231 |
+
const all = threadOf(model, activeId);
|
| 232 |
+
const [limit, setLimit] = useState(WINDOW);
|
| 233 |
+
const ref = useRef(null), prevId = useRef(activeId), anchorH = useRef(0), pin = useRef(true);
|
| 234 |
+
const toBottom = (el) => { el.scrollTop = el.scrollHeight; };
|
| 235 |
+
useEffect(() => { setLimit(WINDOW); }, [activeId]);
|
| 236 |
+
// Runs after every render (no deps): a fresh chat, or new messages arriving, both flow through here.
|
| 237 |
+
// While "pinned" we stay glued to the newest message — exactly like WhatsApp.
|
| 238 |
+
useLayoutEffect(() => { const el = ref.current; if (!el) return;
|
| 239 |
+
if (prevId.current !== activeId) { prevId.current = activeId; pin.current = true; anchorH.current = 0; toBottom(el); return; }
|
| 240 |
+
if (anchorH.current) { el.scrollTop = el.scrollHeight - anchorH.current; anchorH.current = 0; return; }
|
| 241 |
+
if (pin.current) toBottom(el); });
|
| 242 |
+
// Media resolves async (placeholder first, <img> swapped in later) and reflows the thread AFTER layout.
|
| 243 |
+
// A ResizeObserver on the scroll container won't catch it (its own box size never changes), and per-image
|
| 244 |
+
// listeners miss images added later — so listen for `load` in the CAPTURE phase on the container, which
|
| 245 |
+
// fires for ANY descendant image whenever it appears/loads. Keeps us glued to the newest message.
|
| 246 |
+
useEffect(() => { const el = ref.current; if (!el) return; pin.current = true; toBottom(el);
|
| 247 |
+
requestAnimationFrame(() => { if (pin.current && ref.current) toBottom(ref.current); });
|
| 248 |
+
const stick = () => { if (pin.current && ref.current) toBottom(ref.current); };
|
| 249 |
+
el.addEventListener("load", stick, true); el.addEventListener("error", stick, true);
|
| 250 |
+
return () => { el.removeEventListener("load", stick, true); el.removeEventListener("error", stick, true); };
|
| 251 |
+
}, [activeId]);
|
| 252 |
+
// Detach when the user scrolls up; re-attach ("jump to latest") once they're back near the bottom.
|
| 253 |
+
const onScroll = useCallback((e) => { const el = e.currentTarget;
|
| 254 |
+
pin.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
| 255 |
+
if (el.scrollTop < 120 && limit < all.length) { anchorH.current = el.scrollHeight; setLimit((l) => Math.min(all.length, l + WINDOW)); } }, [limit, all.length]);
|
| 256 |
+
const start = Math.max(0, all.length - limit), shown = all.slice(start);
|
| 257 |
+
if (!all.length) return h("div", { className: "thread", ref }, h("div", { className: "empty" }, "No messages yet. Say hello 👋"));
|
| 258 |
+
const rows = [h("div", { className: "e2ewrap", key: "e2e" }, h("div", { className: "e2e" }, "🔒 Messages are end-to-end encrypted")), h("div", { className: "day", key: "day" }, "Today")];
|
| 259 |
+
if (start > 0) rows.push(h("div", { className: "more", key: "more" }, `↑ ${start.toLocaleString()} earlier — scroll up`));
|
| 260 |
+
let lastOut = null;
|
| 261 |
+
shown.forEach((m, i) => { const out = isOut(m); const hasMedia = m.media && (m.media.url || m.media.thumb || m.media.ref || m.media.lazy || m.media.kind);
|
| 262 |
+
rows.push(h("div", { key: start + i, className: "bub " + (out ? "out" : "in") + (out !== lastOut ? " grp" : ""), style: hasMedia ? { maxWidth: "min(360px,72%)" } : null },
|
| 263 |
+
hasMedia ? h(MediaView, { media: m.media, model, onMedia }) : null,
|
| 264 |
+
m.text ? h("div", { style: hasMedia ? { marginTop: "5px" } : null }, m.text) : null,
|
| 265 |
+
h("span", { className: "tt" }, (m.time || m.ts || ""), out ? h("span", { className: "rc" }, " " + rcMark(m)) : null)));
|
| 266 |
+
lastOut = out; });
|
| 267 |
+
return h("div", { className: "thread", ref, onScroll }, rows);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
const Composer = memo(function Composer({ activeId, onSend, onPlus }) {
|
| 271 |
+
const [draft, setDraft] = useState("");
|
| 272 |
+
const send = () => { const t = draft.trim(); if (!t) return; onSend(activeId, t); setDraft(""); };
|
| 273 |
+
return h("div", { className: "composer" },
|
| 274 |
+
Ico(IC.plus(), onPlus, "Attach"), Ico(IC.emoji(), null, "Emoji"),
|
| 275 |
+
h("div", { className: "grow" }, h(Input, { placeholder: "Type a message", value: draft, autoFocus: true,
|
| 276 |
+
onChange: (e) => setDraft(e.target.value), onKeyDown: (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } },
|
| 277 |
+
style: { background: "#2a3942", border: "0", borderRadius: "10px", height: "44px", color: "#e9edef", paddingLeft: "16px" } })),
|
| 278 |
+
h("button", { className: "send", onClick: send, "aria-label": "Send" }, draft.trim() ? IC.send() : IC.mic()));
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
const TrayBtn = (glyph, label, onClick, disabled) => h("button", { onClick, disabled }, h("span", { style: { fontSize: "17px" } }, glyph), label);
|
| 282 |
+
|
| 283 |
+
function PaySheet({ model, activeId, conv, sheet, setSheet }) {
|
| 284 |
+
const set = (patch) => setSheet((s) => ({ ...s, ...patch, err: null }));
|
| 285 |
+
const amt = parseFloat(sheet.amount);
|
| 286 |
+
const go = async () => { set({ busy: true }); let r;
|
| 287 |
+
try { r = await model.holoPay(sheet.genesis || activeId, { kind: sheet.kind, amount: amt, memo: (sheet.memo || "").trim() }); } catch (e) { r = { ok: false, error: String(e && e.message || e) }; }
|
| 288 |
+
if (r && r.ok) setSheet(null); else setSheet((s) => ({ ...s, busy: false, err: (r && r.error) || "Couldn't create the payment" })); };
|
| 289 |
+
return h("div", { className: "paywrap", onClick: () => setSheet(null) },
|
| 290 |
+
h("div", { className: "pay", onClick: (e) => e.stopPropagation() },
|
| 291 |
+
h("h3", {}, sheet.kind === "send" ? "Send money" : "Request money"), h("div", { className: "psub" }, (sheet.kind === "send" ? "To " : "From ") + (conv.name || "this chat")),
|
| 292 |
+
h("div", { className: "seg" }, h("button", { className: sheet.kind === "send" ? "on" : "", onClick: () => set({ kind: "send" }) }, "Send"), h("button", { className: sheet.kind === "request" ? "on" : "", onClick: () => set({ kind: "request" }) }, "Request")),
|
| 293 |
+
h("input", { className: "amt", inputMode: "decimal", placeholder: "$0", value: sheet.amount, onChange: (e) => set({ amount: e.target.value.replace(/[^0-9.]/g, "") }), autoFocus: true }),
|
| 294 |
+
h("div", { className: "chips" }, [10, 20, 50, 100].map((a) => h("button", { key: a, className: amt === a ? "on" : "", onClick: () => set({ amount: String(a) }) }, "$" + a))),
|
| 295 |
+
h("input", { className: "memo", placeholder: "What's it for? (optional)", maxLength: 120, value: sheet.memo || "", onChange: (e) => set({ memo: e.target.value }) }),
|
| 296 |
+
sheet.err ? h("div", { className: "err" }, sheet.err) : null,
|
| 297 |
+
h("button", { className: "go", disabled: sheet.busy || !(amt > 0), onClick: go }, sheet.busy ? (sheet.kind === "send" ? "Confirm in your wallet…" : "Creating…") : ((sheet.kind === "send" ? "Send" : "Request") + (amt > 0 ? " $" + sheet.amount : " money"))),
|
| 298 |
+
h("button", { className: "cancel", onClick: () => setSheet(null) }, "Cancel")));
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
const previewOf = (model, c) => { if (c.preview) return c.preview; const t = threadOf(model, c.id); const last = t.length ? t[t.length - 1] : null; return (last && (last.text || (last.media ? "📎 media" : ""))) || ""; };
|
| 302 |
+
|
| 303 |
+
return function App({ model }) {
|
| 304 |
+
const convs = Array.isArray(model.conversations) ? model.conversations : [];
|
| 305 |
+
const [activeId, setActiveId] = useState(null);
|
| 306 |
+
const [filter, setFilter] = useState("All");
|
| 307 |
+
const [q, setQ] = useState("");
|
| 308 |
+
const [lb, setLb] = useState(null);
|
| 309 |
+
const [attachOpen, setAttachOpen] = useState(false);
|
| 310 |
+
const [paySheet, setPaySheet] = useState(null);
|
| 311 |
+
const fileRef = useRef(null);
|
| 312 |
+
const onSelect = useCallback((id) => { setActiveId(id); setAttachOpen(false); }, []);
|
| 313 |
+
const active = (activeId && convs.some((c) => c.id === activeId)) ? activeId : ((convs.find((c) => c.active) || convs[0] || {}).id || null);
|
| 314 |
+
const conv = convs.find((c) => c.id === active) || {};
|
| 315 |
+
const onSend = useCallback((id, text) => { try { model.onSend && model.onSend(id, text); } catch (e) { console.error("[shadcn-ui] onSend", e); } }, [model]);
|
| 316 |
+
const call = useCallback((video) => { try { model.startCall && model.startCall(active, { video }); } catch (e) { console.error("[shadcn-ui] startCall", e); } }, [model, active]);
|
| 317 |
+
const pickFile = (accept) => { if (fileRef.current) { fileRef.current.accept = accept; fileRef.current.click(); } setAttachOpen(false); };
|
| 318 |
+
const onFile = (e) => { const f = e.target.files && e.target.files[0]; if (f) { try { model.onAttach && model.onAttach(active, f); } catch (er) { console.error("[shadcn-ui] onAttach", er); } } e.target.value = ""; };
|
| 319 |
+
const tryM = (fn, ...a) => { setAttachOpen(false); try { const r = model[fn] && model[fn](...a); if (r && r.catch) r.catch(() => {}); } catch (e) { console.error("[shadcn-ui] " + fn, e); } };
|
| 320 |
+
const nets = Array.isArray(model.networks) && model.networks.length ? model.networks
|
| 321 |
+
: Object.values(convs.reduce((a, c) => { const id = (c.network || c.platform); if (id && !a[id]) a[id] = { id, label: id[0].toUpperCase() + id.slice(1), unread: 0 }; if (id) a[id].unread += (c.unread || 0); return a; }, {}));
|
| 322 |
+
const totalUnread = convs.reduce((n, c) => n + (c.unread || 0), 0);
|
| 323 |
+
const signalUnread = model.signalUnread != null ? model.signalUnread : convs.filter((c) => c.unread).length;
|
| 324 |
+
const qs = q.trim().toLowerCase();
|
| 325 |
+
const shown = convs.filter((c) => (filter === "All" || (c.network || c.platform) === filter) && (!qs || (c.name || "").toLowerCase().includes(qs) || (previewOf(model, c) || "").toLowerCase().includes(qs)));
|
| 326 |
+
const sub = conv.presence || (conv.isQ ? "online · on your device" : (conv.status || conv.network || conv.platform || ""));
|
| 327 |
+
const runSearch = () => { const t = q.trim(); if (!t) return; try { if (model.qCommand) model.qCommand(t); else if (model.onNewChat) model.onNewChat(t); } catch {} };
|
| 328 |
+
|
| 329 |
+
return h("div", { className: "wa" },
|
| 330 |
+
h(Rail, { nets, active: filter, totalUnread, onPick: (id) => { if (id === "+") { try { model.openNetworks && model.openNetworks(); } catch {} } else setFilter(id); } }),
|
| 331 |
+
h("div", { className: "side" },
|
| 332 |
+
h("div", { className: "stitle" }, h("span", { className: "t" }, "Messenger"),
|
| 333 |
+
Ico(IC.spark(), () => { try { (model.askQ || model.qCommand) && (model.askQ ? model.askQ() : null); } catch {} }, "Ask Q", "sk"), Ico(IC.newchat(), () => { try { model.onNewChat && model.onNewChat(""); } catch {} }, "New chat"), Ico(IC.menu(), () => {}, "Menu")),
|
| 334 |
+
h("div", { className: "search" }, h("span", { className: "si" }, IC.search()),
|
| 335 |
+
h(Input, { placeholder: "Search, ask Q, or start a chat…", value: q, onChange: (e) => setQ(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") runSearch(); },
|
| 336 |
+
style: { background: "#202c33", border: "0", borderRadius: "10px", height: "42px", color: "#e9edef", paddingLeft: "40px", fontSize: "14.5px" } })),
|
| 337 |
+
h("div", { className: "filt" }, h("button", { className: "chip on" }, "All"), h("button", { className: "caret" }, "▾")),
|
| 338 |
+
signalUnread ? h("div", { className: "triage" }, h("span", { className: "sk" }, IC.spark(18)), `${signalUnread} need you · ~${Math.max(1, Math.round(signalUnread * 0.4))} min`) : null,
|
| 339 |
+
h("div", { className: "chats" }, shown.map((c) => h(Row, { key: c.id, c, on: c.id === active, preview: previewOf(model, c), receipt: c.receipt || "", onSelect })))),
|
| 340 |
+
h("div", { className: "main" },
|
| 341 |
+
h("div", { className: "wp", style: { backgroundImage: `url(${WP})` } }),
|
| 342 |
+
h("div", { className: "mh" }, Av(conv.name, 40, conv.isQ ? "#12b886" : hue(conv.name), conv.avatar),
|
| 343 |
+
h("div", { className: "who" }, h("div", { className: "n" }, conv.name || ""), sub ? h("div", { className: "s" }, sub) : null),
|
| 344 |
+
h("div", { className: "sp" }), h("div", { className: "icons" }, Ico(IC.video(), () => call(true), "Video call"), Ico(IC.phone(), () => call(false), "Voice call"), Ico(IC.menu(), () => { try { model.onFavourite && model.onFavourite(active); } catch {} }, "Menu"))),
|
| 345 |
+
h(Thread, { model, activeId: active, onMedia: setLb }),
|
| 346 |
+
h("input", { ref: fileRef, type: "file", style: { display: "none" }, onChange: onFile }),
|
| 347 |
+
attachOpen ? h("div", { className: "tray" },
|
| 348 |
+
TrayBtn("🖼", "Photos & videos", () => pickFile("image/*,video/*")),
|
| 349 |
+
TrayBtn("📄", "Document", () => pickFile("*/*")),
|
| 350 |
+
(conv.network && !conv.isQ && model.holoPay) ? TrayBtn("💸", "Send money", () => { setAttachOpen(false); setPaySheet({ kind: "send", amount: "", memo: "", busy: false, err: null }); }) : null,
|
| 351 |
+
h("div", { className: "sep" }),
|
| 352 |
+
model.startMeet ? TrayBtn("👥", "Start a meeting", () => tryM("startMeet", active, { video: true })) : null,
|
| 353 |
+
model.startTogether ? TrayBtn("🖥", "Share my screen", () => tryM("startTogether", active, { kind: "tab", title: "" })) : null,
|
| 354 |
+
model.startTogether ? TrayBtn("📝", "Co-edit a doc", () => tryM("startTogether", active, { kind: "doc", title: "" })) : null,
|
| 355 |
+
(conv.network && !conv.isQ && model.qDraft) ? [h("div", { className: "sep", key: "s" }), TrayBtn("✨", "Draft a reply with Q", () => tryM("qDraft", active))] : null) : null,
|
| 356 |
+
h(Composer, { key: active, activeId: active, onSend, onPlus: () => setAttachOpen((v) => !v) })),
|
| 357 |
+
lb ? h("div", { className: "lb", onClick: () => setLb(null) }, h("button", { className: "x", "aria-label": "Close" }, "✕"),
|
| 358 |
+
lb.kind === "video" ? h("video", { src: lb.url, controls: true, autoPlay: true, onClick: (e) => e.stopPropagation() }) : h("img", { src: lb.url, alt: "", onClick: (e) => e.stopPropagation() })) : null,
|
| 359 |
+
paySheet ? h(PaySheet, { model, activeId: active, conv, sheet: paySheet, setSheet: setPaySheet }) : null);
|
| 360 |
+
};
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
async function mount(el, model) {
|
| 364 |
+
const d = await deps();
|
| 365 |
+
const App = buildApp(d);
|
| 366 |
+
let cur = model;
|
| 367 |
+
const root = d.createRoot(el);
|
| 368 |
+
const render = (mdl) => { cur = mdl || cur; root.render(d.React.createElement(App, { model: cur })); };
|
| 369 |
+
render(model);
|
| 370 |
+
console.log("[shadcn-ui] Holo Messenger (Pass 1) mounted from κ-components ·", (model.conversations || []).length, "chats");
|
| 371 |
+
return { update: render, unmount: () => { try { root.unmount(); } catch {} } };
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
if (WANT && typeof window !== "undefined") {
|
| 375 |
+
const prev = window.HoloMessengerUI;
|
| 376 |
+
window.HoloMessengerUI = { mount, version: ((prev && prev.version) || "m1") + "+shadcn", _chatscope: prev };
|
| 377 |
+
console.log("[shadcn-ui] streaming-κ shadcn UI ACTIVE (opt out: ?ui=chatscope)");
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
export { mount };
|
b/c741295f6742e247afb4eabc8b1f7423a4171c56f8bc933aa7a696e8ef97756d
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/d80d218db7c66a0414c0c02204d0e0398249b2ec619f903d02d97781fef92102
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en"><head>
|
| 3 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 4 |
+
<title>Holo Messenger</title>
|
| 5 |
+
<link rel="stylesheet" href="/apps/holo-messenger/_vendor/ui/chat-ui.bundle.css?v=f1ff491ce68b">
|
| 6 |
+
<style>html,body{height:100%;margin:0;background:#0b141a;color-scheme:dark}#root{height:100vh}</style>
|
| 7 |
+
<link rel="preload" as="image" href="/apps/holo-messenger/_vendor/wallpaper-default.jpg" fetchpriority="high">
|
| 8 |
+
</head><body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module">
|
| 11 |
+
// ?v=N - bump on each deploy so the browser can never serve a stale cached bundle (the cause of a frozen/old build).
|
| 12 |
+
import "/apps/holo-messenger/_vendor/ui/chat-ui.bundle.js?v=e811350a91c0"; // → window.HoloMessengerUI
|
| 13 |
+
// Opt-in: render the WhatsApp UX from STREAMING shadcn κ-components instead of the chatscope bundle.
|
| 14 |
+
// Additive — only takes over window.HoloMessengerUI.mount when ?ui=shadcn (or localStorage["holo.ui.shadcn"]="1");
|
| 15 |
+
// the default boot stays the untouched chatscope UI. Runs before boot() so it is fed the same live model.
|
| 16 |
+
import "/apps/holo-messenger/messenger-shadcn-ui.mjs";
|
| 17 |
+
import { boot } from "/apps/holo-messenger/holo-messenger-app.mjs?v=076005621416";
|
| 18 |
+
boot(document.getElementById("root")).catch(e => {
|
| 19 |
+
document.body.innerHTML = "<pre style='color:#f88;padding:16px;white-space:pre-wrap'>boot error: " + (e && e.stack || e) + "</pre>";
|
| 20 |
+
});
|
| 21 |
+
// Intelligent Inbox (holo-mail) - additive, feature-flagged OFF by default. Dynamic import + catch so it
|
| 22 |
+
// can NEVER block or break the messenger boot. Enable per-operator: localStorage["holo.mail.enabled"]="1".
|
| 23 |
+
import("/apps/holo-messenger/mail/holo-mail-attach.mjs").catch(() => {});
|
| 24 |
+
// HoloChat, woven into THIS messenger — one canonical surface, no separate overlay. Cards render natively
|
| 25 |
+
// in the bubbles; the composer "+" opens the unified tray (apps · Spaces · money · identity · prove).
|
| 26 |
+
// Additive + catch-guarded so it can never block boot. Enable: localStorage["holo.chat.enabled"]="1" or ?chat=1.
|
| 27 |
+
import("/apps/holo-messenger/holo-messenger-weave.mjs").catch(() => {});
|
| 28 |
+
// Colour re-tone picker (Default · Graphite · Warm · Contrast · Light) — colours only, all features
|
| 29 |
+
// intact. Gated: shows only with ?skins=1 or localStorage["holo.skins"]="1". Additive + catch-guarded.
|
| 30 |
+
import("/apps/holo-messenger/messenger-skin.mjs").catch(() => {});
|
| 31 |
+
// Asset cache (service worker) — repeat opens serve wallpaper/logos/bundle/css/js NETWORK-FREE. Caches
|
| 32 |
+
// only own-origin static assets (stale-while-revalidate); all dynamic/API/bridge/media traffic passes
|
| 33 |
+
// through untouched. Additive + catch-guarded. Escape hatch: open with ?nosw=1 to unregister.
|
| 34 |
+
import("/apps/holo-messenger/messenger-sw-register.mjs").catch(() => {});
|
| 35 |
+
</script>
|
| 36 |
+
</body></html>
|
b/e811350a91c04a5999d552279aca78f833ec7946ae929e0af42575fb62c061c0
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/ec9aea0c39ca06aa2001b22aef511d449b8e64c2c2b095ffedf46e2492f03b19
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/f1ff491ce68b55a5a5d68a825e6a78dcb0ff53edd426f99c2b095573e93acd01
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f6caa9eeead2c99deeb2cdb70b7e25bbaa2cf5c60555f5b4030ecf6b6d9efa51
|
| 3 |
+
size 546613
|