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);
+}