diff --git a/package.json b/package.json index 021a2ec..64031cc 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@tiptap/extension-code-block-lowlight": "^2.0.0-beta.73", "@tiptap/extension-document": "^2.0.0-beta.196", "@tiptap/extension-heading": "^2.0.0-beta.196", + "@tiptap/extension-collaboration": "^2.0.0-beta.196", + "@tiptap/extension-collaboration-cursor": "^2.0.0-beta.196", "@tiptap/extension-highlight": "^2.0.0-beta.35", "@tiptap/extension-history": "^2.0.0-beta.196", "@tiptap/extension-italic": "^2.0.0-beta.196", @@ -42,7 +44,10 @@ "react-router-dom": "^6.2.1", "react-scripts": "5.0.0", "tippy.js": "^6.3.7", - "web-vitals": "^2.1.0" + "web-vitals": "^2.1.0", + "y-webrtc": "^10.2.3", + "yjs": "^13.5.41", + "color-hash": "^2.0.1" }, "devDependencies": { "@testing-library/jest-dom": "^5.14.1", @@ -54,6 +59,7 @@ "@types/react": "^17.0.20", "@types/react-beautiful-dnd": "^13.1.0", "@types/react-dom": "^17.0.9", + "@types/color-hash": "^1.0.2", "node-sass": "^7.0.1", "typescript": "^4.4.2" }, diff --git a/src/index.tsx b/src/index.tsx index 188783e..0cc2cf0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,7 +20,7 @@ HistorySynchronizer.controlLog.level = LogLevel.INFO; const main = async () => { - const configBackend = new WorkerSafeIdbBackend('hyper-browser-config-0.3'); + const configBackend = new WorkerSafeIdbBackend('hyper-browser-config-0.4'); let configBackendError: (string|undefined) = undefined; diff --git a/src/model/text/TextSpace.ts b/src/model/text/TextSpace.ts index 018f60d..1d97456 100644 --- a/src/model/text/TextSpace.ts +++ b/src/model/text/TextSpace.ts @@ -15,9 +15,7 @@ class TextSpace extends HashedObject implements SpaceEntryPoint this.setRandomId(); - const content = new MutableReference(); - content.typeConstraints = ['string']; - + const content = new MutableReference({acceptedTypes: ['string']}); this.addDerivedField('content', content); } @@ -54,7 +52,7 @@ class TextSpace extends HashedObject implements SpaceEntryPoint } } - if (!Types.checkTypeConstraint(this.content.typeConstraints, ['string'])) { + if (!this.content.validateAcceptedTypes(['string'])) { return false; } diff --git a/src/pages/space/wiki/BlockToolbar.tsx b/src/pages/space/wiki/BlockToolbar.tsx index 09b0625..7f0f617 100644 --- a/src/pages/space/wiki/BlockToolbar.tsx +++ b/src/pages/space/wiki/BlockToolbar.tsx @@ -1,7 +1,6 @@ import type { Editor } from '@tiptap/core' import { Button, ButtonGroup } from '@mui/material' import CodeIcon from '@mui/icons-material/Code'; -import HighlightIcon from '@mui/icons-material/Highlight'; import LinkIcon from '@mui/icons-material/Link'; diff --git a/src/pages/space/wiki/WikiSpaceBlock.scss b/src/pages/space/wiki/WikiSpaceBlock.scss index 8ad9607..cf47dde 100644 --- a/src/pages/space/wiki/WikiSpaceBlock.scss +++ b/src/pages/space/wiki/WikiSpaceBlock.scss @@ -104,4 +104,22 @@ a:not(.existing-page-link) { text-decoration: underline currentcolor dotted; } -@import url('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/github-dark.min.css') \ No newline at end of file +.collaboration-cursor__caret { + display: inline-block; + margin: 0; + padding: 0; + // position: absolute; +} + +// .collaboration-cursor__caret::after { +// content: "}{"; +// position: absolute; +// } + +.collaboration-cursor__label { + color: white; + // position: absolute; + // top: -1em; +} + +@import url('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/github-dark.min.css') diff --git a/src/pages/space/wiki/WikiSpaceBlock.tsx b/src/pages/space/wiki/WikiSpaceBlock.tsx index 5165be3..719125c 100644 --- a/src/pages/space/wiki/WikiSpaceBlock.tsx +++ b/src/pages/space/wiki/WikiSpaceBlock.tsx @@ -4,6 +4,9 @@ import { Fragment, useEffect, useRef, useState } from 'react'; import { Add, DragIndicator, Delete } from '@mui/icons-material'; import { Block, BlockType, WikiSpace } from '@hyper-hyper-space/wiki-collab'; +import Collaboration from '@tiptap/extension-collaboration' +import CollaborationCursor from '@tiptap/extension-collaboration-cursor'; + import Document from '@tiptap/extension-document' import Paragraph from '@tiptap/extension-paragraph' import Text from '@tiptap/extension-text' @@ -18,7 +21,7 @@ import TextAlign from '@tiptap/extension-text-align' import Underline from '@tiptap/extension-underline' import WikiLink from './WikiLink'; import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' -import History from '@tiptap/extension-history'; +// import History from '@tiptap/extension-history'; import { lowlight } from 'lowlight/lib/all.js' import { EditorContent, useEditor } from '@tiptap/react' import { MutableReference, MutationEvent } from '@hyper-hyper-space/core'; @@ -27,6 +30,9 @@ import { Icon, Tooltip } from '@mui/material'; import { useOutletContext } from 'react-router'; import { WikiContext } from './WikiSpaceView'; import { Box } from '@mui/system'; + +import ColorHash from 'color-hash'; +const colorHash = new ColorHash({lightness: 0.4}); // other extensions from the tiptap StarterKit: // import BlockQuote from '@tiptap/extension-blockquote'; // import BulletList from '@tiptap/extension-bullet-list'; @@ -39,11 +45,13 @@ import { Box } from '@mui/system'; function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEditing?: any, idx: number, showAddBlockMenu: (newAnchorEl: HTMLElement, newBlockIdx?: number) => void, removeBlock: () => void}, ) { - const { spaceContext } = useOutletContext(); + const { spaceContext, ydoc, yjsProvider } = useOutletContext(); const resources = spaceContext.resources; const blockState = useObjectState(props.block, {debounceFreq: 250}); const blockContentsState = useObjectState(props.block?.contents, {debounceFreq: 250}); + const [editorFieldId, setEditorFieldId] = useState(props.block?.contents?.hash()) + const { wiki } = useOutletContext(); const pageSetState = useObjectState(wiki, {filterMutations: (ev: MutationEvent) => ev.emitter === wiki?.pages, debounceFreq: 250}); @@ -117,6 +125,7 @@ function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEdit console.log('SAVED BLOCK') }, 1500)) + console.log('space context author name', spaceContext.home?.getAuthor()?.info?.name) const editor = useEditor({ extensions: [ Document, @@ -126,13 +135,18 @@ function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEdit Strike, Italic, Heading, - History, + // History, // conflicts with Collaboration plugin Highlight, TextAlign, Underline, WikiLink.configure({ definedPageNames: [...pageSetState?.getValue()?.pages?.values()!].map(page => page.name!) }), + Collaboration.configure({document: ydoc, field: (editorFieldId || '')}), + CollaborationCursor.configure({ provider: yjsProvider, user: { + name: spaceContext.home?.getAuthor()?.info?.name || "anonymous", + color: colorHash.hex(spaceContext.home?.getAuthor()?.info?.name || "anonymous"), + }}), CodeBlockLowlight.configure({lowlight}), Placeholder.configure({ placeholder: 'Write something...' }) ], @@ -140,6 +154,10 @@ function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEdit preserveWhitespace: 'full' }, onUpdate: async ({ editor }) => { + if (blockState?.value?.type === BlockType.Image) { + console.log('NOT UPDATING IMAGE BLOCK') + return + } console.log('UPDATE') console.log(blockContentsState) if (blockContentsState && !editor.isDestroyed) { @@ -149,7 +167,7 @@ function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEdit editable, onBlur: stoppedEditing, onFocus: startedEditing - }); + }, [editorFieldId, blockState?.value?.type]); /*editor?.on('focus', () => { console.log('focusing editor') @@ -157,6 +175,7 @@ function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEdit });*/ useEffect(() => { + setEditorFieldId(blockContentsState?.getValue()?.hash()) const newText = blockContentsState?.getValue()?.getValue(); if (!newText) { @@ -164,9 +183,12 @@ function WikiSpaceBlock(props: { block: Block, startedEditing?: any, stoppedEdit } if (!editor?.isDestroyed && newText !== editor?.getHTML()) { + console.log('setting contents of block ' + props.block.getLastHash() + ' to:'); + console.log(newText); editor?.commands.setContent(newText, false, { preserveWhitespace: 'full' }) } - }, [blockContentsState, editor])//, editor, blockState]) + + }, [blockContentsState, editor, blockState])//, editor, blockState]) const handleAddBlock = (event: React.MouseEvent) => { props.showAddBlockMenu(event.currentTarget, props.idx + 1); diff --git a/src/pages/space/wiki/WikiSpacePage.tsx b/src/pages/space/wiki/WikiSpacePage.tsx index e2a32a9..864e3e5 100644 --- a/src/pages/space/wiki/WikiSpacePage.tsx +++ b/src/pages/space/wiki/WikiSpacePage.tsx @@ -21,7 +21,6 @@ function WikiSpacePage(props: {noNavigation: boolean, navigationWidth: string, c const { pageName } = useParams(); const { wiki, nav } = useOutletContext(); - //const wikiState = useObjectState(wiki, {debounceFreq: 250}); const wikiTitleState = useObjectState(wiki, {filterMutations: (ev: MutationEvent) => ev.emitter === wiki?.title, debounceFreq: 250}); const pageSetState = useObjectState(wiki, {filterMutations: (ev: MutationEvent) => ev.emitter === wiki?.pages, debounceFreq: 250}); diff --git a/src/pages/space/wiki/WikiSpaceView.tsx b/src/pages/space/wiki/WikiSpaceView.tsx index dc78135..fe66fa2 100644 --- a/src/pages/space/wiki/WikiSpaceView.tsx +++ b/src/pages/space/wiki/WikiSpaceView.tsx @@ -6,6 +6,9 @@ import { Outlet, Route, Routes, useNavigate, useOutletContext } from 'react-rout import NewPage from './NewPage'; import WikiSpaceNavigation from './WikiSpaceNavigation'; import { SpaceContext } from '../SpaceFrame'; +import { WebrtcProvider } from 'y-webrtc' +import { Doc } from 'yjs'; +import { memoize } from 'lodash-es'; type WikiNav = { goToPage: (pageName: string) => void, @@ -16,13 +19,19 @@ type WikiNav = { type WikiContext = { wiki : WikiSpace, nav : WikiNav, - spaceContext : SpaceContext + spaceContext : SpaceContext, + yjsProvider : WebrtcProvider, + ydoc: Doc } +const getYdoc = memoize(id => new Doc()) +const getYjsProvider = memoize(id => new WebrtcProvider(id, getYdoc(id))) + function WikiSpaceView(props: { entryPoint: WikiSpace, path?: string }) { const spaceContext = useOutletContext(); + console.log(spaceContext) const wiki = props.entryPoint; @@ -57,7 +66,9 @@ function WikiSpaceView(props: { entryPoint: WikiSpace, path?: string }) { goToAddPage: goToAddPage, goToIndex: goToIndex }, - spaceContext: spaceContext + spaceContext: spaceContext, + yjsProvider: getYjsProvider(wiki.getId()), + ydoc: getYdoc(wiki.getId()) } const theme = useTheme(); @@ -102,33 +113,6 @@ function WikiSpaceView(props: { entryPoint: WikiSpace, path?: string }) { - - {/* - - - - }} - > - {currentPage === undefined && - Loading... - } - {currentPage !== undefined && - - } - - */}; } export type { WikiContext };