Add renderer ready state to global application state

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-06-18 20:42:17 +02:00
parent 2dad1d565e
commit cfb2de8909
12 changed files with 144 additions and 52 deletions

View file

@ -10,6 +10,7 @@ import { ShowIf } from '../../../common/show-if/show-if'
import { DocumentInfoLine } from './document-info-line'
import { UnitalicBoldText } from './unitalic-bold-text'
import { useIFrameEditorToRendererCommunicator } from '../../render-context/iframe-editor-to-renderer-communicator-context-provider'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Creates a new info line for the document information dialog that holds the
@ -19,17 +20,23 @@ export const DocumentInfoLineWordCount: React.FC = () => {
useTranslation()
const iframeEditorToRendererCommunicator = useIFrameEditorToRendererCommunicator()
const [wordCount, setWordCount] = useState<number | null>(null)
const rendererReady = useApplicationState((state) => state.editorConfig.rendererReady)
useEffect(() => {
iframeEditorToRendererCommunicator?.onWordCountCalculated((words) => {
iframeEditorToRendererCommunicator.onWordCountCalculated((words) => {
setWordCount(words)
})
iframeEditorToRendererCommunicator?.sendGetWordCount()
return () => {
iframeEditorToRendererCommunicator?.onWordCountCalculated(undefined)
iframeEditorToRendererCommunicator.onWordCountCalculated(undefined)
}
}, [iframeEditorToRendererCommunicator, setWordCount])
useEffect(() => {
if (rendererReady) {
iframeEditorToRendererCommunicator.sendGetWordCount()
}
}, [iframeEditorToRendererCommunicator, rendererReady])
return (
<DocumentInfoLine icon={'align-left'} size={'2x'}>
<ShowIf condition={wordCount === null}>

View file

@ -31,7 +31,7 @@ export const IframeRendererToEditorCommunicatorContextProvider: React.FC = ({ ch
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
const currentIFrameCommunicator = useMemo<IframeRendererToEditorCommunicator>(() => {
const newCommunicator = new IframeRendererToEditorCommunicator()
newCommunicator.setOtherSide(window.parent, editorOrigin)
newCommunicator.setMessageTarget(window.parent, editorOrigin)
return newCommunicator
}, [editorOrigin])

View file

@ -19,12 +19,12 @@ export const useOnIframeLoad = (
return useCallback(() => {
const frame = frameReference.current
if (!frame || !frame.contentWindow) {
iframeCommunicator.unsetOtherSide()
iframeCommunicator.unsetMessageTarget()
return
}
if (sendToRenderPage.current) {
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
iframeCommunicator.setMessageTarget(frame.contentWindow, rendererOrigin)
sendToRenderPage.current = false
return
} else {

View file

@ -7,6 +7,7 @@ import equal from 'fast-deep-equal'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
import { setRendererReady } from '../../../redux/editor/methods'
import { isTestMode } from '../../../utils/test-modes'
import { RendererProps } from '../../render-page/markdown-document'
import { ImageDetails, RendererType } from '../../render-page/rendering-message'
@ -16,7 +17,6 @@ import { useOnIframeLoad } from './hooks/use-on-iframe-load'
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox'
export interface RenderIframeProps extends RendererProps {
onRendererReadyChange?: (rendererReady: boolean) => void
rendererType: RendererType
forcedDarkMode?: boolean
frameClasses?: string
@ -31,13 +31,11 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
onScroll,
onMakeScrollSource,
frameClasses,
onRendererReadyChange,
rendererType,
forcedDarkMode
}) => {
const savedDarkMode = useIsDarkModeActivated()
const darkMode = forcedDarkMode ?? savedDarkMode
const [rendererReady, setRendererReady] = useState<boolean>(false)
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null)
@ -54,29 +52,51 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
)
const [frameHeight, setFrameHeight] = useState<number>(0)
useEffect(() => {
onRendererReadyChange?.(rendererReady)
}, [onRendererReadyChange, rendererReady])
const rendererReady = useApplicationState((state) => state.editorConfig.rendererReady)
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
useEffect(
() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange),
[iframeCommunicator, onFirstHeadingChange]
() => () => {
iframeCommunicator.unregisterEventListener()
setRendererReady(false)
},
[iframeCommunicator]
)
useEffect(
() => iframeCommunicator.onFrontmatterChange(onFrontmatterChange),
[iframeCommunicator, onFrontmatterChange]
)
useEffect(() => iframeCommunicator.onSetScrollState(onScroll), [iframeCommunicator, onScroll])
useEffect(
() => iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource),
[iframeCommunicator, onMakeScrollSource]
)
useEffect(
() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange),
[iframeCommunicator, onTaskCheckedChange]
)
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
useEffect(() => {
iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange)
return () => iframeCommunicator.onFirstHeadingChange(undefined)
}, [iframeCommunicator, onFirstHeadingChange])
useEffect(() => {
iframeCommunicator.onFrontmatterChange(onFrontmatterChange)
return () => iframeCommunicator.onFrontmatterChange(undefined)
}, [iframeCommunicator, onFrontmatterChange])
useEffect(() => {
iframeCommunicator.onSetScrollState(onScroll)
return () => iframeCommunicator.onSetScrollState(undefined)
}, [iframeCommunicator, onScroll])
useEffect(() => {
iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource)
return () => iframeCommunicator.onSetScrollSourceToRenderer(undefined)
}, [iframeCommunicator, onMakeScrollSource])
useEffect(() => {
iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange)
return () => iframeCommunicator.onTaskCheckboxChange(undefined)
}, [iframeCommunicator, onTaskCheckedChange])
useEffect(() => {
iframeCommunicator.onImageClicked(setLightboxDetails)
return () => iframeCommunicator.onImageClicked(undefined)
}, [iframeCommunicator])
useEffect(() => {
iframeCommunicator.onHeightChange(setFrameHeight)
return () => iframeCommunicator.onHeightChange(undefined)
}, [iframeCommunicator])
useEffect(() => {
iframeCommunicator.onRendererReady(() => {
iframeCommunicator.sendSetBaseConfiguration({
@ -85,8 +105,8 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
})
setRendererReady(true)
})
}, [darkMode, rendererType, iframeCommunicator, rendererReady, scrollState])
useEffect(() => iframeCommunicator.onHeightChange(setFrameHeight), [iframeCommunicator])
return () => iframeCommunicator.onRendererReady(undefined)
}, [iframeCommunicator, rendererType])
useEffect(() => {
if (rendererReady) {

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useState } from 'react'
import React from 'react'
import { Trans } from 'react-i18next'
import { Branding } from '../common/branding/branding'
import {
@ -20,10 +20,11 @@ import { ShowIf } from '../common/show-if/show-if'
import { RendererType } from '../render-page/rendering-message'
import { WaitSpinner } from '../common/wait-spinner/wait-spinner'
import { IframeEditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider'
import { useApplicationState } from '../../hooks/common/use-application-state'
export const IntroPage: React.FC = () => {
const introPageContent = useIntroPageContent()
const [rendererReady, setRendererReady] = useState<boolean>(true)
const rendererReady = useApplicationState((state) => state.editorConfig.rendererReady)
return (
<IframeEditorToRendererCommunicatorContextProvider>
@ -45,7 +46,6 @@ export const IntroPage: React.FC = () => {
<RenderIframe
frameClasses={'w-100 overflow-y-hidden'}
markdownContent={introPageContent as string}
onRendererReadyChange={setRendererReady}
rendererType={RendererType.INTRO}
forcedDarkMode={true}
/>

View file

@ -4,38 +4,74 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Error that will be thrown if a message couldn't be sent.
*/
export class IframeCommunicatorSendingError extends Error {}
/**
* Base class for communication between renderer and editor.
*/
export abstract class IframeCommunicator<SEND, RECEIVE> {
protected otherSide?: Window
protected otherOrigin?: string
private messageTarget?: Window
private targetOrigin?: string
private communicationEnabled: boolean
constructor() {
window.addEventListener('message', this.handleEvent.bind(this))
this.communicationEnabled = false
}
public unregisterEventListener(): void {
window.removeEventListener('message', this.handleEvent.bind(this))
}
public setOtherSide(otherSide: Window, otherOrigin: string): void {
this.otherSide = otherSide
this.otherOrigin = otherOrigin
/**
* Sets the target for message sending.
* Messages can be sent as soon as the communication is enabled.
*
* @see enableCommunication
* @param otherSide The target {@link Window} that should receive the messages.
* @param otherOrigin The origin from the URL of the target. If this isn't correct then the message sending will produce CORS errors.
*/
public setMessageTarget(otherSide: Window, otherOrigin: string): void {
this.messageTarget = otherSide
this.targetOrigin = otherOrigin
this.communicationEnabled = false
}
public unsetOtherSide(): void {
this.otherSide = undefined
this.otherOrigin = undefined
/**
* Unsets the message target. Should be used if the old target isn't available anymore.
*/
public unsetMessageTarget(): void {
this.messageTarget = undefined
this.targetOrigin = undefined
this.communicationEnabled = false
}
public getOtherSide(): Window | undefined {
return this.otherSide
/**
* Enables the message communication.
* Should be called as soon as the other sides is ready to receive messages.
*/
protected enableCommunication(): void {
this.communicationEnabled = true
}
/**
* Sends a message to the message target.
*
* @param message The message to send.
*/
protected sendMessageToOtherSide(message: SEND): void {
if (this.otherSide === undefined || this.otherOrigin === undefined) {
console.error("Can't send message because otherSide is null", message)
return
if (this.messageTarget === undefined || this.targetOrigin === undefined) {
throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`)
}
this.otherSide.postMessage(message, this.otherOrigin)
if (!this.communicationEnabled) {
throw new IframeCommunicatorSendingError(
`Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}`
)
}
this.messageTarget.postMessage(message, this.targetOrigin)
}
protected abstract handleEvent(event: MessageEvent<RECEIVE>): void

View file

@ -106,6 +106,7 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.enableCommunication()
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:

View file

@ -38,7 +38,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
useEffect(
() => iframeCommunicator?.onGetWordCount(countWordsInRenderedDocument),
() => iframeCommunicator.onGetWordCount(countWordsInRenderedDocument),
[iframeCommunicator, countWordsInRenderedDocument]
)

View file

@ -46,6 +46,7 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
}
public sendRendererReady(): void {
this.enableCommunication()
this.sendMessageToOtherSide({
type: RenderIframeMessageType.RENDERER_READY
})

View file

@ -14,7 +14,8 @@ import {
SetEditorLigaturesAction,
SetEditorPreferencesAction,
SetEditorSmartPasteAction,
SetEditorSyncScrollAction
SetEditorSyncScrollAction,
SetRendererReadyAction
} from './types'
export const loadFromLocalStorage = (): EditorConfig | undefined => {
@ -46,6 +47,19 @@ export const setEditorMode = (editorMode: EditorMode): void => {
store.dispatch(action)
}
/**
* Dispatches a global application state change for the "renderer ready" state.
*
* @param rendererReady The new renderer ready state.
*/
export const setRendererReady = (rendererReady: boolean): void => {
const action: SetRendererReadyAction = {
type: EditorConfigActionType.SET_RENDERER_READY,
rendererReady
}
store.dispatch(action)
}
export const setEditorSyncScroll = (syncScroll: boolean): void => {
const action: SetEditorSyncScrollAction = {
type: EditorConfigActionType.SET_SYNC_SCROLL,

View file

@ -15,7 +15,8 @@ import {
SetEditorLigaturesAction,
SetEditorPreferencesAction,
SetEditorSmartPasteAction,
SetEditorSyncScrollAction
SetEditorSyncScrollAction,
SetRendererReadyAction
} from './types'
const initialState: EditorConfig = {
@ -23,6 +24,7 @@ const initialState: EditorConfig = {
ligatures: true,
syncScroll: true,
smartPaste: true,
rendererReady: false,
preferences: {
theme: 'one-dark',
keyMap: 'sublime',
@ -55,6 +57,11 @@ export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
}
saveToLocalStorage(newState)
return newState
case EditorConfigActionType.SET_RENDERER_READY:
return {
...state,
rendererReady: (action as SetRendererReadyAction).rendererReady
}
case EditorConfigActionType.SET_LIGATURES:
newState = {
...state,

View file

@ -13,7 +13,8 @@ export enum EditorConfigActionType {
SET_SYNC_SCROLL = 'editor/syncScroll/set',
MERGE_EDITOR_PREFERENCES = 'editor/preferences/merge',
SET_LIGATURES = 'editor/preferences/setLigatures',
SET_SMART_PASTE = 'editor/preferences/setSmartPaste'
SET_SMART_PASTE = 'editor/preferences/setSmartPaste',
SET_RENDERER_READY = 'editor/rendererReady/set'
}
export interface EditorConfig {
@ -21,6 +22,7 @@ export interface EditorConfig {
syncScroll: boolean
ligatures: boolean
smartPaste: boolean
rendererReady: boolean
preferences: EditorConfiguration
}
@ -28,6 +30,10 @@ export interface EditorConfigActions extends Action<EditorConfigActionType> {
type: EditorConfigActionType
}
export interface SetRendererReadyAction extends EditorConfigActions {
rendererReady: boolean
}
export interface SetEditorSyncScrollAction extends EditorConfigActions {
syncScroll: boolean
}