import { ComponentAction, FeatureStatus, SNComponent, dateToLocalizedString, ComponentViewer, ComponentViewerEvent, ComponentViewerError, } from '@standardnotes/snjs' import { WebApplication } from '@/UIModels/Application' import { FunctionalComponent } from 'preact' import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' import { observer } from 'mobx-react-lite' import { OfflineRestricted } from '@/Components/ComponentView/OfflineRestricted' import { UrlMissing } from '@/Components/ComponentView/UrlMissing' import { IsDeprecated } from '@/Components/ComponentView/IsDeprecated' import { IsExpired } from '@/Components/ComponentView/IsExpired' import { IssueOnLoading } from '@/Components/ComponentView/IssueOnLoading' import { AppState } from '@/UIModels/AppState' import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' interface IProps { application: WebApplication appState: AppState componentViewer: ComponentViewer requestReload?: (viewer: ComponentViewer, force?: boolean) => void onLoad?: (component: SNComponent) => void manualDealloc?: boolean } /** * The maximum amount of time we'll wait for a component * to load before displaying error */ const MaxLoadThreshold = 4000 const VisibilityChangeKey = 'visibilitychange' const MSToWaitAfterIframeLoadToAvoidFlicker = 35 export const ComponentView: FunctionalComponent = observer( ({ application, onLoad, componentViewer, requestReload }) => { const iframeRef = useRef(null) const excessiveLoadingTimeout = useRef | undefined>(undefined) const [hasIssueLoading, setHasIssueLoading] = useState(false) const [isLoading, setIsLoading] = useState(true) const [featureStatus, setFeatureStatus] = useState( componentViewer.getFeatureStatus(), ) const [isComponentValid, setIsComponentValid] = useState(true) const [error, setError] = useState(undefined) const [deprecationMessage, setDeprecationMessage] = useState(undefined) const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false) const [didAttemptReload, setDidAttemptReload] = useState(false) const component = componentViewer.component const manageSubscription = useCallback(() => { openSubscriptionDashboard(application) }, [application]) const reloadValidityStatus = useCallback(() => { setFeatureStatus(componentViewer.getFeatureStatus()) if (!componentViewer.lockReadonly) { componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled) } setIsComponentValid(componentViewer.shouldRender()) if (isLoading && !isComponentValid) { setIsLoading(false) } setError(componentViewer.getError()) setDeprecationMessage(component.deprecationMessage) }, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading]) useEffect(() => { reloadValidityStatus() }, [reloadValidityStatus]) const dismissDeprecationMessage = () => { setIsDeprecationMessageDismissed(true) } const onVisibilityChange = useCallback(() => { if (document.visibilityState === 'hidden') { return } if (hasIssueLoading) { requestReload?.(componentViewer) } }, [hasIssueLoading, componentViewer, requestReload]) const handleIframeTakingTooLongToLoad = useCallback(async () => { setIsLoading(false) setHasIssueLoading(true) if (!didAttemptReload) { setDidAttemptReload(true) requestReload?.(componentViewer) } else { document.addEventListener(VisibilityChangeKey, onVisibilityChange) } }, [didAttemptReload, onVisibilityChange, componentViewer, requestReload]) useMemo(() => { const loadTimeout = setTimeout(() => { handleIframeTakingTooLongToLoad().catch(console.error) }, MaxLoadThreshold) excessiveLoadingTimeout.current = loadTimeout return () => { excessiveLoadingTimeout.current && clearTimeout(excessiveLoadingTimeout.current) } }, [handleIframeTakingTooLongToLoad]) const onIframeLoad = useCallback(() => { const iframe = iframeRef.current as HTMLIFrameElement const contentWindow = iframe.contentWindow as Window excessiveLoadingTimeout.current && clearTimeout(excessiveLoadingTimeout.current) try { componentViewer.setWindow(contentWindow) } catch (error) { console.error(error) } setTimeout(() => { setIsLoading(false) setHasIssueLoading(false) onLoad?.(component) }, MSToWaitAfterIframeLoadToAvoidFlicker) }, [componentViewer, onLoad, component, excessiveLoadingTimeout]) useEffect(() => { const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => { if (event === ComponentViewerEvent.FeatureStatusUpdated) { setFeatureStatus(componentViewer.getFeatureStatus()) } }) return () => { removeFeaturesChangedObserver() } }, [componentViewer]) useEffect(() => { const removeActionObserver = componentViewer.addActionObserver((action, data) => { switch (action) { case ComponentAction.KeyDown: application.io.handleComponentKeyDown(data.keyboardModifier) break case ComponentAction.KeyUp: application.io.handleComponentKeyUp(data.keyboardModifier) break case ComponentAction.Click: application.getAppState().notes.setContextMenuOpen(false) break default: return } }) return () => { removeActionObserver() } }, [componentViewer, application]) useEffect(() => { const unregisterDesktopObserver = application .getDesktopService() ?.registerUpdateObserver((updatedComponent: SNComponent) => { if (updatedComponent.uuid === component.uuid && updatedComponent.active) { requestReload?.(componentViewer) } }) return () => { unregisterDesktopObserver?.() } }, [application, requestReload, componentViewer, component.uuid]) return ( <> {hasIssueLoading && ( { reloadValidityStatus(), requestReload?.(componentViewer, true) }} /> )} {featureStatus !== FeatureStatus.Entitled && ( )} {deprecationMessage && !isDeprecationMessageDismissed && ( )} {error === ComponentViewerError.OfflineRestricted && } {error === ComponentViewerError.MissingUrl && } {component.uuid && isComponentValid && ( )} {isLoading &&
} ) }, )