From 93391376137c59b5b44022098f0760708cee1174 Mon Sep 17 00:00:00 2001 From: Avea-marina Date: Thu, 18 Jun 2026 21:38:03 +0300 Subject: [PATCH 1/4] fix(pages.note): add current users at note's header --- .../modules.editor/src/hooks/useYjsStore.ts | 33 +++++++++++++++++-- packages/modules.editor/src/types/index.ts | 5 +++ packages/modules.editor/src/ui/Editor.tsx | 30 ++++++++++++----- .../src/ui/components/TiptapEditor.tsx | 14 ++++++-- packages/pages.notes/package.json | 1 + packages/pages.notes/src/types.ts | 4 +++ packages/pages.notes/src/ui/Header.tsx | 25 +++++++++++++- packages/pages.notes/src/ui/Note.tsx | 8 +++-- packages/pages.notes/src/utils.ts | 5 +++ pnpm-lock.yaml | 3 ++ 10 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 packages/pages.notes/src/types.ts create mode 100644 packages/pages.notes/src/utils.ts diff --git a/packages/modules.editor/src/hooks/useYjsStore.ts b/packages/modules.editor/src/hooks/useYjsStore.ts index dad6f7f7c..0af20e756 100644 --- a/packages/modules.editor/src/hooks/useYjsStore.ts +++ b/packages/modules.editor/src/hooks/useYjsStore.ts @@ -13,6 +13,7 @@ import { type onAuthenticationFailedParameters, } from '@hocuspocus/provider'; import { generateUserColor } from '../utils/userColor'; +import { TUser } from '../types'; type UseYjsStoreArgs = { hostUrl: string; @@ -30,6 +31,7 @@ export type UseCollaborativeTiptapReturn = { isReadOnly: boolean; storageToken: string; storageItem: StorageItemT; + users: TUser[]; }; export function useYjsStore({ @@ -60,6 +62,8 @@ export function useYjsStore({ return { provider, ydoc }; }); + const [users, setUsers] = useState([]); + /* ========================================================== * 2. Readonly state * ========================================================== */ @@ -70,9 +74,10 @@ export function useYjsStore({ * ========================================================== */ const { data: currentUser } = useCurrentUser(); const userData = useMemo(() => { + const id = currentUser?.id; const name = currentUser?.display_name || currentUser?.username || 'Участник'; const idForColor = currentUser?.id?.toString() ?? 'anonymous'; - return { name, color: generateUserColor(idForColor) }; + return { id, name, color: generateUserColor(idForColor) }; }, [currentUser?.id, currentUser?.display_name, currentUser?.username]); /* ========================================================== @@ -131,6 +136,29 @@ export function useYjsStore({ }; }, [provider, userData]); + useEffect(() => { + const handleGetUserIds = () => { + if (!provider.awareness) return; + + const awarenessUsers: TUser[] = [...provider.awareness.getStates()] + .map((arr) => ({ id: arr[1].user?.id, userName: arr[1].user.name })) + .filter((user) => user.id); + if (JSON.stringify(awarenessUsers) !== JSON.stringify(users)) { + setUsers(awarenessUsers); + } + }; + + if (provider.awareness) { + provider.awareness.on('change', handleGetUserIds); + } + + return () => { + if (provider.awareness) { + provider.awareness.off('change', handleGetUserIds); + } + }; + }, [provider.awareness, users]); + /* ========================================================== * 6. Editor — extensions в deps: при загрузке currentUser * userData обновляется, пересоздаём редактор с правильным именем/цветом для курсора. @@ -180,7 +208,8 @@ export function useYjsStore({ isReadOnly, storageToken, storageItem, + users, }), - [editor, undo, redo, canUndo, canRedo, isReadOnly, storageToken, storageItem], + [editor, undo, redo, canUndo, canRedo, isReadOnly, storageToken, storageItem, users], ); } diff --git a/packages/modules.editor/src/types/index.ts b/packages/modules.editor/src/types/index.ts index 4f34b5326..ae8fc8a9b 100644 --- a/packages/modules.editor/src/types/index.ts +++ b/packages/modules.editor/src/types/index.ts @@ -1,3 +1,8 @@ export type { TextFormatTypeT } from './bubbleMenu'; export type { BlockTypeT } from './blockMenu'; export type { ActiveBlockT } from './activeBlock'; + +export type TUser = { + id: number; + userName: string; +}; diff --git a/packages/modules.editor/src/ui/Editor.tsx b/packages/modules.editor/src/ui/Editor.tsx index 0d4ff248f..886343e9c 100644 --- a/packages/modules.editor/src/ui/Editor.tsx +++ b/packages/modules.editor/src/ui/Editor.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { YjsProvider } from '../providers/YjsProvider'; import { TiptapEditor } from './components/TiptapEditor'; import { useParams } from '@tanstack/react-router'; @@ -10,8 +9,23 @@ import { } from 'common.services'; import { StorageItemT } from 'common.types'; import { LoadingScreen } from 'common.ui'; +import { TUser } from '../types'; -const EditorWithoutData = () => { +type TEditorWithoutData = { + setUsers: (users: TUser[]) => void; +}; + +type TEditorWithData = { + storageItem: StorageItemT; + setUsers: (users: TUser[]) => void; +}; + +type TEditor = { + storageItem?: StorageItemT; + setUsers: (users: TUser[]) => void; +}; + +const EditorWithoutData = ({ setUsers }: TEditorWithoutData) => { const { classroomId, noteId, materialId } = useParams({ strict: false }); const { data: user } = useCurrentUser(); @@ -56,25 +70,25 @@ const EditorWithoutData = () => {
- +
); }; -const EditorWithData = ({ storageItem }: { storageItem: StorageItemT }) => { +const EditorWithData = ({ storageItem, setUsers }: TEditorWithData) => { return ( - + ); }; -export const Editor = ({ storageItem }: { storageItem?: StorageItemT }) => { +export const Editor = ({ storageItem, setUsers }: TEditor) => { if (storageItem) { - return ; + return ; } - return ; + return ; }; diff --git a/packages/modules.editor/src/ui/components/TiptapEditor.tsx b/packages/modules.editor/src/ui/components/TiptapEditor.tsx index 4622a8f64..4cd501852 100644 --- a/packages/modules.editor/src/ui/components/TiptapEditor.tsx +++ b/packages/modules.editor/src/ui/components/TiptapEditor.tsx @@ -1,11 +1,21 @@ +import { useEffect } from 'react'; import { EditorContent } from '@tiptap/react'; import { EditorToolkit } from './EditorToolkit'; import { useYjsContext } from '../../hooks/useYjsContext'; +import { TUser } from '../../types'; import '../editor.css'; -export const TiptapEditor = () => { - const { editor, isReadOnly } = useYjsContext(); +type TTiptapEditor = { + setUsers: (users: TUser[]) => void; +}; + +export const TiptapEditor = ({ setUsers }: TTiptapEditor) => { + const { editor, isReadOnly, users } = useYjsContext(); + + useEffect(() => { + setUsers(users); + }, [setUsers, users]); if (!editor) { return ( diff --git a/packages/pages.notes/package.json b/packages/pages.notes/package.json index bf1592a3c..47699744e 100644 --- a/packages/pages.notes/package.json +++ b/packages/pages.notes/package.json @@ -17,6 +17,7 @@ "@xipkg/icons": "3.0.20", "@xipkg/form": "4.2.2", "@xipkg/input": "2.2.14", + "@xipkg/avatar": "3.3.0", "common.services": "workspace:*", "common.ui": "workspace:*", "modules.editor": "workspace:*" diff --git a/packages/pages.notes/src/types.ts b/packages/pages.notes/src/types.ts new file mode 100644 index 000000000..6ca7f8e8f --- /dev/null +++ b/packages/pages.notes/src/types.ts @@ -0,0 +1,4 @@ +export type TUser = { + id: number; + userName: string; +}; diff --git a/packages/pages.notes/src/ui/Header.tsx b/packages/pages.notes/src/ui/Header.tsx index 25054bbeb..6a1fcd67b 100644 --- a/packages/pages.notes/src/ui/Header.tsx +++ b/packages/pages.notes/src/ui/Header.tsx @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import { Avatar, AvatarGroup, AvatarGroupCount, AvatarImage } from '@xipkg/avatar'; import { useParams, useRouter } from '@tanstack/react-router'; import { useCurrentUser, @@ -10,8 +11,16 @@ import { Skeleton } from 'common.ui'; import { EditableTitle } from './EditableTitle'; import { Button } from '@xipkg/button'; import { ArrowLeft } from '@xipkg/icons'; +import { getAvatarUrlByUserId } from '../utils'; +import { TUser } from '../types'; -export const Header = () => { +type THeaderProps = { + users: TUser[]; +}; + +const MAX_VISIBLE_AVATARS = 3; + +export const Header = ({ users }: THeaderProps) => { const { classroomId, noteId, materialId } = useParams({ strict: false }); const router = useRouter(); @@ -54,6 +63,9 @@ export const Header = () => { } }; + const overflowCount = Math.max(0, users.length - MAX_VISIBLE_AVATARS); + const visibleUsers = users.slice(0, MAX_VISIBLE_AVATARS); + return (
@@ -73,6 +85,17 @@ export const Header = () => { )}
+ + {visibleUsers.map((user) => { + const avatarUrl = getAvatarUrlByUserId(user.id); + return ( + + {avatarUrl && } + + ); + })} + {overflowCount > 0 && +{overflowCount}} +
diff --git a/packages/pages.notes/src/ui/Note.tsx b/packages/pages.notes/src/ui/Note.tsx index 36127b2ab..cdb8dac65 100644 --- a/packages/pages.notes/src/ui/Note.tsx +++ b/packages/pages.notes/src/ui/Note.tsx @@ -1,11 +1,15 @@ +import { useState } from 'react'; import { Editor } from 'modules.editor'; import { Header } from './Header'; +import { TUser } from '../types'; export const Note = () => { + const [users, setUsers] = useState([]); + return (
-
- +
+
); }; diff --git a/packages/pages.notes/src/utils.ts b/packages/pages.notes/src/utils.ts new file mode 100644 index 000000000..6bdab2f56 --- /dev/null +++ b/packages/pages.notes/src/utils.ts @@ -0,0 +1,5 @@ +const AVATAR_API_BASE = 'https://api.sovlium.ru/files/users'; + +export const getAvatarUrlByUserId = (id: number): string | undefined => { + return `${AVATAR_API_BASE}/${id}/avatar.webp`; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40c34eb2a..ac81ebbe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4872,6 +4872,9 @@ importers: '@tanstack/react-router': specifier: ^1.166.6 version: 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@xipkg/avatar': + specifier: 3.3.0 + version: 3.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@xipkg/button': specifier: 4.1.0 version: 4.1.0(@types/react@19.2.17)(react@19.2.7) From 07154e6de9f531ef10e0397fde1b0bcac61e736e Mon Sep 17 00:00:00 2001 From: Avea-marina Date: Thu, 18 Jun 2026 23:23:42 +0300 Subject: [PATCH 2/4] fix(pages.notes): fix backward compatibility with classrooms --- packages/modules.editor/src/ui/Editor.tsx | 10 +++------- .../modules.editor/src/ui/components/TiptapEditor.tsx | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/modules.editor/src/ui/Editor.tsx b/packages/modules.editor/src/ui/Editor.tsx index 886343e9c..d135c3103 100644 --- a/packages/modules.editor/src/ui/Editor.tsx +++ b/packages/modules.editor/src/ui/Editor.tsx @@ -11,21 +11,17 @@ import { StorageItemT } from 'common.types'; import { LoadingScreen } from 'common.ui'; import { TUser } from '../types'; -type TEditorWithoutData = { - setUsers: (users: TUser[]) => void; -}; - type TEditorWithData = { storageItem: StorageItemT; - setUsers: (users: TUser[]) => void; + setUsers?: (users: TUser[]) => void; }; type TEditor = { storageItem?: StorageItemT; - setUsers: (users: TUser[]) => void; + setUsers?: (users: TUser[]) => void; }; -const EditorWithoutData = ({ setUsers }: TEditorWithoutData) => { +const EditorWithoutData = ({ setUsers }: TEditor) => { const { classroomId, noteId, materialId } = useParams({ strict: false }); const { data: user } = useCurrentUser(); diff --git a/packages/modules.editor/src/ui/components/TiptapEditor.tsx b/packages/modules.editor/src/ui/components/TiptapEditor.tsx index 4cd501852..aadf31d7d 100644 --- a/packages/modules.editor/src/ui/components/TiptapEditor.tsx +++ b/packages/modules.editor/src/ui/components/TiptapEditor.tsx @@ -7,14 +7,14 @@ import { TUser } from '../../types'; import '../editor.css'; type TTiptapEditor = { - setUsers: (users: TUser[]) => void; + setUsers?: (users: TUser[]) => void; }; export const TiptapEditor = ({ setUsers }: TTiptapEditor) => { const { editor, isReadOnly, users } = useYjsContext(); useEffect(() => { - setUsers(users); + setUsers?.(users); }, [setUsers, users]); if (!editor) { From e37a6c72474d2102b46d2f1e60b888aff4aa6b26 Mon Sep 17 00:00:00 2001 From: Avea-marina Date: Mon, 22 Jun 2026 20:46:40 +0300 Subject: [PATCH 3/4] fix(pages.notes): review fixes --- .../src/hooks/useCollaborators.ts | 10 ++++ .../modules.editor/src/hooks/useYjsStore.ts | 38 ++++++------- packages/modules.editor/src/index.ts | 1 + .../src/store/collaboratorsStore.ts | 21 +++++++ packages/modules.editor/src/types/index.ts | 2 +- packages/modules.editor/src/ui/Editor.tsx | 17 +++--- .../src/ui/components/TiptapEditor.tsx | 14 +---- packages/pages.notes/package.json | 3 +- packages/pages.notes/src/types.ts | 4 +- .../src/ui/CollaboratorAvatars.tsx | 57 +++++++++++++++++++ packages/pages.notes/src/ui/Header.tsx | 37 +++++------- packages/pages.notes/src/ui/Note.tsx | 8 +-- pnpm-lock.yaml | 3 + 13 files changed, 141 insertions(+), 74 deletions(-) create mode 100644 packages/modules.editor/src/hooks/useCollaborators.ts create mode 100644 packages/modules.editor/src/store/collaboratorsStore.ts create mode 100644 packages/pages.notes/src/ui/CollaboratorAvatars.tsx diff --git a/packages/modules.editor/src/hooks/useCollaborators.ts b/packages/modules.editor/src/hooks/useCollaborators.ts new file mode 100644 index 000000000..d9ec81bab --- /dev/null +++ b/packages/modules.editor/src/hooks/useCollaborators.ts @@ -0,0 +1,10 @@ +import { useCollaboratorsStore } from '../store/collaboratorsStore'; + +export const useCollaborators = () => { + const collaborators = useCollaboratorsStore((s) => s.collaborators); + const setCollaborators = useCollaboratorsStore((s) => s.setCollaborators); + const setCollaboratorsIfChanged = useCollaboratorsStore((s) => s.setCollaborators); + const reset = useCollaboratorsStore((s) => s.reset); + + return { collaborators, setCollaborators, setCollaboratorsIfChanged, reset }; +}; diff --git a/packages/modules.editor/src/hooks/useYjsStore.ts b/packages/modules.editor/src/hooks/useYjsStore.ts index 0af20e756..ab96e5d89 100644 --- a/packages/modules.editor/src/hooks/useYjsStore.ts +++ b/packages/modules.editor/src/hooks/useYjsStore.ts @@ -13,7 +13,8 @@ import { type onAuthenticationFailedParameters, } from '@hocuspocus/provider'; import { generateUserColor } from '../utils/userColor'; -import { TUser } from '../types'; +import { useCollaborators } from './useCollaborators'; +import { TCollaborator } from '../types'; type UseYjsStoreArgs = { hostUrl: string; @@ -31,7 +32,6 @@ export type UseCollaborativeTiptapReturn = { isReadOnly: boolean; storageToken: string; storageItem: StorageItemT; - users: TUser[]; }; export function useYjsStore({ @@ -62,7 +62,8 @@ export function useYjsStore({ return { provider, ydoc }; }); - const [users, setUsers] = useState([]); + const { awareness } = provider; + const { setCollaboratorsIfChanged, reset } = useCollaborators(); /* ========================================================== * 2. Readonly state @@ -101,8 +102,8 @@ export function useYjsStore({ }, 0); // Awareness - if (provider.awareness) { - provider.awareness.setLocalStateField('user', userData); + if (awareness) { + awareness.setLocalStateField('user', userData); } // Auth events @@ -134,30 +135,26 @@ export function useYjsStore({ // ignore } }; - }, [provider, userData]); + }, [provider, awareness, userData]); useEffect(() => { - const handleGetUserIds = () => { - if (!provider.awareness) return; + if (!awareness) return; - const awarenessUsers: TUser[] = [...provider.awareness.getStates()] + const handleSyncUsersFromAwareness = () => { + const awarenessUsers: TCollaborator[] = [...awareness.getStates()] .map((arr) => ({ id: arr[1].user?.id, userName: arr[1].user.name })) .filter((user) => user.id); - if (JSON.stringify(awarenessUsers) !== JSON.stringify(users)) { - setUsers(awarenessUsers); - } + setCollaboratorsIfChanged(awarenessUsers); }; - if (provider.awareness) { - provider.awareness.on('change', handleGetUserIds); - } + awareness.on('update', handleSyncUsersFromAwareness); + handleSyncUsersFromAwareness(); return () => { - if (provider.awareness) { - provider.awareness.off('change', handleGetUserIds); - } + awareness.off('update', handleSyncUsersFromAwareness); + reset(); }; - }, [provider.awareness, users]); + }, [awareness, setCollaboratorsIfChanged, reset]); /* ========================================================== * 6. Editor — extensions в deps: при загрузке currentUser @@ -208,8 +205,7 @@ export function useYjsStore({ isReadOnly, storageToken, storageItem, - users, }), - [editor, undo, redo, canUndo, canRedo, isReadOnly, storageToken, storageItem, users], + [editor, undo, redo, canUndo, canRedo, isReadOnly, storageToken, storageItem], ); } diff --git a/packages/modules.editor/src/index.ts b/packages/modules.editor/src/index.ts index 2671c25ea..a5e4adb9e 100644 --- a/packages/modules.editor/src/index.ts +++ b/packages/modules.editor/src/index.ts @@ -1,6 +1,7 @@ export { Editor } from './ui/Editor'; export { useInterfaceStore } from './store/interfaceStore'; export { useYjsContext } from './hooks/useYjsContext'; +export { useCollaborators } from './hooks/useCollaborators'; export { isUrl, isImageUrl } from './utils/isUrl'; export { normalizeTokens } from './utils/normalizeTokens'; diff --git a/packages/modules.editor/src/store/collaboratorsStore.ts b/packages/modules.editor/src/store/collaboratorsStore.ts new file mode 100644 index 000000000..65c99b3d6 --- /dev/null +++ b/packages/modules.editor/src/store/collaboratorsStore.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand'; +import type { TCollaborator } from '../types'; + +type TCollaboratorsStore = { + collaborators: TCollaborator[]; + setCollaborators: (collaborators: TCollaborator[]) => void; + setCollaboratorsIfChanged: (collaborators: TCollaborator[]) => void; + reset: () => void; +}; + +export const useCollaboratorsStore = create()((set, get) => ({ + collaborators: [], + setCollaborators: (collaborators) => set({ collaborators }), + setCollaboratorsIfChanged: (collaborators) => { + const s = get(); + if (JSON.stringify(s.collaborators) !== JSON.stringify(collaborators)) { + set({ collaborators }); + } + }, + reset: () => set({ collaborators: [] }), +})); diff --git a/packages/modules.editor/src/types/index.ts b/packages/modules.editor/src/types/index.ts index ae8fc8a9b..50604ec75 100644 --- a/packages/modules.editor/src/types/index.ts +++ b/packages/modules.editor/src/types/index.ts @@ -2,7 +2,7 @@ export type { TextFormatTypeT } from './bubbleMenu'; export type { BlockTypeT } from './blockMenu'; export type { ActiveBlockT } from './activeBlock'; -export type TUser = { +export type TCollaborator = { id: number; userName: string; }; diff --git a/packages/modules.editor/src/ui/Editor.tsx b/packages/modules.editor/src/ui/Editor.tsx index d135c3103..f13c1931f 100644 --- a/packages/modules.editor/src/ui/Editor.tsx +++ b/packages/modules.editor/src/ui/Editor.tsx @@ -9,19 +9,16 @@ import { } from 'common.services'; import { StorageItemT } from 'common.types'; import { LoadingScreen } from 'common.ui'; -import { TUser } from '../types'; type TEditorWithData = { storageItem: StorageItemT; - setUsers?: (users: TUser[]) => void; }; type TEditor = { storageItem?: StorageItemT; - setUsers?: (users: TUser[]) => void; }; -const EditorWithoutData = ({ setUsers }: TEditor) => { +const EditorWithoutData = () => { const { classroomId, noteId, materialId } = useParams({ strict: false }); const { data: user } = useCurrentUser(); @@ -66,25 +63,25 @@ const EditorWithoutData = ({ setUsers }: TEditor) => {
- +
); }; -const EditorWithData = ({ storageItem, setUsers }: TEditorWithData) => { +const EditorWithData = ({ storageItem }: TEditorWithData) => { return ( - + ); }; -export const Editor = ({ storageItem, setUsers }: TEditor) => { +export const Editor = ({ storageItem }: TEditor) => { if (storageItem) { - return ; + return ; } - return ; + return ; }; diff --git a/packages/modules.editor/src/ui/components/TiptapEditor.tsx b/packages/modules.editor/src/ui/components/TiptapEditor.tsx index aadf31d7d..4622a8f64 100644 --- a/packages/modules.editor/src/ui/components/TiptapEditor.tsx +++ b/packages/modules.editor/src/ui/components/TiptapEditor.tsx @@ -1,21 +1,11 @@ -import { useEffect } from 'react'; import { EditorContent } from '@tiptap/react'; import { EditorToolkit } from './EditorToolkit'; import { useYjsContext } from '../../hooks/useYjsContext'; -import { TUser } from '../../types'; import '../editor.css'; -type TTiptapEditor = { - setUsers?: (users: TUser[]) => void; -}; - -export const TiptapEditor = ({ setUsers }: TTiptapEditor) => { - const { editor, isReadOnly, users } = useYjsContext(); - - useEffect(() => { - setUsers?.(users); - }, [setUsers, users]); +export const TiptapEditor = () => { + const { editor, isReadOnly } = useYjsContext(); if (!editor) { return ( diff --git a/packages/pages.notes/package.json b/packages/pages.notes/package.json index 47699744e..aae07ea5d 100644 --- a/packages/pages.notes/package.json +++ b/packages/pages.notes/package.json @@ -12,12 +12,13 @@ }, "dependencies": { "@tanstack/react-router": "^1.166.6", + "@xipkg/avatar": "3.3.0", "@xipkg/utils": "^1.8.0", "@xipkg/button": "4.1.0", "@xipkg/icons": "3.0.20", "@xipkg/form": "4.2.2", "@xipkg/input": "2.2.14", - "@xipkg/avatar": "3.3.0", + "@xipkg/popover": "^2.1.0", "common.services": "workspace:*", "common.ui": "workspace:*", "modules.editor": "workspace:*" diff --git a/packages/pages.notes/src/types.ts b/packages/pages.notes/src/types.ts index 6ca7f8e8f..fedeedcbc 100644 --- a/packages/pages.notes/src/types.ts +++ b/packages/pages.notes/src/types.ts @@ -1,4 +1,6 @@ -export type TUser = { +export type TCollaborator = { id: number; userName: string; + avatarUrl?: string; + initial?: string; }; diff --git a/packages/pages.notes/src/ui/CollaboratorAvatars.tsx b/packages/pages.notes/src/ui/CollaboratorAvatars.tsx new file mode 100644 index 000000000..726891926 --- /dev/null +++ b/packages/pages.notes/src/ui/CollaboratorAvatars.tsx @@ -0,0 +1,57 @@ +import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from '@xipkg/avatar'; +import { Popover, PopoverContent, PopoverTrigger } from '@xipkg/popover'; +import { TCollaborator } from '../types'; + +type TCollaboratorAvatars = { + collaborators: TCollaborator[]; + currentUserId: number; +}; + +const MAX_VISIBLE_AVATARS = 3; + +export const CollaboratorAvatars = ({ collaborators, currentUserId }: TCollaboratorAvatars) => { + const overflowCount = Math.max(0, collaborators.length - MAX_VISIBLE_AVATARS); + const visibleCollaborators = collaborators.slice(0, MAX_VISIBLE_AVATARS); + + return ( + + + + + +
+

Участники в заметке

+ {collaborators.map((collaborator) => ( +
+ + {collaborator.avatarUrl && } + {collaborator.initial} + + + {collaborator.id === currentUserId ? 'Вы' : collaborator.userName} + +
+ ))} +
+
+
+ ); +}; diff --git a/packages/pages.notes/src/ui/Header.tsx b/packages/pages.notes/src/ui/Header.tsx index 6a1fcd67b..46a24b34a 100644 --- a/packages/pages.notes/src/ui/Header.tsx +++ b/packages/pages.notes/src/ui/Header.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { Avatar, AvatarGroup, AvatarGroupCount, AvatarImage } from '@xipkg/avatar'; +import { useMemo } from 'react'; import { useParams, useRouter } from '@tanstack/react-router'; import { useCurrentUser, @@ -11,17 +11,13 @@ import { Skeleton } from 'common.ui'; import { EditableTitle } from './EditableTitle'; import { Button } from '@xipkg/button'; import { ArrowLeft } from '@xipkg/icons'; +import { useCollaborators } from 'modules.editor'; +import { CollaboratorAvatars } from './CollaboratorAvatars'; import { getAvatarUrlByUserId } from '../utils'; -import { TUser } from '../types'; -type THeaderProps = { - users: TUser[]; -}; - -const MAX_VISIBLE_AVATARS = 3; - -export const Header = ({ users }: THeaderProps) => { +export const Header = () => { const { classroomId, noteId, materialId } = useParams({ strict: false }); + const { collaborators } = useCollaborators(); const router = useRouter(); const { data: user } = useCurrentUser(); @@ -63,8 +59,15 @@ export const Header = ({ users }: THeaderProps) => { } }; - const overflowCount = Math.max(0, users.length - MAX_VISIBLE_AVATARS); - const visibleUsers = users.slice(0, MAX_VISIBLE_AVATARS); + const collaboratorsWithAvatars = useMemo( + () => + collaborators.map((collaborator) => ({ + ...collaborator, + avatarUrl: getAvatarUrlByUserId(collaborator.id), + initial: collaborator.userName.charAt(0).toUpperCase(), + })), + [collaborators], + ); return (
@@ -85,17 +88,7 @@ export const Header = ({ users }: THeaderProps) => { )}
- - {visibleUsers.map((user) => { - const avatarUrl = getAvatarUrlByUserId(user.id); - return ( - - {avatarUrl && } - - ); - })} - {overflowCount > 0 && +{overflowCount}} - + diff --git a/packages/pages.notes/src/ui/Note.tsx b/packages/pages.notes/src/ui/Note.tsx index cdb8dac65..36127b2ab 100644 --- a/packages/pages.notes/src/ui/Note.tsx +++ b/packages/pages.notes/src/ui/Note.tsx @@ -1,15 +1,11 @@ -import { useState } from 'react'; import { Editor } from 'modules.editor'; import { Header } from './Header'; -import { TUser } from '../types'; export const Note = () => { - const [users, setUsers] = useState([]); - return (
-
- +
+
); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac81ebbe0..deaf51b98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4887,6 +4887,9 @@ importers: '@xipkg/input': specifier: 2.2.14 version: 2.2.14(react@19.2.7) + '@xipkg/popover': + specifier: ^2.1.0 + version: 2.1.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@xipkg/utils': specifier: ^1.8.0 version: 1.8.0(react@19.2.7) From a18ade52c6be744e1dbbb9f17b74bc89b6fa9218 Mon Sep 17 00:00:00 2001 From: Avea-marina Date: Mon, 22 Jun 2026 21:32:02 +0300 Subject: [PATCH 4/4] fix(pages.notes): fix env for base backend url --- packages/pages.notes/package.json | 1 + packages/pages.notes/src/utils.ts | 4 ++-- pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/pages.notes/package.json b/packages/pages.notes/package.json index aae07ea5d..d079c9b3b 100644 --- a/packages/pages.notes/package.json +++ b/packages/pages.notes/package.json @@ -19,6 +19,7 @@ "@xipkg/form": "4.2.2", "@xipkg/input": "2.2.14", "@xipkg/popover": "^2.1.0", + "common.env": "workspace:*", "common.services": "workspace:*", "common.ui": "workspace:*", "modules.editor": "workspace:*" diff --git a/packages/pages.notes/src/utils.ts b/packages/pages.notes/src/utils.ts index 6bdab2f56..937a53c52 100644 --- a/packages/pages.notes/src/utils.ts +++ b/packages/pages.notes/src/utils.ts @@ -1,5 +1,5 @@ -const AVATAR_API_BASE = 'https://api.sovlium.ru/files/users'; +import { env } from 'common.env'; export const getAvatarUrlByUserId = (id: number): string | undefined => { - return `${AVATAR_API_BASE}/${id}/avatar.webp`; + return `${env.VITE_SERVER_URL_BACKEND}/files/users/${id}/avatar.webp`; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef66e0139..f84ad74a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4893,6 +4893,9 @@ importers: '@xipkg/utils': specifier: ^1.8.0 version: 1.8.0(react@19.2.7) + common.env: + specifier: workspace:* + version: link:../common.env common.services: specifier: workspace:* version: link:../common.services