Skip to content

Commit

Permalink
Show collaborator cursors.
Browse files Browse the repository at this point in the history
  • Loading branch information
aboodman committed Mar 3, 2021
1 parent 27c2413 commit adb5d36
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 12 deletions.
24 changes: 24 additions & 0 deletions frontend/cursor.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.cursor {
position: absolute;
font-family: "Inter", sans-serif;
font-size: 11px;
font-weight: 400;
line-height: 1em;
pointer-events: none;
cursor: pointer;
}

.pointer {
position: absolute;
transform: rotate(-127deg);
font-size: 16px;
}

.userinfo {
margin: 20px 16px;
padding: 5px;
}

.avatar {
margin-right: 10px;
}
26 changes: 26 additions & 0 deletions frontend/cursor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Data } from "./data";
import styles from "./cursor.module.css";

export function Cursor({ data, clientID }: { data: Data; clientID: string }) {
const userInfo = data.useUserInfo(clientID);
const cursor = data.useCursor(clientID);
if (!userInfo || !cursor) {
return null;
}
return (
<div className={styles.cursor} style={{ left: cursor.x, top: cursor.y }}>
<div className={styles.pointer} style={{ color: userInfo.color }}>
</div>
<div
className={styles.userinfo}
style={{
backgroundColor: userInfo.color,
color: "white",
}}
>
{userInfo.avatar}&nbsp;&nbsp;{userInfo.name}
</div>
</div>
);
}
41 changes: 40 additions & 1 deletion frontend/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
getClientState,
overShape,
initClientState,
setCursor,
keyPrefix as clientStatePrefix,
} from "../shared/client-state";
import type Storage from "../shared/storage";
import type { UserInfo } from "../shared/client-state";
Expand Down Expand Up @@ -54,6 +56,16 @@ export function createData(rep: Replicache) {
}
),

setCursor: rep.register(
"setCursor",
async (
tx: WriteTransaction,
args: { id: string; x: number; y: number }
) => {
await setCursor(writeStorage(tx), args);
}
),

overShape: rep.register(
"overShape",
async (
Expand Down Expand Up @@ -86,7 +98,8 @@ export function createData(rep: Replicache) {
);
},

useUserInfo(): UserInfo | null {
useUserInfo(clientID: string): UserInfo | null {
console.log("useUserInfo", { clientID });
return useSubscribe(
rep,
async (tx: ReadTransaction) => {
Expand All @@ -105,6 +118,32 @@ export function createData(rep: Replicache) {
null
);
},

useCollaboratorIDs(clientID: string): Array<string> {
return useSubscribe(
rep,
async (tx: ReadTransaction) => {
const r = [];
for await (let k of tx.scan({ prefix: clientStatePrefix }).keys()) {
if (!k.endsWith(clientID)) {
r.push(k.substr(clientStatePrefix.length));
}
}
return r;
},
[]
);
},

useCursor(clientID: string): { x: number; y: number } | null {
return useSubscribe(
rep,
async (tx: ReadTransaction) => {
return (await getClientState(readStorage(tx), clientID)).cursor;
},
null
);
},
};
}

Expand Down
27 changes: 23 additions & 4 deletions frontend/designer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { CSSProperties, MouseEvent, useState } from "react";
import { Rect } from "./rect";
import { HotKeys } from "react-hotkeys";
import { Data } from "./data";
import { Cursor } from "./cursor";

type LastDrag = { x: number; y: number };

Expand All @@ -12,6 +13,9 @@ export function Designer({ data }: { data: Data }) {
const overID = data.useOverShapeID();
console.log({ overID });

const collaboratorIDs = data.useCollaboratorIDs(data.clientID);
console.log({ collaboratorIDs });

// TODO: This should be stored in Replicache too, since we will be rendering
// other users' selections.
const [selectedID, setSelectedID] = useState("");
Expand All @@ -23,6 +27,12 @@ export function Designer({ data }: { data: Data }) {
};

const onMouseMove = (e: MouseEvent) => {
data.setCursor({
id: data.clientID,
x: e.nativeEvent.offsetX,
y: e.nativeEvent.offsetY,
});

if (lastDrag === null) {
return;
}
Expand Down Expand Up @@ -68,6 +78,7 @@ export function Designer({ data }: { data: Data }) {
>
<svg width="100%" height="100%">
{ids.map((id) => (
// shapes
<Rect
{...{
key: id,
Expand All @@ -84,10 +95,7 @@ export function Designer({ data }: { data: Data }) {
))}

{
// This looks a little odd at first, but we want the selection
// rectangle to be stacked above all objects on the page, so we
// paint the highlighted object again in a special 'highlight'
// mode.
// self highlight
overID && (
<Rect
{...{
Expand All @@ -100,6 +108,17 @@ export function Designer({ data }: { data: Data }) {
)
}
</svg>

{collaboratorIDs.map((id) => (
// collaborator cursors
<Cursor
{...{
key: `key-${id}`,
data,
clientID: id,
}}
/>
))}
</div>
</HotKeys>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { randInt } from "../shared/rand";
const colors = ["red", "blue", "white", "green", "yellow"];

export function Nav({ data }: { data: Data | null }) {
const userInfo = data?.useUserInfo();
const userInfo = data?.useUserInfo(data?.clientID);
console.log({ userInfo });

const onRectangle = async () => {
Expand Down
2 changes: 1 addition & 1 deletion pages/_document.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MyDocument extends Document {
<Head>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;400&display=swap"
rel="stylesheet"
/>
</Head>
Expand Down
24 changes: 21 additions & 3 deletions pages/api/replicache-push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { putShape, moveShape, shape } from "../../shared/shape";
import {
initClientState,
overShape,
setCursor,
userInfo,
} from "../../shared/client-state";
import {
Expand Down Expand Up @@ -43,6 +44,15 @@ const mutation = t.union([
defaultUserInfo: userInfo,
}),
}),
t.type({
id: t.number,
name: t.literal("setCursor"),
args: t.type({
id: t.string,
x: t.number,
y: t.number,
}),
}),
t.type({
id: t.number,
name: t.literal("overShape"),
Expand Down Expand Up @@ -107,12 +117,15 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
case "createShape":
await putShape(s, mutation.args);
break;
case "overShape":
await overShape(s, mutation.args);
break;
case "initClientState":
await initClientState(s, mutation.args);
break;
case "setCursor":
await setCursor(s, mutation.args);
break;
case "overShape":
await overShape(s, mutation.args);
break;
}

await setLastMutationID(executor, push.clientID, expectedMutationID);
Expand Down Expand Up @@ -143,6 +156,11 @@ function collapse(prev: Mutation, next: Mutation): boolean {
next.args.dy += prev.args.dy;
return true;
}
if (prev.name == "setCursor" && next.name == "setCursor") {
next.args.x = prev.args.x;
next.args.y = prev.args.y;
return true;
}
return false;
}

Expand Down
24 changes: 22 additions & 2 deletions shared/client-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export const userInfo = t.type({
// 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({
cursor: t.type({
x: t.number,
y: t.number,
}),
overID: t.string,
userInfo: userInfo,
});
Expand All @@ -60,6 +64,10 @@ export async function initClientState(
await putClientState(storage, {
id,
clientState: {
cursor: {
x: 0,
y: 0,
},
overID: "",
userInfo: defaultUserInfo,
},
Expand All @@ -72,7 +80,7 @@ export async function getClientState(
): Promise<ClientState> {
const jv = await storage.getObject(key(id));
if (!jv) {
throw new Error("Expected clientState to be initialized already");
throw new Error("Expected clientState to be initialized already: " + id);
}
return must(clientState.decode(jv));
}
Expand All @@ -84,6 +92,16 @@ export function putClientState(
return storage.putObject(key(id), clientState);
}

export async function setCursor(
storage: Storage,
{ id, x, y }: { id: string; x: number; y: number }
): Promise<void> {
const clientState = await getClientState(storage, id);
clientState.cursor.x = x;
clientState.cursor.y = y;
await putClientState(storage, { id, clientState });
}

export async function overShape(
storage: Storage,
{ clientID, shapeID }: { clientID: string; shapeID: string }
Expand All @@ -103,5 +121,7 @@ export function randUserInfo() {
}

function key(id: string): string {
return `client-state-${id}`;
return `${keyPrefix}${id}`;
}

export const keyPrefix = `client-state-`;

0 comments on commit adb5d36

Please sign in to comment.