From b2b299ea6742b8d62b38e7d060fb8300231381fb Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 21 Jun 2023 15:49:53 +0200 Subject: [PATCH] Fade out user cursor labels after inactivity Implementation details: * Add a custom CollaborationCursor Tiptap extension * Add CSS to fade out the cursor label after some time. * Listen for Yjs updates. - If it's a doc change by ourself, update the timestamp of own user in awareness state. - If it's a remote awareness update, add back the CSS class to the corresponding cursor. - Wait 50ms before showing the cursor in the DOM to account for cases where the cursor gets re-rendered by y-prosemirror. Fixes: #4126 Signed-off-by: Jonas --- src/components/Editor.vue | 24 +++++++-- src/extensions/CollaborationCursor.js | 75 +++++++++++++++++++++++++++ src/extensions/index.js | 2 + 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/extensions/CollaborationCursor.js diff --git a/src/components/Editor.vue b/src/components/Editor.vue index ae3a0aa6047..5e37edb148e 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -82,7 +82,6 @@ import { getCurrentUser } from '@nextcloud/auth' import { loadState } from '@nextcloud/initial-state' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' import { Collaboration } from '@tiptap/extension-collaboration' -import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' import Autofocus from '../extensions/Autofocus.js' import { Doc } from 'yjs' @@ -108,7 +107,7 @@ import { createEditor, serializePlainText, loadSyntaxHighlight } from './../Edit import { createMarkdownSerializer } from './../extensions/Markdown.js' import markdownit from './../markdownit/index.js' -import { Keymap } from './../extensions/index.js' +import { CollaborationCursor, Keymap } from '../extensions/index.js' import DocumentStatus from './Editor/DocumentStatus.vue' import isMobile from './../mixins/isMobile.js' import setContent from './../mixins/setContent.js' @@ -322,6 +321,7 @@ export default { }, created() { this.$ydoc = new Doc() + this.$ydoc.on('update', this.onYjsUpdate) this.$providers = [] this.$editor = null this.$syncService = null @@ -507,6 +507,8 @@ export default { ? session.displayName : (session?.guestName || t('text', 'Guest')), color: session?.color, + clientId: this.$ydoc.clientID, + lastUpdate: Date.now(), }, }), Keymap.configure({ @@ -642,6 +644,13 @@ export default { this.emit('delete-image-node', imageUrl) }, + onYjsUpdate(_update, origin) { + if (origin.key === 'y-sync$') { + // Update timestamp of own cursor + this.$editor.commands.updateSelf() + } + }, + async close() { if (this.currentSession && this.$syncService) { try { @@ -794,7 +803,6 @@ export default { width: 100%; background-color: var(--color-main-background); } - diff --git a/src/extensions/CollaborationCursor.js b/src/extensions/CollaborationCursor.js new file mode 100644 index 00000000000..927e96074e6 --- /dev/null +++ b/src/extensions/CollaborationCursor.js @@ -0,0 +1,75 @@ +import { CollaborationCursor as TiptapCollaborationCursor } from '@tiptap/extension-collaboration-cursor' + +/** + * Show cursor for client ID + * Wait 50ms for cases where the cursor gets re-rendered + * + * @param {number} clientId The Yjs client ID + */ +function showCursorLabel(clientId) { + setTimeout(() => { + const el = document.getElementById(`collaboration-cursor__label__${clientId}`) + if (!el) { + return + } + + el.classList.add('collaboration-cursor__label__active') + setTimeout(() => { + el?.classList.remove('collaboration-cursor__label__active') + }, 50) + }, 50) +} + +const CollaborationCursor = TiptapCollaborationCursor.extend({ + addOptions() { + return { + provider: null, + user: { + name: null, + clientId: null, + color: null, + lastUpdate: null, + }, + render: user => { + const cursor = document.createElement('span') + + cursor.classList.add('collaboration-cursor__caret') + cursor.setAttribute('style', `border-color: ${user.color}`) + + const label = document.createElement('div') + + label.classList.add('collaboration-cursor__label') + label.id = `collaboration-cursor__label__${user.clientId}` + label.setAttribute('style', `background-color: ${user.color}`) + label.insertBefore(document.createTextNode(user.name), null) + cursor.insertBefore(label, null) + + return cursor + }, + } + }, + + onCreate() { + this.options.provider.awareness.on('change', ({ added, removed, updated }, origin) => { + if (origin !== 'local') { + for (const clientId of [...added, ...updated]) { + if (clientId !== this.options.user.clientId) { + showCursorLabel(clientId) + } + } + } + }) + }, + + addCommands() { + return { + ...this.parent(), + updateSelf: () => ({ editor }) => { + const attributes = { ...this.options.user, lastUpdate: Date.now() } + return editor.commands.updateUser(attributes) + }, + } + }, +}) + +export default CollaborationCursor diff --git a/src/extensions/index.js b/src/extensions/index.js index b0fecc5940e..c6929f7ed2c 100644 --- a/src/extensions/index.js +++ b/src/extensions/index.js @@ -20,6 +20,7 @@ * */ +import CollaborationCursor from './CollaborationCursor.js' import Emoji from './Emoji.js' import Keymap from './Keymap.js' import UserColor from './UserColor.js' @@ -30,6 +31,7 @@ import KeepSyntax from './KeepSyntax.js' import Mention from './Mention.js' export { + CollaborationCursor, Emoji, Keymap, UserColor,