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:
David Sheldrick 2023-05-18 11:59:46 +01:00 committed by GitHub
parent a1e72014c6
commit 6a7dc12162
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 31 additions and 206 deletions

View file

@ -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)

View file

@ -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>

View file

@ -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}

View file

@ -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} />
))}
</>
)
})

View file

@ -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 {

View file

@ -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',

View file

@ -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
} }