diff --git a/code/client/src/ui/pages/document/Document.scss b/code/client/src/ui/pages/document/Document.scss index 516812a4..b708a158 100644 --- a/code/client/src/ui/pages/document/Document.scss +++ b/code/client/src/ui/pages/document/Document.scss @@ -1,2 +1,3 @@ .document { + position: relative; } diff --git a/code/client/src/ui/pages/document/Document.tsx b/code/client/src/ui/pages/document/Document.tsx index fa497194..2a45bc59 100644 --- a/code/client/src/ui/pages/document/Document.tsx +++ b/code/client/src/ui/pages/document/Document.tsx @@ -8,6 +8,7 @@ import useDocumentService from '@services/resource/useResourcesService'; import useConnectors from '@domain/editor/connectors/useConnectors'; import FloatingButtons from '@ui/pages/document/components/floating-buttons/FloatingButtons'; import { DocumentResource } from '@notespace/shared/src/workspace/types/resource'; +import Collaborators from '@ui/pages/document/components/collaborators/Collaborators'; import './Document.scss'; function Document() { @@ -43,15 +44,14 @@ function Document() { setLoaded(true); } fetchDocument(); - return () => { - socket.emit('leaveDocument'); - }; + return () => socket.emit('leaveDocument'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); if (!loaded) return null; return (
+
diff --git a/code/client/src/ui/pages/document/components/collaborators/Collaborators.scss b/code/client/src/ui/pages/document/components/collaborators/Collaborators.scss new file mode 100644 index 00000000..4e554b45 --- /dev/null +++ b/code/client/src/ui/pages/document/components/collaborators/Collaborators.scss @@ -0,0 +1,23 @@ +.collaborators { + position: absolute; + top: 0; + right: 0; + + > div { + width: 4vh; + height: 4vh; + border-radius: 50%; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + padding: 1vh; + margin: 1vh; + + display: flex; + justify-content: center; + align-items: center; + + a { + text-decoration: none !important; + color: white; + } + } +} diff --git a/code/client/src/ui/pages/document/components/collaborators/Collaborators.tsx b/code/client/src/ui/pages/document/components/collaborators/Collaborators.tsx new file mode 100644 index 00000000..a19f9249 --- /dev/null +++ b/code/client/src/ui/pages/document/components/collaborators/Collaborators.tsx @@ -0,0 +1,23 @@ +import useCollaborators from '@ui/pages/document/components/collaborators/useCollaborators'; +import { Link } from 'react-router-dom'; +import './Collaborators.scss'; + +function Collaborators() { + const collaborators = useCollaborators(); + return ( +
+ {collaborators.map(collaborator => { + const nameParts = collaborator.name.split(' '); + const initials = + nameParts.length > 1 ? `${nameParts[0][0]}${nameParts[nameParts.length - 1][0]}` : nameParts[0][0]; + return ( +
+ {initials} +
+ ); + })} +
+ ); +} + +export default Collaborators; diff --git a/code/client/src/ui/pages/document/components/collaborators/useCollaborators.ts b/code/client/src/ui/pages/document/components/collaborators/useCollaborators.ts new file mode 100644 index 00000000..75303838 --- /dev/null +++ b/code/client/src/ui/pages/document/components/collaborators/useCollaborators.ts @@ -0,0 +1,26 @@ +import { useState } from 'react'; +import { Collaborator } from '@notespace/shared/src/users/types'; +import useSocketListeners from '@services/communication/socket/useSocketListeners'; +import { useCommunication } from '@/contexts/communication/useCommunication'; + +function useCollaborators() { + const [collaborators, setCollaborators] = useState([]); + const { socket } = useCommunication(); + + const onCollaboratorsJoined = (users: Collaborator[]) => { + setCollaborators(prev => [...prev, ...users]); + }; + + const onCollaboratorLeft = (id: string) => { + setCollaborators(prev => prev.filter(u => u.id !== id)); + }; + + useSocketListeners(socket, { + joinedDocument: onCollaboratorsJoined, + leftDocument: onCollaboratorLeft, + }); + + return collaborators; +} + +export default useCollaborators; diff --git a/code/client/src/ui/pages/workspace/components/DocumentView.tsx b/code/client/src/ui/pages/workspace/components/DocumentView.tsx index aab14e5a..c22a9849 100644 --- a/code/client/src/ui/pages/workspace/components/DocumentView.tsx +++ b/code/client/src/ui/pages/workspace/components/DocumentView.tsx @@ -5,6 +5,7 @@ import { DocumentResource } from '@notespace/shared/src/workspace/types/resource import { Checkbox } from '@mui/material'; import { formatDate, formatTimePassed } from '@/utils/utils'; import { useEffect, useState } from 'react'; +import useWorkspace from '@/contexts/workspace/useWorkspace'; type DocumentViewProps = { document: DocumentResource; @@ -19,6 +20,7 @@ function DocumentView({ document, onSelect, onDelete, onRename, onDuplicate, sel const { wid } = useParams(); const { component, isEditing, setIsEditing } = useEditing(document.name || 'Untitled', onRename); const [isSelected, setSelected] = useState(selected); + const { isMember } = useWorkspace(); useEffect(() => { setSelected(selected); @@ -43,6 +45,7 @@ function DocumentView({ document, onSelect, onDelete, onRename, onDuplicate, sel onOpenInNewTab={() => window.open(`/workspaces/${wid}/${document.id}`, '_blank')} onDuplicate={onDuplicate} onDelete={onDelete} + enabled={isMember} > {isEditing ? DocumentComponent : {DocumentComponent}} diff --git a/code/client/src/ui/pages/workspaces/components/WorkspaceContextMenu.tsx b/code/client/src/ui/pages/workspaces/components/WorkspaceContextMenu.tsx index cc1fe218..53a44a24 100644 --- a/code/client/src/ui/pages/workspaces/components/WorkspaceContextMenu.tsx +++ b/code/client/src/ui/pages/workspaces/components/WorkspaceContextMenu.tsx @@ -35,9 +35,8 @@ function WorkspaceContextMenu({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentUser]); - if (!isMember) return null; return ( - +