diff --git a/editor/liveblocks.config.ts b/editor/liveblocks.config.ts index 479d9d49b2dc..3b9bc2a81a00 100644 --- a/editor/liveblocks.config.ts +++ b/editor/liveblocks.config.ts @@ -85,6 +85,7 @@ export const { useUpdateMyPresence, useSelf, useOthers, + useOthersListener, useOthersMapped, useOthersConnectionIds, useOther, diff --git a/editor/src/components/canvas/multiplayer-cursors.tsx b/editor/src/components/canvas/multiplayer-cursors.tsx index 67493449d6ea..283bcfb170ab 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -1,6 +1,14 @@ +import type { User } from '@liveblocks/client' import { motion } from 'framer-motion' import React from 'react' -import { useOthers, useRoom, useSelf, useUpdateMyPresence } from '../../../liveblocks.config' +import type { Presence, UserMeta } from '../../../liveblocks.config' +import { + useOthers, + useOthersListener, + useRoom, + useSelf, + useUpdateMyPresence, +} from '../../../liveblocks.config' import { useAddMyselfToCollaborators } from '../../core/commenting/comment-hooks' import type { CanvasPoint } from '../../core/shared/math-utils' import { pointsEqual, windowPoint } from '../../core/shared/math-utils' @@ -10,6 +18,7 @@ import { normalizeOthersList, possiblyUniqueColor, } from '../../core/shared/multiplayer' +import { assertNever } from '../../core/shared/utils' import { useKeepShallowReferenceEquality } from '../../utils/react-performance' import { UtopiaTheme, useColorTheme } from '../../uuiui' import type { EditorAction } from '../editor/action-types' @@ -25,18 +34,18 @@ export const MultiplayerPresence = React.memo(() => { const dispatch = useDispatch() const room = useRoom() - const self = useSelf() - const others = useOthers((list) => normalizeOthersList(self.id, list)) + const me = useSelf() + const others = useOthers((list) => normalizeOthersList(me.id, list)) const updateMyPresence = useUpdateMyPresence() - const selfColorIndex = React.useMemo(() => self.presence.colorIndex, [self.presence]) + const myColorIndex = React.useMemo(() => me.presence.colorIndex, [me.presence]) const otherColorIndices = useKeepShallowReferenceEquality( others.map((other) => other.presence.colorIndex), ) const getColorIndex = React.useCallback(() => { - return selfColorIndex ?? possiblyUniqueColor(otherColorIndices) - }, [selfColorIndex, otherColorIndices]) + return myColorIndex ?? possiblyUniqueColor(otherColorIndices) + }, [myColorIndex, otherColorIndices]) const loginState = useEditorState( Substores.userState, (store) => store.userState.loginState, @@ -107,8 +116,8 @@ export const MultiplayerPresence = React.memo(() => { MultiplayerPresence.displayName = 'MultiplayerPresence' const MultiplayerCursors = React.memo(() => { - const self = useSelf() - const others = useOthers((list) => normalizeOthersList(self.id, list)) + const me = useSelf() + const others = useOthers((list) => normalizeOthersList(me.id, list)) return (
{ const colorTheme = useColorTheme() const dispatch = useDispatch() - const self = useSelf() - const others = useOthers((list) => normalizeOthersList(self.id, list)) + const room = useRoom() const mode = useEditorState( Substores.restOfEditor, @@ -243,34 +251,59 @@ const FollowingOverlay = React.memo(() => { 'FollowingOverlay canvasOffset', ) + const isFollowTarget = React.useCallback( + (other: User): boolean => { + return isFollowMode(mode) && other.id === mode.playerId + }, + [mode], + ) + + const resetFollowed = React.useCallback(() => { + dispatch([switchEditorMode(EditorModes.selectMode(null, false, 'none'))]) + }, [dispatch]) + const followed = React.useMemo(() => { - return others.find((other) => isFollowMode(mode) && other.id === mode.playerId) - }, [others, mode]) + return room.getOthers().find(isFollowTarget) ?? null + }, [room, isFollowTarget]) - React.useEffect(() => { - // when following another player, apply its canvas constraints - if (followed == null) { - if (isFollowMode(mode)) { - // reset if the other player disconnects - dispatch([switchEditorMode(EditorModes.selectMode(null, false, 'none'))]) + const updateCanvasFromOtherPresence = React.useCallback( + (presence: Presence) => { + let actions: EditorAction[] = [] + if (presence.canvasScale != null && presence.canvasScale !== canvasScale) { + actions.push(CanvasActions.zoom(presence.canvasScale, null)) } - return - } + if (presence.canvasOffset != null && !pointsEqual(presence.canvasOffset, canvasOffset)) { + actions.push(CanvasActions.positionCanvas(presence.canvasOffset)) + } + if (actions.length > 0) { + dispatch(actions) + } + }, + [dispatch, canvasScale, canvasOffset], + ) - let actions: EditorAction[] = [] - if (followed.presence.canvasScale != null && followed.presence.canvasScale !== canvasScale) { - actions.push(CanvasActions.zoom(followed.presence.canvasScale, null)) - } - if ( - followed.presence.canvasOffset != null && - !pointsEqual(followed.presence.canvasOffset, canvasOffset) - ) { - actions.push(CanvasActions.positionCanvas(followed.presence.canvasOffset)) - } - if (actions.length > 0) { - dispatch(actions) + useOthersListener((event) => { + if (isFollowMode(mode)) { + switch (event.type) { + case 'enter': + case 'update': + if (isFollowTarget(event.user)) { + updateCanvasFromOtherPresence(event.user.presence) + } + break + case 'leave': + if (isFollowTarget(event.user)) { + resetFollowed() + } + break + case 'reset': + resetFollowed() + break + default: + assertNever(event) + } } - }, [followed, canvasScale, canvasOffset, dispatch, mode]) + }) if (followed == null) { return null diff --git a/editor/src/components/user-bar.tsx b/editor/src/components/user-bar.tsx index 938e8b129787..297eafdd6a44 100644 --- a/editor/src/components/user-bar.tsx +++ b/editor/src/components/user-bar.tsx @@ -61,11 +61,11 @@ const MultiplayerUserBar = React.memo(() => { const dispatch = useDispatch() const colorTheme = useColorTheme() - const self = useSelf() - const myName = normalizeMultiplayerName(self.presence.name) + const me = useSelf() + const myName = normalizeMultiplayerName(me.presence.name) const others = useOthers((list) => - normalizeOthersList(self.id, list).map((other) => ({ + normalizeOthersList(me.id, list).map((other) => ({ id: other.id, name: other.presence.name, colorIndex: other.presence.colorIndex, @@ -98,7 +98,7 @@ const MultiplayerUserBar = React.memo(() => { [dispatch, mode], ) - if (self.presence.name == null) { + if (me.presence.name == null) { // it may still be loading, so fallback until it sorts itself out return } @@ -162,7 +162,7 @@ const MultiplayerUserBar = React.memo(() => { name={multiplayerInitialsFromName(myName)} tooltip={`${myName} (you)`} color={{ background: colorTheme.bg3.value, foreground: colorTheme.fg1.value }} - picture={self.presence.picture} + picture={me.presence.picture} />
diff --git a/editor/src/core/commenting/comment-hooks.tsx b/editor/src/core/commenting/comment-hooks.tsx index af7daf761372..7979b51fb929 100644 --- a/editor/src/core/commenting/comment-hooks.tsx +++ b/editor/src/core/commenting/comment-hooks.tsx @@ -10,8 +10,8 @@ export function useCanvasCommentThread(x: number, y: number): ThreadData