Merge pull request #19796 from overleaf/dp-more-comments-2

Add More Comments buttons

GitOrigin-RevId: 3cd6539683cbc53ceb63488ab9e9b0ffe53e079c
This commit is contained in:
David 2024-10-02 12:58:17 +01:00 committed by Copybot
parent 13545140ee
commit a331ac6116
6 changed files with 291 additions and 55 deletions

View file

@ -881,6 +881,7 @@
"month": "", "month": "",
"more": "", "more": "",
"more_actions": "", "more_actions": "",
"more_comments": "",
"more_info": "", "more_info": "",
"more_options": "", "more_options": "",
"more_options_for_border_settings_coming_soon": "", "more_options_for_border_settings_coming_soon": "",

View file

@ -29,6 +29,8 @@ import ReviewPanelEmptyState from './review-panel-empty-state'
import useEventListener from '@/shared/hooks/use-event-listener' import useEventListener from '@/shared/hooks/use-event-listener'
import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range' import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
import { addCommentStateField } from '@/features/source-editor/extensions/add-comment' 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 = { type AggregatedRanges = {
changes: Change<EditOperation>[] changes: Change<EditOperation>[]
@ -47,59 +49,6 @@ const ReviewPanelCurrentFile: FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null) const containerRef = useRef<HTMLDivElement | null>(null)
const previousFocusedItem = useRef(new Map<string, number>()) const previousFocusedItem = useRef(new Map<string, number>())
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(() => { const buildAggregatedRanges = useCallback(() => {
if (ranges) { if (ranges) {
const output: AggregatedRanges = { const output: AggregatedRanges = {
@ -127,7 +76,7 @@ const ReviewPanelCurrentFile: FC = () => {
if (threads) { if (threads) {
for (const comment of ranges.comments) { 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) output.comments.push(comment)
} }
} }
@ -246,6 +195,71 @@ const ReviewPanelCurrentFile: FC = () => {
return entries return entries
}, [addCommentRanges, positions]) }, [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) { if (!aggregatedRanges) {
return null return null
} }
@ -253,6 +267,12 @@ const ReviewPanelCurrentFile: FC = () => {
return ( return (
<> <>
{showEmptyState && <ReviewPanelEmptyState />} {showEmptyState && <ReviewPanelEmptyState />}
{onMoreCommentsAboveClick && (
<ReviewPanelMoreCommentsButton
onClick={onMoreCommentsAboveClick}
direction="upward"
/>
)}
<div ref={handleContainer}> <div ref={handleContainer}>
{addCommentEntries.map(entry => { {addCommentEntries.map(entry => {
@ -294,6 +314,12 @@ const ReviewPanelCurrentFile: FC = () => {
) )
)} )}
</div> </div>
{onMoreCommentsBelowClick && (
<ReviewPanelMoreCommentsButton
onClick={onMoreCommentsBelowClick}
direction="downward"
/>
)}
</> </>
) )
} }

View file

@ -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 (
<div
className={classNames('review-panel-more-comments-button-container', {
downwards: direction === 'downward',
upwards: direction === 'upward',
})}
>
<Button
bsSize="small"
className="btn-secondary review-panel-more-comments-button"
onClick={onClick}
>
<MaterialIcon type={`arrow_${direction}_alt`} />
{t('more_comments')}
</Button>
</div>
)
}
export default memo(MoreCommentsButton)

View file

@ -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<EditOperation>[],
comments: Change<CommentOperation>[],
newComments: DecorationSet
): {
onEntriesPositioned: () => void
onMoreCommentsAboveClick: null | (() => void)
onMoreCommentsBelowClick: null | (() => void)
} => {
const view = useCodeMirrorViewContext()
const [positionAbove, setPositionAbove] = useState<number | null>(null)
const [positionBelow, setPositionBelow] = useState<number | null>(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<HTMLElement>(
'.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

View file

@ -1,4 +1,6 @@
.review-panel-new { .review-panel-new {
@review-panel-header-height: 69px;
&.review-panel-container { &.review-panel-container {
height: 100%; height: 100%;
flex-shrink: 0; flex-shrink: 0;
@ -125,6 +127,7 @@
position: fixed; position: fixed;
top: var(--review-panel-top); top: var(--review-panel-top);
width: var(--review-panel-width); width: var(--review-panel-width);
height: @review-panel-header-height;
z-index: 2; z-index: 2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -396,7 +399,7 @@
.review-panel-overview { .review-panel-overview {
padding: 4px; padding: 4px;
position: absolute; position: absolute;
top: 69px; top: @review-panel-header-height;
bottom: 59px; bottom: 59px;
width: 100%; width: 100%;
overflow: auto; 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-subview-overview {
&.review-panel-container { &.review-panel-container {
overflow-y: hidden; overflow-y: hidden;

View file

@ -1243,6 +1243,7 @@
"monthly": "Monthly", "monthly": "Monthly",
"more": "More", "more": "More",
"more_actions": "More actions", "more_actions": "More actions",
"more_comments": "More comments",
"more_info": "More Info", "more_info": "More Info",
"more_lowercase": "more", "more_lowercase": "more",
"more_options": "More options", "more_options": "More options",