From 9b1513f7841c0f8c52f6a4c2c0682b7fecadb51b Mon Sep 17 00:00:00 2001 From: shawn Date: Sun, 2 Aug 2026 10:40:54 -0400 Subject: [PATCH 01/17] Incubate decentralized Convex inside Vera --- README.md | 47 +- apps/web/.env.example | 4 + apps/web/README.md | 31 + apps/web/eslint.config.ts | 11 + apps/web/index.html | 208 -- apps/web/package.json | 45 +- apps/web/src/auth-client.ts | 14 + apps/web/src/cloud/AuthForm.tsx | 138 ++ apps/web/src/cloud/AuthenticatedSession.tsx | 64 + apps/web/src/cloud/ChatRoom.tsx | 32 + apps/web/src/cloud/ChatSidebar.tsx | 54 + apps/web/src/cloud/ConnectionDiagnostics.tsx | 44 + apps/web/src/cloud/ConversationHeader.tsx | 38 + apps/web/src/cloud/HomeServerSession.tsx | 36 + apps/web/src/cloud/Message.tsx | 27 + apps/web/src/cloud/MessageComposer.tsx | 46 + apps/web/src/cloud/MessageList.tsx | 28 + apps/web/src/cloud/ServerLoading.tsx | 12 + apps/web/src/cloud/SessionGate.tsx | 21 + apps/web/src/cloud/actor.tsx | 11 + apps/web/src/cloud/use-chat-room.ts | 53 + apps/web/src/env.d.ts | 10 + .../tanstack-query/root-provider.tsx | 18 + apps/web/src/live/config.ts | 40 + apps/web/src/live/queries.ts | 25 + apps/web/src/routeTree.gen.ts | 68 + apps/web/src/router.tsx | 35 + apps/web/src/routes/__root.tsx | 24 + apps/web/src/routes/index.tsx | 23 + apps/web/src/server-picker/ServerPicker.tsx | 46 + apps/web/src/shell/NotFound.tsx | 9 + apps/web/src/shell/RootDocument.tsx | 16 + apps/web/src/styles.css | 871 +++++++ apps/web/tsconfig.json | 11 + apps/web/tsr.config.json | 3 + apps/web/vercel.json | 25 - apps/web/vite.config.ts | 7 + deps/.gitkeep | 1 + package.json | 2 +- .../eslint.config.ts | 0 .../decentralized-convex-address/package.json | 31 + .../src/address.test.ts | 15 + .../src/address.ts | 40 + .../decentralized-convex-address/src/index.ts | 2 + .../tsconfig.json | 9 + .../eslint.config.ts | 5 + .../decentralized-convex-client/package.json | 31 + .../src/client.test.ts | 97 + .../decentralized-convex-client/src/client.ts | 93 + .../src/connection.ts | 56 + .../decentralized-convex-client/src/index.ts | 23 + .../src/observer.ts | 86 + .../src/snapshot.ts | 57 + .../src/targets.ts | 39 + .../decentralized-convex-client/src/types.ts | 107 + .../decentralized-convex-client/tsconfig.json | 9 + .../eslint.config.ts | 6 + .../decentralized-convex-react/package.json | 36 + .../decentralized-convex-react/src/index.ts | 6 + .../src/provider.tsx | 40 + .../decentralized-convex-react/tsconfig.json | 9 + .../eslint.config.ts | 5 + .../decentralized-convex-server/package.json | 31 + .../src/descriptor.test.ts | 19 + .../src/descriptor.ts | 76 + .../decentralized-convex-server/src/index.ts | 11 + .../decentralized-convex-server/tsconfig.json | 9 + .../eslint.config.ts | 6 + .../package.json | 38 + .../src/index.ts | 4 + .../src/options.ts | 24 + .../src/useFederatedQuery.ts | 102 + .../tsconfig.json | 9 + pnpm-lock.yaml | 2086 ++++++++++++++++- pnpm-workspace.yaml | 4 + services/backend/.env.example | 1 + services/backend/.gitignore | 2 + services/backend/README.md | 16 + services/backend/convex.json | 3 + services/backend/convex/_generated/api.d.ts | 57 + services/backend/convex/_generated/api.js | 23 + .../backend/convex/_generated/dataModel.d.ts | 61 + .../backend/convex/_generated/server.d.ts | 144 ++ services/backend/convex/_generated/server.js | 93 + services/backend/convex/auth.config.ts | 8 + services/backend/convex/auth.ts | 41 + services/backend/convex/convex.config.ts | 7 + services/backend/convex/http.ts | 23 + services/backend/convex/lib.ts | 16 + services/backend/convex/messages.ts | 59 + services/backend/convex/schema.ts | 16 + services/backend/convex/tsconfig.json | 20 + services/backend/eslint.config.ts | 9 + services/backend/package.json | 34 + shared/app-config/eslint.config.ts | 5 + {packages => shared}/app-config/package.json | 0 {packages => shared}/app-config/src/urls.ts | 0 {packages => shared}/app-config/tsconfig.json | 0 {packages => shared}/ui/components.json | 0 {packages => shared}/ui/eslint.config.ts | 0 {packages => shared}/ui/package.json | 0 .../ui/src/components/alert-dialog.tsx | 0 .../ui/src/components/button.tsx | 0 .../ui/src/components/card.tsx | 0 .../ui/src/components/code-block.tsx | 0 .../ui/src/components/context-menu.tsx | 0 .../ui/src/components/dialog.tsx | 0 .../ui/src/components/drawer.tsx | 0 .../ui/src/components/dropdown-menu.tsx | 0 .../ui/src/components/hover-card.tsx | 0 .../ui/src/components/input.tsx | 0 .../ui/src/components/label.tsx | 0 .../ui/src/components/loader.tsx | 0 .../ui/src/components/markdown.tsx | 0 .../ui/src/components/popover.tsx | 0 .../ui/src/components/progress.tsx | 0 .../ui/src/components/scroll-area.tsx | 0 .../ui/src/components/select.tsx | 0 .../ui/src/components/separator.tsx | 0 .../ui/src/components/sheet.tsx | 0 .../ui/src/components/sidebar.tsx | 0 .../ui/src/components/skeleton.tsx | 0 .../ui/src/components/sonner.tsx | 0 .../ui/src/components/switch.tsx | 0 .../ui/src/components/tooltip.tsx | 0 .../ui/src/hooks/use-mobile.ts | 0 {packages => shared}/ui/src/lib/utils.ts | 0 {packages => shared}/ui/tsconfig.json | 0 turbo.json | 12 +- 129 files changed, 5760 insertions(+), 309 deletions(-) create mode 100644 apps/web/.env.example create mode 100644 apps/web/README.md create mode 100644 apps/web/eslint.config.ts delete mode 100644 apps/web/index.html create mode 100644 apps/web/src/auth-client.ts create mode 100644 apps/web/src/cloud/AuthForm.tsx create mode 100644 apps/web/src/cloud/AuthenticatedSession.tsx create mode 100644 apps/web/src/cloud/ChatRoom.tsx create mode 100644 apps/web/src/cloud/ChatSidebar.tsx create mode 100644 apps/web/src/cloud/ConnectionDiagnostics.tsx create mode 100644 apps/web/src/cloud/ConversationHeader.tsx create mode 100644 apps/web/src/cloud/HomeServerSession.tsx create mode 100644 apps/web/src/cloud/Message.tsx create mode 100644 apps/web/src/cloud/MessageComposer.tsx create mode 100644 apps/web/src/cloud/MessageList.tsx create mode 100644 apps/web/src/cloud/ServerLoading.tsx create mode 100644 apps/web/src/cloud/SessionGate.tsx create mode 100644 apps/web/src/cloud/actor.tsx create mode 100644 apps/web/src/cloud/use-chat-room.ts create mode 100644 apps/web/src/env.d.ts create mode 100644 apps/web/src/integrations/tanstack-query/root-provider.tsx create mode 100644 apps/web/src/live/config.ts create mode 100644 apps/web/src/live/queries.ts create mode 100644 apps/web/src/routeTree.gen.ts create mode 100644 apps/web/src/router.tsx create mode 100644 apps/web/src/routes/__root.tsx create mode 100644 apps/web/src/routes/index.tsx create mode 100644 apps/web/src/server-picker/ServerPicker.tsx create mode 100644 apps/web/src/shell/NotFound.tsx create mode 100644 apps/web/src/shell/RootDocument.tsx create mode 100644 apps/web/src/styles.css create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsr.config.json delete mode 100644 apps/web/vercel.json create mode 100644 apps/web/vite.config.ts create mode 100644 deps/.gitkeep rename packages/{app-config => decentralized-convex-address}/eslint.config.ts (100%) create mode 100644 packages/decentralized-convex-address/package.json create mode 100644 packages/decentralized-convex-address/src/address.test.ts create mode 100644 packages/decentralized-convex-address/src/address.ts create mode 100644 packages/decentralized-convex-address/src/index.ts create mode 100644 packages/decentralized-convex-address/tsconfig.json create mode 100644 packages/decentralized-convex-client/eslint.config.ts create mode 100644 packages/decentralized-convex-client/package.json create mode 100644 packages/decentralized-convex-client/src/client.test.ts create mode 100644 packages/decentralized-convex-client/src/client.ts create mode 100644 packages/decentralized-convex-client/src/connection.ts create mode 100644 packages/decentralized-convex-client/src/index.ts create mode 100644 packages/decentralized-convex-client/src/observer.ts create mode 100644 packages/decentralized-convex-client/src/snapshot.ts create mode 100644 packages/decentralized-convex-client/src/targets.ts create mode 100644 packages/decentralized-convex-client/src/types.ts create mode 100644 packages/decentralized-convex-client/tsconfig.json create mode 100644 packages/decentralized-convex-react/eslint.config.ts create mode 100644 packages/decentralized-convex-react/package.json create mode 100644 packages/decentralized-convex-react/src/index.ts create mode 100644 packages/decentralized-convex-react/src/provider.tsx create mode 100644 packages/decentralized-convex-react/tsconfig.json create mode 100644 packages/decentralized-convex-server/eslint.config.ts create mode 100644 packages/decentralized-convex-server/package.json create mode 100644 packages/decentralized-convex-server/src/descriptor.test.ts create mode 100644 packages/decentralized-convex-server/src/descriptor.ts create mode 100644 packages/decentralized-convex-server/src/index.ts create mode 100644 packages/decentralized-convex-server/tsconfig.json create mode 100644 packages/decentralized-convex-tanstack-query/eslint.config.ts create mode 100644 packages/decentralized-convex-tanstack-query/package.json create mode 100644 packages/decentralized-convex-tanstack-query/src/index.ts create mode 100644 packages/decentralized-convex-tanstack-query/src/options.ts create mode 100644 packages/decentralized-convex-tanstack-query/src/useFederatedQuery.ts create mode 100644 packages/decentralized-convex-tanstack-query/tsconfig.json create mode 100644 services/backend/.env.example create mode 100644 services/backend/.gitignore create mode 100644 services/backend/README.md create mode 100644 services/backend/convex.json create mode 100644 services/backend/convex/_generated/api.d.ts create mode 100644 services/backend/convex/_generated/api.js create mode 100644 services/backend/convex/_generated/dataModel.d.ts create mode 100644 services/backend/convex/_generated/server.d.ts create mode 100644 services/backend/convex/_generated/server.js create mode 100644 services/backend/convex/auth.config.ts create mode 100644 services/backend/convex/auth.ts create mode 100644 services/backend/convex/convex.config.ts create mode 100644 services/backend/convex/http.ts create mode 100644 services/backend/convex/lib.ts create mode 100644 services/backend/convex/messages.ts create mode 100644 services/backend/convex/schema.ts create mode 100644 services/backend/convex/tsconfig.json create mode 100644 services/backend/eslint.config.ts create mode 100644 services/backend/package.json create mode 100644 shared/app-config/eslint.config.ts rename {packages => shared}/app-config/package.json (100%) rename {packages => shared}/app-config/src/urls.ts (100%) rename {packages => shared}/app-config/tsconfig.json (100%) rename {packages => shared}/ui/components.json (100%) rename {packages => shared}/ui/eslint.config.ts (100%) rename {packages => shared}/ui/package.json (100%) rename {packages => shared}/ui/src/components/alert-dialog.tsx (100%) rename {packages => shared}/ui/src/components/button.tsx (100%) rename {packages => shared}/ui/src/components/card.tsx (100%) rename {packages => shared}/ui/src/components/code-block.tsx (100%) rename {packages => shared}/ui/src/components/context-menu.tsx (100%) rename {packages => shared}/ui/src/components/dialog.tsx (100%) rename {packages => shared}/ui/src/components/drawer.tsx (100%) rename {packages => shared}/ui/src/components/dropdown-menu.tsx (100%) rename {packages => shared}/ui/src/components/hover-card.tsx (100%) rename {packages => shared}/ui/src/components/input.tsx (100%) rename {packages => shared}/ui/src/components/label.tsx (100%) rename {packages => shared}/ui/src/components/loader.tsx (100%) rename {packages => shared}/ui/src/components/markdown.tsx (100%) rename {packages => shared}/ui/src/components/popover.tsx (100%) rename {packages => shared}/ui/src/components/progress.tsx (100%) rename {packages => shared}/ui/src/components/scroll-area.tsx (100%) rename {packages => shared}/ui/src/components/select.tsx (100%) rename {packages => shared}/ui/src/components/separator.tsx (100%) rename {packages => shared}/ui/src/components/sheet.tsx (100%) rename {packages => shared}/ui/src/components/sidebar.tsx (100%) rename {packages => shared}/ui/src/components/skeleton.tsx (100%) rename {packages => shared}/ui/src/components/sonner.tsx (100%) rename {packages => shared}/ui/src/components/switch.tsx (100%) rename {packages => shared}/ui/src/components/tooltip.tsx (100%) rename {packages => shared}/ui/src/hooks/use-mobile.ts (100%) rename {packages => shared}/ui/src/lib/utils.ts (100%) rename {packages => shared}/ui/tsconfig.json (100%) diff --git a/README.md b/README.md index db19fc82..bd0eb25f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,31 @@ - +# Vera -
-
- Animated GIF of advertisement for Vera, showing off its search capability. -
-
-
-
- - vera.chat - -
-
-
-
-
+Vera is a decentralized messaging application built on independently hosted +Convex deployments. The active application also serves as the real-world +incubator for the decentralized Convex toolkit while its API is still changing +quickly. -A great starting point for building agentic applications for the web. +## Workspace -For more information, head over to the [docs](https://docs.vera.chat) +- `apps/web` — the TanStack Start Vera client +- `services/backend` — the Convex backend deployed to each Vera server +- `packages/decentralized-convex-*` — reusable federation packages +- `shared` — private code shared by Vera workspace projects +- `deps` — source dependencies that Vera may need to maintain directly +- `legacy` — the archived centralized Vera application -
-
+The standalone decentralized-convex repository remains intact as a milestone. +During incubation, changes are developed and exercised here without an npm +publish/install cycle. -[x.com/bentsignal](https://x.com/bentsignal) +## Development -
+Install dependencies and start the active app: -
+```sh +pnpm install +pnpm --filter @vera/web dev +``` + +The web app runs at `http://localhost:5173`. Copy the checked-in environment +examples before connecting different Convex deployments. diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 00000000..bc86437f --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,4 @@ +VITE_HOME_A_CONVEX_URL=https://your-a-deployment.convex.cloud +VITE_HOME_A_SITE_URL=https://your-a-deployment.convex.site +VITE_HOME_B_CONVEX_URL=https://your-b-deployment.convex.cloud +VITE_HOME_B_SITE_URL=https://your-b-deployment.convex.site diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 00000000..f5a20dee --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,31 @@ +# Vera web + +A TanStack Start messaging client that reads one conversation across independent +Convex deployments. + +```sh +pnpm --filter @vera/web dev +``` + +Open `http://localhost:5173` and: + +1. Choose a Convex deployment and create a username and password. +2. Open a second tab. +3. Choose the other deployment and create another account. +4. Send messages in either tab. + +Each signup and outgoing message is stored only on the selected home +deployment. Both tabs run the same federated query against both deployments and +combine the results through TanStack Query. + +The account address is derived from the selected deployment, such as +`alice@your-a-deployment.convex.cloud`. The user chooses only `alice`; the +server owns the domain. Better Auth is used by this example, not required by the +decentralized-convex packages. + +The checked-in `.env.example` documents the required deployment URLs. Deploy +the same `convex/` backend to each target and set `SITE_URL` and +`FEDERATION_DOMAIN` on each deployment. `FEDERATION_DOMAIN` must be the +deployment's actual `.convex.cloud` hostname or its configured custom hostname. +Reads are intentionally public in this auth-development room; portable +private-read authorization is not implemented. diff --git a/apps/web/eslint.config.ts b/apps/web/eslint.config.ts new file mode 100644 index 00000000..d9d8cf1b --- /dev/null +++ b/apps/web/eslint.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "eslint/config"; + +import { baseConfig, strictConfig } from "@acme/eslint-config/base"; +import { reactConfig } from "@acme/eslint-config/react"; + +export default defineConfig( + { ignores: ["src/routeTree.gen.ts"] }, + baseConfig, + strictConfig, + reactConfig, +); diff --git a/apps/web/index.html b/apps/web/index.html deleted file mode 100644 index 9b390577..00000000 --- a/apps/web/index.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - - - - Vera — Rebuilding - - - -
- -
-
A brief intermission
-

Vera

-

Vera is being rebuilt. Check back soon.

-
-
- Still thinking - vera.chat -
-
- - diff --git a/apps/web/package.json b/apps/web/package.json index dc1603d7..07250b80 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,23 +1,50 @@ { - "name": "@acme/web", + "name": "@vera/web", "version": "0.1.0", "private": true, "type": "module", + "imports": { + "#/*": "./src/*" + }, "scripts": { "build": "vite build", - "dev": "portless", - "dev:app": "vite", - "preview": "vite preview", + "clean": "git clean -xdf .cache .turbo .output node_modules", + "dev": "vite --port 5173 --strictPort", "format": "prettier --check . --ignore-path ../../.gitignore", "format:fix": "prettier --write . --ignore-path ../../.gitignore", - "clean": "git clean -xdf node_modules .cache .turbo dist" + "generate-routes": "tsr generate", + "lint": "eslint --flag unstable_native_nodejs_ts_config", + "preview": "vite preview", + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@convex-dev/better-auth": "0.12.5", + "@decentralized-convex/client": "workspace:*", + "@decentralized-convex/react": "workspace:*", + "@decentralized-convex/tanstack-query": "workspace:*", + "@tanstack/react-query": "catalog:", + "@tanstack/react-router": "catalog:", + "@tanstack/react-router-ssr-query": "catalog:", + "@tanstack/react-start": "catalog:", + "@vera/backend": "workspace:*", + "better-auth": "~1.6.15", + "convex": "catalog:", + "react": "catalog:react19", + "react-dom": "catalog:react19" }, "devDependencies": { + "@acme/eslint-config": "workspace:*", + "@acme/prettier-config": "workspace:*", + "@acme/tsconfig": "workspace:*", + "@tanstack/router-cli": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:react19", + "@types/react-dom": "catalog:react19", + "@vitejs/plugin-react": "catalog:", + "eslint": "catalog:", "prettier": "catalog:", + "typescript": "catalog:", "vite": "catalog:" }, - "portless": { - "name": "www.vera", - "script": "dev:app" - } + "prettier": "@acme/prettier-config" } diff --git a/apps/web/src/auth-client.ts b/apps/web/src/auth-client.ts new file mode 100644 index 00000000..e9d83a83 --- /dev/null +++ b/apps/web/src/auth-client.ts @@ -0,0 +1,14 @@ +import { + convexClient, + crossDomainClient, +} from "@convex-dev/better-auth/client/plugins"; +import { createAuthClient } from "better-auth/react"; + +export function createHomeAuthClient(siteUrl: string) { + return createAuthClient({ + baseURL: siteUrl, + plugins: [convexClient(), crossDomainClient()], + }); +} + +export type HomeAuthClient = ReturnType; diff --git a/apps/web/src/cloud/AuthForm.tsx b/apps/web/src/cloud/AuthForm.tsx new file mode 100644 index 00000000..6e2cac92 --- /dev/null +++ b/apps/web/src/cloud/AuthForm.tsx @@ -0,0 +1,138 @@ +import type { FormEvent } from "react"; +import { useState } from "react"; + +import type { HomeAuthClient } from "../auth-client.ts"; +import type { HomeServer } from "../live/config.ts"; + +interface AuthFormProps { + authClient: HomeAuthClient; + home: HomeServer; + onChooseServer: () => void; +} + +export function AuthForm(props: AuthFormProps) { + const form = useAuthForm(props); + const isSignUp = form.mode === "sign-up"; + + return ( +
+
+ +
+ +
+

{isSignUp ? "Create account" : "Sign in"}

+

{props.home.domain}

+
+
+
void form.submit(event)}> + + + {form.error === undefined ? null : ( +

{form.error}

+ )} + +
+ +
+
+ ); +} + +function useAuthForm({ authClient, home }: AuthFormProps) { + const [error, setError] = useState(); + const [mode, setMode] = useState<"sign-in" | "sign-up">("sign-up"); + const [password, setPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [username, setUsername] = useState(""); + + async function submit(event: FormEvent) { + event.preventDefault(); + setError(undefined); + const normalized = username.trim().toLowerCase(); + if (!/^[a-z0-9][a-z0-9._-]{1,31}$/.test(normalized)) { + setError( + "Use 2–32 lowercase letters, numbers, dots, dashes, or underscores.", + ); + return; + } + + setSubmitting(true); + try { + const email = `${normalized}@${home.domain}`; + const result = + mode === "sign-up" + ? await authClient.signUp.email({ email, name: normalized, password }) + : await authClient.signIn.email({ email, password }); + if (result.error !== null) { + setError(result.error.message ?? "Authentication failed"); + } + } finally { + setSubmitting(false); + } + } + + return { + error, + mode, + password, + setPassword, + setUsername, + submit, + submitting, + toggleMode: () => setMode(mode === "sign-in" ? "sign-up" : "sign-in"), + username, + }; +} diff --git a/apps/web/src/cloud/AuthenticatedSession.tsx b/apps/web/src/cloud/AuthenticatedSession.tsx new file mode 100644 index 00000000..13b8d84b --- /dev/null +++ b/apps/web/src/cloud/AuthenticatedSession.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +import { DecentralizedConvexClient } from "@decentralized-convex/client"; +import { + DecentralizedConvexProvider, + useQuery, +} from "@decentralized-convex/react"; +import { api } from "@vera/backend/api"; + +import type { HomeAuthClient } from "../auth-client.ts"; +import type { HomeServer } from "../live/config.ts"; +import { ChatRoom } from "./ChatRoom.tsx"; +import { ServerLoading } from "./ServerLoading.tsx"; + +interface AuthenticatedSessionProps { + authClient: HomeAuthClient; + home: HomeServer; + onChooseServer: () => void; +} + +export function AuthenticatedSession(props: AuthenticatedSessionProps) { + const user = useQuery(api.auth.currentUser); + const client = useFederationClient(props.authClient, props.home); + + if (user === undefined || user === null || client === undefined) { + return ; + } + + return ( + + props.authClient.signOut()} + user={user} + /> + + ); +} + +function useFederationClient(authClient: HomeAuthClient, home: HomeServer) { + const [client, setClient] = useState(); + + useEffect(() => { + let active = true; + const nextClient = new DecentralizedConvexClient({ + getAuthToken: async ({ url }) => { + if (url !== home.convexUrl) return null; + const token = await authClient.convex.token({ + fetchOptions: { throw: false }, + }); + return token.data?.token ?? null; + }, + }); + queueMicrotask(() => { + if (active) setClient(nextClient); + }); + return () => { + active = false; + void nextClient.close(); + }; + }, [authClient, home.convexUrl]); + + return client; +} diff --git a/apps/web/src/cloud/ChatRoom.tsx b/apps/web/src/cloud/ChatRoom.tsx new file mode 100644 index 00000000..999016b3 --- /dev/null +++ b/apps/web/src/cloud/ChatRoom.tsx @@ -0,0 +1,32 @@ +import type { HomeServer } from "../live/config.ts"; +import { ChatSidebar } from "./ChatSidebar.tsx"; +import { ConversationHeader } from "./ConversationHeader.tsx"; +import { MessageComposer } from "./MessageComposer.tsx"; +import { MessageList } from "./MessageList.tsx"; +import { useChatRoom } from "./use-chat-room.ts"; + +interface CurrentUser { + actor: string; +} + +interface ChatRoomProps { + home: HomeServer; + onChooseServer: () => void; + onSignOut: () => Promise; + user: CurrentUser; +} + +export function ChatRoom(props: ChatRoomProps) { + const chat = useChatRoom({ home: props.home, user: props.user }); + + return ( +
+ +
+ + + +
+
+ ); +} diff --git a/apps/web/src/cloud/ChatSidebar.tsx b/apps/web/src/cloud/ChatSidebar.tsx new file mode 100644 index 00000000..3a10096d --- /dev/null +++ b/apps/web/src/cloud/ChatSidebar.tsx @@ -0,0 +1,54 @@ +import type { HomeServer } from "../live/config.ts"; +import { ActorAvatar, actorUsername } from "./actor.tsx"; + +interface ChatSidebarProps { + home: HomeServer; + onChooseServer: () => void; + onSignOut: () => Promise; + user: { actor: string }; +} + +export function ChatSidebar({ + home, + onChooseServer, + onSignOut, + user, +}: ChatSidebarProps) { + return ( + + ); +} diff --git a/apps/web/src/cloud/ConnectionDiagnostics.tsx b/apps/web/src/cloud/ConnectionDiagnostics.tsx new file mode 100644 index 00000000..dccb91f2 --- /dev/null +++ b/apps/web/src/cloud/ConnectionDiagnostics.tsx @@ -0,0 +1,44 @@ +import type { HomeServer } from "../live/config.ts"; +import type { FederationSource } from "./ConversationHeader.tsx"; + +interface ConnectionDiagnosticsProps { + home: HomeServer; + sources: readonly FederationSource[]; +} + +export function ConnectionDiagnostics({ + home, + sources, +}: ConnectionDiagnosticsProps) { + return ( +
+ Debug +
+ Connections +
+ {sources.map((source) => ( +
+ + {new URL(source.target.url).hostname} + {source.status} +
+ ))} +
+
+
+
Write target
+
{home.domain}
+
+
+
Local auth
+
Better Auth
+
+
+
Remote reads
+
Public during federation development
+
+
+
+
+ ); +} diff --git a/apps/web/src/cloud/ConversationHeader.tsx b/apps/web/src/cloud/ConversationHeader.tsx new file mode 100644 index 00000000..ee24ad95 --- /dev/null +++ b/apps/web/src/cloud/ConversationHeader.tsx @@ -0,0 +1,38 @@ +import type { HomeServer } from "../live/config.ts"; +import { ConnectionDiagnostics } from "./ConnectionDiagnostics.tsx"; + +export interface FederationSource { + status: string; + target: { ids: readonly string[]; url: string }; +} + +interface ConversationHeaderProps { + home: HomeServer; + messages: { + federation: { sources: readonly FederationSource[]; status: string }; + }; +} + +export function ConversationHeader({ + home, + messages, +}: ConversationHeaderProps) { + const { sources, status } = messages.federation; + const live = sources.filter((source) => source.status === "live").length; + + return ( +
+
+

# general

+

Messages from every participant's home server

+
+
+ + + {live}/{sources.length} connected + + +
+
+ ); +} diff --git a/apps/web/src/cloud/HomeServerSession.tsx b/apps/web/src/cloud/HomeServerSession.tsx new file mode 100644 index 00000000..a3f4e2ce --- /dev/null +++ b/apps/web/src/cloud/HomeServerSession.tsx @@ -0,0 +1,36 @@ +import type { AuthClient } from "@convex-dev/better-auth/react"; +import { useState } from "react"; +import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; +import { ConvexReactClient } from "convex/react"; + +import type { HomeServer } from "../live/config.ts"; +import { createHomeAuthClient } from "../auth-client.ts"; +import { SessionGate } from "./SessionGate.tsx"; + +interface HomeServerSessionProps { + home: HomeServer; + onChooseServer: () => void; +} + +export function HomeServerSession({ + home, + onChooseServer, +}: HomeServerSessionProps) { + const [authClient] = useState(() => createHomeAuthClient(home.siteUrl)); + const [convex] = useState( + () => new ConvexReactClient(home.convexUrl, { expectAuth: true }), + ); + // The provider's exported union loses the concrete plugin inference from createAuthClient. + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const providerAuthClient = authClient as unknown as AuthClient; + + return ( + + + + ); +} diff --git a/apps/web/src/cloud/Message.tsx b/apps/web/src/cloud/Message.tsx new file mode 100644 index 00000000..da5f4b38 --- /dev/null +++ b/apps/web/src/cloud/Message.tsx @@ -0,0 +1,27 @@ +import type { ChatMessage } from "../live/queries.ts"; +import { ActorAvatar, actorUsername } from "./actor.tsx"; + +export function Message({ message }: { message: ChatMessage }) { + return ( +
+ +
+
+ {actorUsername(message.author)} + @{message.origin} + +
+

{message.body}

+
+
+ ); +} + +function formatTime(timestamp: number) { + return new Intl.DateTimeFormat("en", { + hour: "numeric", + minute: "2-digit", + }).format(timestamp); +} diff --git a/apps/web/src/cloud/MessageComposer.tsx b/apps/web/src/cloud/MessageComposer.tsx new file mode 100644 index 00000000..c6eb0ac1 --- /dev/null +++ b/apps/web/src/cloud/MessageComposer.tsx @@ -0,0 +1,46 @@ +import type { FormEvent } from "react"; + +interface MessageComposerProps { + draft: string; + error?: string; + sending: boolean; + setDraft: (draft: string) => void; + submit: (event: FormEvent) => Promise; +} + +export function MessageComposer({ + draft, + error, + sending, + setDraft, + submit, +}: MessageComposerProps) { + return ( +
void submit(event)}> +