Merge pull request #2 from cudr/inner_annotations

Inner annotations
This commit is contained in:
George 2019-10-19 22:10:40 +03:00 committed by GitHub
commit 31599caa71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 440 additions and 95 deletions

View File

@ -2,6 +2,7 @@ import io from 'socket.io'
import { ValueJSON } from 'slate' import { ValueJSON } from 'slate'
import * as Automerge from 'automerge' import * as Automerge from 'automerge'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
import merge from 'lodash/merge'
import { toSync, toJS } from '@slate-collaborative/bridge' import { toSync, toJS } from '@slate-collaborative/bridge'
@ -18,7 +19,7 @@ class Connection {
this.io = io(options.port, options.connectOpts) this.io = io(options.port, options.connectOpts)
this.docSet = new Automerge.DocSet() this.docSet = new Automerge.DocSet()
this.connections = {} this.connections = {}
this.options = options this.options = merge(defaultOptions, options)
this.configure() this.configure()
} }
@ -32,16 +33,28 @@ class Connection {
private appendDoc = (path: string, value: ValueJSON) => { private appendDoc = (path: string, value: ValueJSON) => {
const sync = toSync(value) const sync = toSync(value)
sync.annotations = {}
const doc = Automerge.from(sync) const doc = Automerge.from(sync)
this.docSet.setDoc(path, doc) this.docSet.setDoc(path, doc)
} }
private saveDoc = throttle(pathname => { private saveDoc = throttle(pathname => {
if (this.options.onDocumentSave) { try {
const doc = this.docSet.getDoc(pathname) if (this.options.onDocumentSave) {
const doc = this.docSet.getDoc(pathname)
this.options.onDocumentSave(pathname, toJS(doc)) if (doc) {
const data = toJS(doc)
delete data.annotations
this.options.onDocumentSave(pathname, data)
}
}
} catch (e) {
console.log(e)
} }
}, (this.options && this.options.saveTreshold) || 2000) }, (this.options && this.options.saveTreshold) || 2000)
@ -96,6 +109,8 @@ class Connection {
socket.on('operation', this.onOperation(id, name)) socket.on('operation', this.onOperation(id, name))
socket.on('disconnect', this.onDisconnect(id, socket)) socket.on('disconnect', this.onDisconnect(id, socket))
this.garbageCursors(name)
} }
private onOperation = (id, name) => data => { private onOperation = (id, name) => data => {
@ -103,6 +118,8 @@ class Connection {
this.connections[id].receiveMsg(data) this.connections[id].receiveMsg(data)
this.saveDoc(name) this.saveDoc(name)
this.garbageCursors(name)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
@ -114,6 +131,9 @@ class Connection {
socket.leave(id) socket.leave(id)
this.garbageCursor(socket.nsp.name, id)
this.garbageCursors(socket.nsp.name)
this.garbageNsp() this.garbageNsp()
} }
@ -127,6 +147,35 @@ class Connection {
}) })
} }
garbageCursor = (nsp, id) => {
const doc = this.docSet.getDoc(nsp)
if (!doc.annotations) return
const change = Automerge.change(doc, `remove cursor ${id}`, (d: any) => {
delete d.annotations[id]
})
this.docSet.setDoc(nsp, change)
}
garbageCursors = nsp => {
const doc = this.docSet.getDoc(nsp)
if (!doc.annotations) return
const namespace = this.io.of(nsp)
Object.keys(doc.annotations).forEach(key => {
if (
!namespace.sockets[key] &&
doc.annotations[key].type === this.options.cursorAnnotationType
) {
this.garbageCursor(nsp, key)
}
})
}
removeDoc = async nsp => { removeDoc = async nsp => {
const doc = this.docSet.getDoc(nsp) const doc = this.docSet.getDoc(nsp)

View File

@ -5,6 +5,7 @@ export interface ConnectionOptions {
connectOpts?: SocketIO.ServerOptions connectOpts?: SocketIO.ServerOptions
defaultValue?: ValueJSON defaultValue?: ValueJSON
saveTreshold?: number saveTreshold?: number
cursorAnnotationType?: string
onAuthRequest?: ( onAuthRequest?: (
query: Object, query: Object,
socket?: SocketIO.Socket socket?: SocketIO.Socket

View File

@ -7,7 +7,8 @@ export const getClients = (io, nsp) =>
export const defaultOptions = { export const defaultOptions = {
port: 9000, port: 9000,
saveTreshold: 2000 saveTreshold: 2000,
cursorAnnotationType: 'collaborative_selection'
} }
export { defaultValue } export { defaultValue }

View File

@ -1,14 +1,17 @@
import { Operation, SyncDoc } from '../model' import { Operation, SyncDoc } from '../model/index'
export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { export const addAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => {
console.log('addAnnotation!!!', op.toJS())
return doc return doc
} }
export const removeAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { export const removeAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => {
console.log('removeAnnotation!!!', op.toJS())
return doc return doc
} }
export const setAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => { export const setAnnotation = (doc: SyncDoc, op: Operation): SyncDoc => {
console.log('setAnnotation!!!', op.toJS())
return doc return doc
} }

View File

@ -6,23 +6,25 @@ import text from './text'
import annotation from './annotation' import annotation from './annotation'
const setSelection = doc => doc const setSelection = doc => doc
const setValue = (doc, op) => doc const setValue = doc => doc
const opType: any = { const opType: any = {
...text, ...text,
...annotation, ...annotation,
...node, ...node,
...mark, ...mark,
set_selection: setSelection
set_selection: setSelection, // set_value: setValue
set_value: setValue
} }
export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => { export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => {
try { try {
const applyOp = opType[op.type] const applyOp = opType[op.type]
if (!applyOp) throw new TypeError('Invalid operation type!') if (!applyOp) {
console.log('operation', op.toJS())
throw new TypeError(`Unsupported operation type: ${op.type}!`)
}
return applyOp(doc, op) return applyOp(doc, op)
} catch (e) { } catch (e) {

View File

@ -14,7 +14,7 @@ const byAction = {
const rootKey = '00000000-0000-0000-0000-000000000000' const rootKey = '00000000-0000-0000-0000-000000000000'
const toSlateOp = (ops: Automerge.Diff[], currentTree) => { const toSlateOp = (ops: Automerge.Diff[], doc) => {
const iterate = (acc, op) => { const iterate = (acc, op) => {
const action = byAction[op.action] const action = byAction[op.action]
@ -30,7 +30,7 @@ const toSlateOp = (ops: Automerge.Diff[], currentTree) => {
[] []
]) ])
return defer.flatMap(op => op(tempTree, currentTree)) return defer.flatMap(op => op(tempTree, doc)).filter(op => op)
} }
export { toSlateOp } export { toSlateOp }

View File

@ -15,18 +15,20 @@ const insertNodeOp = ({ value, obj, index, path }: Automerge.Diff) => map => {
const inserate = ({ nodes, ...json }: any, path) => { const inserate = ({ nodes, ...json }: any, path) => {
const node = nodes ? { ...json, nodes: [] } : json const node = nodes ? { ...json, nodes: [] } : json
if (node.object === 'mark') { if (node.object) {
ops.push({ if (node.object === 'mark') {
type: 'add_mark', ops.push({
path: path.slice(0, -1), type: 'add_mark',
mark: node path: path.slice(0, -1),
}) mark: node
} else { })
ops.push({ } else {
type: 'insert_node', ops.push({
path, type: 'insert_node',
node path,
}) node
})
}
} }
nodes && nodes.forEach((n, i) => inserate(n, [...path, i])) nodes && nodes.forEach((n, i) => inserate(n, [...path, i]))
@ -50,7 +52,7 @@ const opInsert = (op: Automerge.Diff, [map, ops]) => {
if (link && map[obj]) { if (link && map[obj]) {
map[obj].splice(index, 0, map[value] || value) map[obj].splice(index, 0, map[value] || value)
} else if (type === 'text' && !path) { } else if ((type === 'text' || type === 'list') && !path) {
map[obj] = map[obj] map[obj] = map[obj]
? map[obj] ? map[obj]
.slice(0, index) .slice(0, index)

View File

@ -40,10 +40,22 @@ const removeNodesOp = ({ index, obj, path }: Automerge.Diff) => (map, doc) => {
} }
} }
const removeAnnotationOp = ({ key }: Automerge.Diff) => (map, doc) => {
const annotation = toJS(doc.annotations[key])
if (annotation) {
return {
type: 'remove_annotation',
annotation
}
}
}
const removeByType = { const removeByType = {
text: removeTextOp, text: removeTextOp,
nodes: removeNodesOp, nodes: removeNodesOp,
marks: removeMarkOp marks: removeMarkOp,
annotations: removeAnnotationOp
} }
const opRemove = (op: Automerge.Diff, [map, ops]) => { const opRemove = (op: Automerge.Diff, [map, ops]) => {

View File

@ -10,6 +10,32 @@ const setDataOp = ({ path, value }: Automerge.Diff) => map => ({
} }
}) })
const AnnotationSetOp = ({ key, value }: Automerge.Diff) => (map, doc) => {
if (!doc.annotations) {
doc.annotations = {}
}
let op
/**
* Looks like set_annotation option is broken, temporary disabled
*/
// if (!doc.annotations[key]) {
op = {
type: 'add_annotation',
annotation: map[value]
}
// } else {
// op = {
// type: 'set_annotation',
// properties: toJS(doc.annotations[key]),
// newProperties: map[value]
// }
// }
return op
}
const setByType = { const setByType = {
data: setDataOp data: setDataOp
} }
@ -21,10 +47,17 @@ const opSet = (op: Automerge.Diff, [map, ops]) => {
if (set && path) { if (set && path) {
ops.push(set(op)) ops.push(set(op))
} else { } else if (map[obj]) {
map[obj][key] = link ? map[value] : value map[obj][key] = link ? map[value] : value
} }
/**
* Annotation
*/
if (path && path.length === 1 && path[0] === 'annotations') {
ops.push(AnnotationSetOp(op))
}
return [map, ops] return [map, ops]
} catch (e) { } catch (e) {
console.error(e, op, toJS(map)) console.error(e, op, toJS(map))

View File

@ -0,0 +1,69 @@
import { Operation, Selection } from 'slate'
import * as Immutable from 'immutable'
import merge from 'lodash/merge'
import { toJS } from '../utils'
import { SyncDoc, CursorKey } from '../model'
export const setCursor = (
doc: SyncDoc,
key: CursorKey,
selection: Selection,
type,
data
) => {
if (!doc) return
if (!doc.annotations) {
doc.annotations = {}
}
if (!doc.annotations[key]) {
doc.annotations[key] = {
key,
type,
data: {}
}
}
const annotation = toJS(doc.annotations[key])
annotation.focus = selection.end.toJSON()
annotation.anchor = selection.start.toJSON()
annotation.data = merge(annotation.data, data, {
isBackward: selection.isBackward,
targetPath: selection.isBackward
? annotation.anchor.path
: annotation.focus.path
})
doc.annotations[key] = annotation
return doc
}
export const removeCursor = (doc: SyncDoc, key: CursorKey) => {
if (doc.annotations && doc.annotations[key]) {
delete doc.annotations[key]
}
return doc
}
export const cursorOpFilter = (ops: Immutable.List<Operation>, type: string) =>
ops.filter(op => {
if (op.type === 'set_annotation') {
return !(
(op.properties && op.properties.type === type) ||
(op.newProperties && op.newProperties.type === type)
)
} else if (
op.type === 'add_annotation' ||
op.type === 'remove_annotation'
) {
return op.annotation.type !== type
}
return true
})

View File

@ -1,3 +1,5 @@
export * from './apply' export * from './apply'
export * from './convert' export * from './convert'
export * from './utils' export * from './utils'
export * from './cursor'
export * from './model'

View File

@ -1,3 +1,3 @@
import { Doc } from 'automerge' export type CursorKey = string
export type SyncDoc = Doc<any> export type SyncDoc = any

View File

@ -1,7 +1,14 @@
import toSync from './toSync' import toSync from './toSync'
import hexGen from './hexGen' import hexGen from './hexGen'
export const toJS = node => JSON.parse(JSON.stringify(node)) export const toJS = node => {
try {
return JSON.parse(JSON.stringify(node))
} catch (e) {
console.error('Convert to js failed!!! Return null')
return null
}
}
export const cloneNode = node => toSync(toJS(node)) export const cloneNode = node => toSync(toJS(node))

View File

@ -3,9 +3,16 @@ import Immutable from 'immutable'
import io from 'socket.io-client' import io from 'socket.io-client'
import { Value, Operation } from 'slate' import { Value, Operation } from 'slate'
import { ConnectionModel } from './model' import { ConnectionModel, ExtendedEditor } from './model'
import { applySlateOps, toSlateOp, toJS } from '@slate-collaborative/bridge' import {
setCursor,
removeCursor,
cursorOpFilter,
applySlateOps,
toSlateOp,
toJS
} from '@slate-collaborative/bridge'
class Connection { class Connection {
url: string url: string
@ -13,8 +20,10 @@ class Connection {
docSet: Automerge.DocSet<any> docSet: Automerge.DocSet<any>
connection: Automerge.Connection<any> connection: Automerge.Connection<any>
socket: SocketIOClient.Socket socket: SocketIOClient.Socket
editor: any editor: ExtendedEditor
connectOpts: any connectOpts: any
annotationDataMixin: any
cursorAnnotationType: string
onConnect?: () => void onConnect?: () => void
onDisconnect?: () => void onDisconnect?: () => void
@ -23,11 +32,16 @@ class Connection {
url, url,
connectOpts, connectOpts,
onConnect, onConnect,
onDisconnect onDisconnect,
cursorAnnotationType,
annotationDataMixin
}: ConnectionModel) { }: ConnectionModel) {
this.url = url this.url = url
this.editor = editor this.editor = editor
this.connectOpts = connectOpts this.connectOpts = connectOpts
this.cursorAnnotationType = cursorAnnotationType
this.annotationDataMixin = annotationDataMixin
this.onConnect = onConnect this.onConnect = onConnect
this.onDisconnect = onDisconnect this.onDisconnect = onDisconnect
@ -68,7 +82,9 @@ class Connection {
}) })
}) })
setTimeout(() => (this.editor.remote = false), 5) await Promise.resolve()
this.editor.remote = false
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -81,8 +97,20 @@ class Connection {
if (!doc) return if (!doc) return
const {
value: { selection }
} = this.editor
const withCursor = selection.isFocused ? setCursor : removeCursor
const changed = Automerge.change(doc, message, (d: any) => const changed = Automerge.change(doc, message, (d: any) =>
applySlateOps(d, operations) withCursor(
applySlateOps(d, cursorOpFilter(operations, this.cursorAnnotationType)),
this.socket.id,
selection,
this.cursorAnnotationType,
this.annotationDataMixin
)
) )
this.docSet.setDoc(this.docId, changed) this.docSet.setDoc(this.docId, changed)
@ -109,7 +137,7 @@ class Connection {
} }
connect = () => { connect = () => {
this.socket = io(this.url, this.connectOpts) this.socket = io(this.url, { ...this.connectOpts })
this.socket.on('connect', () => { this.socket.on('connect', () => {
this.connection = new Automerge.Connection(this.docSet, this.sendData) this.connection = new Automerge.Connection(this.docSet, this.sendData)
@ -125,6 +153,8 @@ class Connection {
disconnect = () => { disconnect = () => {
this.onDisconnect() this.onDisconnect()
console.log('disconnect', this.socket)
this.connection && this.connection.close() this.connection && this.connection.close()
delete this.connection delete this.connection
@ -137,6 +167,7 @@ class Connection {
this.onDisconnect() this.onDisconnect()
this.socket.close() this.socket.close()
// this.socket.destroy()
} }
} }

View File

@ -4,7 +4,6 @@ import { KeyUtils } from 'slate'
import { hexGen } from '@slate-collaborative/bridge' import { hexGen } from '@slate-collaborative/bridge'
import Connection from './Connection' import Connection from './Connection'
import { ControllerProps } from './model' import { ControllerProps } from './model'
class Controller extends Component<ControllerProps> { class Controller extends Component<ControllerProps> {
@ -15,7 +14,13 @@ class Controller extends Component<ControllerProps> {
} }
componentDidMount() { componentDidMount() {
const { editor, url, connectOpts } = this.props const {
editor,
url,
cursorAnnotationType,
annotationDataMixin,
connectOpts
} = this.props
KeyUtils.setGenerator(() => hexGen()) KeyUtils.setGenerator(() => hexGen())
@ -23,6 +28,8 @@ class Controller extends Component<ControllerProps> {
editor, editor,
url, url,
connectOpts, connectOpts,
cursorAnnotationType,
annotationDataMixin,
onConnect: this.onConnect, onConnect: this.onConnect,
onDisconnect: this.onDisconnect onDisconnect: this.onDisconnect
}) })
@ -37,10 +44,10 @@ class Controller extends Component<ControllerProps> {
} }
render() { render() {
const { children, preloader } = this.props const { children, renderPreloader } = this.props
const { preloading } = this.state const { preloading } = this.state
if (preloader && preloading) return preloader() if (renderPreloader && preloading) return renderPreloader()
return children return children
} }

View File

@ -0,0 +1,47 @@
import React, { Fragment } from 'react'
const cursorStyleBase = {
position: 'absolute',
top: -2,
pointerEvents: 'none',
userSelect: 'none',
transform: 'translateY(-100%)',
fontSize: 10,
color: 'white',
background: 'palevioletred',
whiteSpace: 'nowrap'
} as any
const caretStyleBase = {
position: 'absolute',
top: 0,
pointerEvents: 'none',
userSelect: 'none',
height: '100%',
width: 2,
background: 'palevioletred'
} as any
const Cursor = ({ color, isBackward, name }) => {
const cursorStyles = {
...cursorStyleBase,
background: color,
left: isBackward ? '0%' : '100%'
}
const caretStyles = {
...caretStyleBase,
background: color,
left: isBackward ? '0%' : '100%'
}
return (
<Fragment>
<span contentEditable={false} style={cursorStyles}>
{name}
</span>
<span contentEditable={false} style={caretStyles} />
</Fragment>
)
}
export default Cursor

View File

@ -1,29 +0,0 @@
import { ReactNode } from 'react'
import onChange from './onChange'
import renderEditor from './renderEditor'
import Connection from './Connection'
export interface PluginOptions {
url?: string
connectOpts?: SocketIOClient.ConnectOpts
preloader?: () => ReactNode
onConnect?: (connection: Connection) => void
onDisconnect?: (connection: Connection) => void
}
const defaultOpts = {
url: 'http://localhost:9000'
}
const plugin = (opts: PluginOptions = {}) => {
const options = { ...defaultOpts, ...opts }
return {
onChange: onChange(options),
renderEditor: renderEditor(options)
}
}
export default plugin

View File

@ -0,0 +1,30 @@
import onChange from './onChange'
import renderEditor from './renderEditor'
import renderAnnotation from './renderAnnotation'
import renderCursor from './renderCursor'
import { PluginOptions } from './model'
export const defaultOpts = {
url: 'http://localhost:9000',
cursorAnnotationType: 'collaborative_selection',
renderCursor,
annotationDataMixin: {
name: 'an collaborator name',
color: 'palevioletred',
alphaColor: 'rgba(233, 30, 99, 0.2)'
}
}
const plugin = (opts: PluginOptions = defaultOpts) => {
const options = { ...defaultOpts, ...opts }
return {
onChange: onChange(options),
renderEditor: renderEditor(options),
renderAnnotation: renderAnnotation(options)
}
}
export default plugin

View File

@ -1,16 +1,28 @@
import { Editor } from 'slate' import { ReactNode } from 'react'
import { PluginOptions } from './index' import { Editor, Controller, Value } from 'slate'
import Connection from './Connection' import Connection from './Connection'
export interface ConnectionModel extends PluginOptions { type Data = {
editor: Editor [key: string]: any
onConnect: () => void }
onDisconnect: () => void
interface FixedController extends Controller {
setValue: (value: Value) => void
} }
export interface ExtendedEditor extends Editor { export interface ExtendedEditor extends Editor {
remote: boolean remote?: boolean
connection: Connection connection?: Connection
controller: FixedController
setFocus: () => void
}
export interface ConnectionModel extends PluginOptions {
editor: ExtendedEditor
cursorAnnotationType: string
onConnect: () => void
onDisconnect: () => void
} }
export interface ControllerProps extends PluginOptions { export interface ControllerProps extends PluginOptions {
@ -18,3 +30,14 @@ export interface ControllerProps extends PluginOptions {
url?: string url?: string
connectOpts?: SocketIOClient.ConnectOpts connectOpts?: SocketIOClient.ConnectOpts
} }
export interface PluginOptions {
url?: string
connectOpts?: SocketIOClient.ConnectOpts
cursorAnnotationType?: string
annotationDataMixin?: Data
renderPreloader?: () => ReactNode
renderCursor?: (data: Data) => ReactNode | any
onConnect?: (connection: Connection) => void
onDisconnect?: (connection: Connection) => void
}

View File

@ -1,7 +1,7 @@
import { ExtendedEditor } from './model' import { ExtendedEditor } from './model'
const onChange = opts => (editor: ExtendedEditor, next: () => void) => { const onChange = opts => (editor: ExtendedEditor, next: () => void) => {
if (!editor.remote) { if (editor.connection && !editor.remote) {
const operations: any = editor.operations const operations: any = editor.operations
editor.connection.receiveSlateOps(operations) editor.connection.receiveSlateOps(operations)

View File

@ -0,0 +1,31 @@
import React from 'react'
const renderAnnotation = ({ cursorAnnotationType, renderCursor }) => (
props,
editor,
next
) => {
const { children, annotation, attributes, node } = props
if (annotation.type !== cursorAnnotationType) return next()
const data = annotation.data.toJS()
const { targetPath, alphaColor } = data
const { document } = editor.value
const targetNode = document.getNode(targetPath)
const showCursor = targetNode && targetNode.key === node.key
return (
<span
{...attributes}
style={{ position: 'relative', background: alphaColor }}
>
{showCursor ? renderCursor(data) : null}
{children}
</span>
)
}
export default renderAnnotation

View File

@ -0,0 +1,7 @@
import React from 'react'
import Cursor from './Cursor'
const renderCursor = data => <Cursor {...data} />
export default renderCursor

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import { PluginOptions } from './index' import { PluginOptions } from './model'
import Controller from './Controller' import Controller from './Controller'
const renderEditor = (opts: PluginOptions) => ( const renderEditor = (opts: PluginOptions) => (

View File

@ -16,6 +16,8 @@
"concurrently": "^4.1.2", "concurrently": "^4.1.2",
"faker": "^4.1.0", "faker": "^4.1.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"nodemon": "^1.19.2",
"randomcolor": "^0.5.4",
"react": "^16.9.0", "react": "^16.9.0",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-scripts": "3.1.1", "react-scripts": "3.1.1",
@ -25,10 +27,9 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build",
"dev": "concurrently \"yarn start\" \"yarn serve\"", "dev": "concurrently \"yarn start\" \"yarn serve\"",
"serve": "nodemon --watch ../backend/lib --inspect server.js", "serve": "nodemon --watch ../backend/lib --inspect server.js"
"test": "react-scripts test",
"eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
@ -44,8 +45,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"nodemon": "^1.19.2"
} }
} }

View File

@ -19,9 +19,11 @@ class App extends Component<{}, { rooms: string[] }> {
return ( return (
<Container> <Container>
<AddButton type="button" onClick={this.addRoom}> <Panel>
Add Room <AddButton type="button" onClick={this.addRoom}>
</AddButton> Add Room
</AddButton>
</Panel>
{rooms.map(room => ( {rooms.map(room => (
<Room key={room} slug={room} removeRoom={this.removeRoom(room)} /> <Room key={room} slug={room} removeRoom={this.removeRoom(room)} />
))} ))}
@ -46,6 +48,10 @@ export default App
const Container = styled.div`` const Container = styled.div``
const Panel = styled.div`
display: flex;
`
const Button = styled.button` const Button = styled.button`
padding: 6px 14px; padding: 6px 14px;
display: block; display: block;

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { Value, ValueJSON } from 'slate' import { Value, ValueJSON } from 'slate'
import { Editor } from 'slate-react' import { Editor } from 'slate-react'
import randomColor from 'randomcolor'
import styled from '@emotion/styled' import styled from '@emotion/styled'
@ -23,12 +24,18 @@ class Client extends Component<ClienProps> {
state = { state = {
value: Value.fromJSON(defaultValue as ValueJSON), value: Value.fromJSON(defaultValue as ValueJSON),
isOnline: true, isOnline: false,
plugins: [] plugins: []
} }
componentDidMount() { componentDidMount() {
const plugin = ClientPlugin({ const color = randomColor({
luminosity: 'dark',
format: 'rgba',
alpha: 1
})
const options = {
url: `http://localhost:9000/${this.props.slug}`, url: `http://localhost:9000/${this.props.slug}`,
connectOpts: { connectOpts: {
query: { query: {
@ -37,10 +44,17 @@ class Client extends Component<ClienProps> {
slug: this.props.slug slug: this.props.slug
} }
}, },
// preloader: () => <div>PRELOADER!!!!!!</div>, annotationDataMixin: {
name: this.props.name,
color,
alphaColor: color.slice(0, -2) + '0.2)'
},
// renderPreloader: () => <div>PRELOADER!!!!!!</div>,
onConnect: this.onConnect, onConnect: this.onConnect,
onDisconnect: this.onDisconnect onDisconnect: this.onDisconnect
}) }
const plugin = ClientPlugin(options)
this.setState({ this.setState({
plugins: [plugin] plugins: [plugin]