Updates cursor logic
This commit is contained in:
parent
467424d93e
commit
49baa47a0e
17 changed files with 377 additions and 100 deletions
|
@ -1,19 +1,18 @@
|
|||
import styled from 'styles'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import state, { useSelector } from 'state'
|
||||
import React, { useRef } from 'react'
|
||||
import useZoomEvents from 'hooks/useZoomEvents'
|
||||
import useCamera from 'hooks/useCamera'
|
||||
import Defs from './defs'
|
||||
import Page from './page'
|
||||
import Brush from './brush'
|
||||
import Cursor from './cursor'
|
||||
import Bounds from './bounds/bounding-box'
|
||||
import BoundsBg from './bounds/bounds-bg'
|
||||
import Handles from './bounds/handles'
|
||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||
import Brush from './brush'
|
||||
import ContextMenu from './context-menu/context-menu'
|
||||
import { deepCompareArrays } from 'utils'
|
||||
import Coop from './coop/coop'
|
||||
import Defs from './defs'
|
||||
import Handles from './bounds/handles'
|
||||
import Page from './page'
|
||||
import React, { useRef } from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import useCamera from 'hooks/useCamera'
|
||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||
import useZoomEvents from 'hooks/useZoomEvents'
|
||||
|
||||
function resetError() {
|
||||
null
|
||||
|
@ -40,10 +39,10 @@ export default function Canvas(): JSX.Element {
|
|||
<g ref={rGroup} id="shapes">
|
||||
<BoundsBg />
|
||||
<Page />
|
||||
<Coop />
|
||||
<Bounds />
|
||||
<Handles />
|
||||
<Brush />
|
||||
<Peers />
|
||||
</g>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
@ -52,32 +51,6 @@ export default function Canvas(): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
function Peers(): JSX.Element {
|
||||
const peerIds = useSelector((s) => {
|
||||
return s.data.room ? Object.keys(s.data.room?.peers) : []
|
||||
}, deepCompareArrays)
|
||||
|
||||
return (
|
||||
<>
|
||||
{peerIds.map((id) => (
|
||||
<Peer key={id} id={id} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Peer({ id }: { id: string }): JSX.Element {
|
||||
const hasPeer = useSelector((s) => {
|
||||
return s.data.room && s.data.room.peers[id] !== undefined
|
||||
})
|
||||
|
||||
const point = useSelector(
|
||||
(s) => hasPeer && s.data.room.peers[id].cursor.point
|
||||
)
|
||||
|
||||
return <Cursor point={point} />
|
||||
}
|
||||
|
||||
const MainSVG = styled('svg', {
|
||||
position: 'fixed',
|
||||
overflow: 'hidden',
|
||||
|
|
27
components/canvas/coop/coop.tsx
Normal file
27
components/canvas/coop/coop.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Cursor from './cursor'
|
||||
import { useCoopSelector } from 'state/coop/coop-state'
|
||||
|
||||
export default function Presence(): JSX.Element {
|
||||
const others = useCoopSelector((s) => s.data.others)
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(others).map(({ connectionId, presence }) => {
|
||||
if (presence == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Cursor
|
||||
key={`cursor-${connectionId}`}
|
||||
color={'red'}
|
||||
duration={presence.duration}
|
||||
times={presence.times}
|
||||
bufferedXs={presence.bufferedXs}
|
||||
bufferedYs={presence.bufferedYs}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,25 +1,45 @@
|
|||
import React from 'react'
|
||||
import styled from 'styles'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
// const transition = {
|
||||
// type: 'spring',
|
||||
// mass: 2,
|
||||
// damping: 20,
|
||||
// }
|
||||
|
||||
export default function Cursor({
|
||||
color = 'dodgerblue',
|
||||
point = [0, 0],
|
||||
duration = 0,
|
||||
bufferedXs = [],
|
||||
bufferedYs = [],
|
||||
times = [],
|
||||
}: {
|
||||
color?: string
|
||||
point: number[]
|
||||
color: string
|
||||
duration: number
|
||||
bufferedXs: number[]
|
||||
bufferedYs: number[]
|
||||
times: number[]
|
||||
}): JSX.Element {
|
||||
const transform = `translate(${point[0] - 12} ${point[1] - 10})`
|
||||
|
||||
return (
|
||||
<StyledCursor
|
||||
color={color}
|
||||
transform={transform}
|
||||
initial={false}
|
||||
animate={{
|
||||
x: bufferedXs,
|
||||
y: bufferedYs,
|
||||
transition: {
|
||||
type: 'tween',
|
||||
ease: 'linear',
|
||||
duration,
|
||||
times,
|
||||
},
|
||||
}}
|
||||
width="35px"
|
||||
height="35px"
|
||||
viewBox="0 0 35 35"
|
||||
version="1.1"
|
||||
pointerEvents="none"
|
||||
opacity="0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
|
@ -43,7 +63,7 @@ export default function Cursor({
|
|||
)
|
||||
}
|
||||
|
||||
const StyledCursor = styled('g', {
|
||||
const StyledCursor = styled(motion.g, {
|
||||
position: 'absolute',
|
||||
zIndex: 1000,
|
||||
top: 0,
|
|
@ -1,11 +1,14 @@
|
|||
import { useStateDesigner } from '@state-designer/react'
|
||||
import state from 'state'
|
||||
import { useCoopSelector } from 'state/coop/coop-state'
|
||||
import styled from 'styles'
|
||||
|
||||
const size: any = { '@sm': 'small' }
|
||||
|
||||
export default function StatusBar(): JSX.Element {
|
||||
const local = useStateDesigner(state)
|
||||
const status = useCoopSelector((s) => s.data.status)
|
||||
const others = useCoopSelector((s) => s.data.others)
|
||||
|
||||
const active = local.active.slice(1).map((s) => {
|
||||
const states = s.split('.')
|
||||
|
@ -17,7 +20,8 @@ export default function StatusBar(): JSX.Element {
|
|||
return (
|
||||
<StatusBarContainer size={size}>
|
||||
<Section>
|
||||
{active.join(' | ')} | {log} | {local.data.room?.status}
|
||||
{active.join(' | ')} | {log} | {status} (
|
||||
{Object.values(others).length || 0})
|
||||
</Section>
|
||||
</StatusBarContainer>
|
||||
)
|
||||
|
|
|
@ -31,22 +31,12 @@ export default function useCanvasEvents(
|
|||
|
||||
if (state.isIn('draw.editing')) {
|
||||
fastDrawUpdate(info)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isIn('brushSelecting')) {
|
||||
} else if (state.isIn('brushSelecting')) {
|
||||
fastBrushSelect(info.point)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isIn('translatingSelection')) {
|
||||
} else if (state.isIn('translatingSelection')) {
|
||||
fastTranslate(info)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isIn('transformingSelection')) {
|
||||
} else if (state.isIn('transformingSelection')) {
|
||||
fastTransform(info)
|
||||
return
|
||||
}
|
||||
|
||||
state.send('MOVED_POINTER', info)
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@liveblocks/client": "^0.8.1",
|
||||
"@liveblocks/node": "^0.3.0",
|
||||
"@liveblocks/react": "^0.8.0",
|
||||
"@monaco-editor/react": "^4.2.1",
|
||||
"@radix-ui/react-checkbox": "^0.0.16",
|
||||
"@radix-ui/react-context-menu": "^0.0.22",
|
||||
|
|
48
pages/api/auth-liveblocks.ts
Normal file
48
pages/api/auth-liveblocks.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { authorize } from '@liveblocks/node'
|
||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY
|
||||
|
||||
const Auth: NextApiHandler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) => {
|
||||
if (!API_KEY) {
|
||||
return res.status(403).end()
|
||||
}
|
||||
|
||||
const room = req.body.room
|
||||
|
||||
if (room === 'example-live-cursors-avatars') {
|
||||
const response = await authorize({
|
||||
room,
|
||||
secret: API_KEY,
|
||||
userInfo: {
|
||||
name: NAMES[Math.floor(Math.random() * NAMES.length)],
|
||||
picture: `/assets/avatars/${Math.floor(Math.random() * 10)}.png`,
|
||||
},
|
||||
})
|
||||
|
||||
return res.status(response.status).end(response.body)
|
||||
}
|
||||
|
||||
const response = await authorize({
|
||||
room,
|
||||
secret: API_KEY,
|
||||
})
|
||||
|
||||
return res.status(response.status).end(response.body)
|
||||
}
|
||||
|
||||
export default Auth
|
||||
|
||||
const NAMES = [
|
||||
'Charlie Layne',
|
||||
'Mislav Abha',
|
||||
'Tatum Paolo',
|
||||
'Anjali Wanda',
|
||||
'Jody Hekla',
|
||||
'Emil Joyce',
|
||||
'Jory Quispe',
|
||||
'Quinn Elton',
|
||||
]
|
0
pages/api/nodes.md
Normal file
0
pages/api/nodes.md
Normal file
|
@ -1,10 +1,18 @@
|
|||
import dynamic from 'next/dynamic'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { getSession } from 'next-auth/client'
|
||||
import { useEffect } from 'react'
|
||||
import coopState from 'state/coop/coop-state'
|
||||
|
||||
const Editor = dynamic(() => import('components/editor'), { ssr: false })
|
||||
|
||||
export default function Room({ id }: { id: string }): JSX.Element {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
coopState.send('LEFT_ROOM')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <Editor roomId={id} />
|
||||
}
|
||||
|
||||
|
|
131
state/coop/client-liveblocks.ts
Normal file
131
state/coop/client-liveblocks.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Client, Room, createClient } from '@liveblocks/client'
|
||||
import coopState from './coop-state'
|
||||
import { CoopPresence } from 'types'
|
||||
import {
|
||||
ConnectionCallback,
|
||||
MyPresenceCallback,
|
||||
OthersEventCallback,
|
||||
} from '@liveblocks/client/lib/cjs/types'
|
||||
import { uniqueId } from 'utils'
|
||||
|
||||
class CoopClient {
|
||||
id = uniqueId()
|
||||
roomId: string
|
||||
lastCursorEventTime = 0
|
||||
client: Client
|
||||
room: Room
|
||||
bufferedXs: number[] = []
|
||||
bufferedYs: number[] = []
|
||||
bufferedTs: number[] = []
|
||||
|
||||
constructor() {
|
||||
this.client = createClient({
|
||||
authEndpoint: '/api/auth-liveblocks',
|
||||
})
|
||||
}
|
||||
|
||||
private handleConnectionEvent: ConnectionCallback = (status) => {
|
||||
coopState.send('CHANGED_CONNECTION_STATUS', { status })
|
||||
}
|
||||
|
||||
private handleMyPresenceEvent: MyPresenceCallback<CoopPresence> = () => {
|
||||
null
|
||||
}
|
||||
|
||||
private handleOthersEvent: OthersEventCallback<CoopPresence> = (_, event) => {
|
||||
switch (event.type) {
|
||||
case 'enter': {
|
||||
coopState.send('OTHER_USER_ENTERED', event)
|
||||
break
|
||||
}
|
||||
case 'leave': {
|
||||
coopState.send('OTHER_USER_LEFT', event)
|
||||
break
|
||||
}
|
||||
case 'update': {
|
||||
coopState.send('OTHER_USER_UPDATED', event)
|
||||
break
|
||||
}
|
||||
case 'reset': {
|
||||
coopState.send('RESET_OTHER_USERS', event)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect(roomId: string) {
|
||||
if (this.roomId) {
|
||||
this.client.leave(this.roomId)
|
||||
}
|
||||
|
||||
this.roomId = roomId
|
||||
|
||||
this.room = this.client.enter(roomId, { cursor: null })
|
||||
this.room.subscribe('connection', this.handleConnectionEvent)
|
||||
this.room.subscribe('my-presence', this.handleMyPresenceEvent)
|
||||
this.room.subscribe('others', this.handleOthersEvent)
|
||||
|
||||
coopState.send('JOINED_ROOM', { others: this.room.getOthers().toArray() })
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.room.unsubscribe('connection', this.handleConnectionEvent)
|
||||
this.room.unsubscribe('my-presence', this.handleMyPresenceEvent)
|
||||
this.room.unsubscribe('others', this.handleOthersEvent)
|
||||
|
||||
this.client.leave(this.roomId)
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this.connect(this.roomId)
|
||||
}
|
||||
|
||||
moveCursor(pageId: string, point: number[]) {
|
||||
if (!this.room) return
|
||||
|
||||
const now = Date.now()
|
||||
let elapsed = now - this.lastCursorEventTime
|
||||
|
||||
if (elapsed > 200) {
|
||||
// The animation's total duration (in seconds)
|
||||
const duration = this.bufferedTs[this.bufferedTs.length - 1]
|
||||
|
||||
// Normalized times (0 - 1)
|
||||
const times = this.bufferedTs.map((t) => t / duration)
|
||||
|
||||
// Make sure the array includes both a 0 and a 1
|
||||
if (times.length === 1) {
|
||||
this.bufferedXs.unshift(this.bufferedXs[0])
|
||||
this.bufferedYs.unshift(this.bufferedYs[0])
|
||||
times.unshift(0)
|
||||
}
|
||||
|
||||
// Send the event to the service
|
||||
this.room.updatePresence<CoopPresence>({
|
||||
bufferedXs: this.bufferedXs,
|
||||
bufferedYs: this.bufferedYs,
|
||||
times,
|
||||
duration,
|
||||
})
|
||||
|
||||
// Reset data for next update
|
||||
this.lastCursorEventTime = now
|
||||
this.bufferedXs = []
|
||||
this.bufferedYs = []
|
||||
this.bufferedTs = []
|
||||
elapsed = 0
|
||||
}
|
||||
|
||||
this.bufferedXs.push(point[0])
|
||||
this.bufferedYs.push(point[1])
|
||||
this.bufferedTs.push(elapsed / 1000)
|
||||
}
|
||||
|
||||
clearCursor() {
|
||||
this.room.updatePresence({ cursor: null })
|
||||
}
|
||||
}
|
||||
|
||||
export default new CoopClient()
|
64
state/coop/coop-state.ts
Normal file
64
state/coop/coop-state.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { createSelectorHook, createState } from '@state-designer/react'
|
||||
import { CoopPresence } from 'types'
|
||||
import { User } from '@liveblocks/client'
|
||||
import client from 'state/coop/client-liveblocks'
|
||||
|
||||
type ConnectionState =
|
||||
| 'closed'
|
||||
| 'authenticating'
|
||||
| 'unavailable'
|
||||
| 'failed'
|
||||
| 'open'
|
||||
| 'connecting'
|
||||
|
||||
const coopState = createState({
|
||||
data: {
|
||||
status: 'closed' as ConnectionState,
|
||||
others: {} as Record<string, User<CoopPresence>>,
|
||||
},
|
||||
on: {
|
||||
JOINED_ROOM: 'setOthers',
|
||||
LEFT_ROOM: 'disconnectFromRoom',
|
||||
CHANGED_CONNECTION_STATUS: 'setStatus',
|
||||
OTHER_USER_ENTERED: 'addOtherUser',
|
||||
OTHER_USER_LEFT: 'removeOtherUser',
|
||||
OTHER_USER_UPDATED: 'updateOtherUser',
|
||||
RESET_OTHER_USERS: 'resetOtherUsers',
|
||||
},
|
||||
actions: {
|
||||
connectToRoom(data, payload: { id: string }) {
|
||||
client.connect(payload.id)
|
||||
},
|
||||
disconnectFromRoom() {
|
||||
client.disconnect()
|
||||
},
|
||||
setStatus(data, payload: { status: ConnectionState }) {
|
||||
data.status = payload.status
|
||||
},
|
||||
setOthers(data, payload: { others: User<CoopPresence>[] }) {
|
||||
const { others } = payload
|
||||
data.others = Object.fromEntries(
|
||||
others.map((user) => [user.connectionId, user])
|
||||
)
|
||||
},
|
||||
addOtherUser(data, payload: { user: User<CoopPresence> }) {
|
||||
const { user } = payload
|
||||
data.others[user.connectionId] = user
|
||||
},
|
||||
removeOtherUser(data, payload: { user: User<CoopPresence> }) {
|
||||
const { user } = payload
|
||||
delete data.others[user.connectionId]
|
||||
},
|
||||
updateOtherUser(data, payload: { user: User<CoopPresence>; changes: any }) {
|
||||
const { user } = payload
|
||||
data.others[user.connectionId] = user
|
||||
},
|
||||
resetOtherUsers(data) {
|
||||
data.others = {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const useCoopSelector = createSelectorHook(coopState)
|
||||
|
||||
export default coopState
|
|
@ -3,6 +3,7 @@ import { deepClone, setToArray } from 'utils'
|
|||
import tld from 'utils/tld'
|
||||
import { freeze } from 'immer'
|
||||
import session from './session'
|
||||
import coopClient from 'state/coop/client-liveblocks'
|
||||
import state from './state'
|
||||
import vec from 'utils/vec'
|
||||
import * as Session from './sessions'
|
||||
|
@ -17,6 +18,8 @@ import * as Session from './sessions'
|
|||
export function fastDrawUpdate(info: PointerInfo): void {
|
||||
const data = { ...state.data }
|
||||
|
||||
coopClient.moveCursor(data.currentPageId, info.point)
|
||||
|
||||
session.update<Session.DrawSession>(
|
||||
data,
|
||||
tld.screenToWorld(info.point, data),
|
||||
|
|
|
@ -7,7 +7,7 @@ import history from './history'
|
|||
import storage from './storage'
|
||||
import clipboard from './clipboard'
|
||||
import * as Sessions from './sessions'
|
||||
import coopClient from './coop/client-pusher'
|
||||
import coopClient from './coop/client-liveblocks'
|
||||
import commands from './commands'
|
||||
import {
|
||||
getCommonBounds,
|
||||
|
@ -163,18 +163,17 @@ const state = createState({
|
|||
},
|
||||
on: {
|
||||
// Network-Related
|
||||
// RT_LOADED_ROOM: [
|
||||
// 'clearRoom',
|
||||
// { if: 'hasRoom', do: ['clearDocument', 'connectToRoom'] },
|
||||
// ],
|
||||
RT_LOADED_ROOM: [
|
||||
'clearRoom',
|
||||
{ if: 'hasRoom', do: ['clearDocument', 'connectToRoom'] },
|
||||
],
|
||||
RT_CHANGED_STATUS: 'setRtStatus',
|
||||
MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
|
||||
// RT_UNLOADED_ROOM: ['clearRoom', 'clearDocument'],
|
||||
// RT_DISCONNECTED_ROOM: ['clearRoom', 'clearDocument'],
|
||||
// RT_CREATED_SHAPE: 'addRtShape',
|
||||
// RT_CHANGED_STATUS: 'setRtStatus',
|
||||
// RT_DELETED_SHAPE: 'deleteRtShape',
|
||||
// RT_EDITED_SHAPE: 'editRtShape',
|
||||
// RT_MOVED_CURSOR: 'moveRtCursor',
|
||||
// MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
|
||||
// Client
|
||||
RESIZED_WINDOW: 'resetPageState',
|
||||
RESET_PAGE: 'resetPage',
|
||||
|
@ -554,7 +553,7 @@ const state = createState({
|
|||
onEnter: 'startTransformSession',
|
||||
onExit: 'completeSession',
|
||||
on: {
|
||||
// MOVED_POINTER: 'updateTransformSession', using hacks.fastTransform
|
||||
// MOVED_POINTER: 'updateTransformSession', (see hacks)
|
||||
PANNED_CAMERA: 'updateTransformSession',
|
||||
PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
|
||||
RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
|
||||
|
@ -567,7 +566,7 @@ const state = createState({
|
|||
onExit: 'completeSession',
|
||||
on: {
|
||||
STARTED_PINCHING: { to: 'pinching' },
|
||||
MOVED_POINTER: 'updateTranslateSession',
|
||||
// MOVED_POINTER: 'updateTranslateSession', (see hacks)
|
||||
PANNED_CAMERA: 'updateTranslateSession',
|
||||
PRESSED_SHIFT_KEY: 'keyUpdateTranslateSession',
|
||||
RELEASED_SHIFT_KEY: 'keyUpdateTranslateSession',
|
||||
|
@ -604,7 +603,7 @@ const state = createState({
|
|||
'startBrushSession',
|
||||
],
|
||||
on: {
|
||||
// MOVED_POINTER: 'updateBrushSession', using hacks.fastBrushSelect
|
||||
// MOVED_POINTER: 'updateBrushSession', (see hacks)
|
||||
PANNED_CAMERA: 'updateBrushSession',
|
||||
STOPPED_POINTING: { to: 'selecting' },
|
||||
STARTED_PINCHING: { to: 'pinching' },
|
||||
|
@ -640,7 +639,7 @@ const state = createState({
|
|||
},
|
||||
pinching: {
|
||||
on: {
|
||||
// PINCHED: { do: 'pinchCamera' }, using hacks.fastPinchCamera
|
||||
// PINCHED: { do: 'pinchCamera' }, (see hacks)
|
||||
},
|
||||
initial: 'selectPinching',
|
||||
onExit: { secretlyDo: 'updateZoomCSS' },
|
||||
|
@ -701,7 +700,7 @@ const state = createState({
|
|||
},
|
||||
PRESSED_SHIFT: 'keyUpdateDrawSession',
|
||||
RELEASED_SHIFT: 'keyUpdateDrawSession',
|
||||
// MOVED_POINTER: 'updateDrawSession',
|
||||
// MOVED_POINTER: 'updateDrawSession', (see hacks)
|
||||
PANNED_CAMERA: 'updateDrawSession',
|
||||
},
|
||||
},
|
||||
|
@ -1164,23 +1163,6 @@ const state = createState({
|
|||
const point = tld.screenToWorld(payload.point, data)
|
||||
coopClient.moveCursor(data.currentPageId, point)
|
||||
},
|
||||
moveRtCursor(
|
||||
data,
|
||||
payload: { id: string; pageId: string; point: number[] }
|
||||
) {
|
||||
const { room } = data
|
||||
|
||||
if (room.peers[payload.id] === undefined) {
|
||||
room.peers[payload.id] = {
|
||||
id: payload.id,
|
||||
cursor: {
|
||||
point: payload.point,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
room.peers[payload.id].cursor.point = payload.point
|
||||
},
|
||||
clearRoom(data) {
|
||||
data.room = undefined
|
||||
},
|
||||
|
|
11
types.ts
11
types.ts
|
@ -17,7 +17,7 @@ export interface Data {
|
|||
room?: {
|
||||
id: string
|
||||
status: string
|
||||
peers: Record<string, Peer>
|
||||
peers: Record<string, CoopPresence>
|
||||
}
|
||||
currentStyle: ShapeStyles
|
||||
activeTool: ShapeType | 'select'
|
||||
|
@ -38,11 +38,12 @@ export interface Data {
|
|||
/* Document */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface Peer {
|
||||
export type CoopPresence = {
|
||||
id: string
|
||||
cursor: {
|
||||
point: number[]
|
||||
}
|
||||
bufferedXs: number[]
|
||||
bufferedYs: number[]
|
||||
times: number[]
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface TLDocument {
|
||||
|
|
|
@ -142,6 +142,7 @@ export default class ProjectUtils {
|
|||
|
||||
/**
|
||||
* Get the next child index above a shape.
|
||||
* TODO: Make work for grouped shapes, make faster.
|
||||
* @param data
|
||||
* @param id
|
||||
*/
|
||||
|
@ -158,9 +159,7 @@ export default class ProjectUtils {
|
|||
|
||||
const nextSibling = siblings[index + 1]
|
||||
|
||||
if (!nextSibling) {
|
||||
return shape.childIndex + 1
|
||||
}
|
||||
if (!nextSibling) return shape.childIndex + 1
|
||||
|
||||
let nextIndex = (shape.childIndex + nextSibling.childIndex) / 2
|
||||
|
||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -1206,6 +1206,30 @@
|
|||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@liveblocks/client@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.8.0.tgz#b2cd1cc197d1ada76f4083d3a9065ee9f8fa1dc4"
|
||||
integrity sha512-p7h7ZZpkyNjC/asdzjcZOzyTjINpQkgI5zrZGT7323VLXbn9ge/a+YL83N5sUosMtbjycfGLGHNN8fpSRgl7pA==
|
||||
|
||||
"@liveblocks/client@^0.8.1":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.8.1.tgz#4220542c84473d71fb4032442e6a9861a5983ba3"
|
||||
integrity sha512-+5LNtyOUA7RyxsK2uRupEZ6SzNhi1p9119fuWFrbrgP0dMabV40U7SVuvMnMxIsGzFqC+RoDQWEQ+iJFcuBVaQ==
|
||||
|
||||
"@liveblocks/node@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/node/-/node-0.3.0.tgz#f22ff0c3415502af2baf22250431852e198b12e0"
|
||||
integrity sha512-3IJ6uN3QU71z6WXiDM97wW17fVVvrG9zMy4G4PY3zYzmeRfMnA+KBSBT1uPvlfgWm2D3d6/HNIXWxhwyv7bkfw==
|
||||
dependencies:
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@liveblocks/react@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/react/-/react-0.8.0.tgz#e538be3e0e3bcb5fe47e2f8ad5fcd2e9c379fc5d"
|
||||
integrity sha512-eCCVOz15ldmeDIT0AB08ExQrBROeAig6EBI0hY9tUe5iehADDDSAJmrkujg0K/lwPvtvzjjrzGq1qW79x2ggkg==
|
||||
dependencies:
|
||||
"@liveblocks/client" "0.8.0"
|
||||
|
||||
"@monaco-editor/loader@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.1.1.tgz#37db648c81a86946d0febd391de00df9c28a0a3d"
|
||||
|
|
Loading…
Reference in a new issue