mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-28 22:23:17 -05:00
Merge pull request #19796 from overleaf/dp-more-comments-2
Add More Comments buttons GitOrigin-RevId: 3cd6539683cbc53ceb63488ab9e9b0ffe53e079c
This commit is contained in:
parent
13545140ee
commit
a331ac6116
6 changed files with 291 additions and 55 deletions
|
@ -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": "",
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue