diff --git a/backend/rds.ts b/backend/rds.ts index d6e1e1e..37f6d3f 100644 --- a/backend/rds.ts +++ b/backend/rds.ts @@ -90,17 +90,21 @@ export async function ensureDatabase() { } async function createDatabase() { - await executeStatementInDatabase(null, "CREATE DATABASE " + dbName); + await executeStatementInDatabase( + null, + `CREATE DATABASE ${dbName} + CHARACTER SET utf8mb4` + ); await executeStatement(`CREATE TABLE Cookie ( Version BIGINT NOT NULL)`); await executeStatement(`CREATE TABLE Client ( - Id VARCHAR(255) PRIMARY KEY NOT NULL, + Id VARCHAR(100) PRIMARY KEY NOT NULL, LastMutationID BIGINT NOT NULL, LastModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)`); await executeStatement(`CREATE TABLE Object ( - K VARCHAR(255) PRIMARY KEY NOT NULL, + K VARCHAR(100) PRIMARY KEY NOT NULL, V TEXT NOT NULL, Version BIGINT NOT NULL, LastModified TIMESTAMP NOT NULL diff --git a/frontend/data.ts b/frontend/data.ts index 9c62753..4e47299 100644 --- a/frontend/data.ts +++ b/frontend/data.ts @@ -1,8 +1,13 @@ import Replicache, { ReadTransaction, WriteTransaction } from "replicache"; import { useSubscribe } from "replicache-react-util"; import { getShape, Shape, putShape, moveShape } from "../shared/shape"; -import { getClientState, overShape } from "../shared/client-state"; +import { + getClientState, + overShape, + initClientState, +} from "../shared/client-state"; import type Storage from "../shared/storage"; +import type { UserInfo } from "../shared/client-state"; import { newID } from "../shared/id"; /** @@ -39,6 +44,16 @@ export function createData(rep: Replicache) { } ), + initClientState: rep.register( + "initClientState", + async ( + tx: WriteTransaction, + args: { id: string; defaultUserInfo: UserInfo } + ) => { + await initClientState(writeStorage(tx), args); + } + ), + overShape: rep.register( "overShape", async ( @@ -71,6 +86,16 @@ export function createData(rep: Replicache) { ); }, + useUserInfo(): UserInfo | null { + return useSubscribe( + rep, + async (tx: ReadTransaction) => { + return (await getClientState(readStorage(tx), clientID)).userInfo; + }, + null + ); + }, + useOverShapeID(): string | null { return useSubscribe( rep, diff --git a/frontend/nav.module.css b/frontend/nav.module.css index ae7bd66..a9bb3ec 100644 --- a/frontend/nav.module.css +++ b/frontend/nav.module.css @@ -18,3 +18,20 @@ background-color: rgb(75, 158, 244); opacity: 1; } + +.user { + color: white; + display: flex; + margin-left: auto; + margin-right: 12px; + margin-top: auto; + margin-bottom: auto; + padding: 0 10px; + height: 30px; + align-items: center; + font-family: "Inter", sans-serif; + font-size: 13px; + font-weight: 400; + border-radius: 6px; + line-height: 1em; +} diff --git a/frontend/nav.tsx b/frontend/nav.tsx index 434a43f..d0cea83 100644 --- a/frontend/nav.tsx +++ b/frontend/nav.tsx @@ -1,15 +1,14 @@ import styles from "./nav.module.css"; import { Data } from "./data"; import { newID } from "../shared/id"; +import { randInt } from "../shared/rand"; const colors = ["red", "blue", "white", "green", "yellow"]; -function randInt(min: number, max: number): number { - const range = max - min; - return Math.round(Math.random() * range); -} - export function Nav({ data }: { data: Data | null }) { + const userInfo = data?.useUserInfo(); + console.log({ userInfo }); + const onRectangle = async () => { if (!data) { return; @@ -98,6 +97,16 @@ export function Nav({ data }: { data: Data | null }) { > + {userInfo && ( +
+ {userInfo.avatar} {userInfo.name} +
+ )} ); } diff --git a/pages/_document.js b/pages/_document.js new file mode 100644 index 0000000..1d8afc3 --- /dev/null +++ b/pages/_document.js @@ -0,0 +1,28 @@ +import Document, { Html, Head, Main, NextScript } from "next/document"; + +class MyDocument extends Document { + static async getInitialProps(ctx) { + const initialProps = await Document.getInitialProps(ctx); + return { ...initialProps }; + } + + render() { + return ( + + + + + + +
+ + + + ); + } +} + +export default MyDocument; diff --git a/pages/api/replicache-push.ts b/pages/api/replicache-push.ts index 5539caa..832459b 100644 --- a/pages/api/replicache-push.ts +++ b/pages/api/replicache-push.ts @@ -1,7 +1,11 @@ import * as t from "io-ts"; import { ExecuteStatementFn, transact } from "../../backend/rds"; import { putShape, moveShape, shape } from "../../shared/shape"; -import { overShape } from "../../shared/client-state"; +import { + initClientState, + overShape, + userInfo, +} from "../../shared/client-state"; import { getObject, putObject, @@ -31,6 +35,14 @@ const mutation = t.union([ dy: t.number, }), }), + t.type({ + id: t.number, + name: t.literal("initClientState"), + args: t.type({ + id: t.string, + defaultUserInfo: userInfo, + }), + }), t.type({ id: t.number, name: t.literal("overShape"), @@ -98,6 +110,9 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { case "overShape": await overShape(s, mutation.args); break; + case "initClientState": + await initClientState(s, mutation.args); + break; } await setLastMutationID(executor, push.clientID, expectedMutationID); diff --git a/pages/index.tsx b/pages/index.tsx index 2b5a0f2..d6a92bc 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,6 +6,7 @@ import { Nav } from "../frontend/nav"; import Pusher from "pusher-js"; import type { Data } from "../frontend/data"; +import { randUserInfo } from "../shared/client-state"; export default function Home() { const [data, setData] = useState(null); @@ -26,6 +27,13 @@ export default function Home() { useMemstore: true, pushDelay: 1, }); + + const defaultUserInfo = randUserInfo(); + const d = createData(rep); + d.initClientState({ + id: d.clientID, + defaultUserInfo, + }); rep.sync(); Pusher.logToConsole = true; @@ -37,7 +45,7 @@ export default function Home() { rep.pull(); }); - setData(createData(rep)); + setData(d); }); return ( diff --git a/shared/client-state.ts b/shared/client-state.ts index 3327ed8..4287a9a 100644 --- a/shared/client-state.ts +++ b/shared/client-state.ts @@ -1,27 +1,85 @@ import * as t from "io-ts"; import { must } from "../backend/decode"; import Storage from "./storage"; +import { randInt } from "./rand"; + +const colors = [ + "#f94144", + "#f3722c", + "#f8961e", + "#f9844a", + "#f9c74f", + "#90be6d", + "#43aa8b", + "#4d908e", + "#577590", + "#277da1", +]; +const avatars = [ + ["🐶", "Puppy"], + ["🐱", "Kitty"], + ["🐭", "Mouse"], + ["🐹", "Hamster"], + ["🐰", "Bunny"], + ["🦊", "Fox"], + ["🐻", "Bear"], + ["🐼", "Panda"], + ["🐻‍❄️", "Polar Bear"], + ["🐨", "Koala"], + ["🐯", "Tiger"], + ["🦁", "Lion"], + ["🐮", "Cow"], + ["🐷", "Piggy"], + ["🐵", "Monkey"], + ["🐣", "Chick"], +]; + +export const userInfo = t.type({ + avatar: t.string, + name: t.string, + color: t.string, +}); // TODO: It would be good to merge this with the first-class concept of `client` // that Replicache itself manages if possible. export const clientState = t.type({ overID: t.string, + userInfo: userInfo, }); +export type UserInfo = t.TypeOf; export type ClientState = t.TypeOf; +export async function initClientState( + storage: Storage, + { id, defaultUserInfo }: { id: string; defaultUserInfo: UserInfo } +): Promise { + if (await storage.getObject(key(id))) { + return; + } + await putClientState(storage, { + id, + clientState: { + overID: "", + userInfo: defaultUserInfo, + }, + }); +} + export async function getClientState( storage: Storage, id: string ): Promise { const jv = await storage.getObject(key(id)); - return jv ? must(clientState.decode(jv)) : { overID: "" }; + if (!jv) { + throw new Error("Expected clientState to be initialized already"); + } + return must(clientState.decode(jv)); } export function putClientState( storage: Storage, - id: string, - clientState: ClientState + { id, clientState }: { id: string; clientState: ClientState } ): Promise { return storage.putObject(key(id), clientState); } @@ -32,7 +90,16 @@ export async function overShape( ): Promise { const client = await getClientState(storage, clientID); client.overID = shapeID; - await putClientState(storage, clientID, client); + await putClientState(storage, { id: clientID, clientState: client }); +} + +export function randUserInfo() { + const [avatar, name] = avatars[randInt(0, avatars.length - 1)]; + return { + avatar, + name: "Anonymous " + name, + color: colors[randInt(0, colors.length - 1)], + }; } function key(id: string): string { diff --git a/shared/rand.ts b/shared/rand.ts new file mode 100644 index 0000000..b078929 --- /dev/null +++ b/shared/rand.ts @@ -0,0 +1,4 @@ +export function randInt(min: number, max: number): number { + const range = max - min; + return Math.round(Math.random() * range); +}