[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": {
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"signia": "0.1.1",
|
||||
"signia-react": "0.1.1"
|
||||
"signia": "0.1.4",
|
||||
"signia-react": "0.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2650,9 +2650,15 @@ export const useApp: () => App;
|
|||
// @public (undocumented)
|
||||
export function useContainer(): HTMLDivElement;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function usePeerIds(): TLUserId[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function usePrefersReducedMotion(): boolean;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function usePresence(userId: TLUserId): null | TLInstancePresence;
|
||||
|
||||
// @public (undocumented)
|
||||
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 { useContainer } from './lib/hooks/useContainer'
|
||||
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 { useReactor } from './lib/hooks/useReactor'
|
||||
export { useUrlState } from './lib/hooks/useUrlState'
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useQuickReactor } from '../hooks/useQuickReactor'
|
|||
import { useScreenBounds } from '../hooks/useScreenBounds'
|
||||
import { debugFlags } from '../utils/debug-flags'
|
||||
import { LiveCollaborators } from './LiveCollaborators'
|
||||
import { LiveCollaboratorsNext } from './LiveCollaboratorsNext'
|
||||
import { SelectionBg } from './SelectionBg'
|
||||
import { SelectionFg } from './SelectionFg'
|
||||
import { Shape } from './Shape'
|
||||
|
@ -134,7 +135,11 @@ export const Canvas = track(function Canvas({
|
|||
<HintedShapeIndicator />
|
||||
<SnapLinesWrapper />
|
||||
<HandlesWrapper />
|
||||
{process.env.NEXT_PUBLIC_TLDRAW_NEW_COLLABORATORS ? (
|
||||
<LiveCollaboratorsNext />
|
||||
) : (
|
||||
<LiveCollaborators />
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</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-dom: ^18.2.0
|
||||
react-router-dom: ^6.9.0
|
||||
signia: 0.1.1
|
||||
signia-react: 0.1.1
|
||||
signia: 0.1.4
|
||||
signia-react: 0.1.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
@ -16724,21 +16724,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"signia-react@npm:0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "signia-react@npm:0.1.1"
|
||||
"signia-react@npm:0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "signia-react@npm:0.1.4"
|
||||
dependencies:
|
||||
signia: 0.1.1
|
||||
signia: 0.1.4
|
||||
peerDependencies:
|
||||
react: ^18
|
||||
checksum: 134e32dad5f9b4e54f13d00e8ea0ed56d3b80a4fa9dcd469071906ee2501fec7f0dde8097f1c410be27c8dabf0bf81d3d73d76d579da22b3904a14d5005e872a
|
||||
checksum: 7e443c9b94e60cd1eeb3726d048d66ab4ff6b26c14b4ca0fc9542ce2cd5997071b1d7a870481e5d88e7d0442332d65a14239a831b1f987463f4a9ca196d7de45
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"signia@npm:0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "signia@npm:0.1.1"
|
||||
checksum: 0fc961c533266604241543d079edd8a8d28264ec769d28c660a61d88d0c1da4ddbab8a04f89b5ff2c30474adfc36da98db8a22c881364de127d53c8315613b61
|
||||
"signia@npm:0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "signia@npm:0.1.4"
|
||||
checksum: 0daab872c3e335c74a464a3b592cfa0fc5e1e0b65e61eecdf4e0476830791543add8a661b9b33ce9e0f6c3f0f8093ef1965955a64ec510cfcaaf15066bd25e94
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in a new issue