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