Initial commit
CI / schema-and-codegen (push) Failing after 29s

This commit is contained in:
root
2026-06-20 03:54:40 -04:00
commit 9494f85b78
57 changed files with 3324 additions and 0 deletions
+360
View File
@@ -0,0 +1,360 @@
import { gql, useMutation, useQuery, useSubscription } from "@apollo/client";
import { useMemo, useState } from "react";
const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
accessToken
refreshToken
user {
id
displayName
}
}
}
`;
const REGISTER = gql`
mutation Register(
$email: String!
$password: String!
$displayName: String!
) {
register(email: $email, password: $password, displayName: $displayName) {
accessToken
refreshToken
user {
id
displayName
}
}
}
`;
const ME = gql`
query Me {
me {
id
displayName
email
}
}
`;
const CHATS = gql`
query Chats {
chats {
__typename
id
... on DirectThread {
peer {
id
displayName
}
}
... on Group {
title
memberCount
}
... on Channel {
title
subscriberCount
}
lastMessage {
id
body
seq
}
unreadCount
}
}
`;
const MESSAGES = gql`
query Messages($chatId: ID!, $first: Int) {
messages(chatId: $chatId, first: $first) {
edges {
cursor
node {
id
body
seq
sender {
displayName
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const SEND = gql`
mutation Send($chatId: ID!, $body: String) {
sendMessage(chatId: $chatId, body: $body) {
id
seq
body
}
}
`;
const CREATE_DM = gql`
mutation CreateDm($peerUserId: ID!) {
createDirectThread(peerUserId: $peerUserId) {
id
peer {
id
displayName
}
}
}
`;
const ON_CHAT = gql`
subscription OnChat($chatId: ID!) {
onChatEvent(chatId: $chatId) {
type
chatId
message {
id
body
seq
}
}
}
`;
function chatLabel(c: {
__typename: string;
id: string;
peer?: { displayName: string };
title?: string | null;
}) {
if (c.__typename === "DirectThread") return c.peer?.displayName ?? c.id;
return c.title ?? c.id;
}
export default function App() {
const [email, setEmail] = useState("alice@example.com");
const [password, setPassword] = useState("password123");
const [displayName, setDisplayName] = useState("Alice");
const [peerId, setPeerId] = useState("");
const [activeChatId, setActiveChatId] = useState<string | null>(null);
const [draft, setDraft] = useState("");
const token = localStorage.getItem("accessToken");
const authed = Boolean(token);
const meQ = useQuery(ME, { skip: !authed });
const chatsQ = useQuery(CHATS, { skip: !authed });
const msgsQ = useQuery(MESSAGES, {
skip: !authed || !activeChatId,
variables: { chatId: activeChatId!, first: 50 },
});
const [login, loginM] = useMutation(LOGIN);
const [register, regM] = useMutation(REGISTER);
const [send, sendM] = useMutation(SEND);
const [createDm, dmM] = useMutation(CREATE_DM);
useSubscription(ON_CHAT, {
skip: !authed || !activeChatId,
variables: { chatId: activeChatId! },
onData: () => {
void msgsQ.refetch();
void chatsQ.refetch();
},
});
const sortedEdges = useMemo(() => {
const edges = [...(msgsQ.data?.messages.edges ?? [])];
edges.sort((a, b) => a.node.seq - b.node.seq);
return edges;
}, [msgsQ.data]);
async function doLogin() {
const r = await login({ variables: { email, password } });
const t = r.data?.login.accessToken;
if (t) {
localStorage.setItem("accessToken", t);
localStorage.setItem("refreshToken", r.data!.login.refreshToken);
window.location.reload();
}
}
async function doRegister() {
const r = await register({
variables: { email, password, displayName },
});
const t = r.data?.register.accessToken;
if (t) {
localStorage.setItem("accessToken", t);
localStorage.setItem("refreshToken", r.data!.register.refreshToken);
window.location.reload();
}
}
function logout() {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
window.location.reload();
}
async function doCreateDm() {
if (!peerId) return;
const r = await createDm({ variables: { peerUserId: peerId } });
const id = r.data?.createDirectThread.id;
if (id) {
setActiveChatId(id);
await chatsQ.refetch();
}
}
async function doSend() {
if (!activeChatId || !draft.trim()) return;
await send({ variables: { chatId: activeChatId, body: draft } });
setDraft("");
await msgsQ.refetch();
await chatsQ.refetch();
}
if (!authed) {
return (
<div style={{ maxWidth: 420, margin: "48px auto", fontFamily: "system-ui" }}>
<h1>TLM</h1>
<label>
Email
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
style={{ width: "100%", display: "block", marginBottom: 8 }}
/>
</label>
<label>
Password
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ width: "100%", display: "block", marginBottom: 8 }}
/>
</label>
<label>
Display name (register)
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
style={{ width: "100%", display: "block", marginBottom: 8 }}
/>
</label>
<div style={{ display: "flex", gap: 8 }}>
<button type="button" onClick={() => void doLogin()}>
Login
</button>
<button type="button" onClick={() => void doRegister()}>
Register
</button>
</div>
<p style={{ color: "#666" }}>
{(loginM.error ?? regM.error)?.message}
</p>
</div>
);
}
return (
<div
style={{
display: "grid",
gridTemplateColumns: "280px 1fr",
height: "100vh",
fontFamily: "system-ui",
}}
>
<aside style={{ borderRight: "1px solid #ddd", padding: 12 }}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<strong>{meQ.data?.me.displayName}</strong>
<button type="button" onClick={logout}>
Logout
</button>
</div>
<hr />
<div style={{ display: "flex", gap: 6, marginBottom: 8 }}>
<input
placeholder="Peer user id"
value={peerId}
onChange={(e) => setPeerId(e.target.value)}
style={{ flex: 1 }}
/>
<button type="button" onClick={() => void doCreateDm()}>
DM
</button>
</div>
<div style={{ fontSize: 12, color: "#666", marginBottom: 8 }}>
Your id: {meQ.data?.me.id}
</div>
<div style={{ overflow: "auto" }}>
{(chatsQ.data?.chats ?? []).map((c) => (
<button
key={c.id}
type="button"
onClick={() => setActiveChatId(c.id)}
style={{
display: "block",
width: "100%",
textAlign: "left",
padding: 8,
marginBottom: 4,
border:
c.id === activeChatId ? "2px solid #2a6" : "1px solid #eee",
background: c.id === activeChatId ? "#f6fff8" : "#fff",
}}
>
<div>{chatLabel(c)}</div>
<div style={{ fontSize: 12, color: "#666" }}>
unread {c.unreadCount}
</div>
</button>
))}
</div>
</aside>
<main style={{ display: "flex", flexDirection: "column" }}>
<header style={{ padding: 12, borderBottom: "1px solid #ddd" }}>
{activeChatId ? `Chat ${activeChatId}` : "Select a chat"}
</header>
<div style={{ flex: 1, overflow: "auto", padding: 12 }}>
{sortedEdges.map((e) => (
<div key={e.node.id} style={{ marginBottom: 8 }}>
<strong>{e.node.sender?.displayName ?? "?"}</strong>:{" "}
{e.node.body}
</div>
))}
</div>
<footer style={{ padding: 12, borderTop: "1px solid #ddd" }}>
<div style={{ display: "flex", gap: 8 }}>
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
style={{ flex: 1 }}
placeholder="Message"
onKeyDown={(e) => {
if (e.key === "Enter") void doSend();
}}
/>
<button
type="button"
disabled={!activeChatId || sendM.loading}
onClick={() => void doSend()}
>
Send
</button>
</div>
</footer>
</main>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import {
ApolloClient,
HttpLink,
InMemoryCache,
split,
} from "@apollo/client/core";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
import { createClient } from "graphql-ws";
const httpUri = import.meta.env.VITE_GRAPHQL_HTTP ?? "/graphql";
const wsUri = import.meta.env.VITE_GRAPHQL_WS ?? `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/graphql`;
function getAuthHeader() {
const t = localStorage.getItem("accessToken");
return t ? { authorization: `Bearer ${t}` } : {};
}
const httpLink = new HttpLink({
uri: httpUri,
headers: getAuthHeader,
});
const wsLink = new GraphQLWsLink(
createClient({
url: wsUri,
connectionParams: () => getAuthHeader(),
lazy: true,
}),
);
const link = split(
({ query }) => {
const def = getMainDefinition(query);
return def.kind === "OperationDefinition" && def.operation === "subscription";
},
wsLink,
httpLink,
);
export const apollo = new ApolloClient({
link,
cache: new InMemoryCache(),
});
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { ApolloProvider } from "@apollo/client";
import App from "./App";
import { apollo } from "./apollo";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ApolloProvider client={apollo}>
<App />
</ApolloProvider>
</React.StrictMode>,
);
+10
View File
@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_GRAPHQL_HTTP?: string;
readonly VITE_GRAPHQL_WS?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}