import { ComponentAction, FeatureStatus, ComponentViewerInterface, ComponentViewerEvent, ComponentViewerError, ComponentInterface, SubscriptionManagerEvent, } from '@standardnotes/snjs' import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react' 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 NotEntitledBanner from '@/Components/ComponentView/NotEntitledBanner' import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading' import { useApplication } from '../ApplicationProvider' interface Props { componentViewer: ComponentViewerInterface requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void onLoad?: () => void } /** * 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 const IframeFeatureView: FunctionComponent = ({ onLoad, componentViewer, requestReload }) => { const application = useApplication() const iframeRef = useRef(null) const [loadTimeout, setLoadTimeout] = useState | 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 uiFeature = componentViewer.getComponentOrFeatureItem() 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(uiFeature.deprecationMessage) }, [componentViewer, uiFeature, featureStatus, isComponentValid, isLoading]) useEffect(() => { reloadValidityStatus() }, [reloadValidityStatus]) useEffect(() => { return application.subscriptions.addEventObserver((event) => { if (event === SubscriptionManagerEvent.DidFetchSubscription) { reloadValidityStatus() } }) }, [application.subscriptions, reloadValidityStatus]) const dismissDeprecationMessage = () => { setIsDeprecationMessageDismissed(true) } const onVisibilityChange = useCallback(() => { if (document.visibilityState === 'hidden') { return } if (hasIssueLoading) { requestReload?.(componentViewer) } }, [hasIssueLoading, componentViewer, requestReload]) useEffect(() => { const loadTimeout = setTimeout(() => { setIsLoading(false) setHasIssueLoading(true) if (!didAttemptReload) { setDidAttemptReload(true) requestReload?.(componentViewer) } else { document.addEventListener(VisibilityChangeKey, onVisibilityChange) } }, MaxLoadThreshold) setLoadTimeout(loadTimeout) return () => { if (loadTimeout) { clearTimeout(loadTimeout) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [componentViewer]) const onIframeLoad = useCallback(() => { const iframe = iframeRef.current as HTMLIFrameElement const contentWindow = iframe.contentWindow as Window if (loadTimeout) { clearTimeout(loadTimeout) } try { componentViewer.setWindow(contentWindow) } catch (error) { console.error(error) } setTimeout(() => { setIsLoading(false) setHasIssueLoading(false) onLoad?.() }, MSToWaitAfterIframeLoadToAvoidFlicker) }, [componentViewer, onLoad, loadTimeout]) 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.keyboardService.handleComponentKeyDown(data.keyboardModifier) break case ComponentAction.KeyUp: application.keyboardService.handleComponentKeyUp(data.keyboardModifier) break case ComponentAction.Click: application.controllers.notesController.setContextMenuOpen(false) break default: return } }) return () => { removeActionObserver() } }, [componentViewer, application]) useEffect(() => { const unregisterDesktopObserver = application .getDesktopService() ?.registerUpdateObserver((updatedComponent: ComponentInterface) => { if (updatedComponent.uuid === uiFeature.uniqueIdentifier.value) { requestReload?.(componentViewer) } }) return () => { unregisterDesktopObserver?.() } }, [application, requestReload, componentViewer, uiFeature]) const sandboxAttributes = useMemo(() => { const attributes = [ 'allow-scripts', 'allow-top-navigation-by-user-activation', 'allow-popups', 'allow-modals', 'allow-forms', 'allow-downloads', ] if (uiFeature.isNativeFeature) { attributes.push('allow-popups-to-escape-sandbox') } if (application.isNativeMobileWeb()) { /** * Native mobile web serves native components through the file:// protocol. * Native mobile web also does not use localStorage, unlike the web app. * So, components served through the file:// (precompiled editors) will be treated * as same origin as the parent app, but will not have meaningful access. * * Third party components will have a non-file:// origin, and thus don't need this attribute. */ if (uiFeature.isNativeFeature) { attributes.push('allow-same-origin') } } return attributes }, [application, uiFeature]) return ( <> {hasIssueLoading && ( { reloadValidityStatus(), requestReload?.(componentViewer, true) }} /> )} {featureStatus !== FeatureStatus.Entitled && ( )} {deprecationMessage && !isDeprecationMessageDismissed && ( )} {error === ComponentViewerError.OfflineRestricted && } {error === ComponentViewerError.MissingUrl && } {uiFeature.uniqueIdentifier && isComponentValid && ( )} {isLoading &&
} ) } export default observer(IframeFeatureView)