mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 11:50:56 +00:00
UBER-1077 Better collaborators representation (#4267)
This commit is contained in:
parent
cc730092c1
commit
2a345280eb
@ -39,7 +39,9 @@
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/view": "^0.6.9",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"svelte": "^4.2.5",
|
||||
"@tiptap/core": "^2.1.12",
|
||||
|
@ -0,0 +1,34 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { CollaborationUser } from '../types'
|
||||
|
||||
export let user: CollaborationUser
|
||||
</script>
|
||||
|
||||
<Component
|
||||
is={view.component.ObjectPresenter}
|
||||
props={{
|
||||
objectId: user.id,
|
||||
_class: contact.class.PersonAccount,
|
||||
shouldShowAvatar: true,
|
||||
shouldShowName: true
|
||||
}}
|
||||
/>
|
@ -15,10 +15,9 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import { IntlString, getMetadata, translate } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { Button, IconSize, Loading, getPlatformColorForText, themeStore } from '@hcengineering/ui'
|
||||
import { Button, IconSize, Loading, themeStore } from '@hcengineering/ui'
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
|
||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||
@ -38,11 +37,11 @@
|
||||
TextFormatCategory,
|
||||
TextNodeAction
|
||||
} from '../types'
|
||||
import { copyDocumentContent, copyDocumentField } from '../utils'
|
||||
import { copyDocumentContent, copyDocumentField, getCollaborationUser } from '../utils'
|
||||
|
||||
import ImageStyleToolbar from './ImageStyleToolbar.svelte'
|
||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||
import { noSelectionRender } from './editor/collaboration'
|
||||
import { noSelectionRender, renderCursor } from './editor/collaboration'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { FileAttachFunction, ImageExtension } from './extension/imageExt'
|
||||
@ -106,8 +105,6 @@
|
||||
let loading = true
|
||||
void provider.loaded.then(() => (loading = false))
|
||||
|
||||
const currentUser = getCurrentAccount()
|
||||
|
||||
let editor: Editor
|
||||
let element: HTMLElement
|
||||
let textToolbarElement: HTMLElement
|
||||
@ -215,80 +212,79 @@
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void ph.then(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
|
||||
extensions: [
|
||||
...defaultExtensions,
|
||||
...optionalExtensions,
|
||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||
InlineStyleToolbarExtension.configure({
|
||||
tippyOptions,
|
||||
element: textToolbarElement,
|
||||
isSupported: () => showTextStyleToolbar,
|
||||
isSelectionOnly: () => false
|
||||
}),
|
||||
InlinePopupExtension.configure({
|
||||
pluginKey: 'show-image-actions-popup',
|
||||
element: imageToolbarElement,
|
||||
tippyOptions: {
|
||||
...tippyOptions,
|
||||
appendTo: () => boundary ?? element
|
||||
},
|
||||
shouldShow: ({ editor }) => {
|
||||
if (readonly || !canShowPopups) {
|
||||
return false
|
||||
}
|
||||
return editor.isActive('image')
|
||||
}
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
field
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider,
|
||||
user: {
|
||||
name: currentUser.email,
|
||||
color: getPlatformColorForText(currentUser.email, $themeStore.dark)
|
||||
},
|
||||
selectionRender: noSelectionRender
|
||||
}),
|
||||
Completion.configure({
|
||||
...completionConfig,
|
||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||
dispatch('open-document', { event, _id, _class })
|
||||
}
|
||||
}),
|
||||
EmojiExtension.configure(),
|
||||
...extensions
|
||||
],
|
||||
parseOptions: {
|
||||
preserveWhitespace: 'full'
|
||||
},
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor
|
||||
},
|
||||
onBlur: ({ event }) => {
|
||||
focused = false
|
||||
dispatch('blur', event)
|
||||
},
|
||||
onFocus: () => {
|
||||
focused = true
|
||||
dispatch('focus')
|
||||
},
|
||||
onUpdate: ({ transaction }) => {
|
||||
// ignore non-document changes
|
||||
if (!transaction.docChanged) return
|
||||
// ignore non-local changes
|
||||
if (isChangeOrigin(transaction)) return
|
||||
onMount(async () => {
|
||||
await ph
|
||||
const user = await getCollaborationUser()
|
||||
|
||||
dispatch('update')
|
||||
}
|
||||
})
|
||||
editor = new Editor({
|
||||
element,
|
||||
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
|
||||
extensions: [
|
||||
...defaultExtensions,
|
||||
...optionalExtensions,
|
||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||
InlineStyleToolbarExtension.configure({
|
||||
tippyOptions,
|
||||
element: textToolbarElement,
|
||||
isSupported: () => showTextStyleToolbar,
|
||||
isSelectionOnly: () => false
|
||||
}),
|
||||
InlinePopupExtension.configure({
|
||||
pluginKey: 'show-image-actions-popup',
|
||||
element: imageToolbarElement,
|
||||
tippyOptions: {
|
||||
...tippyOptions,
|
||||
appendTo: () => boundary ?? element
|
||||
},
|
||||
shouldShow: ({ editor }) => {
|
||||
if (readonly || !canShowPopups) {
|
||||
return false
|
||||
}
|
||||
return editor.isActive('image')
|
||||
}
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
field
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider,
|
||||
user,
|
||||
render: renderCursor,
|
||||
selectionRender: noSelectionRender
|
||||
}),
|
||||
Completion.configure({
|
||||
...completionConfig,
|
||||
showDoc (event: MouseEvent, _id: string, _class: string) {
|
||||
dispatch('open-document', { event, _id, _class })
|
||||
}
|
||||
}),
|
||||
EmojiExtension.configure(),
|
||||
...extensions
|
||||
],
|
||||
parseOptions: {
|
||||
preserveWhitespace: 'full'
|
||||
},
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor
|
||||
},
|
||||
onBlur: ({ event }) => {
|
||||
focused = false
|
||||
dispatch('blur', event)
|
||||
},
|
||||
onFocus: () => {
|
||||
focused = true
|
||||
dispatch('focus')
|
||||
},
|
||||
onUpdate: ({ transaction }) => {
|
||||
// ignore non-document changes
|
||||
if (!transaction.docChanged) return
|
||||
// ignore non-local changes
|
||||
if (isChangeOrigin(transaction)) return
|
||||
|
||||
dispatch('update')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -14,5 +14,21 @@
|
||||
//
|
||||
|
||||
import { type DecorationAttrs } from '@tiptap/pm/view'
|
||||
import { showTooltip } from '@hcengineering/ui'
|
||||
import { type CollaborationUser } from '../../types'
|
||||
import CollaborationUserPopup from '../CollaborationUserPopup.svelte'
|
||||
|
||||
export const noSelectionRender = (_user: Record<string, any>): DecorationAttrs => ({})
|
||||
export const renderCursor = (user: CollaborationUser): HTMLElement => {
|
||||
const cursor = document.createElement('span')
|
||||
|
||||
cursor.classList.add('collaboration-cursor__caret')
|
||||
cursor.setAttribute('style', `border-color: ${user.color}`)
|
||||
|
||||
cursor.addEventListener('mousemove', () => {
|
||||
showTooltip(undefined, cursor, 'top', CollaborationUserPopup, { user })
|
||||
})
|
||||
|
||||
return cursor
|
||||
}
|
||||
|
||||
export const noSelectionRender = (_user: CollaborationUser): DecorationAttrs => ({})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import { type Account, type Doc, type Ref } from '@hcengineering/core'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { type Editor, type SingleCommands } from '@tiptap/core'
|
||||
|
||||
@ -95,3 +95,13 @@ export interface TextEditorCommandHandler {
|
||||
command: (command: TextEditorCommand) => boolean
|
||||
chain: (...commands: TextEditorCommand[]) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface CollaborationUser {
|
||||
id: Ref<Account>
|
||||
name: string
|
||||
email: string
|
||||
color: string
|
||||
}
|
||||
|
@ -15,9 +15,21 @@
|
||||
|
||||
import { type onStatelessParameters } from '@hocuspocus/provider'
|
||||
import { type Attribute } from '@tiptap/core'
|
||||
import { get } from 'svelte/store'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import contact, { type PersonAccount, formatName, AvatarType } from '@hcengineering/contact'
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
type ColorDefinition,
|
||||
getPlatformAvatarColorByName,
|
||||
getPlatformAvatarColorForTextDef,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
|
||||
import { type DocumentId, TiptapCollabProvider } from './provider'
|
||||
import { type CollaborationUser } from './types'
|
||||
|
||||
type ProviderData = (
|
||||
| {
|
||||
@ -106,3 +118,27 @@ export function getDataAttribute (
|
||||
...(options ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
function getAvatarColor (name: string, avatar: string, darkTheme: boolean): ColorDefinition {
|
||||
const [type, color] = avatar.split('://')
|
||||
if (type === AvatarType.COLOR) {
|
||||
return getPlatformAvatarColorByName(color, darkTheme)
|
||||
}
|
||||
return getPlatformAvatarColorForTextDef(name, darkTheme)
|
||||
}
|
||||
|
||||
export async function getCollaborationUser (): Promise<CollaborationUser> {
|
||||
const client = getClient()
|
||||
|
||||
const me = getCurrentAccount() as PersonAccount
|
||||
const person = await client.findOne(contact.class.Person, { _id: me.person })
|
||||
const name = person !== undefined ? formatName(person.name) : me.email
|
||||
const color = getAvatarColor(name, person?.avatar ?? '', get(themeStore).dark)
|
||||
|
||||
return {
|
||||
id: me._id,
|
||||
name,
|
||||
email: me.email,
|
||||
color: color.icon ?? 'var(--theme-button-default)'
|
||||
}
|
||||
}
|
||||
|
@ -153,29 +153,27 @@
|
||||
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-cursor__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-cursor__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
left: -1px;
|
||||
line-height: normal;
|
||||
padding: 0.1rem 0.3rem;
|
||||
position: absolute;
|
||||
top: -1.4em;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: -2px;
|
||||
left: -4px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top-width: 4px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
cmark {
|
||||
|
Loading…
Reference in New Issue
Block a user