diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 6d0bca680c4..f2c831ae46c 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -82,8 +82,8 @@ 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 { Doc } from 'yjs' +import debounce from 'debounce' import { EDITOR, @@ -98,7 +98,7 @@ import { import ReadonlyBar from './Menu/ReadonlyBar.vue' import { logger } from '../helpers/logger.js' -import { getDocumentState, applyDocumentState } from '../helpers/yjs.js' +import { yjsMessageTypes, getDocumentState, applyDocumentState, getMessageType, getAwarenessMessageClientIds } from '../helpers/yjs.js' import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService.js' import createSyncServiceProvider from './../services/SyncServiceProvider.js' import AttachmentResolver from './../services/AttachmentResolver.js' @@ -107,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' @@ -321,10 +321,12 @@ export default { }, created() { this.$ydoc = new Doc() + this.$ydoc.on('update', this.onYjsUpdate) this.$providers = [] this.$editor = null this.$syncService = null this.$attachmentResolver = null + this.$cursorLabelTimeouts = {} }, beforeDestroy() { if (!this.richWorkspace) { @@ -642,6 +644,35 @@ export default { this.emit('delete-image-node', imageUrl) }, + onYjsUpdate(update, _origin, _doc, _tr) { + // Update cursor labels for clients from awareness message + if (getMessageType(update) === yjsMessageTypes.awareness) { + if (!this.$editor) { + return + } + + const clientIds = getAwarenessMessageClientIds(update) + const updateUsers = this.$editor.storage.collaborationCursor.users + .filter(u => clientIds.includes(u.clientId)) + for (const user of updateUsers) { + this.updateCursorLabel(user) + } + } + }, + + updateCursorLabel: debounce(function(user) { + // Remove CSS class that fades out and hides the cursor label and bring it back after five seconds. + // Limit this to once every second to prevent it from happening with each awareness update. + const cursorLabelEls = document.getElementsByClassName(`collaboration-cursor__label__${user.name}`) + for (const el of cursorLabelEls) { + el.classList.remove('collaboration-cursor__label_fadeout') + clearTimeout(this.$cursorLabelTimeouts[user.clientId]) + this.$cursorLabelTimeouts[user.clientId] = setTimeout(() => { + el.classList.add('collaboration-cursor__label_fadeout') + }, 5000) + } + }, 1000, true), + async close() { if (this.currentSession && this.$syncService) { try { @@ -782,7 +813,6 @@ export default { width: 100%; background-color: var(--color-main-background); } - diff --git a/src/helpers/yjs.js b/src/helpers/yjs.js index d843bc0b7e0..ef796427b1e 100644 --- a/src/helpers/yjs.js +++ b/src/helpers/yjs.js @@ -78,6 +78,8 @@ export function getAwarenessMessageClientIds(update) { const clientIds = [] for (let i = 0; i < len; i++) { clientIds.push(decoding.readVarUint(decoder)) + decoding.readVarUint(decoder) // clock + decoding.readVarString(decoder) // state } return clientIds }