Switch to new collaborators component (#1405)
Followup to https://github.com/tldraw/brivate/pull/1584 - Removes the old collaborators component, replacing with the new one. - Removes the associated debug flag ### Change Type <!-- 💡 Indicate the type of change your pull request is. --> <!-- 🤷♀️ If you're not sure, don't select anything --> <!-- ✂️ Feel free to delete unselected options --> <!-- To select one, put an x in the box: [x] --> - [ ] `patch` — Bug Fix - [ ] `minor` — New Feature - [x] `major` — Breaking Change - [ ] `dependencies` — Dependency Update (publishes a `patch` release, for devDependencies use `internal`) - [ ] `documentation` — Changes to the documentation only (will not publish a new version) - [ ] `tests` — Changes to any testing-related code only (will not publish a new version) - [ ] `internal` — Any other changes that don't affect the published package (will not publish a new version) ### Test Plan Check that multiplayer presence UI renders correctly - cursors - cursor hints (when a peer's cursor goes off the screen) - selection brush box - selection/erasing brush - selected shape(s) outline ### Release Notes - [Breaking] Removes the old version of LiveCollaborators, replacing it with the new one based on `TLInstancePresence`
This commit is contained in:
parent
a1e72014c6
commit
6a7dc12162
7 changed files with 31 additions and 206 deletions
|
@ -619,7 +619,6 @@ export const debugFlags: {
|
||||||
peopleMenu: Atom<boolean, unknown>;
|
peopleMenu: Atom<boolean, unknown>;
|
||||||
logMessages: Atom<never[], unknown>;
|
logMessages: Atom<never[], unknown>;
|
||||||
resetConnectionEveryPing: Atom<boolean, unknown>;
|
resetConnectionEveryPing: Atom<boolean, unknown>;
|
||||||
newLiveCollaborators: Atom<boolean, unknown>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { useQuickReactor } from '../hooks/useQuickReactor'
|
||||||
import { useScreenBounds } from '../hooks/useScreenBounds'
|
import { useScreenBounds } from '../hooks/useScreenBounds'
|
||||||
import { debugFlags } from '../utils/debug-flags'
|
import { debugFlags } from '../utils/debug-flags'
|
||||||
import { LiveCollaborators } from './LiveCollaborators'
|
import { LiveCollaborators } from './LiveCollaborators'
|
||||||
import { LiveCollaboratorsNext } from './LiveCollaboratorsNext'
|
|
||||||
import { SelectionBg } from './SelectionBg'
|
import { SelectionBg } from './SelectionBg'
|
||||||
import { SelectionFg } from './SelectionFg'
|
import { SelectionFg } from './SelectionFg'
|
||||||
import { Shape } from './Shape'
|
import { Shape } from './Shape'
|
||||||
|
@ -128,11 +127,7 @@ export const Canvas = track(function Canvas({
|
||||||
<HintedShapeIndicator />
|
<HintedShapeIndicator />
|
||||||
<SnapLinesWrapper />
|
<SnapLinesWrapper />
|
||||||
<SelectionFg />
|
<SelectionFg />
|
||||||
{debugFlags.newLiveCollaborators.value ? (
|
<LiveCollaborators />
|
||||||
<LiveCollaboratorsNext />
|
|
||||||
) : (
|
|
||||||
<LiveCollaborators />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,60 +1,24 @@
|
||||||
import { Box2d } from '@tldraw/primitives'
|
import { TLUserId } from '@tldraw/tlschema'
|
||||||
import { TLUserPresence } from '@tldraw/tlschema'
|
import { track } from 'signia-react'
|
||||||
import React, { useMemo } from 'react'
|
|
||||||
import { track, useAtom } from 'signia-react'
|
|
||||||
import { useApp } from '../hooks/useApp'
|
import { useApp } from '../hooks/useApp'
|
||||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||||
|
import { usePeerIds } from '../hooks/usePeerIds'
|
||||||
export const useActivePresences = () => {
|
import { usePresence } from '../hooks/usePresence'
|
||||||
const app = useApp()
|
|
||||||
const time = useAtom('time', Date.now())
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const interval = setInterval(() => time.set(Date.now()), 1000 * 5)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [time])
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
app.store.query.records('user_presence', () => ({
|
|
||||||
lastActivityTimestamp: { gt: time.value - COLLABORATOR_INACTIVITY_TIMEOUT },
|
|
||||||
userId: { neq: app.userId },
|
|
||||||
})),
|
|
||||||
[app, time]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LiveCollaborators = track(function Collaborators() {
|
export const LiveCollaborators = track(function Collaborators() {
|
||||||
const app = useApp()
|
const peerIds = usePeerIds()
|
||||||
const { viewportPageBounds, zoomLevel } = app
|
|
||||||
const presences = useActivePresences()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{presences.value.map((userPresence) => (
|
{peerIds.map((id) => (
|
||||||
<Collaborator
|
<Collaborator key={id} userId={id} />
|
||||||
key={userPresence.id}
|
|
||||||
presence={userPresence}
|
|
||||||
viewport={viewportPageBounds}
|
|
||||||
zoom={zoomLevel}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const COLLABORATOR_INACTIVITY_TIMEOUT = 1000 * 10
|
const Collaborator = track(function Collaborator({ userId }: { userId: TLUserId }) {
|
||||||
|
|
||||||
const Collaborator = track(function Collaborator({
|
|
||||||
presence,
|
|
||||||
viewport,
|
|
||||||
zoom,
|
|
||||||
}: {
|
|
||||||
presence: TLUserPresence
|
|
||||||
viewport: Box2d
|
|
||||||
zoom: number
|
|
||||||
}) {
|
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
|
const { viewportPageBounds, zoomLevel } = app
|
||||||
|
|
||||||
const {
|
const {
|
||||||
CollaboratorBrush,
|
CollaboratorBrush,
|
||||||
|
@ -64,48 +28,21 @@ const Collaborator = track(function Collaborator({
|
||||||
CollaboratorShapeIndicator,
|
CollaboratorShapeIndicator,
|
||||||
} = useEditorComponents()
|
} = useEditorComponents()
|
||||||
|
|
||||||
const { userId, color, cursor, lastUsedInstanceId } = presence
|
const latestPresence = usePresence(userId)
|
||||||
|
if (!latestPresence) return null
|
||||||
const pageState = useMemo(
|
|
||||||
() =>
|
|
||||||
lastUsedInstanceId
|
|
||||||
? app.store.query.record('instance_page_state', () => ({
|
|
||||||
instanceId: { eq: lastUsedInstanceId },
|
|
||||||
pageId: { eq: app.currentPageId },
|
|
||||||
}))
|
|
||||||
: null,
|
|
||||||
[app, lastUsedInstanceId]
|
|
||||||
)
|
|
||||||
|
|
||||||
const user = useMemo(
|
|
||||||
() =>
|
|
||||||
app.store.query.record('user', () => ({
|
|
||||||
id: { eq: userId },
|
|
||||||
})),
|
|
||||||
[app, userId]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!lastUsedInstanceId || !pageState || !user) return null
|
|
||||||
const instance = app.store.get(lastUsedInstanceId)
|
|
||||||
if (!instance) return null
|
|
||||||
|
|
||||||
// if the collaborator is on another page, ignore them
|
// if the collaborator is on another page, ignore them
|
||||||
if (instance.currentPageId !== app.currentPageId) return null
|
if (latestPresence.currentPageId !== app.currentPageId) return null
|
||||||
|
|
||||||
if (!pageState.value) return null
|
const { brush, scribble, selectedIds, userName, cursor, color } = latestPresence
|
||||||
if (!user.value) return null
|
|
||||||
|
|
||||||
const { brush, scribble } = instance
|
|
||||||
const { selectedIds } = pageState.value
|
|
||||||
const { name } = user.value
|
|
||||||
|
|
||||||
// Add a little padding to the top-left of the viewport
|
// Add a little padding to the top-left of the viewport
|
||||||
// so that the cursor doesn't get cut off
|
// so that the cursor doesn't get cut off
|
||||||
const isCursorInViewport = !(
|
const isCursorInViewport = !(
|
||||||
cursor.x < viewport.minX - 12 / zoom ||
|
cursor.x < viewportPageBounds.minX - 12 / zoomLevel ||
|
||||||
cursor.y < viewport.minY - 16 / zoom ||
|
cursor.y < viewportPageBounds.minY - 16 / zoomLevel ||
|
||||||
cursor.x > viewport.maxX - 12 / zoom ||
|
cursor.x > viewportPageBounds.maxX - 12 / zoomLevel ||
|
||||||
cursor.y > viewport.maxY - 16 / zoom
|
cursor.y > viewportPageBounds.maxY - 16 / zoomLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -125,8 +62,8 @@ const Collaborator = track(function Collaborator({
|
||||||
key={userId + '_cursor'}
|
key={userId + '_cursor'}
|
||||||
point={cursor}
|
point={cursor}
|
||||||
color={color}
|
color={color}
|
||||||
zoom={zoom}
|
zoom={zoomLevel}
|
||||||
name={name !== 'New User' ? name : null}
|
name={userName !== 'New User' ? userName : null}
|
||||||
/>
|
/>
|
||||||
) : CollaboratorHint ? (
|
) : CollaboratorHint ? (
|
||||||
<CollaboratorHint
|
<CollaboratorHint
|
||||||
|
@ -134,8 +71,8 @@ const Collaborator = track(function Collaborator({
|
||||||
key={userId + '_cursor_hint'}
|
key={userId + '_cursor_hint'}
|
||||||
point={cursor}
|
point={cursor}
|
||||||
color={color}
|
color={color}
|
||||||
zoom={zoom}
|
zoom={zoomLevel}
|
||||||
viewport={viewport}
|
viewport={viewportPageBounds}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{scribble && CollaboratorScribble ? (
|
{scribble && CollaboratorScribble ? (
|
||||||
|
@ -144,7 +81,7 @@ const Collaborator = track(function Collaborator({
|
||||||
key={userId + '_scribble'}
|
key={userId + '_scribble'}
|
||||||
scribble={scribble}
|
scribble={scribble}
|
||||||
color={color}
|
color={color}
|
||||||
zoom={zoom}
|
zoom={zoomLevel}
|
||||||
opacity={0.1}
|
opacity={0.1}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
import { TLUserId } from '@tldraw/tlschema'
|
|
||||||
import { track } from 'signia-react'
|
|
||||||
import { useApp } from '../hooks/useApp'
|
|
||||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
|
||||||
import { usePeerIds } from '../hooks/usePeerIds'
|
|
||||||
import { usePresence } from '../hooks/usePresence'
|
|
||||||
|
|
||||||
export const LiveCollaboratorsNext = track(function Collaborators() {
|
|
||||||
const peerIds = usePeerIds()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{peerIds.map((id) => (
|
|
||||||
<Collaborator key={id} userId={id} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const Collaborator = track(function Collaborator({ userId }: { userId: TLUserId }) {
|
|
||||||
const app = useApp()
|
|
||||||
const { viewportPageBounds, zoomLevel } = app
|
|
||||||
|
|
||||||
const {
|
|
||||||
CollaboratorBrush,
|
|
||||||
CollaboratorScribble,
|
|
||||||
CollaboratorCursor,
|
|
||||||
CollaboratorHint,
|
|
||||||
CollaboratorShapeIndicator,
|
|
||||||
} = useEditorComponents()
|
|
||||||
|
|
||||||
const latestPresence = usePresence(userId)
|
|
||||||
if (!latestPresence) return null
|
|
||||||
|
|
||||||
// if the collaborator is on another page, ignore them
|
|
||||||
if (latestPresence.currentPageId !== app.currentPageId) return null
|
|
||||||
|
|
||||||
const { brush, scribble, selectedIds, userName, cursor, color } = latestPresence
|
|
||||||
|
|
||||||
// Add a little padding to the top-left of the viewport
|
|
||||||
// so that the cursor doesn't get cut off
|
|
||||||
const isCursorInViewport = !(
|
|
||||||
cursor.x < viewportPageBounds.minX - 12 / zoomLevel ||
|
|
||||||
cursor.y < viewportPageBounds.minY - 16 / zoomLevel ||
|
|
||||||
cursor.x > viewportPageBounds.maxX - 12 / zoomLevel ||
|
|
||||||
cursor.y > viewportPageBounds.maxY - 16 / zoomLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{brush && CollaboratorBrush ? (
|
|
||||||
<CollaboratorBrush key={userId + '_brush'} brush={brush} color={color} />
|
|
||||||
) : null}
|
|
||||||
{isCursorInViewport && CollaboratorCursor ? (
|
|
||||||
<CollaboratorCursor
|
|
||||||
key={userId + '_cursor'}
|
|
||||||
point={cursor}
|
|
||||||
color={color}
|
|
||||||
zoom={zoomLevel}
|
|
||||||
name={userName !== 'New User' ? userName : null}
|
|
||||||
/>
|
|
||||||
) : CollaboratorHint ? (
|
|
||||||
<CollaboratorHint
|
|
||||||
key={userId + '_cursor_hint'}
|
|
||||||
point={cursor}
|
|
||||||
color={color}
|
|
||||||
zoom={zoomLevel}
|
|
||||||
viewport={viewportPageBounds}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{scribble && CollaboratorScribble ? (
|
|
||||||
<CollaboratorScribble
|
|
||||||
key={userId + '_scribble'}
|
|
||||||
scribble={scribble}
|
|
||||||
color={color}
|
|
||||||
zoom={zoomLevel}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{CollaboratorShapeIndicator &&
|
|
||||||
selectedIds.map((shapeId) => (
|
|
||||||
<CollaboratorShapeIndicator key={userId + '_' + shapeId} id={shapeId} color={color} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -23,7 +23,6 @@ export const debugFlags = {
|
||||||
peopleMenu: createDebugValue('tldrawPeopleMenu', false),
|
peopleMenu: createDebugValue('tldrawPeopleMenu', false),
|
||||||
logMessages: createDebugValue('tldrawUiLog', []),
|
logMessages: createDebugValue('tldrawUiLog', []),
|
||||||
resetConnectionEveryPing: createDebugValue('tldrawResetConnectionEveryPing', false),
|
resetConnectionEveryPing: createDebugValue('tldrawResetConnectionEveryPing', false),
|
||||||
newLiveCollaborators: createDebugValue('tldrawNewLiveCollaborators', false),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { Box2d, intersectPolygonPolygon, Vec2d } from '@tldraw/primitives'
|
import { Box2d, intersectPolygonPolygon, Vec2d } from '@tldraw/primitives'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { track, useAtom } from 'signia-react'
|
import { track } from 'signia-react'
|
||||||
import { MinimapManager } from './MinimapManager'
|
import { MinimapManager } from './MinimapManager'
|
||||||
|
|
||||||
export interface MinimapProps {
|
export interface MinimapProps {
|
||||||
|
@ -18,26 +18,6 @@ export interface MinimapProps {
|
||||||
viewportFill: string
|
viewportFill: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLLABORATOR_INACTIVITY_TIMEOUT = 10000
|
|
||||||
|
|
||||||
export const useActivePresences = () => {
|
|
||||||
const app = useApp()
|
|
||||||
const time = useAtom('time', Date.now())
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const interval = setInterval(() => time.set(Date.now()), 1000 * 5)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [time])
|
|
||||||
return React.useMemo(
|
|
||||||
() =>
|
|
||||||
app.store.query.records('user_presence', () => ({
|
|
||||||
lastActivityTimestamp: { gt: time.value - COLLABORATOR_INACTIVITY_TIMEOUT },
|
|
||||||
userId: { neq: app.userId },
|
|
||||||
})),
|
|
||||||
[app, time]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Minimap = track(function Minimap({
|
export const Minimap = track(function Minimap({
|
||||||
shapeFill,
|
shapeFill,
|
||||||
selectFill,
|
selectFill,
|
||||||
|
@ -196,7 +176,9 @@ export const Minimap = track(function Minimap({
|
||||||
[app, minimap]
|
[app, minimap]
|
||||||
)
|
)
|
||||||
|
|
||||||
const presences = useActivePresences()
|
const presences = React.useMemo(() => {
|
||||||
|
return app.store.query.records('instance_presence')
|
||||||
|
}, [app])
|
||||||
|
|
||||||
useQuickReactor(
|
useQuickReactor(
|
||||||
'minimap render when pagebounds or collaborators changes',
|
'minimap render when pagebounds or collaborators changes',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { App, TLShapeId, TLUserPresence, uniqueId } from '@tldraw/editor'
|
import { App, TLInstancePresence, TLShapeId, uniqueId } from '@tldraw/editor'
|
||||||
import { Box2d, PI2, Vec2d, clamp } from '@tldraw/primitives'
|
import { Box2d, PI2, Vec2d, clamp } from '@tldraw/primitives'
|
||||||
|
|
||||||
export class MinimapManager {
|
export class MinimapManager {
|
||||||
|
@ -13,7 +13,7 @@ export class MinimapManager {
|
||||||
id = uniqueId()
|
id = uniqueId()
|
||||||
cvs: HTMLCanvasElement | null = null
|
cvs: HTMLCanvasElement | null = null
|
||||||
pageBounds: (Box2d & { id: TLShapeId })[] = []
|
pageBounds: (Box2d & { id: TLShapeId })[] = []
|
||||||
collaborators: TLUserPresence[] = []
|
collaborators: TLInstancePresence[] = []
|
||||||
|
|
||||||
canvasScreenBounds = new Box2d()
|
canvasScreenBounds = new Box2d()
|
||||||
canvasPageBounds = new Box2d()
|
canvasPageBounds = new Box2d()
|
||||||
|
@ -291,13 +291,10 @@ export class MinimapManager {
|
||||||
|
|
||||||
const { currentPageId } = app
|
const { currentPageId } = app
|
||||||
|
|
||||||
let collaborator: TLUserPresence
|
let collaborator: TLInstancePresence
|
||||||
for (let i = 0; i < this.collaborators.length; i++) {
|
for (let i = 0; i < this.collaborators.length; i++) {
|
||||||
collaborator = this.collaborators[i]
|
collaborator = this.collaborators[i]
|
||||||
const instance = collaborator.lastUsedInstanceId
|
if (collaborator.currentPageId !== currentPageId) {
|
||||||
? app.store.get(collaborator.lastUsedInstanceId)
|
|
||||||
: null
|
|
||||||
if (instance?.currentPageId !== currentPageId) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue