From 76eabac15969387665da2f4d6adeae63df14c65e Mon Sep 17 00:00:00 2001 From: Maciej Wasilewski Date: Thu, 14 Mar 2024 10:57:39 +0100 Subject: [PATCH] [#55805] Show people editing the document --- src/MystEditor.js | 11 ++++++++--- src/components/Avatars.js | 30 ++++++++++++++++++++++++++++++ src/components/CodeMirror.js | 6 +++--- src/hooks/useCollaboration.js | 11 +++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 src/components/Avatars.js diff --git a/src/MystEditor.js b/src/MystEditor.js index 18986e5..0a74fa2 100644 --- a/src/MystEditor.js +++ b/src/MystEditor.js @@ -1,5 +1,5 @@ import { render } from 'preact'; -import { useState, useEffect } from 'preact/hooks'; +import { useState, useEffect, useReducer } from 'preact/hooks'; import { html } from 'htm/preact'; import { StyleSheetManager, styled } from 'styled-components'; @@ -11,6 +11,7 @@ import Preview from './components/Preview'; import Diff from './components/Diff'; import { resetCache } from './hooks/markdownReplacer'; import { useText } from './hooks/useText'; +import Avatars from './components/Avatars'; if (!window.myst_editor?.isFresh) { resetCache(); @@ -121,12 +122,15 @@ const MystEditor = ({ collaboration = {}, spellcheckOpts = { dict: "en_US", dictionaryPath: "/dictionaries" }, customRoles = [], - transforms = [] + transforms = [], + // this will create a bogus random avatar when no specific getAvatar function is provided + getAvatar = (login) => `https://secure.gravatar.com/avatar/${login}?s=30&d=identicon`, }) => { const [mode, setMode] = useState(initialMode); const [fullscreen, setFullscreen] = useState(false); const text = useText(initialText, transforms, customRoles); const [alert, setAlert] = useState(null); + const [users, setUsers] = useReducer((_, currentUsers) => currentUsers.map(u => ({...u, avatarUrl: getAvatar(u.login)})), []); const alertFor = (alertText, secs) => { setAlert(alertText); @@ -162,6 +166,7 @@ const MystEditor = ({ ${alert && html`<${Alert}> ${alert} `} <${TopbarRight}> + <${Avatars} users=${users}/> <${TopbarButton} type="button" onClick=${(event) => printCallback(event)}>Export as PDF <${TemplateManager} text=${text} templatelist=${templatelist} /> <${Separator} /> @@ -169,7 +174,7 @@ const MystEditor = ({ <${MystWrapper} fullscreen=${fullscreen}> - <${CodeMirror} mode=${mode} text=${text} name=${name} id=${id} collaboration=${collaboration} spellcheckOpts=${spellcheckOpts} highlights=${transforms}/> + <${CodeMirror} setUsers=${setUsers} mode=${mode} text=${text} name=${name} id=${id} collaboration=${collaboration} spellcheckOpts=${spellcheckOpts} highlights=${transforms}/> <${Preview} $mode=${mode} dangerouslySetInnerHTML=${{ __html: text.renderAndSanitize() }}/> ${mode === 'Diff' ? html`<${Diff} oldText=${initialText} text=${text}/>` : "" } diff --git a/src/components/Avatars.js b/src/components/Avatars.js new file mode 100644 index 0000000..df4ec29 --- /dev/null +++ b/src/components/Avatars.js @@ -0,0 +1,30 @@ +import { html, h } from 'htm/preact'; +import { styled } from 'styled-components'; + +const AvatarsWrapper = styled.div` + width: 200px; + + .avatar { + border-radius: 50%; + margin-top: 5px; + float: right; + border: 3px solid; + margin: 5px 0px 5px 5px; + }` + +const Avatar = ({ login, color, avatarUrl }) => html` + ` + +const Avatars = ({ users }) => html` + <${AvatarsWrapper}> + ${users.map(user => + html`<${Avatar} ...${user}/>` + )} + `; + + +export default Avatars \ No newline at end of file diff --git a/src/components/CodeMirror.js b/src/components/CodeMirror.js index c0b8762..321cecf 100644 --- a/src/components/CodeMirror.js +++ b/src/components/CodeMirror.js @@ -118,7 +118,7 @@ const setEditorText = (editor, text) => { }); } -const CodeMirror = ({ text, id, name, className, mode, collaboration, spellcheckOpts, highlights }) => { +const CodeMirror = ({ text, id, name, className, mode, collaboration, spellcheckOpts, highlights, setUsers }) => { const editorRef = useRef(null); const [initialized, setInitialized] = useState(false); @@ -172,8 +172,8 @@ const CodeMirror = ({ text, id, name, className, mode, collaboration, spellcheck } text.onSync(currentText => setEditorText(editorRef.current, currentText)) - - ycomments?.updateMainCodeMirror(); + ycomments?.updateMainCodeMirror(); + provider.watchCollabolators(setUsers) }, [ready, initialized]); return html` diff --git a/src/hooks/useCollaboration.js b/src/hooks/useCollaboration.js index bf47a81..bf363cc 100644 --- a/src/hooks/useCollaboration.js +++ b/src/hooks/useCollaboration.js @@ -3,6 +3,17 @@ import * as awarenessProtocol from "y-protocols/awareness.js"; import { WebsocketProvider } from 'y-websocket'; import { useMemo, useState } from "preact/hooks"; +WebsocketProvider.prototype.watchCollabolators = function (hook) { + this.awareness.on('change', ({ added, removed }) => { + if (added || removed) { + let collabolators = Array.from(this.awareness.states) + .map(([key, { user }]) => ({ login: user.name, color: user.color })) + .reduce((curr, data) => { curr[data.login] = data; return curr }, {}); + hook(Object.values(collabolators)); + } + }); +} + export default function useCollaboration(settings) { if (!settings.enabled) { return {};