diff --git a/editor/src/components/canvas/controls/comment-indicator.tsx b/editor/src/components/canvas/controls/comment-indicator.tsx index 534ed8e57b20..c73ff95d0ecb 100644 --- a/editor/src/components/canvas/controls/comment-indicator.tsx +++ b/editor/src/components/canvas/controls/comment-indicator.tsx @@ -1,16 +1,17 @@ /** @jsxRuntime classic */ /** @jsx jsx */ import { jsx } from '@emotion/react' -import type { Interpolation } from '@emotion/react' +import type { CSSObject, Interpolation } from '@emotion/react' import type { ThreadData } from '@liveblocks/client' import React from 'react' import type { ThreadMetadata } from '../../../../liveblocks.config' -import { useEditThreadMetadata, useStorage } from '../../../../liveblocks.config' +import { useEditThreadMetadata, useStorage, useThreads } from '../../../../liveblocks.config' import { useCanvasLocationOfThread, useActiveThreads, useCanvasCommentThreadAndLocation, useMyThreadReadStatus, + getCollaboratorById, } from '../../../core/commenting/comment-hooks' import type { CanvasPoint, WindowPoint } from '../../../core/shared/math-utils' import { @@ -26,7 +27,7 @@ import { normalizeMultiplayerName, openCommentThreadActions, } from '../../../core/shared/multiplayer' -import { MultiplayerWrapper } from '../../../utils/multiplayer-wrapper' +import { CommentWrapper, MultiplayerWrapper } from '../../../utils/multiplayer-wrapper' import type { Theme } from '../../../uuiui' import { UtopiaStyles, colorTheme } from '../../../uuiui' import { isCommentMode, isExistingComment } from '../../editor/editor-modes' @@ -35,10 +36,10 @@ import { Substores, useEditorState, useRefEditorState } from '../../editor/store import { AvatarPicture } from '../../user-bar' import { canvasPointToWindowPoint } from '../dom-lookup' import { useRemixNavigationContext } from '../remix/utopia-remix-root-component' -import { assertNever } from '../../../core/shared/utils' import { optionalMap } from '../../../core/shared/optional-utils' import { setRightMenuTab } from '../../editor/actions/action-creators' -import { RightMenuTab } from '../../editor/store/editor-state' +import { RightMenuTab, getCurrentTheme } from '../../editor/store/editor-state' +import { when } from '../../../utils/react-conditionals' const IndicatorSize = 24 const MagnifyScale = 1.15 @@ -186,8 +187,6 @@ interface CommentIndicatorUIProps { fgColor: string avatarInitials: string avatarUrl?: string | null - onClick?: (e: React.MouseEvent) => void - onMouseDown?: (e: React.MouseEvent) => void isActive: boolean read?: boolean } @@ -195,8 +194,6 @@ interface CommentIndicatorUIProps { export const CommentIndicatorUI = React.memo((props) => { const { position, - onClick, - onMouseDown, bgColor, fgColor, avatarUrl, @@ -248,7 +245,7 @@ export const CommentIndicatorUI = React.memo((props) => } return ( -
+
{ - const dispatch = useDispatch() const collabs = useStorage((storage) => storage.collaborators) const canvasScale = useEditorState( @@ -291,8 +287,6 @@ const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => { const { location, scene: commentScene } = useCanvasLocationOfThread(thread) - const { onMouseDown, didDrag, dragPosition } = useDragging(thread, location) - const remixLocationRoute = thread.metadata.remixLocationRoute ?? null const remixState = useRemixNavigationContext(commentScene) @@ -302,22 +296,6 @@ const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => { const readByMe = useMyThreadReadStatus(thread) - const onClick = React.useCallback(() => { - if (didDrag) { - return - } - if (isOnAnotherRoute) { - if (remixState == null) { - return - } - remixState.navigate(remixLocationRoute) - } - dispatch([ - ...openCommentThreadActions(thread.id, commentScene), - setRightMenuTab(RightMenuTab.Comments), - ]) - }, [dispatch, thread.id, remixState, remixLocationRoute, isOnAnotherRoute, commentScene, didDrag]) - const { initials, color, avatar } = (() => { const firstComment = thread.comments[0] if (firstComment == null) { @@ -335,8 +313,8 @@ const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => { })() const position = React.useMemo( - () => canvasPointToWindowPoint(dragPosition ?? location, canvasScale, canvasOffset), - [location, canvasScale, canvasOffset, dragPosition], + () => canvasPointToWindowPoint(location, canvasScale, canvasOffset), + [location, canvasScale, canvasOffset], ) const isActive = useEditorState( @@ -349,27 +327,157 @@ const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => { 'CommentIndicator isActive', ) + const { hovered, onMouseOver, onMouseOut, cancelHover } = useHover() + + const [dragging, setDragging] = React.useState(false) + + const draggingCallback = React.useCallback((isDragging: boolean) => setDragging(isDragging), []) + return ( - +
+ {when( + (isActive || !hovered) && !dragging, + , + )} + + {when( + !isActive, +
) }) CommentIndicator.displayName = 'CommentIndicator' +interface HoveredCommentIndicatorProps { + thread: ThreadData + hidden: boolean + cancelHover: () => void + draggingCallback: (isDragging: boolean) => void +} + +const HoveredCommentIndicator = React.memo((props: HoveredCommentIndicatorProps) => { + const { thread, hidden, cancelHover, draggingCallback } = props + + const dispatch = useDispatch() + const theme = useEditorState( + Substores.userState, + (store) => getCurrentTheme(store.userState), + 'HoveredCommentIndicator theme', + ) + + const { location, scene: commentScene } = useCanvasLocationOfThread(thread) + + const { onMouseDown, didDrag, dragPosition } = useDragging(thread, location, draggingCallback) + + const remixLocationRoute = thread.metadata.remixLocationRoute ?? null + + const remixState = useRemixNavigationContext(commentScene) + + const isOnAnotherRoute = + remixLocationRoute != null && remixLocationRoute !== remixState?.location.pathname + + const canvasScale = useEditorState( + Substores.canvasOffset, + (store) => store.editor.canvas.scale, + 'HoveredCommentIndicator canvasScale', + ) + + const canvasOffset = useEditorState( + Substores.canvasOffset, + (store) => store.editor.canvas.roundedCanvasOffset, + 'HoveredCommentIndicator canvasOffset', + ) + + const position = React.useMemo( + () => canvasPointToWindowPoint(dragPosition ?? location, canvasScale, canvasOffset), + [location, canvasScale, canvasOffset, dragPosition], + ) + + const onClick = React.useCallback(() => { + if (didDrag) { + return + } + if (isOnAnotherRoute) { + if (remixState == null) { + return + } + remixState.navigate(remixLocationRoute) + } + cancelHover() + dispatch([ + ...openCommentThreadActions(thread.id, commentScene), + setRightMenuTab(RightMenuTab.Comments), + ]) + }, [ + dispatch, + thread.id, + remixState, + remixLocationRoute, + isOnAnotherRoute, + commentScene, + didDrag, + cancelHover, + ]) + + const collabs = useStorage((storage) => storage.collaborators) + if (hidden && dragPosition == null) { + return null + } + + const comment = thread.comments[0] + if (comment == null) { + return null + } + const user = getCollaboratorById(collabs, comment.userId) + if (user == null) { + return null + } + + return ( +
+ +
+ ) +}) +HoveredCommentIndicator.displayName = 'HoveredCommentIndicator' + const COMMENT_DRAG_THRESHOLD = 5 // square px -function useDragging(thread: ThreadData, originalLocation: CanvasPoint) { +function useDragging( + thread: ThreadData, + originalLocation: CanvasPoint, + draggingCallback: (isDragging: boolean) => void, +) { const editThreadMetadata = useEditThreadMetadata() const [dragPosition, setDragPosition] = React.useState(null) const [didDrag, setDidDrag] = React.useState(false) @@ -379,6 +487,7 @@ function useDragging(thread: ThreadData, originalLocation: Canva const onMouseDown = React.useCallback( (event: React.MouseEvent) => { setDidDrag(false) + draggingCallback(true) const mouseDownPoint = windowPoint({ x: event.clientX, y: event.clientY }) @@ -404,6 +513,7 @@ function useDragging(thread: ThreadData, originalLocation: Canva upEvent.stopPropagation() window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mouseup', onMouseUp) + draggingCallback(false) const mouseUpPoint = windowPoint({ x: upEvent.clientX, y: upEvent.clientY }) @@ -426,8 +536,33 @@ function useDragging(thread: ThreadData, originalLocation: Canva window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) }, - [canvasScaleRef, editThreadMetadata, thread.id, originalLocation, thread.metadata], + [ + canvasScaleRef, + editThreadMetadata, + thread.id, + originalLocation, + thread.metadata, + draggingCallback, + ], ) return { onMouseDown, dragPosition, didDrag } } + +function useHover() { + const [hovered, setHovered] = React.useState(false) + + const onMouseOver = React.useCallback(() => { + setHovered(true) + }, []) + + const onMouseOut = React.useCallback(() => { + setHovered(false) + }, []) + + const cancelHover = React.useCallback(() => { + setHovered(false) + }, []) + + return { hovered, onMouseOver, onMouseOut, cancelHover } +} diff --git a/editor/src/components/user-bar.tsx b/editor/src/components/user-bar.tsx index f6924edc7a35..75af6028c600 100644 --- a/editor/src/components/user-bar.tsx +++ b/editor/src/components/user-bar.tsx @@ -371,6 +371,7 @@ export const AvatarPicture = React.memo((props: AvatarPictureProps) => { height: size ?? '100%', borderRadius: '100%', filter: props.isOffline ? 'grayscale(1)' : undefined, + pointerEvents: 'none', }} src={url} referrerPolicy='no-referrer'