diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 544108d058..8b35478418 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -881,6 +881,7 @@ "month": "", "more": "", "more_actions": "", + "more_comments": "", "more_info": "", "more_options": "", "more_options_for_border_settings_coming_soon": "", diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-current-file.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-current-file.tsx index 3f829f645d..929c970583 100644 --- a/services/web/frontend/js/features/review-panel-new/components/review-panel-current-file.tsx +++ b/services/web/frontend/js/features/review-panel-new/components/review-panel-current-file.tsx @@ -29,6 +29,8 @@ import ReviewPanelEmptyState from './review-panel-empty-state' import useEventListener from '@/shared/hooks/use-event-listener' import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range' import { addCommentStateField } from '@/features/source-editor/extensions/add-comment' +import ReviewPanelMoreCommentsButton from './review-panel-more-comments-button' +import useMoreCommments from '../hooks/use-more-comments' type AggregatedRanges = { changes: Change[] @@ -47,59 +49,6 @@ const ReviewPanelCurrentFile: FC = () => { const containerRef = useRef(null) const previousFocusedItem = useRef(new Map()) - const updatePositions = useCallback(() => { - const docId = ranges?.docId - - if (containerRef.current && docId) { - const positioningRes = positionItems( - containerRef.current, - previousFocusedItem.current.get(docId) || 0, - docId - ) - - if (positioningRes) { - previousFocusedItem.current.set( - positioningRes.docId, - positioningRes.activeItemIndex - ) - } - } - }, [ranges?.docId]) - - useEffect(() => { - const timer = window.setTimeout(() => { - updatePositions() - }, 50) - - return () => { - window.clearTimeout(timer) - } - }, [state, updatePositions]) - - const handleContainer = useCallback( - (element: HTMLDivElement | null) => { - containerRef.current = element - if (containerRef.current) { - containerRef.current.addEventListener( - 'review-panel:position', - updatePositions - ) - } - }, - [updatePositions] - ) - - useEffect(() => { - return () => { - if (containerRef.current) { - containerRef.current.removeEventListener( - 'review-panel:position', - updatePositions - ) - } - } - }, [updatePositions]) - const buildAggregatedRanges = useCallback(() => { if (ranges) { const output: AggregatedRanges = { @@ -127,7 +76,7 @@ const ReviewPanelCurrentFile: FC = () => { if (threads) { for (const comment of ranges.comments) { - if (!threads[comment.op.t]?.resolved) { + if (threads[comment.op.t] && !threads[comment.op.t]?.resolved) { output.comments.push(comment) } } @@ -246,6 +195,71 @@ const ReviewPanelCurrentFile: FC = () => { return entries }, [addCommentRanges, positions]) + const { + onEntriesPositioned, + onMoreCommentsAboveClick, + onMoreCommentsBelowClick, + } = useMoreCommments( + aggregatedRanges?.changes ?? [], + aggregatedRanges?.comments ?? [], + addCommentRanges + ) + + const updatePositions = useCallback(() => { + const docId = ranges?.docId + + if (containerRef.current && docId) { + const positioningRes = positionItems( + containerRef.current, + previousFocusedItem.current.get(docId) || 0, + docId + ) + + onEntriesPositioned() + + if (positioningRes) { + previousFocusedItem.current.set( + positioningRes.docId, + positioningRes.activeItemIndex + ) + } + } + }, [ranges?.docId, onEntriesPositioned]) + + useEffect(() => { + const timer = window.setTimeout(() => { + updatePositions() + }, 50) + + return () => { + window.clearTimeout(timer) + } + }, [state, updatePositions]) + + const handleContainer = useCallback( + (element: HTMLDivElement | null) => { + containerRef.current = element + if (containerRef.current) { + containerRef.current.addEventListener( + 'review-panel:position', + updatePositions + ) + } + }, + [updatePositions] + ) + + useEffect(() => { + return () => { + if (containerRef.current) { + containerRef.current.removeEventListener( + 'review-panel:position', + updatePositions + ) + } + } + }, [updatePositions]) + if (!aggregatedRanges) { return null } @@ -253,6 +267,12 @@ const ReviewPanelCurrentFile: FC = () => { return ( <> {showEmptyState && } + {onMoreCommentsAboveClick && ( + + )}
{addCommentEntries.map(entry => { @@ -294,6 +314,12 @@ const ReviewPanelCurrentFile: FC = () => { ) )}
+ {onMoreCommentsBelowClick && ( + + )} ) } diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-more-comments-button.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-more-comments-button.tsx new file mode 100644 index 0000000000..164d07b2a0 --- /dev/null +++ b/services/web/frontend/js/features/review-panel-new/components/review-panel-more-comments-button.tsx @@ -0,0 +1,32 @@ +import { FC, memo } from 'react' +import { Button } from 'react-bootstrap' +import MaterialIcon from '@/shared/components/material-icon' +import classNames from 'classnames' +import { useTranslation } from 'react-i18next' + +const MoreCommentsButton: FC<{ + onClick: () => void + direction: 'upward' | 'downward' +}> = ({ onClick, direction }) => { + const { t } = useTranslation() + + return ( +
+ +
+ ) +} + +export default memo(MoreCommentsButton) diff --git a/services/web/frontend/js/features/review-panel-new/hooks/use-more-comments.ts b/services/web/frontend/js/features/review-panel-new/hooks/use-more-comments.ts new file mode 100644 index 0000000000..2122259e3d --- /dev/null +++ b/services/web/frontend/js/features/review-panel-new/hooks/use-more-comments.ts @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-editor' +import { + Change, + CommentOperation, + EditOperation, +} from '../../../../../types/change' +import { DecorationSet, EditorView } from '@codemirror/view' +import { EditorSelection } from '@codemirror/state' +import _ from 'lodash' + +const useMoreCommments = ( + changes: Change[], + comments: Change[], + newComments: DecorationSet +): { + onEntriesPositioned: () => void + onMoreCommentsAboveClick: null | (() => void) + onMoreCommentsBelowClick: null | (() => void) +} => { + const view = useCodeMirrorViewContext() + + const [positionAbove, setPositionAbove] = useState(null) + const [positionBelow, setPositionBelow] = useState(null) + + const updateEntryPositions = useMemo( + () => + _.debounce( + () => + view.requestMeasure({ + key: 'review-panel-more-comments', + read(view) { + const container = view.scrollDOM + + if (!container) { + return { positionAbove: null, positionBelow: null } + } + + const containerTop = container.scrollTop + const containerBottom = containerTop + container.clientHeight + + // First check for any entries in view by looking for the actual rendered entries + for (const entryElt of container.querySelectorAll( + '.review-panel-entry' + )) { + const entryTop = entryElt?.offsetTop ?? 0 + const entryBottom = entryTop + (entryElt?.offsetHeight ?? 0) + + if (entryBottom > containerTop && entryTop < containerBottom) { + // Some part of the entry is in view + return { positionAbove: null, positionBelow: null } + } + } + + // Find the max and min positions in the visible part of the viewport + const visibleFrom = view.lineBlockAtHeight(containerTop).from + const visibleTo = view.lineBlockAtHeight(containerBottom).to + + // Then go through the positions to find the first entry above and below the visible viewport. + // We can't use the rendered entries for this because only the entries that are in the viewport (or + // have been in the viewport during the current page view session) are actually rendered. + let firstEntryAbove: number | null = null + let firstEntryBelow: number | null = null + + const updateFirstEntryAboveBelowPositions = ( + position: number + ) => { + if (visibleFrom === null || position < visibleFrom) { + firstEntryAbove = Math.max(firstEntryAbove ?? 0, position) + } + + if (visibleTo === null || position > visibleTo) { + firstEntryBelow = Math.min( + firstEntryBelow ?? Number.MAX_VALUE, + position + ) + } + } + + for (const entry of [...changes, ...comments]) { + updateFirstEntryAboveBelowPositions(entry.op.p) + } + + const cursor = newComments.iter() + while (cursor.value) { + updateFirstEntryAboveBelowPositions(cursor.from) + cursor.next() + } + + return { + positionAbove: firstEntryAbove, + positionBelow: firstEntryBelow, + } + }, + write({ positionAbove, positionBelow }) { + setPositionAbove(positionAbove) + setPositionBelow(positionBelow) + }, + }), + 200 + ), + [changes, comments, newComments, view] + ) + + useEffect(() => { + const scrollerElt = document.getElementsByClassName('cm-scroller')[0] + if (scrollerElt) { + scrollerElt.addEventListener('scroll', updateEntryPositions) + return () => { + scrollerElt.removeEventListener('scroll', updateEntryPositions) + } + } + }, [updateEntryPositions]) + + const onMoreCommentsClick = useCallback( + (position: number) => { + view.dispatch({ + effects: EditorView.scrollIntoView(position, { + y: 'center', + }), + selection: EditorSelection.cursor(position), + }) + }, + [view] + ) + + const onMoreCommentsAboveClick = useCallback(() => { + if (positionAbove !== null) { + onMoreCommentsClick(positionAbove) + } + }, [positionAbove, onMoreCommentsClick]) + + const onMoreCommentsBelowClick = useCallback(() => { + if (positionBelow !== null) { + onMoreCommentsClick(positionBelow) + } + }, [positionBelow, onMoreCommentsClick]) + + return { + onEntriesPositioned: updateEntryPositions, + onMoreCommentsAboveClick: + positionAbove !== null ? onMoreCommentsAboveClick : null, + onMoreCommentsBelowClick: + positionBelow !== null ? onMoreCommentsBelowClick : null, + } +} + +export default useMoreCommments diff --git a/services/web/frontend/stylesheets/app/editor/review-panel-new.less b/services/web/frontend/stylesheets/app/editor/review-panel-new.less index 7880b339d3..da21503870 100644 --- a/services/web/frontend/stylesheets/app/editor/review-panel-new.less +++ b/services/web/frontend/stylesheets/app/editor/review-panel-new.less @@ -1,4 +1,6 @@ .review-panel-new { + @review-panel-header-height: 69px; + &.review-panel-container { height: 100%; flex-shrink: 0; @@ -125,6 +127,7 @@ position: fixed; top: var(--review-panel-top); width: var(--review-panel-width); + height: @review-panel-header-height; z-index: 2; display: flex; flex-direction: column; @@ -396,7 +399,7 @@ .review-panel-overview { padding: 4px; position: absolute; - top: 69px; + top: @review-panel-header-height; bottom: 59px; width: 100%; overflow: auto; @@ -501,6 +504,31 @@ } } + .review-panel-more-comments-button-container { + position: fixed; + width: var(--review-panel-width); + display: flex; + justify-content: center; + z-index: 3; + + &.downwards { + // TODO: fix this to not use a magic number when we have updated the footer ui + top: calc(100% - 102px); + } + + &.upwards { + top: calc(var(--review-panel-top) + @review-panel-header-height + 16px); + } + } + + .review-panel-more-comments-button { + display: flex; + justify-content: center; + align-items: center; + padding: 2px 8px; + height: 24px; + } + &.review-panel-subview-overview { &.review-panel-container { overflow-y: hidden; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 804525c50a..469269f5cf 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1243,6 +1243,7 @@ "monthly": "Monthly", "more": "More", "more_actions": "More actions", + "more_comments": "More comments", "more_info": "More Info", "more_lowercase": "more", "more_options": "More options",