[feat] new LiveCollaborators behind feature flag (#1219)

In this PR I'm adding new versions of the `LiveCollaborators` and
`Collaborators` components for the ephemeral state work. They are behind
a feature flag for now.
This commit is contained in:
David Sheldrick 2023-05-03 14:48:46 +01:00 committed by GitHub
parent 9e5de0c48e
commit 00e3d20dc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 167 additions and 14 deletions

View file

@ -44,7 +44,7 @@
"dependencies": { "dependencies": {
"@tldraw/tldraw": "workspace:*", "@tldraw/tldraw": "workspace:*",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"signia": "0.1.1", "signia": "0.1.4",
"signia-react": "0.1.1" "signia-react": "0.1.4"
} }
} }

View file

@ -2650,9 +2650,15 @@ export const useApp: () => App;
// @public (undocumented) // @public (undocumented)
export function useContainer(): HTMLDivElement; export function useContainer(): HTMLDivElement;
// @internal (undocumented)
export function usePeerIds(): TLUserId[];
// @public (undocumented) // @public (undocumented)
export function usePrefersReducedMotion(): boolean; export function usePrefersReducedMotion(): boolean;
// @internal (undocumented)
export function usePresence(userId: TLUserId): null | TLInstancePresence;
// @public (undocumented) // @public (undocumented)
export function useQuickReactor(name: string, reactFn: () => void, deps?: any[]): void; export function useQuickReactor(name: string, reactFn: () => void, deps?: any[]): void;

View file

@ -171,6 +171,8 @@ export { normalizeWheel } from './lib/hooks/shared'
export { useApp } from './lib/hooks/useApp' export { useApp } from './lib/hooks/useApp'
export { useContainer } from './lib/hooks/useContainer' export { useContainer } from './lib/hooks/useContainer'
export type { TLEditorComponents } from './lib/hooks/useEditorComponents' export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
export { usePeerIds } from './lib/hooks/usePeerIds'
export { usePresence } from './lib/hooks/usePresence'
export { useQuickReactor } from './lib/hooks/useQuickReactor' export { useQuickReactor } from './lib/hooks/useQuickReactor'
export { useReactor } from './lib/hooks/useReactor' export { useReactor } from './lib/hooks/useReactor'
export { useUrlState } from './lib/hooks/useUrlState' export { useUrlState } from './lib/hooks/useUrlState'

View file

@ -17,6 +17,7 @@ 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'
@ -134,7 +135,11 @@ export const Canvas = track(function Canvas({
<HintedShapeIndicator /> <HintedShapeIndicator />
<SnapLinesWrapper /> <SnapLinesWrapper />
<HandlesWrapper /> <HandlesWrapper />
{process.env.NEXT_PUBLIC_TLDRAW_NEW_COLLABORATORS ? (
<LiveCollaboratorsNext />
) : (
<LiveCollaborators /> <LiveCollaborators />
)}
</g> </g>
</svg> </svg>
</div> </div>

View file

@ -0,0 +1,84 @@
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 (
<g>
{peerIds.map((id) => (
<Collaborator key={id} userId={id} />
))}
</g>
)
})
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}
nameTag={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

@ -0,0 +1,25 @@
import uniq from 'lodash.uniq'
import { useMemo } from 'react'
import { useComputed, useValue } from 'signia-react'
import { useApp } from './useApp'
// TODO: maybe move this to a computed property on the App class?
/**
* @returns The list of peer UserIDs
* @internal
*/
export function usePeerIds() {
const app = useApp()
const $presences = useMemo(() => {
return app.store.query.records('instance_presence')
}, [app])
const $userIds = useComputed(
'userIds',
() => uniq($presences.value.map((p) => p.userId)).sort(),
{ isEqual: (a, b) => a.join(',') === b.join?.(',') },
[$presences]
)
return useValue($userIds)
}

View file

@ -0,0 +1,31 @@
import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
import { useMemo } from 'react'
import { useValue } from 'signia-react'
import { useApp } from './useApp'
// TODO: maybe move this to a computed property on the App class?
/**
* @returns The list of peer UserIDs
* @internal
*/
export function usePresence(userId: TLUserId): TLInstancePresence | null {
const app = useApp()
const $presences = useMemo(() => {
return app.store.query.records('instance_presence', () => ({
userId: { eq: userId },
}))
}, [app, userId])
const latestPresence = useValue(
`latestPresence:${userId}`,
() => {
return $presences.value
.slice()
.sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0]
},
[]
)
return latestPresence ?? null
}

View file

@ -9483,8 +9483,8 @@ __metadata:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
react-router-dom: ^6.9.0 react-router-dom: ^6.9.0
signia: 0.1.1 signia: 0.1.4
signia-react: 0.1.1 signia-react: 0.1.4
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -16724,21 +16724,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"signia-react@npm:0.1.1": "signia-react@npm:0.1.4":
version: 0.1.1 version: 0.1.4
resolution: "signia-react@npm:0.1.1" resolution: "signia-react@npm:0.1.4"
dependencies: dependencies:
signia: 0.1.1 signia: 0.1.4
peerDependencies: peerDependencies:
react: ^18 react: ^18
checksum: 134e32dad5f9b4e54f13d00e8ea0ed56d3b80a4fa9dcd469071906ee2501fec7f0dde8097f1c410be27c8dabf0bf81d3d73d76d579da22b3904a14d5005e872a checksum: 7e443c9b94e60cd1eeb3726d048d66ab4ff6b26c14b4ca0fc9542ce2cd5997071b1d7a870481e5d88e7d0442332d65a14239a831b1f987463f4a9ca196d7de45
languageName: node languageName: node
linkType: hard linkType: hard
"signia@npm:0.1.1": "signia@npm:0.1.4":
version: 0.1.1 version: 0.1.4
resolution: "signia@npm:0.1.1" resolution: "signia@npm:0.1.4"
checksum: 0fc961c533266604241543d079edd8a8d28264ec769d28c660a61d88d0c1da4ddbab8a04f89b5ff2c30474adfc36da98db8a22c881364de127d53c8315613b61 checksum: 0daab872c3e335c74a464a3b592cfa0fc5e1e0b65e61eecdf4e0476830791543add8a661b9b33ce9e0f6c3f0f8093ef1965955a64ec510cfcaaf15066bd25e94
languageName: node languageName: node
linkType: hard linkType: hard