import { h, render } from "preact";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import htm from "htm";
const html = htm.bind(h);
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
const DEFAULTS = {
serverUrl: "https://meet.quineglobal.com",
identityUrl: "https://identity.quineglobal.com",
jitsiBase: "https://meet.jit.si",
};
const POLL_INTERVAL_MS = 3000;
const CONTACTS_REFRESH_MS = 4000;
// ---- Backend helpers ---------------------------------------------------------
async function loadConfig() {
const saved = await invoke("load_config");
return {
serverUrl: saved.server_url || DEFAULTS.serverUrl,
identityUrl: saved.identity_url || DEFAULTS.identityUrl,
jitsiBase: saved.jitsi_base || DEFAULTS.jitsiBase,
};
}
async function saveConfig(config) {
await invoke("save_config", {
config: { server_url: config.serverUrl, identity_url: config.identityUrl, jitsi_base: config.jitsiBase },
});
}
const sameUrl = (a, b) => a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
/**
* A fetch wrapper for the signaling server that sends the Quine access token,
* retrying once with a fresh token if it's rejected.
*/
function makeApi(serverUrl, accessToken) {
return async (path, options = {}) => {
const send = async (token) =>
fetch(serverUrl.replace(/\/+$/, "") + path, {
...options,
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
});
let res = await send(await accessToken());
if (res.status === 401) {
// The token may have been revoked or the key rotated; retry once fresh.
res = await send(await accessToken(true));
}
const body = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(body.error || res.statusText || `request failed (${res.status})`);
err.status = res.status;
throw err;
}
return body;
};
}
/** The host of a Jitsi server URL, for display; "" if it isn't one. */
function jitsiHost(url) {
try {
return new URL(url.includes("://") ? url : `https://${url}`).host;
} catch (_) {
return "";
}
}
/**
* Meetings on the public meet.jit.si only start once a signed-in user joins,
* so it's worth signing in to it once before calling there.
*/
const isPublicJitsi = (url) => jitsiHost(url) === "meet.jit.si";
function openMeeting(me, jitsiUrl, meetingId) {
return invoke("open_meeting", { jitsiBase: jitsiUrl, room: meetingId, displayName: me.username });
}
// ---- App: gates everything behind Quine ------------------------------------
function App() {
const [config, setConfig] = useState(null);
/** @type {[{username: string, accessToken: string, expiresAt: number} | null, Function]} */
const [session, setSession] = useState(null);
/** Who the signaling server says we are: {username, defaultJitsiUrl}. */
const [me, setMe] = useState(null);
const [meError, setMeError] = useState("");
const [jitsiSignedIn, setJitsiSignedIn] = useState(false);
const [booting, setBooting] = useState(true);
const [bootError, setBootError] = useState("");
const [showSettings, setShowSettings] = useState(false);
useEffect(() => {
(async () => {
const cfg = await loadConfig();
setConfig(cfg);
setJitsiSignedIn(await invoke("jitsi_signed_in"));
try {
setSession(await invoke("quine_restore", { issuer: cfg.identityUrl }));
} catch (err) {
setBootError(String(err));
}
setBooting(false);
})();
const unlisten = listen("jitsi-auth-changed", (e) => setJitsiSignedIn(e.payload === true));
return () => unlisten.then((f) => f());
}, []);
/**
* Returns a current access token, refreshing as needed. If the Quine
* sign-in has lapsed (expired, revoked, account removed), drops back to the
* sign-in screen and throws.
*/
const accessToken = useCallback(async (force = false) => {
try {
const fresh = await invoke("quine_access_token", { force });
setSession(fresh);
return fresh.accessToken;
} catch (err) {
if (String(err) === "signed_out") setSession(null);
throw new Error(String(err));
}
}, []);
const signedIn = session !== null;
const api = config ? makeApi(config.serverUrl, accessToken) : null;
// Once signed in, check the signaling server accepts us.
const loadMe = useCallback(async () => {
setMeError("");
try {
setMe(await api("/me"));
} catch (err) {
if (signedIn) setMeError(err.message);
}
}, [config, signedIn]);
useEffect(() => {
setMe(null);
if (signedIn && config) loadMe();
}, [signedIn, config && config.serverUrl]);
/** Signs out of everything: Quine, and meet.jit.si with it. */
const signOut = useCallback(async () => {
setSession(null);
setMe(null);
setShowSettings(false);
await invoke("jitsi_sign_out").catch(() => {});
await invoke("quine_sign_out");
}, []);
const updateConfig = useCallback(
async (next) => {
const identityChanged = !sameUrl(next.identityUrl, config.identityUrl);
await saveConfig(next);
setConfig(next);
// A different identity server means a different account.
if (identityChanged && session) await signOut();
},
[config, session, signOut],
);
if (booting || !config) {
return html`<${Message} text="Signing you in…" />`;
}
if (!session) {
return html`<${SignIn} config=${config} initialError=${bootError} onConfig=${updateConfig} onSignedIn=${setSession} />`;
}
if (meError) {
return html`<${Message} text=${`Can't use ${config.serverUrl}: ${meError}`}>
/>`;
}
if (!me) {
return html`<${Message} text="Connecting…" />`;
}
if (showSettings) {
return html`<${Settings}
config=${config}
me=${me}
canSignOutOfJitsi=${jitsiSignedIn}
onSave=${async (next) => {
await updateConfig(next);
setShowSettings(false);
}}
onCancel=${() => setShowSettings(false)}
onSignOut=${signOut}
/>`;
}
return html`<${Dashboard}
me=${me}
api=${api}
jitsiBase=${config.jitsiBase}
jitsiSignedIn=${jitsiSignedIn}
onJitsiBase=${(jitsiBase) => updateConfig({ ...config, jitsiBase })}
onSettings=${() => setShowSettings(true)}
/>`;
}
function Message({ text, children }) {
return html`
${text}
Sign in with your Quine account. Your Quine username is what people call you by.
${error} Finish signing in in your browser, then come back here.Welcome to Quple
Advanced settings
<${ServerFields} config=${config} submitLabel="Save" onSubmit=${onConfig} />
meet.jit.si only starts a meeting once someone signed in joins it.${" "}
`}${status.text}
Signed in as ${me.username}.
<${ServerFields} config=${config} submitLabel="Save" onSubmit=${onSave} onCancel=${onCancel} />