Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store canvas position of scene comments. #4791

Merged
merged 16 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions editor/liveblocks.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,31 @@ export type RoomEvent = ControlChangedRoomEvent
export type ThreadMetadata = {
// quote: string;
// time: number;
type: 'canvas'
x: number // x and y is global when sceneId is undefined, and local to the scene when sceneId is not null
x: number
y: number
sceneId?: string
sceneX?: number
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this! what's the point of these being optional here and then have a separate SceneThreadMetadata where they are declared as non optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is that canvas comments don't have these properties (sceneId, sceneX, sceneY), and we are only allowed to have string, number and boolean properties here, so no way to create a structured type.

Copy link
Contributor

@balazsbajorics balazsbajorics Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not CanvasThreadMetadata without these fields and SceneThreadMetadata with these fields, and then ThreadMetadata = CanvasThreadMetadata | SceneThreadMetadata

but better yet, why not make it a discriminated union type with a type: 'canvas' | 'scene'? I see on master there was some notion to do that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can not use discriminated union types because I can only add string/boolean/number fields.
Undiscrimated union types are not that nice to work with, but you can be right maybe this solution is better: 68b2418

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turned out the problem was not discriminated union types, but that I used interfaces and not types.... Here is the proper solution: dcc67bd

sceneY?: number
remixLocationRoute?: string
resolved: boolean
}

export type SceneThreadMetadata = {
// quote: string;
// time: number;
x: number
y: number
sceneId: string
sceneX: number
sceneY: number
remixLocationRoute?: string
resolved: boolean
}

export function isSceneThreadMetadata(metadata: ThreadMetadata): metadata is SceneThreadMetadata {
return metadata.sceneId != null && metadata.sceneX != null && metadata.sceneY != null
}

export const {
suspense: {
RoomProvider,
Expand Down
48 changes: 7 additions & 41 deletions editor/src/components/canvas/controls/comment-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,18 @@ import type { ThreadMetadata } from '../../../../liveblocks.config'
import { useEditThreadMetadata, useStorage } from '../../../../liveblocks.config'
import {
getCollaboratorById,
getThreadLocationOnCanvas,
useActiveThreads,
useCanvasCommentThreadAndLocation,
useCanvasLocationOfThread,
useMyThreadReadStatus,
} from '../../../core/commenting/comment-hooks'
import type {
CanvasPoint,
CanvasRectangle,
LocalPoint,
MaybeInfinityCanvasRectangle,
} from '../../../core/shared/math-utils'
import type { CanvasPoint } from '../../../core/shared/math-utils'
import {
canvasPoint,
distance,
getLocalPointInNewParentContext,
isNotNullFiniteRectangle,
localPoint,
nullIfInfinity,
offsetPoint,
pointDifference,
windowPoint,
Expand Down Expand Up @@ -384,36 +378,6 @@ const CommentIndicator = React.memo(({ thread }: CommentIndicatorProps) => {
})
CommentIndicator.displayName = 'CommentIndicator'

function canvasPositionOfThread(
sceneGlobalFrame: CanvasRectangle,
locationInScene: LocalPoint,
): CanvasPoint {
return canvasPoint({
x: sceneGlobalFrame.x + locationInScene.x,
y: sceneGlobalFrame.y + locationInScene.y,
})
}

function getThreadOriginalLocationOnCanvas(
thread: ThreadData<ThreadMetadata>,
startingSceneGlobalFrame: MaybeInfinityCanvasRectangle | null,
): CanvasPoint {
const sceneId = thread.metadata.sceneId
if (sceneId == null) {
return canvasPoint({ x: thread.metadata.x, y: thread.metadata.y })
}

const globalFrame = nullIfInfinity(startingSceneGlobalFrame)
if (globalFrame == null) {
throw new Error('Found thread attached to scene with invalid global frame')
}

return canvasPositionOfThread(
globalFrame,
localPoint({ x: thread.metadata.x, y: thread.metadata.y }),
)
}

const COMMENT_DRAG_THRESHOLD = 5 // square px

function useDragging(
Expand Down Expand Up @@ -448,7 +412,7 @@ function useDragging(
scenesRef.current,
)

const originalThreadPosition = getThreadOriginalLocationOnCanvas(
const originalThreadPosition = getThreadLocationOnCanvas(
thread,
maybeStartingSceneUnderPoint?.globalFrame ?? null,
)
Expand Down Expand Up @@ -534,9 +498,11 @@ function useDragging(
editThreadMetadata({
threadId: thread.id,
metadata: {
x: localPointInScene.x,
y: localPointInScene.y,
sceneX: localPointInScene.x,
sceneY: localPointInScene.y,
sceneId: sceneIdToUse,
x: newPositionOnCanvas.x,
y: newPositionOnCanvas.y,
remixLocationRoute: remixRoute != null ? remixRoute.location.pathname : undefined,
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function useCommentModeSelectAndHover(comment: CommentId | null): MouseCa
setHighlightedView(scene.elementPath),
switchEditorMode(
EditorModes.commentMode(
newComment(sceneCommentLocation(sceneIdToUse, offset)),
newComment(sceneCommentLocation(sceneIdToUse, offset, loc.canvasPositionRounded)),
'not-dragging',
),
),
Expand Down
8 changes: 4 additions & 4 deletions editor/src/components/canvas/controls/comment-popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => {
body,
metadata: {
resolved: false,
type: 'canvas',
x: comment.location.position.x,
y: comment.location.position.y,
},
Expand Down Expand Up @@ -184,10 +183,11 @@ const CommentThread = React.memo(({ comment }: CommentThreadProps) => {
body,
metadata: {
resolved: false,
type: 'canvas',
x: comment.location.offset.x,
y: comment.location.offset.y,
x: comment.location.position.x,
y: comment.location.position.x,
sceneId: sceneId,
sceneX: comment.location.offset.x,
sceneY: comment.location.offset.y,
remixLocationRoute: remixRoute != null ? remixRoute.location.pathname : undefined,
},
})
Expand Down
7 changes: 3 additions & 4 deletions editor/src/components/editor/editor-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ import type { Storage, Presence, RoomEvent, UserMeta } from '../../../liveblocks
import LiveblocksProvider from '@liveblocks/yjs'
import { isRoomId, projectIdToRoomId } from '../../core/shared/multiplayer'
import { EditorModes } from './editor-modes'
import { checkIsMyProject } from './store/collaborative-editing'
import { useCanComment, useDataThemeAttributeOnBody } from '../../core/commenting/comment-hooks'
import { useDataThemeAttributeOnBody } from '../../core/commenting/comment-hooks'
import { CollaborationStateUpdater } from './store/collaboration-state'
import { GithubRepositoryCloneFlow } from '../github/github-repository-clone-flow'
import { getPermissions } from './store/permissions'
import { CommentMaintainer } from '../../core/commenting/comment-maintainer'

const liveModeToastId = 'play-mode-toast'

Expand Down Expand Up @@ -363,8 +363,6 @@ export const EditorComponentInner = React.memo((props: EditorProps) => {
[dispatch],
)

const canComment = useCanComment()

useSelectorWithCallback(
Substores.userStateAndProjectServerState,
(store) => ({ projectServerState: store.projectServerState, userState: store.userState }),
Expand Down Expand Up @@ -485,6 +483,7 @@ export const EditorComponentInner = React.memo((props: EditorProps) => {
keyDown={onWindowKeyDown}
keyUp={onWindowKeyUp}
/>
<CommentMaintainer />
</>
)
})
Expand Down
8 changes: 7 additions & 1 deletion editor/src/components/editor/editor-modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,19 @@ export interface SceneCommentLocation {
type: 'scene'
sceneId: string
offset: LocalPoint
position: CanvasPoint
}

export function sceneCommentLocation(sceneId: string, offset: LocalPoint): SceneCommentLocation {
export function sceneCommentLocation(
sceneId: string,
offset: LocalPoint,
position: CanvasPoint,
): SceneCommentLocation {
return {
type: 'scene',
sceneId: sceneId,
offset: offset,
position: position,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3231,11 +3231,13 @@ export const CanvasCommentLocationKeepDeepEquality: KeepDeepEqualityCall<CanvasC
combine1EqualityCall((loc) => loc.position, CanvasPointKeepDeepEquality, canvasCommentLocation)

export const SceneCommentLocationKeepDeepEquality: KeepDeepEqualityCall<SceneCommentLocation> =
combine2EqualityCalls(
combine3EqualityCalls(
(loc) => loc.sceneId,
StringKeepDeepEquality,
(loc) => loc.offset,
LocalPointKeepDeepEquality,
(loc) => loc.position,
CanvasPointKeepDeepEquality,
sceneCommentLocation,
)

Expand Down
56 changes: 47 additions & 9 deletions editor/src/core/commenting/comment-hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React from 'react'
import type { User } from '@liveblocks/client'
import { LiveObject, type ThreadData } from '@liveblocks/client'
import type { Presence, ThreadMetadata, UserMeta } from '../../../liveblocks.config'
import type {
Presence,
SceneThreadMetadata,
ThreadMetadata,
UserMeta,
} from '../../../liveblocks.config'
import {
isSceneThreadMetadata,
useEditThreadMetadata,
useMutation,
useSelf,
Expand All @@ -14,18 +20,23 @@ import { normalizeMultiplayerName, possiblyUniqueColor } from '../shared/multipl
import { isLoggedIn } from '../../common/user'
import type { CommentId, SceneCommentLocation } from '../../components/editor/editor-modes'
import { assertNever } from '../shared/utils'
import type { CanvasPoint } from '../shared/math-utils'
import type {
CanvasPoint,
CanvasRectangle,
LocalPoint,
MaybeInfinityCanvasRectangle,
} from '../shared/math-utils'
import {
canvasPoint,
getCanvasPointWithCanvasOffset,
isNotNullFiniteRectangle,
localPoint,
zeroCanvasPoint,
nullIfInfinity,
} from '../shared/math-utils'
import { MetadataUtils } from '../model/element-metadata-utils'
import { getIdOfScene } from '../../components/canvas/controls/comment-mode/comment-mode-hooks'
import type { ElementPath } from '../shared/project-file-types'
import type { ElementInstanceMetadata } from '../shared/element-template'
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'
Expand Down Expand Up @@ -64,7 +75,7 @@ export function useCanvasCommentThreadAndLocation(comment: CommentId): {
})

if (scene == null || !isNotNullFiniteRectangle(scene.globalFrame)) {
return getCanvasPointWithCanvasOffset(zeroCanvasPoint, comment.location.offset)
return comment.location.position
}
return getCanvasPointWithCanvasOffset(scene.globalFrame, comment.location.offset)
default:
Expand All @@ -75,7 +86,8 @@ export function useCanvasCommentThreadAndLocation(comment: CommentId): {
if (thread == null) {
return null
}
if (thread.metadata.sceneId == null) {

if (!isSceneThreadMetadata(thread.metadata)) {
return canvasPoint(thread.metadata)
}
const scene = scenes.find((s) => getIdOfScene(s) === thread.metadata.sceneId)
Expand All @@ -86,7 +98,7 @@ export function useCanvasCommentThreadAndLocation(comment: CommentId): {
if (!isNotNullFiniteRectangle(scene.globalFrame)) {
return canvasPoint(thread.metadata)
}
return getCanvasPointWithCanvasOffset(scene.globalFrame, localPoint(thread.metadata))
return getCanvasPointWithCanvasOffset(scene.globalFrame, positionInScene(thread.metadata))

default:
assertNever(comment)
Expand Down Expand Up @@ -209,7 +221,7 @@ export function useCanvasLocationOfThread(thread: ThreadData<ThreadMetadata>): {
} {
const scenes = useScenesWithId()

if (thread.metadata.sceneId == null) {
if (!isSceneThreadMetadata(thread.metadata)) {
return { location: canvasPoint(thread.metadata), scene: null }
}
const scene = scenes.find((s) => getIdOfScene(s) === thread.metadata.sceneId)
Expand All @@ -218,7 +230,7 @@ export function useCanvasLocationOfThread(thread: ThreadData<ThreadMetadata>): {
return { location: canvasPoint(thread.metadata), scene: null }
}
return {
location: getCanvasPointWithCanvasOffset(scene.globalFrame, localPoint(thread.metadata)),
location: getCanvasPointWithCanvasOffset(scene.globalFrame, positionInScene(thread.metadata)),
scene: scene.elementPath,
}
}
Expand Down Expand Up @@ -388,3 +400,29 @@ export function useCanComment() {

return isFeatureEnabled('Multiplayer') && canComment
}

export function getThreadLocationOnCanvas(
thread: ThreadData<ThreadMetadata>,
startingSceneGlobalFrame: MaybeInfinityCanvasRectangle | null,
): CanvasPoint {
const globalFrame = nullIfInfinity(startingSceneGlobalFrame)
if (!isSceneThreadMetadata(thread.metadata) || globalFrame == null) {
return canvasPoint(thread.metadata)
}

return canvasPositionOfThread(globalFrame, positionInScene(thread.metadata))
}

function canvasPositionOfThread(
sceneGlobalFrame: CanvasRectangle,
locationInScene: LocalPoint,
): CanvasPoint {
return canvasPoint({
x: sceneGlobalFrame.x + locationInScene.x,
y: sceneGlobalFrame.y + locationInScene.y,
})
}

export function positionInScene(metadata: SceneThreadMetadata): LocalPoint {
return localPoint({ x: metadata.sceneX, y: metadata.sceneY })
}
Loading
Loading