mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #15610 from overleaf/td-ide-page-editor-events
React IDE page: hook up events GitOrigin-RevId: 1121a30755fc600023f06925ca3eafa7a8e1ee14
This commit is contained in:
parent
ea9a639734
commit
1c820de200
7 changed files with 165 additions and 20 deletions
|
@ -1,6 +1,5 @@
|
|||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Alerts } from '@/features/ide-react/components/alerts/alerts'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import MainLayout from '@/features/ide-react/components/layout/main-layout'
|
||||
|
@ -12,12 +11,18 @@ import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-ev
|
|||
import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
|
||||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||
import { useOpenFile } from '@/features/ide-react/hooks/use-open-file'
|
||||
import { useEditingSessionHeartbeat } from '@/features/ide-react/hooks/use-editing-session-heartbeat'
|
||||
import { useRegisterUserActivity } from '@/features/ide-react/hooks/use-register-user-activity'
|
||||
import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-error'
|
||||
|
||||
// This is filled with placeholder content while the real content is migrated
|
||||
// away from Angular
|
||||
export default function IdePage() {
|
||||
useLayoutEventTracking()
|
||||
useSocketListeners()
|
||||
useEditingSessionHeartbeat()
|
||||
useRegisterUserActivity()
|
||||
useHasLintingError()
|
||||
|
||||
// This returns a function to open a binary file but for now we just use the
|
||||
// fact that it also patches in ide.binaryFilesManager. Once Angular is gone,
|
||||
|
@ -26,7 +31,7 @@ export default function IdePage() {
|
|||
useOpenFile()
|
||||
|
||||
const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
|
||||
const { connectionState, registerUserActivity } = useConnectionContext()
|
||||
const { connectionState } = useConnectionContext()
|
||||
const { showLockEditorMessageModal } = useModalsContext()
|
||||
|
||||
// Show modal when editor is forcefully disconnected
|
||||
|
@ -40,19 +45,6 @@ export default function IdePage() {
|
|||
showLockEditorMessageModal,
|
||||
])
|
||||
|
||||
// Inform the connection manager when the user is active
|
||||
const listener = useCallback(
|
||||
() => registerUserActivity(),
|
||||
[registerUserActivity]
|
||||
)
|
||||
|
||||
useEventListener('cursor:editor:update', listener)
|
||||
|
||||
useEffect(() => {
|
||||
document.body.addEventListener('click', listener)
|
||||
return () => document.body.removeEventListener('click', listener)
|
||||
}, [listener])
|
||||
|
||||
const { chatIsOpen } = useLayoutContext()
|
||||
|
||||
const mainContent = (
|
||||
|
|
|
@ -28,6 +28,8 @@ import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
|
|||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { EditorType } from '@/features/ide-react/editor/types/editor-type'
|
||||
|
||||
interface GotoOffsetOptions {
|
||||
gotoOffset: number
|
||||
|
@ -41,7 +43,7 @@ interface OpenDocOptions
|
|||
}
|
||||
|
||||
type EditorManager = {
|
||||
getEditorType: () => 'cm6' | 'cm6-rich-text' | null
|
||||
getEditorType: () => EditorType | null
|
||||
showSymbolPalette: boolean
|
||||
currentDocument: Document
|
||||
currentDocumentId: string | null
|
||||
|
@ -113,7 +115,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
const ide = useIdeContext()
|
||||
const { projectId } = useIdeReactContext()
|
||||
const { reportError, eventEmitter, eventLog } = useIdeReactContext()
|
||||
const { socket, disconnect } = useConnectionContext()
|
||||
const { socket, disconnect, connectionState } = useConnectionContext()
|
||||
const { view, setView } = useLayoutContext()
|
||||
const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
|
||||
useModalsContext()
|
||||
|
@ -122,7 +124,6 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
'editor.showSymbolPalette'
|
||||
)
|
||||
const [showVisual] = useScopeValue<boolean>('editor.showVisual')
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [currentDocument, setCurrentDocument] =
|
||||
useScopeValue<Document>('editor.sharejs_doc')
|
||||
const [openDocId, setOpenDocId] = useScopeValue<string | null>(
|
||||
|
@ -207,7 +208,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
})
|
||||
}, [ide.scopeStore, setShowSymbolPalette])
|
||||
|
||||
const getEditorType = useCallback(() => {
|
||||
const getEditorType = useCallback((): EditorType | null => {
|
||||
if (!currentDocument) {
|
||||
return null
|
||||
}
|
||||
|
@ -581,6 +582,34 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
t,
|
||||
])
|
||||
|
||||
useEventListener('editor:insert-symbol', () => {
|
||||
sendMB('symbol-palette-insert')
|
||||
})
|
||||
|
||||
useEventListener('flush-changes', () => {
|
||||
openDocs.flushAll()
|
||||
})
|
||||
|
||||
useEventListener('blur', () => {
|
||||
openDocs.flushAll()
|
||||
})
|
||||
|
||||
// Flush changes before disconnecting
|
||||
useEffect(() => {
|
||||
if (connectionState.forceDisconnected) {
|
||||
openDocs.flushAll()
|
||||
}
|
||||
}, [connectionState.forceDisconnected, openDocs])
|
||||
|
||||
// Watch for changes in wantTrackChanges
|
||||
const previousWantTrackChangesRef = useRef(wantTrackChanges)
|
||||
useEffect(() => {
|
||||
if (wantTrackChanges !== previousWantTrackChangesRef.current) {
|
||||
previousWantTrackChangesRef.current = wantTrackChanges
|
||||
syncTrackChangesState(currentDocument)
|
||||
}
|
||||
}, [currentDocument, syncTrackChangesState, wantTrackChanges])
|
||||
|
||||
const editorManager = useMemo(
|
||||
() => ({
|
||||
getEditorType,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export type EditorType = 'cm6' | 'cm6-rich-text'
|
|
@ -0,0 +1,82 @@
|
|||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { EditorType } from '@/features/ide-react/editor/types/editor-type'
|
||||
import { reportCM6Perf } from '@/infrastructure/cm6-performance'
|
||||
import { putJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import moment from 'moment'
|
||||
import { useCallback, useState } from 'react'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useDomEventListener from '@/shared/hooks/use-dom-event-listener'
|
||||
|
||||
function createEditingSessionHeartbeatData(editorType: EditorType) {
|
||||
const segmentation: Record<string, unknown> = {
|
||||
editorType,
|
||||
}
|
||||
const cm6PerfData = reportCM6Perf()
|
||||
|
||||
// Ignore if no typing has happened
|
||||
if (cm6PerfData.numberOfEntries > 0) {
|
||||
for (const [key, value] of Object.entries(cm6PerfData)) {
|
||||
const segmentationPropName =
|
||||
'cm6Perf' + key.charAt(0).toUpperCase() + key.slice(1)
|
||||
if (value !== null) {
|
||||
segmentation[segmentationPropName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segmentation
|
||||
}
|
||||
|
||||
function sendEditingSessionHeartbeat(
|
||||
projectId: string,
|
||||
segmentation: Record<string, unknown>
|
||||
) {
|
||||
putJSON(`/editingSession/${projectId}`, {
|
||||
body: { segmentation },
|
||||
}).catch(debugConsole.error)
|
||||
}
|
||||
|
||||
export function useEditingSessionHeartbeat() {
|
||||
const { projectId } = useIdeReactContext()
|
||||
const { getEditorType } = useEditorManagerContext()
|
||||
|
||||
// Keep track of how many heartbeats we've sent so that we can calculate how
|
||||
// long wait until the next one
|
||||
const [heartbeatsSent, setHeartbeatsSent] = useState(0)
|
||||
const [nextHeartbeatAt, setNextHeartbeatAt] = useState(() => new Date())
|
||||
|
||||
const editingSessionHeartbeat = useCallback(() => {
|
||||
debugConsole.log('[Event] heartbeat trigger')
|
||||
|
||||
const editorType = getEditorType()
|
||||
if (editorType === null) return
|
||||
|
||||
// If the next heartbeat is in the future, stop
|
||||
if (nextHeartbeatAt > new Date()) return
|
||||
|
||||
const segmentation = createEditingSessionHeartbeatData(editorType)
|
||||
|
||||
debugConsole.log('[Event] send heartbeat request', segmentation)
|
||||
sendEditingSessionHeartbeat(projectId, segmentation)
|
||||
|
||||
setHeartbeatsSent(heartbeatsSent => heartbeatsSent + 1)
|
||||
|
||||
// Send two first heartbeats at 0 and 30s then increase the backoff time
|
||||
// 1min per call until we reach 5 min
|
||||
const backoffSecs =
|
||||
heartbeatsSent <= 2
|
||||
? 30
|
||||
: heartbeatsSent <= 6
|
||||
? (heartbeatsSent - 2) * 60
|
||||
: 300
|
||||
|
||||
setNextHeartbeatAt(moment().add(backoffSecs, 'seconds').toDate())
|
||||
}, [getEditorType, heartbeatsSent, nextHeartbeatAt, projectId])
|
||||
|
||||
// Hook the heartbeat up to editor events
|
||||
useEventListener('cursor:editor:update', editingSessionHeartbeat)
|
||||
useEventListener('scroll:editor:update', editingSessionHeartbeat)
|
||||
useDomEventListener(document, 'click', editingSessionHeartbeat)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
|
||||
|
||||
export function useHasLintingError() {
|
||||
const { setHasLintingError } = useLocalCompileContext()
|
||||
|
||||
// Listen for editor:lint event from CM6 linter and keep compile context
|
||||
// up to date
|
||||
useEventListener('editor:lint', (event: CustomEvent) => {
|
||||
setHasLintingError(event.detail.hasLintingError)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useDomEventListener from '@/shared/hooks/use-dom-event-listener'
|
||||
|
||||
export function useRegisterUserActivity() {
|
||||
const { registerUserActivity } = useConnectionContext()
|
||||
|
||||
useEventListener('cursor:editor:update', registerUserActivity)
|
||||
useDomEventListener(document.body, 'click', registerUserActivity)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { useEffect } from 'react'
|
||||
|
||||
// The use of the EventListener type means that this can only be used for
|
||||
// built-in DOM event types rather than custom events.
|
||||
// There are libraries such as usehooks-ts that provide hooks like this with
|
||||
// support for type-safe custom events that we may want to look into.
|
||||
export default function useDomEventListener(
|
||||
eventTarget: EventTarget,
|
||||
eventName: string,
|
||||
listener: EventListener
|
||||
) {
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(eventName, listener)
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(eventName, listener)
|
||||
}
|
||||
}, [eventTarget, eventName, listener])
|
||||
}
|
Loading…
Reference in a new issue