From ebdae31965c6f827c568d4a2134d9107dff1231b Mon Sep 17 00:00:00 2001 From: Mo Date: Fri, 24 Dec 2021 10:41:02 -0600 Subject: [PATCH] feat: component viewer (#781) * wip: component viewer * feat: get component status from component viewer * fix: remove unused property * chore(deps): snjs 2.29.0 * fix: import location --- app/assets/javascripts/app.ts | 2 - .../components/ComponentView/IsExpired.tsx | 22 +- .../ComponentView/OfflineRestricted.tsx | 35 +- .../components/ComponentView/index.tsx | 277 ++++------- .../QuickSettingsMenu/FocusModeSwitch.tsx | 3 +- .../QuickSettingsMenu/QuickSettingsMenu.tsx | 8 +- .../QuickSettingsMenu/ThemesMenuButton.tsx | 2 +- .../javascripts/components/SearchOptions.tsx | 12 +- .../directives/views/componentModal.ts | 64 --- .../javascripts/directives/views/index.ts | 1 - .../directives/views/permissionsModal.ts | 7 +- .../directives/views/revisionPreviewModal.ts | 113 ++--- .../preferences/panes/CloudLink.tsx | 115 +++++ .../preferences/panes/ExtensionPane.tsx | 37 +- .../preferences/panes/Extensions.tsx | 6 - .../two-factor-auth/AuthAppInfoPopup.tsx | 5 +- .../javascripts/services/desktopManager.ts | 69 +-- .../javascripts/services/themeManager.ts | 41 +- .../ui_models/app_state/app_state.ts | 24 +- .../ui_models/app_state/notes_state.ts | 8 +- .../ui_models/app_state/tags_state.ts | 10 +- .../javascripts/ui_models/application.ts | 35 +- .../javascripts/ui_models/component_group.ts | 100 ---- app/assets/javascripts/ui_models/editor.ts | 75 +-- .../javascripts/ui_models/editor_group.ts | 18 +- .../views/abstract/pure_view_ctrl.ts | 59 ++- .../views/application/application_view.ts | 19 +- .../views/challenge_modal/challenge_modal.tsx | 1 + .../javascripts/views/editor/editor-view.pug | 24 +- .../javascripts/views/editor/editor_view.ts | 432 ++++++++++-------- .../javascripts/views/footer/footer_view.ts | 24 - .../javascripts/views/tags/tags-view.pug | 6 +- .../javascripts/views/tags/tags_view.ts | 131 +++--- .../templates/directives/component-modal.pug | 18 - .../directives/permissions-modal.pug | 6 +- .../directives/revision-preview-modal.pug | 10 +- package.json | 2 +- .../ext.json | 10 + yarn.lock | 55 ++- 39 files changed, 874 insertions(+), 1012 deletions(-) delete mode 100644 app/assets/javascripts/directives/views/componentModal.ts create mode 100644 app/assets/javascripts/preferences/panes/CloudLink.tsx delete mode 100644 app/assets/javascripts/ui_models/component_group.ts delete mode 100644 app/assets/templates/directives/component-modal.pug create mode 100644 public/components/org.standardnotes.simple-markdown-editor/ext.json diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 0392d0c76..69869af55 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -51,7 +51,6 @@ import { import { ActionsMenu, - ComponentModal, EditorMenu, InputModal, MenuRow, @@ -166,7 +165,6 @@ const startApplication: StartApplication = async function startApplication( .directive('accountSwitcher', () => new AccountSwitcher()) .directive('actionsMenu', () => new ActionsMenu()) .directive('challengeModal', () => new ChallengeModal()) - .directive('componentModal', () => new ComponentModal()) .directive('componentView', ComponentViewDirective) .directive('editorMenu', () => new EditorMenu()) .directive('inputModal', () => new InputModal()) diff --git a/app/assets/javascripts/components/ComponentView/IsExpired.tsx b/app/assets/javascripts/components/ComponentView/IsExpired.tsx index 7044aa11a..ce95e0750 100644 --- a/app/assets/javascripts/components/ComponentView/IsExpired.tsx +++ b/app/assets/javascripts/components/ComponentView/IsExpired.tsx @@ -5,11 +5,14 @@ interface IProps { expiredDate: string; componentName: string; featureStatus: FeatureStatus; - reloadStatus: () => void; manageSubscription: () => void; } -const statusString = (featureStatus: FeatureStatus, expiredDate: string, componentName: string) => { +const statusString = ( + featureStatus: FeatureStatus, + expiredDate: string, + componentName: string +) => { switch (featureStatus) { case FeatureStatus.InCurrentPlanButExpired: return `Your subscription expired on ${expiredDate}`; @@ -25,9 +28,8 @@ const statusString = (featureStatus: FeatureStatus, expiredDate: string, compone export const IsExpired: FunctionalComponent = ({ expiredDate, featureStatus, - reloadStatus, componentName, - manageSubscription + manageSubscription, }) => { return (
@@ -50,11 +52,13 @@ export const IsExpired: FunctionalComponent = ({
-
manageSubscription()}> - -
-
reloadStatus()}> - +
manageSubscription()} + > +
diff --git a/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx b/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx index 6882b0220..a60ee0de3 100644 --- a/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx +++ b/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx @@ -1,14 +1,6 @@ import { FunctionalComponent } from 'preact'; -interface IProps { - isReloading: boolean; - reloadStatus: () => void; -} - -export const OfflineRestricted: FunctionalComponent = ({ - isReloading, - reloadStatus, -}) => { +export const OfflineRestricted: FunctionalComponent = () => { return (
@@ -16,38 +8,29 @@ export const OfflineRestricted: FunctionalComponent = ({
- You have restricted this component to be used offline only. + You have restricted this component to not use a hosted version.
- Offline components are not available in the web application. + Locally-installed components are not available in the web + application.
-
You can either:
+
+ To continue, choose from the following options: +
  • - Enable the Hosted option for this component by opening + Enable the Hosted option for this component by opening the Preferences {'>'} General {'>'} Advanced Settings menu and{' '} toggling 'Use hosted when local is unavailable' under this - components's options. Then press Reload below. + component's options. Then press Reload.
  • Use the desktop application.
-
- {isReloading ? ( -
- ) : ( - - )} -
diff --git a/app/assets/javascripts/components/ComponentView/index.tsx b/app/assets/javascripts/components/ComponentView/index.tsx index 3c6a98a6f..e0ec55b9f 100644 --- a/app/assets/javascripts/components/ComponentView/index.tsx +++ b/app/assets/javascripts/components/ComponentView/index.tsx @@ -3,7 +3,9 @@ import { FeatureStatus, SNComponent, dateToLocalizedString, - ApplicationEvent, + ComponentViewer, + ComponentViewerEvent, + ComponentViewerError, } from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; import { FunctionalComponent } from 'preact'; @@ -22,9 +24,9 @@ import { openSubscriptionDashboard } from '@/hooks/manageSubscription'; interface IProps { application: WebApplication; appState: AppState; - componentUuid: string; + componentViewer: ComponentViewer; + requestReload?: (viewer: ComponentViewer) => void; onLoad?: (component: SNComponent) => void; - templateComponent?: SNComponent; manualDealloc?: boolean; } @@ -34,10 +36,10 @@ interface IProps { */ const MaxLoadThreshold = 4000; const VisibilityChangeKey = 'visibilitychange'; -const avoidFlickerTimeout = 7; +const MSToWaitAfterIframeLoadToAvoidFlicker = 35; export const ComponentView: FunctionalComponent = observer( - ({ application, onLoad, componentUuid, templateComponent }) => { + ({ application, onLoad, componentViewer, requestReload }) => { const iframeRef = useRef(null); const excessiveLoadingTimeout = useRef< ReturnType | undefined @@ -45,44 +47,33 @@ export const ComponentView: FunctionalComponent = observer( const [hasIssueLoading, setHasIssueLoading] = useState(false); const [isLoading, setIsLoading] = useState(true); - const [isReloading, setIsReloading] = useState(false); - const [component] = useState( - application.findItem(componentUuid) as SNComponent - ); const [featureStatus, setFeatureStatus] = useState( - application.getFeatureStatus(component.identifier) + componentViewer.getFeatureStatus() ); const [isComponentValid, setIsComponentValid] = useState(true); - const [error, setError] = useState< - 'offline-restricted' | 'url-missing' | undefined - >(undefined); - const [isDeprecated, setIsDeprecated] = useState(false); + const [error, setError] = useState( + undefined + ); const [deprecationMessage, setDeprecationMessage] = useState< string | undefined >(undefined); const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false); const [didAttemptReload, setDidAttemptReload] = useState(false); - const [contentWindow, setContentWindow] = useState(null); + + const component = componentViewer.component; const manageSubscription = useCallback(() => { openSubscriptionDashboard(application); }, [application]); - const reloadIframe = () => { - setTimeout(() => { - setIsReloading(true); - setTimeout(() => { - setIsReloading(false); - }); - }); - }; - useEffect(() => { const loadTimeout = setTimeout(() => { handleIframeTakingTooLongToLoad(); }, MaxLoadThreshold); + excessiveLoadingTimeout.current = loadTimeout; + return () => { excessiveLoadingTimeout.current && clearTimeout(excessiveLoadingTimeout.current); @@ -91,42 +82,25 @@ export const ComponentView: FunctionalComponent = observer( }, []); const reloadValidityStatus = useCallback(() => { - const offlineRestricted = - component.offlineOnly && !isDesktopApplication(); - const hasUrlError = (function () { - if (isDesktopApplication()) { - return !component.local_url && !component.hasValidHostedUrl(); - } else { - return !component.hasValidHostedUrl(); - } - })(); - - const readonlyState = - application.componentManager.getReadonlyStateForComponent(component); - - if (!readonlyState.lockReadonly) { - application.componentManager.setReadonlyStateForComponent( - component, - featureStatus !== FeatureStatus.Entitled - ); + setFeatureStatus(componentViewer.getFeatureStatus()); + if (!componentViewer.lockReadonly) { + componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled); } - setIsComponentValid(!offlineRestricted && !hasUrlError); + setIsComponentValid(componentViewer.shouldRender()); - if (!isComponentValid) { + if (isLoading && !isComponentValid) { setIsLoading(false); } - if (offlineRestricted) { - setError('offline-restricted'); - } else if (hasUrlError) { - setError('url-missing'); - } else { - setError(undefined); - } - setIsDeprecated(component.isDeprecated); - setDeprecationMessage(component.package_info.deprecation_message); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + setError(componentViewer.getError()); + setDeprecationMessage(component.deprecationMessage); + }, [ + componentViewer, + component.deprecationMessage, + featureStatus, + isComponentValid, + isLoading, + ]); useEffect(() => { reloadValidityStatus(); @@ -141,9 +115,9 @@ export const ComponentView: FunctionalComponent = observer( return; } if (hasIssueLoading) { - reloadIframe(); + requestReload?.(componentViewer); } - }, [hasIssueLoading]); + }, [hasIssueLoading, componentViewer, requestReload]); const handleIframeTakingTooLongToLoad = useCallback(async () => { setIsLoading(false); @@ -151,188 +125,133 @@ export const ComponentView: FunctionalComponent = observer( if (!didAttemptReload) { setDidAttemptReload(true); - reloadIframe(); + requestReload?.(componentViewer); } else { document.addEventListener(VisibilityChangeKey, onVisibilityChange); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleIframeLoad = useCallback(async (iframe: HTMLIFrameElement) => { - let hasDesktopError = false; - const canAccessWindowOrigin = isDesktopApplication(); - if (canAccessWindowOrigin) { - try { - const contentWindow = iframe.contentWindow as Window; - if (!contentWindow.origin || contentWindow.origin === 'null') { - hasDesktopError = true; - } - // eslint-disable-next-line no-empty - } catch (e) {} - } - excessiveLoadingTimeout.current && - clearTimeout(excessiveLoadingTimeout.current); - setContentWindow(iframe.contentWindow); - setTimeout(() => { - setIsLoading(false); - setHasIssueLoading(hasDesktopError); - onLoad?.(component); - }, avoidFlickerTimeout); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (contentWindow) { - application.componentManager.registerComponentWindow( - component, - contentWindow - ); - } - return () => { - application.componentManager.onComponentIframeDestroyed(component.uuid); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [contentWindow]); + }, [componentViewer, didAttemptReload, onVisibilityChange, requestReload]); useEffect(() => { if (!iframeRef.current) { - setContentWindow(null); return; } - iframeRef.current.onload = () => { - const iframe = application.componentManager.iframeForComponent( - component.uuid - ); - if (iframe) { - setTimeout(() => { - handleIframeLoad(iframe); - }); + const iframe = iframeRef.current as HTMLIFrameElement; + iframe.onload = () => { + const contentWindow = iframe.contentWindow as Window; + + let hasDesktopError = false; + const canAccessWindowOrigin = isDesktopApplication(); + if (canAccessWindowOrigin) { + try { + if (!contentWindow.origin || contentWindow.origin === 'null') { + hasDesktopError = true; + } + } catch (e) { + console.error(e); + } } + + excessiveLoadingTimeout.current && + clearTimeout(excessiveLoadingTimeout.current); + + componentViewer.setWindow(contentWindow); + + setTimeout(() => { + setIsLoading(false); + setHasIssueLoading(hasDesktopError); + onLoad?.(component); + }, MSToWaitAfterIframeLoadToAvoidFlicker); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [iframeRef.current]); + }, [onLoad, component, componentViewer]); useEffect(() => { - const removeFeaturesChangedObserver = application.addEventObserver( - async () => { - setFeatureStatus(application.getFeatureStatus(component.identifier)); - }, - ApplicationEvent.FeaturesUpdated + const removeFeaturesChangedObserver = componentViewer.addEventObserver( + (event) => { + if (event === ComponentViewerEvent.FeatureStatusUpdated) { + setFeatureStatus(componentViewer.getFeatureStatus()); + } + } ); return () => { removeFeaturesChangedObserver(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [componentViewer]); useEffect(() => { - if (!componentUuid) { - application.componentManager.addTemporaryTemplateComponent( - templateComponent as SNComponent - ); - } - - return () => { - if (templateComponent) { - /** componentManager can be destroyed already via locking */ - application.componentManager?.removeTemporaryTemplateComponent( - templateComponent - ); + 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; + } } - - document.removeEventListener(VisibilityChangeKey, onVisibilityChange); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const unregisterComponentHandler = - application.componentManager.registerHandler({ - identifier: 'component-view-' + Math.random(), - areas: [component.area], - actionHandler: (component, action, data) => { - switch (action) { - case ComponentAction.SetSize: - application.componentManager.handleSetSizeEvent( - component, - data - ); - break; - 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 () => { - unregisterComponentHandler(); + removeActionObserver(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [component]); + }, [componentViewer, application]); useEffect(() => { const unregisterDesktopObserver = application .getDesktopService() .registerUpdateObserver((component: SNComponent) => { if (component.uuid === component.uuid && component.active) { - reloadIframe(); + requestReload?.(componentViewer); } }); return () => { unregisterDesktopObserver(); }; - }, [application]); + }, [application, requestReload, componentViewer]); return ( <> {hasIssueLoading && ( { + reloadValidityStatus(), requestReload?.(componentViewer); + }} /> )} + {featureStatus !== FeatureStatus.Entitled && ( )} - {isDeprecated && !isDeprecationMessageDismissed && ( + {deprecationMessage && !isDeprecationMessageDismissed && ( )} - {error == 'offline-restricted' && ( - + {error === ComponentViewerError.OfflineRestricted && ( + )} - {error == 'url-missing' && ( + {error === ComponentViewerError.MissingUrl && ( )} - {component.uuid && !isReloading && isComponentValid && ( + {component.uuid && isComponentValid && (