diff --git a/editor/src/components/canvas/controls/canvas-offset-wrapper.tsx b/editor/src/components/canvas/controls/canvas-offset-wrapper.tsx index 46caa6238aae..a4fb60951eb6 100644 --- a/editor/src/components/canvas/controls/canvas-offset-wrapper.tsx +++ b/editor/src/components/canvas/controls/canvas-offset-wrapper.tsx @@ -25,6 +25,9 @@ export function useApplyCanvasOffsetToStyle(setScaleToo: boolean): React.RefObje const elementRef = React.useRef(null) const canvasOffsetRef = useRefEditorState((store) => store.editor.canvas.roundedCanvasOffset) const scaleRef = useRefEditorState((store) => store.editor.canvas.scale) + const isScrollAnimationActiveRef = useRefEditorState( + (store) => store.editor.canvas.scrollAnimation, + ) const mode = useEditorState( Substores.restOfEditor, @@ -45,13 +48,15 @@ export function useApplyCanvasOffsetToStyle(setScaleToo: boolean): React.RefObje setScaleToo && scaleRef.current >= 1 ? `${scaleRef.current * 100}%` : '1', ) - elementRef.current.style.setProperty( - 'transition', - isFollowMode(mode) ? `transform ${liveblocksThrottle}ms linear` : 'none', - ) + if (!isScrollAnimationActiveRef.current) { + elementRef.current.style.setProperty( + 'transition', + isFollowMode(mode) ? `transform ${liveblocksThrottle}ms linear` : 'none', + ) + } } }, - [elementRef, setScaleToo, scaleRef, mode], + [setScaleToo, scaleRef, isScrollAnimationActiveRef, mode], ) useSelectorWithCallback( diff --git a/editor/src/components/canvas/controls/comment-indicator.tsx b/editor/src/components/canvas/controls/comment-indicator.tsx index 601cadaccb70..1092c13b770b 100644 --- a/editor/src/components/canvas/controls/comment-indicator.tsx +++ b/editor/src/components/canvas/controls/comment-indicator.tsx @@ -42,6 +42,7 @@ import { setRightMenuTab } from '../../editor/actions/action-creators' import { RightMenuTab } from '../../editor/store/editor-state' import { when } from '../../../utils/react-conditionals' import { CommentRepliesCounter } from './comment-replies-counter' +import { useMyUserId } from '../../../core/shared/multiplayer-hooks' const IndicatorSize = 24 const MagnifyScale = 1.15 @@ -113,20 +114,10 @@ function useCommentBeingComposed(): TemporaryCommentIndicatorProps | null { const collabs = useStorage((storage) => storage.collaborators) - const userId = useEditorState( - Substores.userState, - (store) => { - if (store.userState.loginState.type !== 'LOGGED_IN') { - return null - } - - return store.userState.loginState.user.userId - }, - 'CommentThread userId', - ) + const myUserId = useMyUserId() const collaboratorInfo = React.useMemo(() => { - const collaborator = optionalMap((id) => collabs[id], userId) + const collaborator = optionalMap((id) => collabs[id], myUserId) if (collaborator == null) { return { initials: 'AN', @@ -140,7 +131,7 @@ function useCommentBeingComposed(): TemporaryCommentIndicatorProps | null { color: multiplayerColorFromIndex(collaborator.colorIndex), avatar: collaborator.avatar, } - }, [collabs, userId]) + }, [collabs, myUserId]) if (position == null) { return null @@ -194,6 +185,12 @@ interface CommentIndicatorUIProps { export const CommentIndicatorUI = React.memo((props) => { const { position, bgColor, fgColor, avatarUrl, avatarInitials, resolved, isActive, read } = props + const canvasScale = useEditorState( + Substores.canvas, + (store) => store.editor.canvas.scale, + 'CommentIndicatorUI scale', + ) + function getIndicatorStyle() { const base: Interpolation = { position: 'fixed', @@ -207,7 +204,9 @@ export const CommentIndicatorUI = React.memo((props) => alignItems: 'center', justifyContent: 'center', boxShadow: UtopiaStyles.shadowStyles.mid.boxShadow, + border: '.4px solid #a3a3a340', opacity: resolved ? 0.6 : 'undefined', + zoom: 1 / canvasScale, } const transform: Interpolation = { @@ -274,14 +273,7 @@ const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => { 'CommentIndicator canvasOffset', ) - const { location, scene: commentScene } = useCanvasLocationOfThread(thread) - - const remixLocationRoute = thread.metadata.remixLocationRoute ?? null - - const remixState = useRemixNavigationContext(commentScene) - - const isOnAnotherRoute = - remixLocationRoute != null && remixLocationRoute !== remixState?.location.pathname + const { location } = useCanvasLocationOfThread(thread) const readByMe = useMyThreadReadStatus(thread) @@ -445,12 +437,14 @@ const HoveredCommentIndicator = React.memo((props: HoveredCommentIndicatorProps) width: 250, boxShadow: UtopiaStyles.shadowStyles.mid.boxShadow, background: colorTheme.bg1.value, + border: '.4px solid #a3a3a340', zIndex: 1, position: 'fixed', bottom: canvasHeight - IndicatorSize - position.y, // temporarily moving the hovered comment indicator to align with the not hovered version left: position.x - 3, overflow: 'hidden', + zoom: 1 / canvasScale, }} onMouseDown={onMouseDown} onClick={onClick} diff --git a/editor/src/components/canvas/controls/comment-popup.tsx b/editor/src/components/canvas/controls/comment-popup.tsx index d17b5f987ec7..19f65f23f7de 100644 --- a/editor/src/components/canvas/controls/comment-popup.tsx +++ b/editor/src/components/canvas/controls/comment-popup.tsx @@ -119,6 +119,8 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => { const readByMe = useMyThreadReadStatus(thread) + useScrollWhenOverflowing(listRef) + const commentsCount = React.useMemo( () => thread?.comments.filter((c) => c.deletedAt == null).length ?? 0, [thread], @@ -280,8 +282,8 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => { } const tolerance = 20 // px + const isOverflowing = isOverflowingElement(element) const atBottom = element.scrollHeight - element.scrollTop <= PopupMaxHeight + tolerance - const isOverflowing = element.scrollHeight > PopupMaxHeight setShowShadowBottom(!atBottom && isOverflowing) const atTop = element.scrollTop > tolerance @@ -315,6 +317,7 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => { background: colorTheme.bg0.value, borderRadius: 4, overflow: 'hidden', + zoom: 1 / canvasScale, }} onKeyDown={stopPropagation} onKeyUp={stopPropagation} @@ -570,3 +573,30 @@ const HeaderComment = React.memo( }, ) HeaderComment.displayName = 'HeaderComment' + +function isOverflowingElement(element: HTMLDivElement | null): boolean { + if (element == null) { + return false + } + return element.scrollHeight > PopupMaxHeight +} + +function useScrollWhenOverflowing(listRef: React.MutableRefObject) { + const stopWheelPropagation = React.useCallback( + (event: any) => { + if (isOverflowingElement(listRef.current)) { + event.stopPropagation() + } + }, + [listRef], + ) + + React.useEffect(() => { + const element = listRef.current + if (element == null) { + return + } + element.addEventListener('wheel', stopWheelPropagation) + return () => element.removeEventListener('wheel', stopWheelPropagation) + }, [listRef, stopWheelPropagation]) +} diff --git a/editor/src/components/canvas/controls/new-canvas-controls.tsx b/editor/src/components/canvas/controls/new-canvas-controls.tsx index 40d012c791dd..9dacd48d7ad5 100644 --- a/editor/src/components/canvas/controls/new-canvas-controls.tsx +++ b/editor/src/components/canvas/controls/new-canvas-controls.tsx @@ -66,6 +66,9 @@ import { import { useSelectionArea } from './selection-area-hooks' import { RemixSceneLabelControl } from './select-mode/remix-scene-label' import { NO_OP } from '../../../core/shared/utils' +import { MultiplayerWrapper } from '../../../utils/multiplayer-wrapper' +import { MultiplayerPresence } from '../multiplayer-presence' +import { useStatus } from '../../../../liveblocks.config' export const CanvasControlsContainerID = 'new-canvas-controls-container' @@ -375,6 +378,8 @@ const NewCanvasControlsInner = (props: NewCanvasControlsInnerProps) => { [editorMode, selectModeHooks], ) + const roomStatus = useStatus() + const getResizeStatus = () => { const selectedViews = localSelectedViews if (textEditor != null || keysPressed['z']) { @@ -562,6 +567,12 @@ const NewCanvasControlsInner = (props: NewCanvasControlsInnerProps) => { )} {when(isSelectMode(editorMode), )} + {when( + roomStatus === 'connected', + + + , + )} , )} diff --git a/editor/src/components/canvas/design-panel-root.tsx b/editor/src/components/canvas/design-panel-root.tsx index 459610cb464c..c50b27f8da25 100644 --- a/editor/src/components/canvas/design-panel-root.tsx +++ b/editor/src/components/canvas/design-panel-root.tsx @@ -86,12 +86,6 @@ const DesignPanelRootInner = React.memo(() => { }} > - {when( - roomStatus === 'connected', - - - , - )} } diff --git a/editor/src/components/canvas/multiplayer-presence.tsx b/editor/src/components/canvas/multiplayer-presence.tsx index a3bcd493b337..567a68395f55 100644 --- a/editor/src/components/canvas/multiplayer-presence.tsx +++ b/editor/src/components/canvas/multiplayer-presence.tsx @@ -45,10 +45,9 @@ import CanvasActions from './canvas-actions' import { activeFrameActionToString } from './commands/set-active-frames-command' import { canvasPointToWindowPoint, windowToCanvasCoordinates } from './dom-lookup' import { ActiveRemixSceneAtom, RemixNavigationAtom } from './remix/utopia-remix-root-component' -import { useRemixPresence } from '../../core/shared/multiplayer-hooks' +import { useMyUserId, useRemixPresence } from '../../core/shared/multiplayer-hooks' import { CanvasOffsetWrapper } from './controls/canvas-offset-wrapper' import { when } from '../../utils/react-conditionals' -import { isFeatureEnabled } from '../../utils/feature-switches' import { CommentIndicators } from './controls/comment-indicator' import { CommentPopup } from './controls/comment-popup' @@ -160,6 +159,12 @@ const MultiplayerCursors = React.memo(() => { }) const myRemixPresence = me.presence.remix ?? null + const canvasScale = useEditorState( + Substores.canvas, + (store) => store.editor.canvas.scale, + 'MultiplayerCursors canvasScale', + ) + return (
{ top: 0, left: 0, pointerEvents: 'none', + zoom: 1 / canvasScale, }} > {others.map((other) => { @@ -240,6 +246,7 @@ const MultiplayerCursor = React.memo( style={{ filter: 'drop-shadow(1px 2px 3px rgb(0 0 0 / 0.3))', transform: 'translate(0px, 0px)', + zoom: canvasScale > 1 ? 1 / canvasScale : 1, }} > @@ -257,6 +264,10 @@ const MultiplayerCursor = React.memo( left: 6, top: 15, zoom: canvasScale > 1 ? 1 / canvasScale : 1, + minHeight: 16, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }} > {name} @@ -389,6 +400,7 @@ const FollowingOverlay = React.memo(() => { justifyContent: 'center', paddingBottom: 14, cursor: 'default', + zoom: 1 / canvasScale, }} >
{ FollowingOverlay.displayName = 'FollowingOverlay' const MultiplayerShadows = React.memo(() => { - const me = useSelf() + const myUserId = useMyUserId() const updateMyPresence = useUpdateMyPresence() const collabs = useStorage((store) => store.collaborators) const others = useOthers((list) => { - const presences = normalizeOthersList(me.id, list) + if (myUserId == null) { + return [] + } + const presences = normalizeOthersList(myUserId, list) return presences.map((p) => ({ presenceInfo: p, userInfo: collabs[p.id], @@ -501,6 +516,7 @@ const MultiplayerShadows = React.memo(() => { pointerEvents: 'none', border: `1px dashed ${color.background}`, opacity: 0.5, + zoom: 1 / canvasScale, }} /> { fontSize: 9, color: color.background, border: `1px dashed ${color.background}`, + zoom: 1 / canvasScale, }} > {activeFrameActionToString(action)} diff --git a/editor/src/components/inspector/sections/comment-section.tsx b/editor/src/components/inspector/sections/comment-section.tsx index 740f3b5e4b0f..ba9810ab0d63 100644 --- a/editor/src/components/inspector/sections/comment-section.tsx +++ b/editor/src/components/inspector/sections/comment-section.tsx @@ -25,7 +25,12 @@ import { useMyThreadReadStatus, useReadThreads, } from '../../../core/commenting/comment-hooks' -import { Substores, useEditorState, useSelectorWithCallback } from '../../editor/store/store-hook' +import { + Substores, + useEditorState, + useRefEditorState, + useSelectorWithCallback, +} from '../../editor/store/store-hook' import { when } from '../../../utils/react-conditionals' import { getFirstComment, @@ -231,16 +236,10 @@ const ThreadPreview = React.memo(({ thread }: ThreadPreviewProps) => { const isOnAnotherRoute = remixLocationRoute != null && remixLocationRoute !== remixState?.location.pathname - const canvasScale = useEditorState( - Substores.canvasOffset, - (store) => store.editor.canvas.scale, - 'ThreadPreview canvasScale', - ) - const canvasOffset = useEditorState( - Substores.canvasOffset, - (store) => store.editor.canvas.roundedCanvasOffset, - 'ThreadPreview canvasOffset', - ) + const editorRef = useRefEditorState((store) => ({ + canvasScale: store.editor.canvas.scale, + canvasOffset: store.editor.canvas.roundedCanvasOffset, + })) const onClick = React.useCallback(() => { if (isOnAnotherRoute) { @@ -261,17 +260,29 @@ const ThreadPreview = React.memo(({ thread }: ThreadPreviewProps) => { width: canvasArea.width - visibleAreaTolerance * 2, height: canvasArea.height, }) - const rect = canvasRectangle({ x: location.x, y: location.y, width: 25, height: 25 }) - const windowLocation = canvasPointToWindowPoint(location, canvasScale, canvasOffset) + + const windowLocation = canvasPointToWindowPoint( + location, + editorRef.current.canvasScale, + editorRef.current.canvasOffset, + ) + + // adds a padding of 250px around `location` const windowRect = canvasRectangle({ - x: windowLocation.x, - y: windowLocation.y, - width: rect.width, - height: rect.height, + x: windowLocation.x - 250, + y: windowLocation.y - 250, + width: 500, + height: 500, }) const isVisible = rectangleContainsRectangle(visibleArea, windowRect) if (!isVisible) { - actions.push(scrollToPosition(rect, 'to-center')) + const scrollToRect = canvasRectangle({ + x: location.x, + y: location.y, + width: 25, + height: 25, + }) + actions.push(scrollToPosition(scrollToRect, 'to-center')) } } dispatch(actions) @@ -283,8 +294,7 @@ const ThreadPreview = React.memo(({ thread }: ThreadPreviewProps) => { location, thread.id, commentScene, - canvasScale, - canvasOffset, + editorRef, ]) const resolveThread = useResolveThread() diff --git a/editor/src/core/commenting/comment-hooks.tsx b/editor/src/core/commenting/comment-hooks.tsx index 65c69f36d9b5..39b09e8f9e8e 100644 --- a/editor/src/core/commenting/comment-hooks.tsx +++ b/editor/src/core/commenting/comment-hooks.tsx @@ -28,6 +28,7 @@ import type { ElementPath } from '../shared/project-file-types' import type { ElementInstanceMetadata } from '../shared/element-template' import * as EP from '../shared/element-path' import { getCurrentTheme } from '../../components/editor/store/editor-state' +import { useMyUserId } from '../shared/multiplayer-hooks' export function useCanvasCommentThreadAndLocation(comment: CommentId): { location: CanvasPoint | null @@ -268,17 +269,20 @@ export function useUnresolvedThreads() { export function useReadThreads() { const threads = useThreads() - const self = useSelf() + const myUserId = useMyUserId() const threadReadStatuses = useStorage((store) => store.userReadStatusesByThread) const filteredThreads = threads.threads.filter((thread) => { + if (myUserId == null) { + return false + } if (thread == null) { return false } if (threadReadStatuses[thread.id] == null) { return false } - return threadReadStatuses[thread.id][self.id] === true + return threadReadStatuses[thread.id][myUserId] === true }) return { @@ -301,8 +305,11 @@ export function useSetThreadReadStatusOnMount(thread: ThreadData } export function useMyThreadReadStatus(thread: ThreadData | null): ThreadReadStatus { - const self = useSelf() + const myUserId = useMyUserId() return useStorage((store) => { + if (myUserId == null) { + return 'unread' + } if (thread == null) { return 'unread' } @@ -310,7 +317,7 @@ export function useMyThreadReadStatus(thread: ThreadData | null) if (statusesForThread == null) { return 'unread' } - return statusesForThread[self.id] === true ? 'read' : 'unread' + return statusesForThread[myUserId] === true ? 'read' : 'unread' }) } diff --git a/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap b/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap index 7ea0c7b35a88..ace17ae7bcdc 100644 --- a/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap +++ b/editor/src/core/performance/__snapshots__/performance-regression-tests.spec.tsx.snap @@ -33,6 +33,8 @@ Array [ "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerPresence)", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerWrapper)", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/div:data-testid='new-canvas-controls-container'", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(SelectionAreaRectangle)", @@ -643,6 +645,8 @@ Array [ "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerPresence)", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerWrapper)", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/div:data-testid='new-canvas-controls-container'", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(SelectionAreaRectangle)", @@ -1194,6 +1198,8 @@ Array [ "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerPresence)", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerWrapper)", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/div:data-testid='new-canvas-controls-container'", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(SelectionAreaRectangle)", @@ -1502,6 +1508,8 @@ Array [ "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerPresence)", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerWrapper)", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/div:data-testid='new-canvas-controls-container'", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(SelectionAreaRectangle)", @@ -1944,6 +1952,8 @@ Array [ "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerPresence)", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerWrapper)", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/div:data-testid='new-canvas-controls-container'", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(SelectionAreaRectangle)", @@ -2241,6 +2251,8 @@ Array [ "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)()", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerPresence)", + "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(MultiplayerWrapper)", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/UtopiaSpiedExoticType(Symbol(react.fragment))", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/div:data-testid='new-canvas-controls-container'", "/div/div/UtopiaSpiedFunctionComponent(NewCanvasControlsInner)/Symbol(react.memo)(SelectionAreaRectangle)", diff --git a/editor/src/core/performance/performance-regression-tests.spec.tsx b/editor/src/core/performance/performance-regression-tests.spec.tsx index 8f5b65e72ed1..07edcc7e4de8 100644 --- a/editor/src/core/performance/performance-regression-tests.spec.tsx +++ b/editor/src/core/performance/performance-regression-tests.spec.tsx @@ -65,7 +65,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`700`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`704`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -127,7 +127,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`745`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`749`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -183,7 +183,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`533`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`535`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -249,7 +249,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`605`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`607`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) }) diff --git a/editor/src/core/shared/multiplayer-hooks.tsx b/editor/src/core/shared/multiplayer-hooks.tsx index 6831e3a8ee3a..453398278fe1 100644 --- a/editor/src/core/shared/multiplayer-hooks.tsx +++ b/editor/src/core/shared/multiplayer-hooks.tsx @@ -6,6 +6,8 @@ import { } from '../../components/canvas/remix/utopia-remix-root-component' import type { RemixPresence } from './multiplayer' import * as EP from './element-path' +import { Substores, useEditorState } from '../../components/editor/store/store-hook' +import { isLoggedIn } from '../../common/user' export function useRemixPresence(): RemixPresence | null { const [activeRemixScene] = useAtom(ActiveRemixSceneAtom) @@ -25,3 +27,13 @@ export function useRemixPresence(): RemixPresence | null { return remixPresence } + +export function useMyUserId(): string | null { + const myUserId = useEditorState( + Substores.userState, + (store) => + isLoggedIn(store.userState.loginState) ? store.userState.loginState.user.userId : null, + 'useMyUserId myUserId', + ) + return myUserId +} diff --git a/editor/src/templates/editor-canvas.tsx b/editor/src/templates/editor-canvas.tsx index b918a1187d0e..18eb910cd2e3 100644 --- a/editor/src/templates/editor-canvas.tsx +++ b/editor/src/templates/editor-canvas.tsx @@ -739,6 +739,11 @@ export class EditorCanvas extends React.Component { // Due to the introduction of this https://www.chromestatus.com/features/6662647093133312 combined with // React's lack of support for event handler options (https://github.com/facebook/react/issues/6436) we // have to add this event handler in a rather clunky way to enable us to call preventDefault() on it + // Balint 2024: I commented this out and it did not seem to break anything. I'm not sure we still need this, + // and it can cause headaches: the listener callback here runs earlier than any handlers of the React synthetic events, + // which means all the wheel events will be preventDefaulted in the descendants of EditorCanvas. Which means + // it is not possible to scroll elements inside the canvas, except if you add an event listener manually with + // a ref to stop propagation. In that case the event will not reach the listener here. this.canvasWrapperRef.addEventListener('wheel', this.suppressBrowserNavigation, { passive: false, }) diff --git a/puppeteer-tests/package.json b/puppeteer-tests/package.json index 25cb34555a34..5ab74c9937cb 100644 --- a/puppeteer-tests/package.json +++ b/puppeteer-tests/package.json @@ -7,7 +7,8 @@ "screenshot-test": "ts-node --files src/screenshot-test.ts", "system-test": "ts-node --files src/system-test.ts", "preinstall": "npx only-allow pnpm", - "comments-test": "jest --config ./jest.config.js", + "comments-test": "jest --config ./jest.config.js comments/place-comment.spec.ts", + "collaboration-test": "jest --config ./jest.config.js comments/collaboration-test.spec.ts", "build": "tsc" }, "author": "", diff --git a/puppeteer-tests/src/comments/collaboration-test.spec.tsx b/puppeteer-tests/src/comments/collaboration-test.spec.tsx new file mode 100644 index 000000000000..8df44d74e87a --- /dev/null +++ b/puppeteer-tests/src/comments/collaboration-test.spec.tsx @@ -0,0 +1,75 @@ +import type { ElementHandle, Page } from 'puppeteer' +import { setupBrowser, wait } from '../utils' + +const TIMEOUT = 120000 + +const BRANCH_NAME = process.env.BRANCH_NAME ? `&branch_name=${process.env.BRANCH_NAME}` : '' + +async function signIn(page: Page) { + const signInButton = await page.waitForSelector('div[data-testid="sign-in-button"]') + await signInButton!.click() + await page.waitForSelector('#playground-scene') // wait for the scene to render +} + +async function clickCanvasContainer(page: Page, { x, y }: { x: number; y: number }) { + const canvasControlsContainer = await page.waitForSelector('#new-canvas-controls-container') + await canvasControlsContainer!.click({ offset: { x, y } }) +} + +async function expectNSelectors(page: Page, selector: string, n: number) { + const elementsMatchingSelector = await page.$$(selector) + expect(elementsMatchingSelector).toHaveLength(n) +} + +describe('Collaboration test', () => { + it( + 'can place a comment', + async () => { + const setupBrowser1Promise = setupBrowser( + `http://localhost:8000/p/56a2ac40-caramel-yew?fakeUser=alice&Multiplayer=true${BRANCH_NAME}`, + TIMEOUT, + ) + const setupBrowser2Promise = setupBrowser( + `http://localhost:8000/p/56a2ac40-caramel-yew?fakeUser=bob&Multiplayer=true${BRANCH_NAME}`, + TIMEOUT, + ) + const { page: page1, browser: browser1 } = await setupBrowser1Promise + const { page: page2, browser: browser2 } = await setupBrowser2Promise + + await Promise.all([signIn(page1), signIn(page2)]) + await Promise.all([ + clickCanvasContainer(page1, { x: 500, y: 500 }), + clickCanvasContainer(page2, { x: 500, y: 500 }), + ]) + + const insertTab = (await page1.$x( + "//div[contains(text(), 'Insert')]", + )) as ElementHandle[] + await insertTab!.at(0)!.click() + + const sampleTextOptions = (await page1.$x( + "//span[contains(text(), 'Sample text')]", + )) as ElementHandle[] + await sampleTextOptions!.at(0)!.click() + await clickCanvasContainer(page1, { x: 500, y: 500 }) + + await clickCanvasContainer(page1, { x: 500, y: 500 }) + + const sampleText = await page2.waitForFunction( + 'document.querySelector("body").innerText.includes("Sample text")', + ) + + expect(sampleText).not.toBeNull() + + await page1.keyboard.down('MetaLeft') + await page1.keyboard.press('z', {}) + await page1.keyboard.up('MetaLeft') + + await page1.close() + await browser1.close() + await page2.close() + await browser2.close() + }, + TIMEOUT, + ) +}) diff --git a/server/src/Utopia/Web/Executors/Development.hs b/server/src/Utopia/Web/Executors/Development.hs index 78e6fc76c3c3..8573e84b1f74 100644 --- a/server/src/Utopia/Web/Executors/Development.hs +++ b/server/src/Utopia/Web/Executors/Development.hs @@ -109,12 +109,19 @@ dummyUser cdnRoot = UserDetails { userId = "1" } dummyUserAlice :: Text -> UserDetails -dummyUserAlice cdnRoot = UserDetails { userId = "ab9401d8-f6f0-4642-8239-2435656bf0b2 " +dummyUserAlice cdnRoot = UserDetails { userId = "ab9401d8-f6f0-4642-8239-2435656bf0b2" , email = Just "team1@utopia.app" , name = Just "A real human being" , picture = Just (cdnRoot <> "/editor/avatars/utopino3.png") } +dummyUserBob :: Text -> UserDetails +dummyUserBob cdnRoot = UserDetails { userId = "231f5f05-cac7-4910-8006-d7645c44051c" + , email = Just "team1@utopia.app" + , name = Just "Also a real human being" + , picture = Just (cdnRoot <> "/editor/avatars/utopino2.png") + } + {-| Fallback for validating the authentication code in the case where Auth0 isn't setup locally. -} @@ -167,6 +174,7 @@ innerServerExecutor (CheckAuthCode authCode action) = do let codeCheck = maybe localAuthCodeCheck (auth0CodeCheck metrics pool sessionStore) auth0 case authCode of "alice" -> successfulAuthCheck metrics pool sessionStore action (dummyUserAlice cdnRoot) + "bob" -> successfulAuthCheck metrics pool sessionStore action (dummyUserBob cdnRoot) _ -> codeCheck authCode action innerServerExecutor (Logout cookie pageContents action) = do sessionStore <- fmap _sessionState ask diff --git a/server/src/Utopia/Web/Executors/Production.hs b/server/src/Utopia/Web/Executors/Production.hs index 91bad9ffc0ae..c71117584002 100644 --- a/server/src/Utopia/Web/Executors/Production.hs +++ b/server/src/Utopia/Web/Executors/Production.hs @@ -84,6 +84,14 @@ dummyUserAlice = UserDetails { userId = "ab9401d8-f6f0-4642-8239-2435656bf0b2" , picture = Just "/editor/avatars/utopino3.png" } +dummyUserBob :: UserDetails +dummyUserBob = UserDetails { userId = "231f5f05-cac7-4910-8006-d7645c44051c" + , email = Just "team1@utopia.app" + , name = Just "Also a real human being" + , picture = Just "/editor/avatars/utopino2.png" + } + + {-| Interpretor for a service call, which converts it into side effecting calls ready to be invoked. -} @@ -106,6 +114,7 @@ innerServerExecutor (CheckAuthCode authCode action) = do canUseFakeUser <- fmap _shouldUseFakeUser ask case (canUseFakeUser, authCode) of (True, "alice") -> successfulAuthCheck metrics pool sessionStore action dummyUserAlice + (True, "bob") -> successfulAuthCheck metrics pool sessionStore action dummyUserBob _ -> auth0CodeCheck metrics pool sessionStore auth0 authCode action innerServerExecutor (Logout cookie pageContents action) = do sessionStore <- fmap _sessionState ask