Adds indicators for selected shapes from other users
This commit is contained in:
parent
7eae5c87e0
commit
93827e45dd
14 changed files with 192 additions and 56 deletions
|
@ -8,7 +8,7 @@ import {
|
||||||
useCameraCss,
|
useCameraCss,
|
||||||
useKeyEvents,
|
useKeyEvents,
|
||||||
} from '+hooks'
|
} from '+hooks'
|
||||||
import type { TLBinding, TLPage, TLPageState, TLShape, TLUser, TLUsers } from '+types'
|
import type { TLBinding, TLPage, TLPageState, TLShape, TLUsers } from '+types'
|
||||||
import { ErrorFallback } from '+components/error-fallback'
|
import { ErrorFallback } from '+components/error-fallback'
|
||||||
import { ErrorBoundary } from '+components/error-boundary'
|
import { ErrorBoundary } from '+components/error-boundary'
|
||||||
import { Brush } from '+components/brush'
|
import { Brush } from '+components/brush'
|
||||||
|
@ -16,6 +16,7 @@ import { Page } from '+components/page'
|
||||||
import { Users } from '+components/users'
|
import { Users } from '+components/users'
|
||||||
import { useResizeObserver } from '+hooks/useResizeObserver'
|
import { useResizeObserver } from '+hooks/useResizeObserver'
|
||||||
import { inputs } from '+inputs'
|
import { inputs } from '+inputs'
|
||||||
|
import { UsersIndicators } from '+components/users-indicators'
|
||||||
|
|
||||||
function resetError() {
|
function resetError() {
|
||||||
void null
|
void null
|
||||||
|
@ -24,7 +25,7 @@ function resetError() {
|
||||||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
users?: TLUsers
|
users?: TLUsers<T>
|
||||||
userId?: string
|
userId?: string
|
||||||
hideBounds?: boolean
|
hideBounds?: boolean
|
||||||
hideHandles?: boolean
|
hideHandles?: boolean
|
||||||
|
@ -87,6 +88,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
hideHandles={hideHandles}
|
hideHandles={hideHandles}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
/>
|
/>
|
||||||
|
{users && userId && <UsersIndicators userId={userId} users={users} meta={meta} />}
|
||||||
{pageState.brush && <Brush brush={pageState.brush} />}
|
{pageState.brush && <Brush brush={pageState.brush} />}
|
||||||
{users && <Users userId={userId} users={users} />}
|
{users && <Users userId={userId} users={users} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,7 +20,7 @@ interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
/**
|
/**
|
||||||
* The Page component renders the current page.
|
* The Page component renders the current page.
|
||||||
*/
|
*/
|
||||||
export function Page<T extends TLShape, M extends Record<string, unknown>>({
|
export const Page = React.memo(function Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
hideBounds,
|
hideBounds,
|
||||||
|
@ -82,4 +82,4 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
{!hideHandles && shapeWithHandles && <Handles shape={shapeWithHandles} />}
|
{!hideHandles && shapeWithHandles && <Handles shape={shapeWithHandles} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -8,10 +8,17 @@ interface IndicatorProps<T extends TLShape, M = any> {
|
||||||
meta: M extends any ? M : undefined
|
meta: M extends any ? M : undefined
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
isHovered?: boolean
|
isHovered?: boolean
|
||||||
|
color?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShapeIndicator = React.memo(
|
export const ShapeIndicator = React.memo(
|
||||||
<T extends TLShape, M = any>({ isHovered, isSelected, shape, meta }: IndicatorProps<T, M>) => {
|
<T extends TLShape, M = any>({
|
||||||
|
isHovered,
|
||||||
|
isSelected,
|
||||||
|
shape,
|
||||||
|
color,
|
||||||
|
meta,
|
||||||
|
}: IndicatorProps<T, M>) => {
|
||||||
const { shapeUtils } = useTLContext()
|
const { shapeUtils } = useTLContext()
|
||||||
const utils = shapeUtils[shape.type]
|
const utils = shapeUtils[shape.type]
|
||||||
const bounds = utils.getBounds(shape)
|
const bounds = utils.getBounds(shape)
|
||||||
|
@ -20,10 +27,12 @@ export const ShapeIndicator = React.memo(
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={rPositioned}
|
ref={rPositioned}
|
||||||
className={'tl-indicator tl-absolute ' + (isSelected ? 'tl-selected' : 'tl-hovered')}
|
className={
|
||||||
|
'tl-indicator tl-absolute ' + (color ? '' : isSelected ? 'tl-selected' : 'tl-hovered')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg width="100%" height="100%">
|
<svg width="100%" height="100%">
|
||||||
<g className="tl-centered-g">
|
<g className="tl-centered-g" stroke={color}>
|
||||||
<utils.Indicator
|
<utils.Indicator
|
||||||
shape={shape}
|
shape={shape}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
|
|
1
packages/core/src/components/users-indicators/index.ts
Normal file
1
packages/core/src/components/users-indicators/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './users-indicators'
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { ShapeIndicator } from '+components/shape-indicator'
|
||||||
|
import type { TLShape, TLUsers } from '+types'
|
||||||
|
import Utils from '+utils'
|
||||||
|
import { useTLContext } from '+hooks'
|
||||||
|
|
||||||
|
interface UserIndicatorProps<T extends TLShape> {
|
||||||
|
userId: string
|
||||||
|
users: TLUsers<T>
|
||||||
|
meta: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UsersIndicators<T extends TLShape>({ userId, users, meta }: UserIndicatorProps<T>) {
|
||||||
|
const { shapeUtils } = useTLContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.values(users)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((user) => user.id !== userId && user.selectedIds.length > 0)
|
||||||
|
.map((user) => {
|
||||||
|
const shapes = user.activeShapes //.map((id) => page.shapes[id])
|
||||||
|
|
||||||
|
const bounds = Utils.getCommonBounds(
|
||||||
|
shapes.map((shape) => shapeUtils[shape.type].getBounds(shape))
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={user.id + '_shapes'}>
|
||||||
|
<div
|
||||||
|
className="tl-absolute tl-user-indicator-bounds"
|
||||||
|
style={{
|
||||||
|
backgroundColor: user.color + '0d',
|
||||||
|
borderColor: user.color + '78',
|
||||||
|
transform: `translate(${bounds.minX}px, ${bounds.minY}px)`,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{shapes.map((shape) => (
|
||||||
|
<ShapeIndicator
|
||||||
|
key={`${user.id}_${shape.id}_indicator`}
|
||||||
|
shape={shape}
|
||||||
|
color={user.color}
|
||||||
|
meta={meta}
|
||||||
|
isHovered
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -242,18 +242,23 @@ const tlcss = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-selected {
|
.tl-indicator {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: var(--tl-selectStroke);
|
|
||||||
stroke-width: calc(1.5px * var(--tl-scale));
|
stroke-width: calc(1.5px * var(--tl-scale));
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-hovered {
|
.tl-user-indicator-bounds {
|
||||||
fill: transparent;
|
border-style: solid;
|
||||||
|
border-width: calc(1px * var(--tl-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-selected {
|
||||||
|
stroke: var(--tl-selectStroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-hovered {
|
||||||
stroke: var(--tl-selectStroke);
|
stroke: var(--tl-selectStroke);
|
||||||
stroke-width: calc(1.5px * var(--tl-scale));
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-bounds {
|
.tl-bounds {
|
||||||
|
|
|
@ -16,8 +16,6 @@ export interface TLPage<T extends TLShape, B extends TLBinding> {
|
||||||
bindings: Record<string, B>
|
bindings: Record<string, B>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLUsers<U extends TLUser = TLUser> = Record<string, U>
|
|
||||||
|
|
||||||
export interface TLPageState {
|
export interface TLPageState {
|
||||||
id: string
|
id: string
|
||||||
selectedIds: string[]
|
selectedIds: string[]
|
||||||
|
@ -34,12 +32,16 @@ export interface TLPageState {
|
||||||
currentParentId?: string | null
|
currentParentId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLUser {
|
export interface TLUser<T extends TLShape> {
|
||||||
id: string
|
id: string
|
||||||
color: string
|
color: string
|
||||||
point: number[]
|
point: number[]
|
||||||
|
selectedIds: string[]
|
||||||
|
activeShapes: T[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>
|
||||||
|
|
||||||
export interface TLHandle {
|
export interface TLHandle {
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: number
|
||||||
|
|
|
@ -2,15 +2,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser, Data } from '@tldraw/tldraw'
|
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser, Data } from '@tldraw/tldraw'
|
||||||
import { createClient, Presence } from '@liveblocks/client'
|
import { createClient, Presence } from '@liveblocks/client'
|
||||||
import {
|
import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
|
||||||
LiveblocksProvider,
|
|
||||||
RoomProvider,
|
|
||||||
useErrorListener,
|
|
||||||
useObject,
|
|
||||||
useSelf,
|
|
||||||
useOthers,
|
|
||||||
useMyPresence,
|
|
||||||
} from '@liveblocks/react'
|
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
|
|
||||||
interface TLDrawUserPresence extends Presence {
|
interface TLDrawUserPresence extends Presence {
|
||||||
|
@ -21,9 +13,10 @@ const publicAPIKey = 'pk_live_1LJGGaqBSNLjLT-4Jalkl-U9'
|
||||||
|
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
publicApiKey: publicAPIKey,
|
publicApiKey: publicAPIKey,
|
||||||
|
throttle: 80,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ROOM_ID = 'mp-test-1'
|
const ROOM_ID = 'mp-test-2'
|
||||||
|
|
||||||
export function Multiplayer() {
|
export function Multiplayer() {
|
||||||
return (
|
return (
|
||||||
|
@ -84,7 +77,7 @@ function TLDrawWrapper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the client updates its presence, update the room
|
// When the client updates its presence, update the room
|
||||||
if (reason === 'patch:room:self:update' && state.room) {
|
if (state.room && (reason === 'patch:room:self:update' || reason === 'patch:selected')) {
|
||||||
const room = client.getRoom(ROOM_ID)
|
const room = client.getRoom(ROOM_ID)
|
||||||
if (!room) return
|
if (!room) return
|
||||||
const { userId, users } = state.room
|
const { userId, users } = state.room
|
||||||
|
@ -110,8 +103,7 @@ function TLDrawWrapper() {
|
||||||
tlstate.updateUsers(
|
tlstate.updateUsers(
|
||||||
others
|
others
|
||||||
.toArray()
|
.toArray()
|
||||||
.filter((other) => other.presence)
|
.map((other) => other.presence?.user)
|
||||||
.map((other) => other.presence!.user)
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -131,6 +123,19 @@ function TLDrawWrapper() {
|
||||||
// Only merge the change if it caused by someone else
|
// Only merge the change if it caused by someone else
|
||||||
if (docObject.uuid !== docId) {
|
if (docObject.uuid !== docId) {
|
||||||
tlstate.mergeDocument(docObject.document)
|
tlstate.mergeDocument(docObject.document)
|
||||||
|
} else {
|
||||||
|
tlstate.updateUsers(
|
||||||
|
Object.values(tlstate.state.room.users).map((user) => {
|
||||||
|
const activeShapes = user.activeShapes
|
||||||
|
.map((shape) => docObject.document.pages[tlstate.currentPageId].shapes[shape.id])
|
||||||
|
.filter(Boolean)
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
activeShapes: activeShapes,
|
||||||
|
selectedIds: activeShapes.map((shape) => shape.id),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { SVGContainer, ShapeUtil } from '@tldraw/core'
|
import { SVGContainer, ShapeUtil } from '@tldraw/core'
|
||||||
import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles'
|
import { defaultStyle } from '~shape/shape-styles'
|
||||||
import {
|
import { GroupShape, TLDrawShapeType, TLDrawToolType, ColorStyle, TLDrawMeta } from '~types'
|
||||||
GroupShape,
|
|
||||||
TLDrawShapeType,
|
|
||||||
TLDrawToolType,
|
|
||||||
ColorStyle,
|
|
||||||
DashStyle,
|
|
||||||
TLDrawMeta,
|
|
||||||
} from '~types'
|
|
||||||
import { getBoundsRectangle } from '../shared'
|
import { getBoundsRectangle } from '../shared'
|
||||||
import css from '~styles'
|
import css from '~styles'
|
||||||
|
|
||||||
|
|
|
@ -52,19 +52,20 @@ export class BrushSession implements Session {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// if (
|
const currentSelectedIds = data.document.pageStates[data.appState.currentPageId].selectedIds
|
||||||
// selectedIds.size === pageState.selectedIds.length &&
|
|
||||||
// pageState.selectedIds.every((id) => selectedIds.has(id))
|
const didChange =
|
||||||
// ) {
|
selectedIds.size !== currentSelectedIds.length ||
|
||||||
// return {}
|
currentSelectedIds.some((id) => !selectedIds.has(id))
|
||||||
// }
|
|
||||||
|
const afterSelectedIds = didChange ? Array.from(selectedIds.values()) : currentSelectedIds
|
||||||
|
|
||||||
return {
|
return {
|
||||||
document: {
|
document: {
|
||||||
pageStates: {
|
pageStates: {
|
||||||
[currentPageId]: {
|
[currentPageId]: {
|
||||||
brush,
|
brush,
|
||||||
selectedIds: Array.from(selectedIds.values()),
|
selectedIds: afterSelectedIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { TLDR } from './tldr'
|
||||||
import { defaultStyle } from '~shape'
|
import { defaultStyle } from '~shape'
|
||||||
import * as Sessions from './session'
|
import * as Sessions from './session'
|
||||||
import * as Commands from './command'
|
import * as Commands from './command'
|
||||||
|
import { sample, USER_COLORS } from './utils'
|
||||||
|
|
||||||
const defaultDocument: TLDrawDocument = {
|
const defaultDocument: TLDrawDocument = {
|
||||||
id: 'doc',
|
id: 'doc',
|
||||||
|
@ -103,8 +104,10 @@ const defaultState: Data = {
|
||||||
users: {
|
users: {
|
||||||
[uuid]: {
|
[uuid]: {
|
||||||
id: uuid,
|
id: uuid,
|
||||||
color: 'dodgerBlue',
|
color: sample(USER_COLORS),
|
||||||
point: [100, 100],
|
point: [100, 100],
|
||||||
|
selectedIds: [],
|
||||||
|
activeShapes: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -336,6 +339,8 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentPageId = data.appState.currentPageId
|
||||||
|
|
||||||
// Remove any exited users
|
// Remove any exited users
|
||||||
if (data.room !== prev.room) {
|
if (data.room !== prev.room) {
|
||||||
Object.values(prev.room.users).forEach((user) => {
|
Object.values(prev.room.users).forEach((user) => {
|
||||||
|
@ -345,7 +350,16 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPageId = data.appState.currentPageId
|
const currentPage = data.document.pages[currentPageId]
|
||||||
|
const currentPageState = data.document.pageStates[currentPageId]
|
||||||
|
|
||||||
|
// Update the room presence selected ids
|
||||||
|
data.room.users[data.room.userId].selectedIds = currentPageState.selectedIds
|
||||||
|
|
||||||
|
// Update the room presence active shapes
|
||||||
|
data.room.users[data.room.userId].activeShapes = currentPageState.selectedIds.map(
|
||||||
|
(id) => currentPage.shapes[id]
|
||||||
|
)
|
||||||
|
|
||||||
// Apply selected style change, if any
|
// Apply selected style change, if any
|
||||||
|
|
||||||
|
@ -1471,7 +1485,9 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* @param push Whether to add the ids to the current selection instead.
|
* @param push Whether to add the ids to the current selection instead.
|
||||||
*/
|
*/
|
||||||
private setSelectedIds = (ids: string[], push = false): this => {
|
private setSelectedIds = (ids: string[], push = false): this => {
|
||||||
// Also clear any pasted center
|
const nextIds = push ? [...this.pageState.selectedIds, ...ids] : [...ids]
|
||||||
|
|
||||||
|
console.log('selecting ids', ids, nextIds)
|
||||||
|
|
||||||
return this.patchState(
|
return this.patchState(
|
||||||
{
|
{
|
||||||
|
@ -1482,7 +1498,15 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
document: {
|
document: {
|
||||||
pageStates: {
|
pageStates: {
|
||||||
[this.currentPageId]: {
|
[this.currentPageId]: {
|
||||||
selectedIds: push ? [...this.pageState.selectedIds, ...ids] : [...ids],
|
selectedIds: nextIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room: {
|
||||||
|
users: {
|
||||||
|
[this.currentUser.id]: {
|
||||||
|
...this.currentUser,
|
||||||
|
selectedIds: nextIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3095,6 +3119,10 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentUser() {
|
||||||
|
return this.state.room.users[this.state.room.userId]
|
||||||
|
}
|
||||||
|
|
||||||
get centerPoint() {
|
get centerPoint() {
|
||||||
return Vec.round(Utils.getBoundsCenter(this.bounds))
|
return Vec.round(Utils.getBoundsCenter(this.bounds))
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,3 +52,20 @@ export const EASING_STRINGS: Record<Easing, string> = {
|
||||||
easeOutExpo: `(t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t))`,
|
easeOutExpo: `(t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t))`,
|
||||||
easeInOutExpo: `(t) => t <= 0 ? 0 : t >= 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2`,
|
easeInOutExpo: `(t) => t <= 0 ? 0 : t >= 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const USER_COLORS = [
|
||||||
|
'#EC5E41',
|
||||||
|
'#F2555A',
|
||||||
|
'#F04F88',
|
||||||
|
'#E34BA9',
|
||||||
|
'#BD54C6',
|
||||||
|
'#9D5BD2',
|
||||||
|
'#7B66DC',
|
||||||
|
'#02B1CC',
|
||||||
|
'#11B3A3',
|
||||||
|
'#39B178',
|
||||||
|
'#55B467',
|
||||||
|
'#FF802B',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const sample = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]
|
||||||
|
|
|
@ -40,8 +40,8 @@ export interface TLDrawMeta {
|
||||||
isDarkMode: boolean
|
isDarkMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLDrawUser extends TLUser {
|
export interface TLDrawUser extends TLUser<TLDrawShape> {
|
||||||
id: string
|
activeShapes: TLDrawShape[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShapeProps<
|
export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShapeProps<
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { TLDraw, TLDrawState, Data, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
|
import { TLDraw, TLDrawState, Data, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
|
||||||
import * as gtag from '-utils/gtag'
|
import * as gtag from '-utils/gtag'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
@ -5,7 +6,7 @@ import { createClient, Presence } from '@liveblocks/client'
|
||||||
import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react'
|
import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react'
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
|
|
||||||
interface TLDrawPresence extends Presence {
|
interface TLDrawUserPresence extends Presence {
|
||||||
user: TLDrawUser
|
user: TLDrawUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ function Editor({ id }: { id: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the client updates its presence, update the room
|
// When the client updates its presence, update the room
|
||||||
if (reason === 'patch:room:self:update' && state.room) {
|
if (state.room && (reason === 'patch:room:self:update' || reason === 'patch:selected')) {
|
||||||
const room = client.getRoom(id)
|
const room = client.getRoom(id)
|
||||||
if (!room) return
|
if (!room) return
|
||||||
const { userId, users } = state.room
|
const { userId, users } = state.room
|
||||||
|
@ -98,15 +99,18 @@ function Editor({ id }: { id: string }) {
|
||||||
|
|
||||||
// Update the user's presence with the user from state
|
// Update the user's presence with the user from state
|
||||||
const { users, userId } = tlstate.state.room
|
const { users, userId } = tlstate.state.room
|
||||||
room.updatePresence({ id: userId, user: users[userId] })
|
room.updatePresence({
|
||||||
|
id: userId,
|
||||||
|
user: users[userId],
|
||||||
|
shapes: Object.fromEntries(tlstate.selectedIds.map((id) => [id, tlstate.getShape(id)])),
|
||||||
|
})
|
||||||
|
|
||||||
// Subscribe to presence changes; when others change, update the state
|
// Subscribe to presence changes; when others change, update the state
|
||||||
room.subscribe<TLDrawPresence>('others', (others) => {
|
room.subscribe<TLDrawUserPresence>('others', (others) => {
|
||||||
tlstate.updateUsers(
|
tlstate.updateUsers(
|
||||||
others
|
others
|
||||||
.toArray()
|
.toArray()
|
||||||
.filter((other) => other.presence)
|
.filter((other) => other.presence)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
.map((other) => other.presence!.user)
|
.map((other) => other.presence!.user)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
)
|
)
|
||||||
|
@ -127,6 +131,19 @@ function Editor({ id }: { id: string }) {
|
||||||
// Only merge the change if it caused by someone else
|
// Only merge the change if it caused by someone else
|
||||||
if (docObject.uuid !== docId) {
|
if (docObject.uuid !== docId) {
|
||||||
tlstate.mergeDocument(docObject.document)
|
tlstate.mergeDocument(docObject.document)
|
||||||
|
} else {
|
||||||
|
tlstate.updateUsers(
|
||||||
|
Object.values(tlstate.state.room.users).map((user) => {
|
||||||
|
const activeShapes = user.activeShapes
|
||||||
|
.map((shape) => docObject.document.pages[tlstate.currentPageId].shapes[shape.id])
|
||||||
|
.filter(Boolean)
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
activeShapes: activeShapes,
|
||||||
|
selectedIds: activeShapes.map((shape) => shape.id),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue