"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { EmbeddedApp } from "../../../components/embed/embedded-app"; import { api, errorMessage } from "../../../lib/api-client"; import { useApiResource } from "../../../lib/use-api"; import { useIsMobile } from "../../../hooks/use-is-mobile"; /** * In-app Chat: Element Web, full height inside the Hub shell, signed in as the * Hub user without a Matrix password. * * Handshake (see infra/occupymedia/edge/hub-sso/chat-sso.html): * 1. GET /matrix/identity → the MXID this Hub account maps to. * 2. The frame opens Element's origin at /hub-sso.html#expect=. If * Element already holds a live session for that MXID it goes straight to * Element — no token is minted, the device (and its E2EE keys) survives. * 3. Otherwise the page posts `tmbh:chat-sso:need-token`; we POST * /matrix/session and post the single-use m.login.token back, pinned to * Element's origin. Element completes `/?loginToken=…` and keeps its own * session from then on. * Without SSO configured (or the API offline) the frame shows Element's own * sign-in screen: sign in once there and it stays signed in. */ /** Same host the nav linked to before Chat moved in-app. */ const FALLBACK_CHAT_URL = "https://chat.occupymedia.cc"; /** A frame that keeps asking for tokens is failing token login; stop minting. */ const MAX_MINTS = 3; const ELEMENT_ALLOW = "camera; microphone; display-capture; fullscreen; autoplay; clipboard-read; clipboard-write"; interface MatrixIdentity { configured: boolean; homeserverUrl: string | null; elementUrl: string | null; userId: string | null; } interface MatrixSession { homeserverUrl: string; elementUrl: string; userId: string; loginToken: string; expiresInMs: number; } const identityView = (identity: MatrixIdentity): MatrixIdentity | null => identity; function isNeedToken(data: unknown): boolean { return typeof data === "object" && data !== null && "type" in data && data.type === "tmbh:chat-sso:need-token"; } function originOf(url: string): string | null { try { return new URL(url).origin; } catch { return null; } } export default function ChatPage(): React.ReactElement { const identity = useApiResource("/matrix/identity", identityView, null); const isMobile = useIsMobile(); const frameRef = useRef(null); const mints = useRef(0); const tokenSent = useRef(false); const [busy, setBusy] = useState(null); const [mintError, setMintError] = useState(null); const elementOrigin = originOf(identity.data?.elementUrl ?? FALLBACK_CHAT_URL) ?? FALLBACK_CHAT_URL; const ssoUser = identity.data?.configured === true ? identity.data.userId : null; const src = identity.source === "loading" ? null : ssoUser !== null ? `${elementOrigin}/hub-sso.html#expect=${encodeURIComponent(ssoUser)}` : `${elementOrigin}/`; useEffect(() => { if (ssoUser === null) return; function onMessage(event: MessageEvent): void { const target = frameRef.current?.contentWindow ?? null; if (target === null || event.source !== target || event.origin !== elementOrigin || !isNeedToken(event.data)) return; if (mints.current >= MAX_MINTS) { setBusy(null); setMintError("Chat sign-in keeps failing. Open chat in a new tab, or retry."); return; } mints.current += 1; setBusy("Signing in to chat…"); api .post("/matrix/session") .then((session) => { const current = frameRef.current?.contentWindow ?? null; if (current === null) return; tokenSent.current = true; current.postMessage({ type: "tmbh:chat-sso:token", loginToken: session.loginToken }, elementOrigin); }) .catch((err: unknown) => { setBusy(null); setMintError(errorMessage(err)); }); } window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, [ssoUser, elementOrigin]); // The load after a token was handed over is Element itself: sign-in is done. const handleFrameLoad = useCallback(() => { if (!tokenSent.current) return; tokenSent.current = false; setBusy(null); }, []); const handleRetry = useCallback(() => { mints.current = 0; tokenSent.current = false; setBusy(null); setMintError(null); if (identity.error !== null) identity.reload(); }, [identity]); const notice = identity.source === "sample" ? "Hub offline — sign in to chat directly." : identity.source === "live" && identity.error === null && ssoUser === null ? "Automatic sign-in is not set up — sign in once here and chat stays signed in." : ssoUser !== null ? ssoUser : undefined; if (isMobile) { // Mobile: a framed three-pane Element under the bottom tab bar breaks // (nested scrolling, keyboard collapse, double chrome) — hand off to the // standalone app instead (panel consensus: gateway pattern). return (

Newsroom chat

Chat runs best as its own app on mobile — the full three-pane interface gets cramped inside the Hub frame.

Open chat ↗ {ssoUser !== null && ( Open in a Matrix app )}

{ssoUser !== null ? `Signed in as ${ssoUser}` : "Sign in on desktop to link your account."}

); } return ( ); }