feat(chat): join users to rooms on sign-in

This commit is contained in:
Nathnael
2026-08-17 11:33:26 +00:00
parent 00bd1250ee
commit ab734aecc3
15 changed files with 366 additions and 114 deletions

View File

@@ -1,19 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { chatApi } from "./chatApi";
export const CHAT_SSO_KEY = ["chat", "sso"] as const;
/**
* The login_token this resolves to is single-use and expires in 5 minutes
* (Synapse default) — the global `staleTime: 0` (queryClient.ts) already
* means every fresh mount of the launch page refetches rather than reusing
* a possibly-spent link.
*/
export function useChatSso() {
return useQuery({
queryKey: CHAT_SSO_KEY,
queryFn: chatApi.getSsoUrl,
retry: false,
});
}

View File

@@ -1,17 +1,44 @@
import { Alert, Button, Card, Center, Loader, Stack, Text } from "@mantine/core";
import { Alert, Button, Card, Center, Stack, Text } from "@mantine/core";
import { MessageSquare, TriangleAlert } from "lucide-react";
import { useState } from "react";
import { PageContainer, PageHeader } from "@/components/page";
import { useChatSso } from "@/features/chat/useChatSso";
import { chatApi } from "@/features/chat/chatApi";
/**
* Chat itself lives at chat.edr.et (Element), not in this app — this page's
* only job is a fresh one-click sign-in link into it. A real `<a>` (not
* `window.open()` in a click handler) so the browser never treats it as a
* blocked popup, and no iframe: Element's own CSP refuses to be framed.
* only job is a one-click sign-in link into it. No iframe: Element's own CSP
* refuses to be framed.
*
* The link is minted per click, never on mount and never cached: Synapse's
* login_token is single-use and expires in 5 minutes, and Element reports a
* spent one as "Incorrect username and/or password". A held-onto url is
* therefore wrong on the second click, on a remount served from cache, and on
* any click more than 5 minutes after the page loaded.
*/
export default function ChatLaunchPage() {
const { data: url, isLoading, isError, refetch } = useChatSso();
const [state, setState] = useState<"idle" | "loading" | "error">("idle");
const open = async () => {
// Opened before the await so it still counts as the user's click — a
// window.open() after it is treated as a popup and blocked.
//
// No "noopener" in the features: passing it makes window.open return null,
// which would leave this blank tab orphaned and send Element into the
// current tab instead. Clearing .opener on the handle does the same job.
const tab = window.open("", "_blank");
if (tab) tab.opener = null;
setState("loading");
try {
const url = await chatApi.getSsoUrl();
if (tab) tab.location.replace(url);
else window.location.assign(url); // popup blocked — go in this tab
setState("idle");
} catch {
tab?.close();
setState("error");
}
};
return (
<PageContainer>
@@ -19,41 +46,30 @@ export default function ChatLaunchPage() {
<Card withBorder radius="md" p="xl">
<Center>
<Stack align="center" gap="md" py="xl">
{isLoading && <Loader />}
{isError && (
<Stack align="center" gap="sm">
<Alert
icon={<TriangleAlert size={18} />}
color="red"
title="Couldn't get a sign-in link"
variant="light"
>
Something went wrong reaching chat. Try again.
</Alert>
<Button variant="light" onClick={() => refetch()}>
Retry
</Button>
</Stack>
{state === "error" && (
<Alert
icon={<TriangleAlert size={18} />}
color="red"
title="Couldn't get a sign-in link"
variant="light"
>
Something went wrong reaching chat. Try again.
</Alert>
)}
{url && (
<Stack align="center" gap="sm">
<MessageSquare size={40} strokeWidth={1.5} />
<Text c="dimmed" ta="center" maw={360}>
Opens EDR Chat in a new tab, already signed in as you.
</Text>
<Button
component="a"
href={url}
target="_blank"
rel="noopener noreferrer"
leftSection={<MessageSquare size={16} />}
>
Open EDR Chat
</Button>
</Stack>
)}
<Stack align="center" gap="sm">
<MessageSquare size={40} strokeWidth={1.5} />
<Text c="dimmed" ta="center" maw={360}>
Opens EDR Chat in a new tab, already signed in as you.
</Text>
<Button
onClick={open}
loading={state === "loading"}
leftSection={<MessageSquare size={16} />}
>
Open EDR Chat
</Button>
</Stack>
</Stack>
</Center>
</Card>