[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:
parent
9e5de0c48e
commit
00e3d20dc8
8 changed files with 167 additions and 14 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
|
|
84
packages/editor/src/lib/components/LiveCollaboratorsNext.tsx
Normal file
84
packages/editor/src/lib/components/LiveCollaboratorsNext.tsx
Normal 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} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
25
packages/editor/src/lib/hooks/usePeerIds.ts
Normal file
25
packages/editor/src/lib/hooks/usePeerIds.ts
Normal 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)
|
||||||
|
}
|
31
packages/editor/src/lib/hooks/usePresence.ts
Normal file
31
packages/editor/src/lib/hooks/usePresence.ts
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue