Add user presence to the frontend
This commit is contained in:
parent
56184b569b
commit
8def441972
6 changed files with 298 additions and 12 deletions
20
package-lock.json
generated
20
package-lock.json
generated
|
@ -16,7 +16,8 @@
|
|||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"rustpad-wasm": "file:./rustpad-wasm/pkg"
|
||||
"rustpad-wasm": "file:./rustpad-wasm/pkg",
|
||||
"use-local-storage-state": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.8",
|
||||
|
@ -20550,6 +20551,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-local-storage-state": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-10.0.0.tgz",
|
||||
"integrity": "sha512-NCab0oYOMZA8oT9y4OE7tMT6JS21SiyPsTjZdapnyvHe7bVFlIMSp6LaiuHBdS1OvduuLtG+pX/duFIBkd0PCA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
|
||||
|
@ -38658,6 +38670,12 @@
|
|||
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-local-storage-state": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-10.0.0.tgz",
|
||||
"integrity": "sha512-NCab0oYOMZA8oT9y4OE7tMT6JS21SiyPsTjZdapnyvHe7bVFlIMSp6LaiuHBdS1OvduuLtG+pX/duFIBkd0PCA==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-sidecar": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"rustpad-wasm": "file:./rustpad-wasm/pkg"
|
||||
"rustpad-wasm": "file:./rustpad-wasm/pkg",
|
||||
"use-local-storage-state": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.8",
|
||||
|
|
44
src/App.tsx
44
src/App.tsx
|
@ -18,18 +18,20 @@ import {
|
|||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
VscAccount,
|
||||
VscChevronRight,
|
||||
VscCircleFilled,
|
||||
VscFolderOpened,
|
||||
VscGist,
|
||||
VscRemote,
|
||||
} from "react-icons/vsc";
|
||||
import useStorage from "use-local-storage-state";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import raw from "raw.macro";
|
||||
import Rustpad from "./rustpad";
|
||||
import Rustpad, { UserInfo } from "./rustpad";
|
||||
import languages from "./languages.json";
|
||||
import animals from "./animals.json";
|
||||
import User from "./User";
|
||||
|
||||
set_panic_hook();
|
||||
|
||||
|
@ -43,11 +45,22 @@ const wsUri =
|
|||
window.location.host +
|
||||
`/api/socket/${id}`;
|
||||
|
||||
function generateName() {
|
||||
return "Anonymous " + animals[Math.floor(Math.random() * animals.length)];
|
||||
}
|
||||
|
||||
function generateHue() {
|
||||
return Math.floor(Math.random() * 360);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const toast = useToast();
|
||||
const [language, setLanguage] = useState("plaintext");
|
||||
const [connection, setConnection] =
|
||||
useState<"connected" | "disconnected" | "desynchronized">("disconnected");
|
||||
const [users, setUsers] = useStorage<Record<number, UserInfo>>("users", {});
|
||||
const [name, setName] = useStorage("name", generateName);
|
||||
const [hue, setHue] = useState(generateHue);
|
||||
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
|
||||
const rustpad = useRef<Rustpad>();
|
||||
|
||||
|
@ -75,13 +88,20 @@ function App() {
|
|||
setLanguage(language);
|
||||
}
|
||||
},
|
||||
onChangeUsers: setUsers,
|
||||
});
|
||||
return () => {
|
||||
rustpad.current?.dispose();
|
||||
rustpad.current = undefined;
|
||||
};
|
||||
}
|
||||
}, [editor, toast]);
|
||||
}, [editor, toast, setUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connection === "connected") {
|
||||
rustpad.current?.setInfo({ name, hue });
|
||||
}
|
||||
}, [connection, name, hue]);
|
||||
|
||||
function handleChangeLanguage(language: string) {
|
||||
setLanguage(language);
|
||||
|
@ -209,12 +229,16 @@ function App() {
|
|||
<Heading mt={4} mb={1.5} size="sm">
|
||||
Active Users
|
||||
</Heading>
|
||||
<Stack mb={1.5} fontSize="sm">
|
||||
<HStack p={2} rounded="md" _hover={{ bgColor: "gray.200" }}>
|
||||
<Icon as={VscAccount} />
|
||||
<Text fontWeight="medium">Anonymous Bear</Text>
|
||||
<Text>(you)</Text>
|
||||
</HStack>
|
||||
<Stack spacing={0} mb={1.5} fontSize="sm">
|
||||
<User
|
||||
info={{ name, hue }}
|
||||
isMe
|
||||
onChangeName={(name) => name.length > 0 && setName(name)}
|
||||
onChangeColor={() => setHue(generateHue())}
|
||||
/>
|
||||
{Object.entries(users).map(([id, info]) => (
|
||||
<User key={id} info={info} />
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Heading mt={4} mb={1.5} size="sm">
|
||||
|
@ -248,7 +272,7 @@ function App() {
|
|||
mt={2}
|
||||
onClick={handleLoadSample}
|
||||
>
|
||||
Read the code
|
||||
See the code
|
||||
</Button>
|
||||
</Container>
|
||||
</Flex>
|
||||
|
|
89
src/User.tsx
Normal file
89
src/User.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
HStack,
|
||||
Icon,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useRef } from "react";
|
||||
import { FaPalette } from "react-icons/fa";
|
||||
import { VscAccount } from "react-icons/vsc";
|
||||
import { UserInfo } from "./rustpad";
|
||||
|
||||
type UserProps = {
|
||||
info: UserInfo;
|
||||
isMe?: boolean;
|
||||
onChangeName?: (name: string) => unknown;
|
||||
onChangeColor?: () => unknown;
|
||||
};
|
||||
|
||||
function User({ info, isMe = false, onChangeName, onChangeColor }: UserProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const nameColor = `hsl(${info.hue}, 90%, 15%)`;
|
||||
return (
|
||||
<Popover
|
||||
placement="right"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
initialFocusRef={inputRef}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<HStack
|
||||
p={2}
|
||||
rounded="md"
|
||||
_hover={{ bgColor: "gray.200", cursor: "pointer" }}
|
||||
onClick={() => isMe && onOpen()}
|
||||
>
|
||||
<Icon as={VscAccount} />
|
||||
<Text fontWeight="medium" color={nameColor}>
|
||||
{info.name}
|
||||
</Text>
|
||||
{isMe && <Text>(you)</Text>}
|
||||
</HStack>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverHeader fontWeight="semibold">Update Info</PopoverHeader>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverBody>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
mb={2}
|
||||
value={info.name}
|
||||
maxLength={25}
|
||||
onChange={(event) => onChangeName?.(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
w="100%"
|
||||
leftIcon={<FaPalette />}
|
||||
onClick={onChangeColor}
|
||||
>
|
||||
Change Color
|
||||
</Button>
|
||||
</PopoverBody>
|
||||
<PopoverFooter d="flex" justifyContent="flex-end">
|
||||
<ButtonGroup size="sm">
|
||||
<Button colorScheme="blue" onClick={onClose}>
|
||||
Done
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default User;
|
116
src/animals.json
Normal file
116
src/animals.json
Normal file
|
@ -0,0 +1,116 @@
|
|||
[
|
||||
"Alligator",
|
||||
"Ant",
|
||||
"Anteater",
|
||||
"Antelope",
|
||||
"Arctic Fox",
|
||||
"Armadillo",
|
||||
"Badger",
|
||||
"Bat",
|
||||
"Beaver",
|
||||
"Bee",
|
||||
"Beetle",
|
||||
"Black Bear",
|
||||
"Buffalo",
|
||||
"Butterfly",
|
||||
"Camel",
|
||||
"Cat",
|
||||
"Chameleon",
|
||||
"Cheetah",
|
||||
"Chicken",
|
||||
"Cicada",
|
||||
"Clam",
|
||||
"Cockatoo",
|
||||
"Cockroach",
|
||||
"Cow",
|
||||
"Coyote",
|
||||
"Crab",
|
||||
"Cricket",
|
||||
"Crow",
|
||||
"Deer",
|
||||
"Dog",
|
||||
"Dolphin",
|
||||
"Donkey",
|
||||
"Dove",
|
||||
"Dragonfly",
|
||||
"Duck",
|
||||
"Eagle",
|
||||
"Eel",
|
||||
"Elephant",
|
||||
"Ferret",
|
||||
"Fish",
|
||||
"Fly",
|
||||
"Fox",
|
||||
"Frog",
|
||||
"Gazelle",
|
||||
"Goat",
|
||||
"Grasshopper",
|
||||
"Grizzly Bear",
|
||||
"Groundhog",
|
||||
"Guinea Pig",
|
||||
"Hedgehog",
|
||||
"Hen",
|
||||
"Hippopotamus",
|
||||
"Horse",
|
||||
"Hummingbird",
|
||||
"Hyena",
|
||||
"Koala",
|
||||
"Leopard",
|
||||
"Lion",
|
||||
"Llama",
|
||||
"Lobster",
|
||||
"Lynx",
|
||||
"Meerkat",
|
||||
"Mole",
|
||||
"Moose",
|
||||
"Moth",
|
||||
"Mouse",
|
||||
"Octopus",
|
||||
"Orangutan",
|
||||
"Orca",
|
||||
"Ostrich",
|
||||
"Owl",
|
||||
"Panda Bear",
|
||||
"Panther",
|
||||
"Parrot",
|
||||
"Penguin",
|
||||
"Pig",
|
||||
"Pigeon",
|
||||
"Polar Bear",
|
||||
"Rabbit",
|
||||
"Raccoon",
|
||||
"Reindeer",
|
||||
"Robin",
|
||||
"Sea Lion",
|
||||
"Sea Otter",
|
||||
"Seagull",
|
||||
"Seahorse",
|
||||
"Seal",
|
||||
"Shark",
|
||||
"Sheep",
|
||||
"Shrimp",
|
||||
"Slug",
|
||||
"Snail",
|
||||
"Snake",
|
||||
"Sparrow",
|
||||
"Squid",
|
||||
"Squirrel",
|
||||
"Starfish",
|
||||
"Swan",
|
||||
"Tiger",
|
||||
"Turkey",
|
||||
"Turtle",
|
||||
"Wallaby",
|
||||
"Walrus",
|
||||
"Wasp",
|
||||
"Water Buffalo",
|
||||
"Weasel",
|
||||
"Weaver",
|
||||
"Whale",
|
||||
"Wildcat",
|
||||
"Wilddog",
|
||||
"Wolf",
|
||||
"Wolverine",
|
||||
"Wombat",
|
||||
"Woodpecker"
|
||||
]
|
|
@ -9,9 +9,16 @@ export type RustpadOptions = {
|
|||
readonly onDisconnected?: () => unknown;
|
||||
readonly onDesynchronized?: () => unknown;
|
||||
readonly onChangeLanguage?: (language: string) => unknown;
|
||||
readonly onChangeUsers?: (users: Record<number, UserInfo>) => unknown;
|
||||
readonly reconnectInterval?: number;
|
||||
};
|
||||
|
||||
/** A user currently editing the document. */
|
||||
export type UserInfo = {
|
||||
readonly name: string;
|
||||
readonly hue: number;
|
||||
};
|
||||
|
||||
/** Browser client for Rustpad. */
|
||||
class Rustpad {
|
||||
private ws?: WebSocket;
|
||||
|
@ -28,6 +35,8 @@ class Rustpad {
|
|||
private revision: number = 0;
|
||||
private outstanding?: OpSeq;
|
||||
private buffer?: OpSeq;
|
||||
private users: Record<number, UserInfo> = {};
|
||||
private myInfo?: UserInfo;
|
||||
|
||||
// Intermittent local editor state
|
||||
private lastValue: string = "";
|
||||
|
@ -72,6 +81,12 @@ class Rustpad {
|
|||
return this.ws !== undefined;
|
||||
}
|
||||
|
||||
/** Set the user's information. */
|
||||
setInfo(info: UserInfo) {
|
||||
this.myInfo = info;
|
||||
this.sendInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts a WebSocket connection.
|
||||
*
|
||||
|
@ -91,6 +106,8 @@ class Rustpad {
|
|||
this.connecting = false;
|
||||
this.ws = ws;
|
||||
this.options.onConnected?.();
|
||||
this.users = {};
|
||||
this.options.onChangeUsers?.(this.users);
|
||||
if (this.outstanding) {
|
||||
this.sendOperation(this.outstanding);
|
||||
}
|
||||
|
@ -138,6 +155,17 @@ class Rustpad {
|
|||
}
|
||||
} else if (msg.Language !== undefined) {
|
||||
this.options.onChangeLanguage?.(msg.Language);
|
||||
} else if (msg.UserInfo !== undefined) {
|
||||
const { id, info } = msg.UserInfo;
|
||||
if (id !== this.me) {
|
||||
this.users = { ...this.users };
|
||||
if (info) {
|
||||
this.users[id] = info;
|
||||
} else {
|
||||
delete this.users[id];
|
||||
}
|
||||
this.options.onChangeUsers?.(this.users);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,6 +211,12 @@ class Rustpad {
|
|||
this.ws?.send(`{"Edit":{"revision":${this.revision},"operation":${op}}}`);
|
||||
}
|
||||
|
||||
private sendInfo() {
|
||||
if (this.myInfo) {
|
||||
this.ws?.send(`{"ClientInfo":${JSON.stringify(this.myInfo)}}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The following functions are based on Firepad's monaco-adapter.js
|
||||
|
||||
private applyOperation(operation: OpSeq) {
|
||||
|
@ -281,6 +315,10 @@ type ServerMsg = {
|
|||
operations: UserOperation[];
|
||||
};
|
||||
Language?: string;
|
||||
UserInfo?: {
|
||||
id: number;
|
||||
info: UserInfo | null;
|
||||
};
|
||||
};
|
||||
|
||||
export default Rustpad;
|
||||
|
|
Loading…
Add table
Reference in a new issue