Skip to content

Commit

Permalink
Hover view for comment markers (#4664)
Browse files Browse the repository at this point in the history
* Hover view for comment indicators

* Disable dragging of img

* Fix blinking glitch
  • Loading branch information
gbalint authored Dec 15, 2023
1 parent d916c49 commit 3dbeaa4
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 46 deletions.
227 changes: 181 additions & 46 deletions editor/src/components/canvas/controls/comment-indicator.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -186,17 +187,13 @@ interface CommentIndicatorUIProps {
fgColor: string
avatarInitials: string
avatarUrl?: string | null
onClick?: (e: React.MouseEvent) => void
onMouseDown?: (e: React.MouseEvent) => void
isActive: boolean
read?: boolean
}

export const CommentIndicatorUI = React.memo<CommentIndicatorUIProps>((props) => {
const {
position,
onClick,
onMouseDown,
bgColor,
fgColor,
avatarUrl,
Expand Down Expand Up @@ -248,7 +245,7 @@ export const CommentIndicatorUI = React.memo<CommentIndicatorUIProps>((props) =>
}

return (
<div onClick={onClick} onMouseDown={onMouseDown} css={getIndicatorStyle()}>
<div css={getIndicatorStyle()}>
<div
style={{
height: 18,
Expand All @@ -275,7 +272,6 @@ interface CommentIndicatorProps {
}

const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => {
const dispatch = useDispatch()
const collabs = useStorage((storage) => storage.collaborators)

const canvasScale = useEditorState(
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -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 (
<CommentIndicatorUI
position={position}
opacity={isOnAnotherRoute || thread.metadata.resolved ? 0 : 1}
resolved={thread.metadata.resolved}
onClick={onClick}
onMouseDown={onMouseDown}
bgColor={color.background}
fgColor={color.foreground}
avatarUrl={avatar}
avatarInitials={initials}
isActive={isActive}
read={readByMe === 'read'}
/>
<div onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
{when(
(isActive || !hovered) && !dragging,
<CommentIndicatorUI
position={position}
opacity={isOnAnotherRoute || thread.metadata.resolved ? 0 : 1}
resolved={thread.metadata.resolved}
bgColor={color.background}
fgColor={color.foreground}
avatarUrl={avatar}
avatarInitials={initials}
isActive={isActive}
read={readByMe === 'read'}
/>,
)}

{when(
!isActive,
<HoveredCommentIndicator
thread={thread}
hidden={!hovered}
cancelHover={cancelHover}
draggingCallback={draggingCallback}
/>,
)}
</div>
)
})
CommentIndicator.displayName = 'CommentIndicator'

interface HoveredCommentIndicatorProps {
thread: ThreadData<ThreadMetadata>
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 (
<div
style={{
position: 'fixed',
top: position.y,
left: position.x,
}}
onMouseDown={onMouseDown}
onClick={onClick}
>
<CommentWrapper
style={{ borderRadius: '18px 18px 18px 0px' }}
data-theme={theme}
user={user}
comment={comment}
showActions={false}
/>
</div>
)
})
HoveredCommentIndicator.displayName = 'HoveredCommentIndicator'

const COMMENT_DRAG_THRESHOLD = 5 // square px

function useDragging(thread: ThreadData<ThreadMetadata>, originalLocation: CanvasPoint) {
function useDragging(
thread: ThreadData<ThreadMetadata>,
originalLocation: CanvasPoint,
draggingCallback: (isDragging: boolean) => void,
) {
const editThreadMetadata = useEditThreadMetadata()
const [dragPosition, setDragPosition] = React.useState<CanvasPoint | null>(null)
const [didDrag, setDidDrag] = React.useState(false)
Expand All @@ -379,6 +487,7 @@ function useDragging(thread: ThreadData<ThreadMetadata>, originalLocation: Canva
const onMouseDown = React.useCallback(
(event: React.MouseEvent) => {
setDidDrag(false)
draggingCallback(true)

const mouseDownPoint = windowPoint({ x: event.clientX, y: event.clientY })

Expand All @@ -404,6 +513,7 @@ function useDragging(thread: ThreadData<ThreadMetadata>, originalLocation: Canva
upEvent.stopPropagation()
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
draggingCallback(false)

const mouseUpPoint = windowPoint({ x: upEvent.clientX, y: upEvent.clientY })

Expand All @@ -426,8 +536,33 @@ function useDragging(thread: ThreadData<ThreadMetadata>, 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 }
}
1 change: 1 addition & 0 deletions editor/src/components/user-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 3dbeaa4

Please sign in to comment.