use-more-comments.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { useCallback, useEffect, useMemo, useState } from 'react'
  2. import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-editor'
  3. import {
  4. Change,
  5. CommentOperation,
  6. EditOperation,
  7. } from '../../../../../types/change'
  8. import { DecorationSet, EditorView } from '@codemirror/view'
  9. import { EditorSelection } from '@codemirror/state'
  10. import _ from 'lodash'
  11. import useReviewPanelLayout from './use-review-panel-layout'
  12. const useMoreCommments = (
  13. changes: Change<EditOperation>[],
  14. comments: Change<CommentOperation>[],
  15. newComments: DecorationSet
  16. ): {
  17. onEntriesPositioned: () => void
  18. onMoreCommentsAboveClick: null | (() => void)
  19. onMoreCommentsBelowClick: null | (() => void)
  20. } => {
  21. const view = useCodeMirrorViewContext()
  22. const { showPanel, mini } = useReviewPanelLayout()
  23. const reviewPanelOpen = showPanel && !mini
  24. const [positionAbove, setPositionAbove] = useState<number | null>(null)
  25. const [positionBelow, setPositionBelow] = useState<number | null>(null)
  26. const updateEntryPositions = useMemo(
  27. () =>
  28. _.debounce(
  29. () =>
  30. view.requestMeasure({
  31. key: 'review-panel-more-comments',
  32. read(view) {
  33. const container = view.scrollDOM
  34. if (!container || !reviewPanelOpen) {
  35. return { positionAbove: null, positionBelow: null }
  36. }
  37. const containerTop = container.scrollTop
  38. const containerBottom = containerTop + container.clientHeight
  39. // First check for any entries in view by looking for the actual rendered entries
  40. for (const entryElt of container.querySelectorAll<HTMLElement>(
  41. '.review-panel-entry'
  42. )) {
  43. const entryTop = entryElt?.offsetTop ?? 0
  44. const entryBottom = entryTop + (entryElt?.offsetHeight ?? 0)
  45. if (entryBottom > containerTop && entryTop < containerBottom) {
  46. // Some part of the entry is in view
  47. return { positionAbove: null, positionBelow: null }
  48. }
  49. }
  50. // Find the max and min positions in the visible part of the viewport
  51. const visibleFrom = view.lineBlockAtHeight(containerTop).from
  52. const visibleTo = view.lineBlockAtHeight(containerBottom).to
  53. // Then go through the positions to find the first entry above and below the visible viewport.
  54. // We can't use the rendered entries for this because only the entries that are in the viewport (or
  55. // have been in the viewport during the current page view session) are actually rendered.
  56. let firstEntryAbove: number | null = null
  57. let firstEntryBelow: number | null = null
  58. const updateFirstEntryAboveBelowPositions = (
  59. position: number
  60. ) => {
  61. if (visibleFrom === null || position < visibleFrom) {
  62. firstEntryAbove = Math.max(firstEntryAbove ?? 0, position)
  63. }
  64. if (visibleTo === null || position > visibleTo) {
  65. firstEntryBelow = Math.min(
  66. firstEntryBelow ?? Number.MAX_VALUE,
  67. position
  68. )
  69. }
  70. }
  71. for (const entry of [...changes, ...comments]) {
  72. updateFirstEntryAboveBelowPositions(entry.op.p)
  73. }
  74. const cursor = newComments.iter()
  75. while (cursor.value) {
  76. updateFirstEntryAboveBelowPositions(cursor.from)
  77. cursor.next()
  78. }
  79. return {
  80. positionAbove: firstEntryAbove,
  81. positionBelow: firstEntryBelow,
  82. }
  83. },
  84. write({ positionAbove, positionBelow }) {
  85. setPositionAbove(positionAbove)
  86. setPositionBelow(positionBelow)
  87. },
  88. }),
  89. 200
  90. ),
  91. [changes, comments, newComments, view, reviewPanelOpen]
  92. )
  93. useEffect(() => {
  94. const scrollerElt = document.getElementsByClassName('cm-scroller')[0]
  95. if (scrollerElt) {
  96. scrollerElt.addEventListener('scroll', updateEntryPositions)
  97. return () => {
  98. scrollerElt.removeEventListener('scroll', updateEntryPositions)
  99. }
  100. }
  101. }, [updateEntryPositions])
  102. const onMoreCommentsClick = useCallback(
  103. (position: number) => {
  104. view.dispatch({
  105. effects: EditorView.scrollIntoView(position, {
  106. y: 'center',
  107. }),
  108. selection: EditorSelection.cursor(position),
  109. })
  110. },
  111. [view]
  112. )
  113. const onMoreCommentsAboveClick = useCallback(() => {
  114. if (positionAbove !== null) {
  115. onMoreCommentsClick(positionAbove)
  116. }
  117. }, [positionAbove, onMoreCommentsClick])
  118. const onMoreCommentsBelowClick = useCallback(() => {
  119. if (positionBelow !== null) {
  120. onMoreCommentsClick(positionBelow)
  121. }
  122. }, [positionBelow, onMoreCommentsClick])
  123. return {
  124. onEntriesPositioned: updateEntryPositions,
  125. onMoreCommentsAboveClick:
  126. positionAbove !== null ? onMoreCommentsAboveClick : null,
  127. onMoreCommentsBelowClick:
  128. positionBelow !== null ? onMoreCommentsBelowClick : null,
  129. }
  130. }
  131. export default useMoreCommments