Skip to content

Commit

Permalink
Merge pull request #1 from JavaScript-Mastery-Pro/feat/comments
Browse files Browse the repository at this point in the history
Feat/comments
  • Loading branch information
sujatagunale authored Jan 30, 2024
2 parents 2376a67 + cdb0b1a commit 31d272c
Show file tree
Hide file tree
Showing 24 changed files with 735 additions and 464 deletions.
249 changes: 248 additions & 1 deletion app/App.tsx

Large diffs are not rendered by default.

52 changes: 33 additions & 19 deletions app/Room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,39 @@
import { LiveMap } from "@liveblocks/client";
import { ClientSideSuspense } from "@liveblocks/react";

import { RoomProvider } from "@/liveblocks.config";
import Loader from "@/components/Loader";
import { RoomProvider } from "@/liveblocks.config";

const Room = ({ children }: { children: React.ReactNode }) => (
<RoomProvider
id='figma-room'
initialPresence={{
cursor: null,
cursorColor: null,
editingText: null,
}}
initialStorage={{
canvasObjects: new LiveMap(),
}}
>
<ClientSideSuspense fallback={<Loader />}>
{() => children}
</ClientSideSuspense>
</RoomProvider>
);
const Room = ({ children }: { children: React.ReactNode }) => {
return (
<RoomProvider
id="fig-room"
/**
* initialPresence is used to initialize the presence of the current
* user in the room.
*
* initialPresence: https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider
*/
initialPresence={{ cursor: null, cursorColor: null, editingText: null }}
/**
* initialStorage is used to initialize the storage of the room.
*
* initialStorage: https://liveblocks.io/docs/api-reference/liveblocks-react#RoomProvider
*/
initialStorage={{
/**
* We're using a LiveMap to store the canvas objects
*
* LiveMap: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap
*/
canvasObjects: new LiveMap(),
}}
>
<ClientSideSuspense fallback={<Loader />}>
{() => children}
</ClientSideSuspense>
</RoomProvider>
);
}

export default Room;
export default Room;
5 changes: 5 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import dynamic from "next/dynamic";

/**
* disable ssr to avoid pre-rendering issues of Next.js
*
* we're doing this because we're using a canvas element that can't be pre-rendered by Next.js on the server
*/
const App = dynamic(() => import("./App"), { ssr: false });

export default App;
11 changes: 5 additions & 6 deletions components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@ import Image from "next/image";
import { getShapeInfo } from "@/lib/utils";

const LeftSidebar = ({ allShapes }: { allShapes: Array<any> }) => {
// memoize the result of this function so that it doesn't change on every render but only when there are new shapes
const memoizedShapes = useMemo(
() => (
<section className='sticky left-0 flex h-full min-w-[227px] select-none flex-col overflow-y-auto border-t border-primary-grey-200 bg-primary-black pb-20 text-primary-grey-300 max-sm:hidden'>
<h3 className='border border-primary-grey-200 px-5 py-4 text-xs uppercase'>
Layers
</h3>
<div className='flex flex-col'>
<section className="flex flex-col border-t border-primary-grey-200 bg-primary-black text-primary-grey-300 min-w-[227px] sticky left-0 h-full max-sm:hidden select-none overflow-y-auto pb-20">
<h3 className="border border-primary-grey-200 px-5 py-4 text-xs uppercase">Layers</h3>
<div className="flex flex-col">
{allShapes?.map((shape: any) => {
const info = getShapeInfo(shape[1]?.type);

return (
<div
key={shape[1]?.objectId}
className='group my-1 flex items-center gap-2 px-5 py-2.5 hover:cursor-pointer hover:bg-primary-green hover:text-primary-black'
className="group my-1 flex items-center gap-2 px-5 py-2.5 hover:cursor-pointer hover:bg-primary-green hover:text-primary-black"
>
<Image
src={info?.icon}
Expand Down
100 changes: 61 additions & 39 deletions components/Live.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,14 @@

import { useCallback, useEffect, useState } from "react";

import {
useBroadcastEvent,
useEventListener,
useMyPresence,
useOthers,
} from "@/liveblocks.config";
import { useBroadcastEvent, useEventListener, useMyPresence, useOthers } from "@/liveblocks.config";
import useInterval from "@/hooks/useInterval";
import { CursorMode, CursorState, Reaction, ReactionEvent } from "@/types/type";
import { shortcuts } from "@/constants";

import { Comments } from "./comments/Comments";
import {
CursorChat,
FlyingReaction,
LiveCursors,
ReactionSelector,
} from "./index";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "./ui/context-menu";
import { CursorChat, FlyingReaction, LiveCursors, ReactionSelector } from "./index";
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "./ui/context-menu";

type Props = {
canvasRef: React.MutableRefObject<HTMLCanvasElement | null>;
Expand All @@ -33,32 +18,50 @@ type Props = {
};

const Live = ({ canvasRef, undo, redo }: Props) => {
/**
* useOthers returns the list of other users in the room.
*
* useOthers: https://liveblocks.io/docs/api-reference/liveblocks-react#useOthers
*/
const others = useOthers();

/**
* useMyPresence returns the presence of the current user in the room.
* It also returns a function to update the presence of the current user.
*
* useMyPresence: https://liveblocks.io/docs/api-reference/liveblocks-react#useMyPresence
*/
const [{ cursor }, updateMyPresence] = useMyPresence() as any;

/**
* useBroadcastEvent is used to broadcast an event to all the other users in the room.
*
* useBroadcastEvent: https://liveblocks.io/docs/api-reference/liveblocks-react#useBroadcastEvent
*/
const broadcast = useBroadcastEvent();

// store the reactions created on mouse click
const [reactions, setReactions] = useState<Reaction[]>([]);

// track the state of the cursor (hidden, chat, reaction, reaction selector)
const [cursorState, setCursorState] = useState<CursorState>({
mode: CursorMode.Hidden,
});

// set the reaction of the cursor
const setReaction = useCallback((reaction: string) => {
setCursorState({ mode: CursorMode.Reaction, reaction, isPressed: false });
}, []);

// Remove reactions that are not visible anymore (every 1 sec)
useInterval(() => {
setReactions((reactions) =>
reactions.filter((reaction) => reaction.timestamp > Date.now() - 4000)
);
setReactions((reactions) => reactions.filter((reaction) => reaction.timestamp > Date.now() - 4000));
}, 1000);

// Broadcast the reaction to other users (every 100ms)
useInterval(() => {
if (
cursorState.mode === CursorMode.Reaction &&
cursorState.isPressed &&
cursor
) {
if (cursorState.mode === CursorMode.Reaction && cursorState.isPressed && cursor) {
// concat all the reactions created on mouse click
setReactions((reactions) =>
reactions.concat([
{
Expand All @@ -68,6 +71,8 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
},
])
);

// Broadcast the reaction to other users
broadcast({
x: cursor.x,
y: cursor.y,
Expand All @@ -76,6 +81,12 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
}
}, 100);

/**
* useEventListener is used to listen to events broadcasted by other
* users.
*
* useEventListener: https://liveblocks.io/docs/api-reference/liveblocks-react#useEventListener
*/
useEventListener((eventData) => {
const event = eventData.event as ReactionEvent;
setReactions((reactions) =>
Expand All @@ -89,6 +100,7 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
);
});

// Listen to keyboard events to change the cursor state
useEffect(() => {
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === "/") {
Expand Down Expand Up @@ -120,13 +132,17 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
};
}, [updateMyPresence]);

// Listen to mouse events to change the cursor state
const handlePointerMove = useCallback((event: React.PointerEvent) => {
event.preventDefault();

// if cursor is not in reaction selector mode, update the cursor position
if (cursor == null || cursorState.mode !== CursorMode.ReactionSelector) {
// get the cursor position in the canvas
const x = event.clientX - event.currentTarget.getBoundingClientRect().x;
const y = event.clientY - event.currentTarget.getBoundingClientRect().y;

// broadcast the cursor position to other users
updateMyPresence({
cursor: {
x,
Expand All @@ -136,6 +152,7 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
}
}, []);

// Hide the cursor when the mouse leaves the canvas
const handlePointerLeave = useCallback(() => {
setCursorState({
mode: CursorMode.Hidden,
Expand All @@ -146,6 +163,7 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
});
}, []);

// Show the cursor when the mouse enters the canvas
const handlePointerDown = useCallback(
(event: React.PointerEvent) => {
// get the cursor position in the canvas
Expand All @@ -158,26 +176,24 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
y,
},
});

// if cursor is in reaction mode, set isPressed to true
setCursorState((state: CursorState) =>
cursorState.mode === CursorMode.Reaction
? { ...state, isPressed: true }
: state
cursorState.mode === CursorMode.Reaction ? { ...state, isPressed: true } : state
);
},
[cursorState.mode, setCursorState]
);

// hide the cursor when the mouse is up
const handlePointerUp = useCallback(() => {
setCursorState((state: CursorState) =>
cursorState.mode === CursorMode.Reaction
? { ...state, isPressed: false }
: state
cursorState.mode === CursorMode.Reaction ? { ...state, isPressed: false } : state
);
}, [cursorState.mode, setCursorState]);

// trigger respective actions when the user clicks on the right menu
const handleContextMenuClick = useCallback((key: string) => {
console.log(key);

switch (key) {
case "Chat":
setCursorState({
Expand Down Expand Up @@ -207,8 +223,8 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
return (
<ContextMenu>
<ContextMenuTrigger
className='relative flex h-full w-full flex-1 items-center justify-center'
id='canvas'
className="relative flex h-full w-full flex-1 items-center justify-center"
id="canvas"
style={{
cursor: cursorState.mode === CursorMode.Chat ? "none" : "auto",
}}
Expand All @@ -219,6 +235,7 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
>
<canvas ref={canvasRef} />

{/* Render the reactions */}
{reactions.map((reaction) => (
<FlyingReaction
key={reaction.timestamp.toString()}
Expand All @@ -229,6 +246,7 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
/>
))}

{/* If cursor is in chat mode, show the chat cursor */}
{cursor && (
<CursorChat
cursor={cursor}
Expand All @@ -238,6 +256,7 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
/>
)}

{/* If cursor is in reaction selector mode, show the reaction selector */}
{cursorState.mode === CursorMode.ReactionSelector && (
<ReactionSelector
setReaction={(reaction) => {
Expand All @@ -246,19 +265,22 @@ const Live = ({ canvasRef, undo, redo }: Props) => {
/>
)}

{/* Show the live cursors of other users */}
<LiveCursors others={others} />

{/* Show the comments */}
<Comments />
</ContextMenuTrigger>

<ContextMenuContent className='right-menu-content'>
<ContextMenuContent className="right-menu-content">
{shortcuts.map((item) => (
<ContextMenuItem
key={item.key}
className='right-menu-item'
className="right-menu-item"
onClick={() => handleContextMenuClick(item.name)}
>
<p>{item.name}</p>
<p className='text-xs text-primary-grey-300'>{item.shortcut}</p>
<p className="text-xs text-primary-grey-300">{item.shortcut}</p>
</ContextMenuItem>
))}
</ContextMenuContent>
Expand Down
Loading

0 comments on commit 31d272c

Please sign in to comment.