mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -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": "",
|
||||
"more": "",
|
||||
"more_actions": "",
|
||||
"more_comments": "",
|
||||
"more_info": "",
|
||||
"more_options": "",
|
||||
"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 { 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<EditOperation>[]
|
||||
|
@ -47,59 +49,6 @@ const ReviewPanelCurrentFile: FC = () => {
|
|||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
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(() => {
|
||||
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 && <ReviewPanelEmptyState />}
|
||||
{onMoreCommentsAboveClick && (
|
||||
<ReviewPanelMoreCommentsButton
|
||||
onClick={onMoreCommentsAboveClick}
|
||||
direction="upward"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div ref={handleContainer}>
|
||||
{addCommentEntries.map(entry => {
|
||||
|
@ -294,6 +314,12 @@ const ReviewPanelCurrentFile: FC = () => {
|
|||
)
|
||||
)}
|
||||
</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-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;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue