Files
tonhowtf 123fbaf70a feat(omnidisc): native chat, voice and screen share
Adds OmniDisc, a self-hosted chat/voice/screen-share surface inside OmniGet,
behind the `omnidisc.enabled` setting (off by default). The server it talks to
lives in its own repo; the wire contract is the new `omnidisc-proto` crate.

Client crates:
- `omnidisc-proto`: snowflakes with 10-day buckets, 64-bit permission bitfield
  and its resolution order, gateway opcodes and dispatch events, streaming
  policy, REST bodies. Shared verbatim with the server.
- `omnidisc-media`: the whole media path in Rust, because WKWebView has no
  getDisplayMedia and a webview cannot own the audio graph without stuttering.
  cpal capture/playback with a lock-free mic path, RNNoise, VAD, a mixer with
  per-participant gain and ducking, LiveKit as the SFU, ScreenCaptureKit
  capture feeding our own VideoToolbox session published pre-encoded, and a
  wgpu viewer in a pop-out window. Windows and Linux compile to typed
  "unsupported yet" stubs.
- `omnidisc-mls`: MLS (RFC 9420) via openmls for direct messages, per-file
  XChaCha20-Poly1305 in 1 MiB chunks, and the voice room key derived from the
  group epoch — never minted by the server.

App: 89 Tauri commands across auth, gateway, chat, voice, streaming, uploads,
devices and MLS; session tokens in the OS keyring and never exposed to the
webview; the `/omnidisc` route with a virtualized message list, composer,
friends, profiles, guild settings, search, pins and per-device fingerprints.

Also carries the settings and shell work that was already in progress in the
tree: the grouped settings sidebar, the platform-aware chrome, and the
i18n keys those introduced.
2026-08-27 22:40:40 -03:00

43 lines
1.7 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const dir = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "lib", "i18n");
const checkOnly = process.argv.includes("--check");
const source = JSON.parse(readFileSync(join(dir, "en.json"), "utf8"));
function sync(src, dst, path, stats) {
const out = {};
for (const key of Object.keys(src)) {
const full = path ? `${path}.${key}` : key;
const s = src[key];
const d = dst?.[key];
if (s && typeof s === "object" && !Array.isArray(s)) {
out[key] = sync(s, d && typeof d === "object" ? d : {}, full, stats);
} else if (d === undefined) {
out[key] = s;
stats.added.push(full);
} else {
out[key] = d;
}
}
for (const key of Object.keys(dst ?? {})) {
if (!(key in src)) stats.removed.push(path ? `${path}.${key}` : key);
}
return out;
}
let dirty = false;
for (const file of readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "en.json").sort()) {
const target = JSON.parse(readFileSync(join(dir, file), "utf8"));
const stats = { added: [], removed: [] };
const merged = sync(source, target, "", stats);
const changed = stats.added.length > 0 || stats.removed.length > 0;
if (changed) dirty = true;
console.log(`${file}: +${stats.added.length} missing (filled with English) -${stats.removed.length} stale`);
if (stats.removed.length) console.log(` stale: ${stats.removed.slice(0, 10).join(", ")}${stats.removed.length > 10 ? ", …" : ""}`);
if (!checkOnly && changed) writeFileSync(join(dir, file), JSON.stringify(merged, null, 2) + "\n");
}
if (checkOnly && dirty) process.exit(1);