Something went wrong. Try again.
[WIP] Matrix Client for web and desktop
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243import { createMemo, createSignal, For, Show, onSettled } from "solid-js";import { useNavigate } from "@solidjs/router";import { createClient, ClientEvent, MatrixClient, OAuth2, SyncState, type SyncStateData, type IServerVersions,} from "matrix-js-sdk";import { getSessions, removeSession, getActiveAccountId, setActiveAccountId, type StoredSession, matrixClient,} from "../matrix";
function keyParts(s: StoredSession): string { return `${s.homeserverUrl}/${s.userId}`;}
interface WhoamiInfo { userId: string; displayName: string | null;}
function Identity(props: { client: MatrixClient }) { const [whoami, setWhoami] = createSignal<WhoamiInfo | null>(null); const [tokenValid, setTokenValid] = createSignal<boolean | null>(null);
// onSettled replaces onMount in Solid 2; it's a one-shot setup that runs // once per mounted instance. onSettled(() => { void (async () => { try { const [who, profile] = await Promise.all([ props.client.whoami(), props.client.getProfileInfo(props.client.getUserId() ?? ""), ]); setWhoami({ userId: who.user_id, displayName: profile.displayname ?? null, }); setTokenValid(true); } catch { setTokenValid(false); } })(); });
return ( <section> <h2>Identity</h2> <p> Status:{" "} {tokenValid() === null ? "checking…" : tokenValid() ? "token valid" : "token invalid"} </p> <Show when={whoami()}> {(me) => ( <dl> <dt>user id</dt> <dd>{me().userId}</dd> <dt>display name</dt> <dd>{me().displayName ?? "(none)"}</dd> </dl> )} </Show> </section> );}
interface SyncSnapshot { state: SyncState | null; error?: Error;}
function SyncStatus(props: { client: MatrixClient }) { const [sync, setSync] = createSignal<SyncSnapshot>({ state: null });
onSettled(() => { // Read the reactive client state inside the settled lifecycle scope, // rather than in the component body. setSync({ state: props.client.getSyncState() });
const handler = ( state: SyncState, _prev: SyncState | null, data?: SyncStateData, ) => { setSync({ state, error: data?.error }); }; props.client.on(ClientEvent.Sync, handler); // onSettled accepts a returned cleanup when running under an owner. return () => { props.client.off(ClientEvent.Sync, handler); }; });
return ( <section> <h2>Sync</h2> <p> state: <code>{sync().state ?? "unknown"}</code> </p> <Show when={sync().error}>{(err) => <p>error: {err().message}</p>}</Show> </section> );}
function ServerInfo(props: { client: MatrixClient }) { const [versions, setVersions] = createSignal<IServerVersions | null>(null);
onSettled(() => { void props.client.getVersions().then(setVersions); });
return ( <section> <h2>Server</h2> <p> homeserver: <code>{props.client.getHomeserverUrl()}</code> </p> <Show when={versions()}> {(v) => ( <dl> <dt>latest supported version</dt> <dd>{v().versions.at(-1) ?? "unknown"}</dd> </dl> )} </Show> </section> );}
export default function Status() { const navigate = useNavigate(); const client = createMemo(() => matrixClient());
const sessions = () => getSessions(); const activeId = () => getActiveAccountId();
const logout = (session: StoredSession) => { void (async () => { try { const metadata = await createClient({ baseUrl: session.homeserverUrl, }).getAuthMetadata(); const oauth = new OAuth2(metadata, { clientId: session.clientId, redirectUri: "urn:ietf:oauth:2.0:oob", }); const token = session.accessToken ?? session.refreshToken; if (token) await oauth.revokeToken( token, session.accessToken ? "access_token" : "refresh_token", ); } catch { /* still drop locally if revocation fails */ }
removeSession(session); if (getActiveAccountId() === null) navigate("/auth/login"); window.location.reload(); })(); };
const switchSession = (e: Event & { currentTarget: HTMLSelectElement }) => { setActiveAccountId(e.currentTarget.value || null); window.location.reload(); };
return ( <div> <h1>Client status</h1>
<Show when={client()} fallback={ <div> <p>You aren't signed in.</p> <p> <a href="/auth/login">Go to login</a> </p> </div> } > {(getClient) => ( <> <section> <h2>Active account</h2> <p> <select onChange={switchSession}> <For each={sessions()}> {(s) => ( <option value={keyParts(s)} selected={keyParts(s) === activeId()} > {s.userId} - {s.homeserverUrl} </option> )} </For> </select> <a href="/auth/login">[+] Add account</a> </p> </section>
<Identity client={getClient()} /> <SyncStatus client={getClient()} /> <ServerInfo client={getClient()} />
<nav> <a href="/">home</a> </nav>
<section> <h2>All sessions</h2> <ul> <For each={sessions()}> {(s) => ( <li> {s.userId} on {s.homeserverUrl} (device {s.deviceId}){" "} <button onClick={() => logout(s)}>log out</button> </li> )} </For> </ul> </section> </> )} </Show> </div> );}