void;
+}
+
+export const IsDeprecated: FunctionalComponent
= ({
+ deprecationMessage,
+ dismissDeprecationMessage
+ }) => {
+ return (
+
+
+
+
+
+ {deprecationMessage || 'This extension is deprecated.'}
+
+
+
+
+
+
+ );
+};
diff --git a/app/assets/javascripts/components/ComponentView/IsExpired.tsx b/app/assets/javascripts/components/ComponentView/IsExpired.tsx
new file mode 100644
index 000000000..31d1faa1f
--- /dev/null
+++ b/app/assets/javascripts/components/ComponentView/IsExpired.tsx
@@ -0,0 +1,57 @@
+import { FunctionalComponent } from 'preact';
+
+interface IProps {
+ expiredDate: string;
+ reloadStatus: () => void;
+}
+
+export const IsExpired: FunctionalComponent = ({
+ expiredDate,
+ reloadStatus
+ }) => {
+ return (
+
+
+
+
+
reloadStatus()}>
+ Reload
+
+
+
+
+
+ );
+};
diff --git a/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx b/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx
new file mode 100644
index 000000000..3bec4eab7
--- /dev/null
+++ b/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx
@@ -0,0 +1,30 @@
+import { FunctionalComponent } from 'preact';
+
+interface IProps {
+ componentName: string;
+ reloadIframe: () => void;
+}
+
+export const IssueOnLoading: FunctionalComponent = ({
+ componentName,
+ reloadIframe
+ }) => {
+ return (
+
+
+
+
+
+ There was an issue loading {componentName}
+
+
+
+
+
+
+ );
+};
diff --git a/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx b/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx
new file mode 100644
index 000000000..7de8ef90b
--- /dev/null
+++ b/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx
@@ -0,0 +1,58 @@
+import { FunctionalComponent } from 'preact';
+
+interface IProps {
+ isReloading: boolean;
+ reloadStatus: () => void;
+}
+
+export const OfflineRestricted: FunctionalComponent = ({
+ isReloading,
+ reloadStatus
+ }) => {
+ return (
+
+
+
+
+
+
+ You have restricted this extension to be used offline only.
+
+
+ Offline extensions are not available in the Web app.
+
+
+
+
+
+ You can either:
+
+
+
+
+ Enable the Hosted option for this extension by opening the 'Extensions' menu and{' '}
+ toggling 'Use hosted when local is unavailable' under this extension's options.{' '}
+ Then press Reload below.
+
+
+
+ Use the Desktop application.
+
+
+
+
+
+ {isReloading ?
+
+ :
+
reloadStatus()}>
+ Reload
+
+ }
+
+
+
+
+
+ );
+};
diff --git a/app/assets/javascripts/components/ComponentView/UrlMissing.tsx b/app/assets/javascripts/components/ComponentView/UrlMissing.tsx
new file mode 100644
index 000000000..c2dd6072c
--- /dev/null
+++ b/app/assets/javascripts/components/ComponentView/UrlMissing.tsx
@@ -0,0 +1,26 @@
+import { FunctionalComponent } from 'preact';
+
+interface IProps {
+ componentName: string;
+}
+
+export const UrlMissing: FunctionalComponent = ({ componentName }) => {
+ return (
+
+
+
+
+
+ This extension is not installed correctly.
+
+
Please uninstall {componentName}, then re-install it.
+
+ This issue can occur if you access Standard Notes using an older version of the app.{' '}
+ Ensure you are running at least version 2.1 on all platforms.
+
+
+
+
+
+ );
+};
diff --git a/app/assets/javascripts/components/ComponentView/index.tsx b/app/assets/javascripts/components/ComponentView/index.tsx
new file mode 100644
index 000000000..a83eda6d8
--- /dev/null
+++ b/app/assets/javascripts/components/ComponentView/index.tsx
@@ -0,0 +1,360 @@
+import { ComponentAction, LiveItem, SNComponent } from '@node_modules/@standardnotes/snjs';
+import { WebApplication } from '@/ui_models/application';
+import { FunctionalComponent } from 'preact';
+import { toDirective } from '@/components/utils';
+import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
+import { observer } from 'mobx-react-lite';
+import { isDesktopApplication } from '@/utils';
+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 '@/ui_models/app_state';
+import { ComponentArea } from '@node_modules/@standardnotes/features';
+
+interface IProps {
+ application: WebApplication;
+ appState: AppState;
+ componentUuid: string;
+ onLoad?: (component: SNComponent) => void;
+ templateComponent?: SNComponent;
+ 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 avoidFlickerTimeout = 7;
+
+export const ComponentView: FunctionalComponent = observer(
+ ({
+ application,
+ appState,
+ onLoad,
+ componentUuid,
+ templateComponent,
+ manualDealloc = false,
+ }) => {
+ const liveComponentRef = useRef | null>(null);
+ const iframeRef = useRef(null);
+
+ const [isIssueOnLoading, setIsIssueOnLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isReloading, setIsReloading] = useState(false);
+ const [loadTimeout, setLoadTimeout] = useState(undefined);
+ const [isExpired, setIsExpired] = useState(false);
+ const [isComponentValid, setIsComponentValid] = useState(true);
+ const [error, setError] = useState<'offline-restricted' | 'url-missing' | undefined>(undefined);
+ const [isDeprecated, setIsDeprecated] = useState(false);
+ const [deprecationMessage, setDeprecationMessage] = useState(undefined);
+ const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false);
+ const [didAttemptReload, setDidAttemptReload] = useState(false);
+ const [component, setComponent] = useState(undefined);
+
+ const getComponent = useCallback((): SNComponent => {
+ return (templateComponent || liveComponentRef.current?.item) as SNComponent;
+ }, [templateComponent]);
+
+ const reloadIframe = () => {
+ setTimeout(() => {
+ setIsReloading(true);
+ setTimeout(() => {
+ setIsReloading(false);
+ });
+ });
+ };
+
+ const reloadStatus = useCallback(() => {
+ if (!component) {
+ return;
+ }
+
+ const offlineRestricted = component.offlineOnly && !isDesktopApplication();
+ const hasUrlError = function() {
+ if (isDesktopApplication()) {
+ return !component.local_url && !component.hasValidHostedUrl();
+ } else {
+ return !component.hasValidHostedUrl();
+ }
+ }();
+
+ setIsExpired(component.valid_until && component.valid_until <= new Date());
+
+ const readonlyState = application.componentManager!.getReadonlyStateForComponent(component);
+
+ if (!readonlyState.lockReadonly) {
+ application.componentManager!.setReadonlyStateForComponent(component, isExpired);
+ }
+ setIsComponentValid(!offlineRestricted && !hasUrlError);
+
+ if (!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);
+ }, [application.componentManager, component, isComponentValid, isExpired]);
+
+ const dismissDeprecationMessage = () => {
+ setTimeout(() => {
+ setIsDeprecationMessageDismissed(true);
+ });
+ };
+
+ const onVisibilityChange = useCallback(() => {
+ if (document.visibilityState === 'hidden') {
+ return;
+ }
+ if (isIssueOnLoading) {
+ reloadIframe();
+ }
+ }, [isIssueOnLoading]);
+
+ const handleIframeLoadTimeout =useCallback(async () => {
+ if (isLoading) {
+ setIsLoading(false);
+ setIsIssueOnLoading(true);
+
+ if (!didAttemptReload) {
+ setDidAttemptReload(true);
+ reloadIframe();
+ } else {
+ document.addEventListener(
+ VisibilityChangeKey,
+ onVisibilityChange
+ );
+ }
+ }
+ }, [didAttemptReload, isLoading, onVisibilityChange]);
+
+ const handleIframeLoad = useCallback(async (iframe: HTMLIFrameElement) => {
+ if (!component) {
+ return;
+ }
+
+ let desktopError = false;
+ if (isDesktopApplication()) {
+ try {
+ /** Accessing iframe.contentWindow.origin only allowed in desktop app. */
+ if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') {
+ desktopError = true;
+ }
+ // eslint-disable-next-line no-empty
+ } catch (e) {
+ }
+ }
+ clearTimeout(loadTimeout);
+ await application.componentManager!.registerComponentWindow(
+ component,
+ iframe.contentWindow!
+ );
+
+ setTimeout(() => {
+ setIsLoading(false);
+ setIsIssueOnLoading(desktopError ? true : false);
+ onLoad?.(component!);
+ }, avoidFlickerTimeout);
+ }, [application.componentManager, component, loadTimeout, onLoad]);
+
+ const loadComponent = useCallback(() => {
+ if (!component) {
+ throw Error('Component view is missing component');
+ }
+
+ if (!component.active && !component.isEditor() && component.area !== ComponentArea.Modal) {
+ /** Editors don't need to be active to be displayed */
+ throw Error('Component view component must be active');
+ }
+
+ setIsLoading(true);
+ if (loadTimeout) {
+ clearTimeout(loadTimeout);
+ }
+ const timeoutHandler = setTimeout(() => {
+ handleIframeLoadTimeout();
+ }, MaxLoadThreshold);
+
+ setLoadTimeout(timeoutHandler);
+ }, [component, handleIframeLoadTimeout, loadTimeout]);
+
+ useEffect(() => {
+ if (!iframeRef.current) {
+ return;
+ }
+
+ iframeRef.current.onload = () => {
+ if (!component) {
+ return;
+ }
+
+ const iframe = application.componentManager!.iframeForComponent(
+ component.uuid
+ );
+ if (!iframe) {
+ return;
+ }
+
+ setTimeout(() => {
+ loadComponent();
+ reloadStatus();
+ handleIframeLoad(iframe);
+ });
+ };
+ }, [application.componentManager, component, handleIframeLoad, loadComponent, reloadStatus]);
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const expiredDate = isExpired ? component.dateToLocalizedString(component.valid_until) : '';
+
+ const getUrl = () => {
+ const url = component ? application.componentManager!.urlForComponent(component) : '';
+ return url as string;
+ };
+
+ useEffect(() => {
+ if (componentUuid) {
+ liveComponentRef.current = new LiveItem(componentUuid, application);
+ } else {
+ application.componentManager.addTemporaryTemplateComponent(templateComponent as SNComponent);
+ }
+
+ return () => {
+ if (application.componentManager) {
+ /** Component manager Can be destroyed already via locking */
+ if (component) {
+ application.componentManager.onComponentIframeDestroyed(component.uuid);
+ }
+ if (templateComponent) {
+ application.componentManager.removeTemporaryTemplateComponent(templateComponent);
+ }
+ }
+
+ if (liveComponentRef.current) {
+ liveComponentRef.current.deinit();
+ }
+
+ document.removeEventListener(
+ VisibilityChangeKey,
+ onVisibilityChange
+ );
+ };
+ }, [appState, application, component, componentUuid, onVisibilityChange, reloadStatus, templateComponent]);
+
+ useEffect(() => {
+ // Set/update `component` based on `componentUuid` prop.
+ // It's a hint that the props were changed and we should rerender this component (and particularly, the iframe).
+ if (!component || component.uuid && componentUuid && component.uuid !== componentUuid) {
+ const latestComponentValue = getComponent();
+ setComponent(latestComponentValue);
+ }
+ }, [component, componentUuid, getComponent]);
+
+ useEffect(() => {
+ if (!component) {
+ return;
+ }
+
+ 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();
+ };
+ }, [application, component]);
+
+ useEffect(() => {
+ const unregisterDesktopObserver = application.getDesktopService()
+ .registerUpdateObserver((component: SNComponent) => {
+ if (component.uuid === component.uuid && component.active) {
+ reloadIframe();
+ }
+ });
+
+ return () => {
+ unregisterDesktopObserver();
+ };
+ }, [application]);
+
+ if (!component) {
+ return null;
+ }
+
+ return (
+ <>
+ {isIssueOnLoading && (
+
+ )}
+ {isExpired && (
+
+ )}
+ {isDeprecated && !isDeprecationMessageDismissed && (
+
+ )}
+ {error == 'offline-restricted' && (
+
+ )}
+ {error == 'url-missing' && (
+
+ )}
+ {component.uuid && !isReloading && isComponentValid && (
+
+ )}
+ {isLoading && (
+
+ )}
+ >
+ );
+ });
+
+export const ComponentViewDirective = toDirective(ComponentView, {
+ onLoad: '=',
+ componentUuid: '=',
+ templateComponent: '=',
+ manualDealloc: '='
+});
diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx
index 0ccc69b60..f21c0137f 100644
--- a/app/assets/javascripts/components/Icon.tsx
+++ b/app/assets/javascripts/components/Icon.tsx
@@ -50,6 +50,7 @@ import EyeOffIcon from '../../icons/ic-eye-off.svg';
import LockIcon from '../../icons/ic-lock.svg';
import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg';
import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg';
+import WindowIcon from '../../icons/ic-window.svg';
import { toDirective } from './utils';
import { FunctionalComponent } from 'preact';
@@ -106,6 +107,7 @@ const ICONS = {
'check-bold': CheckBoldIcon,
'account-circle': AccountCircleIcon,
'menu-arrow-down': MenuArrowDownIcon,
+ window: WindowIcon
};
export type IconType = keyof typeof ICONS;
diff --git a/app/assets/javascripts/directives/views/componentView.ts b/app/assets/javascripts/directives/views/componentView.ts
deleted file mode 100644
index 67392b335..000000000
--- a/app/assets/javascripts/directives/views/componentView.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-import { RootScopeMessages } from './../../messages';
-import { WebApplication } from '@/ui_models/application';
-import { SNComponent, ComponentAction, LiveItem } from '@standardnotes/snjs';
-import { WebDirective } from './../../types';
-import template from '%/directives/component-view.pug';
-import { isDesktopApplication } from '../../utils';
-/**
- * The maximum amount of time we'll wait for a component
- * to load before displaying error
- */
-const MaxLoadThreshold = 4000;
-const VisibilityChangeKey = 'visibilitychange';
-
-interface ComponentViewScope {
- componentUuid: string
- onLoad?: (component: SNComponent) => void
- application: WebApplication
-}
-
-class ComponentViewCtrl implements ComponentViewScope {
-
- /** @scope */
- onLoad?: (component: SNComponent) => void
- componentUuid!: string
- templateComponent!: SNComponent
- application!: WebApplication
- liveComponent!: LiveItem
-
- private $rootScope: ng.IRootScopeService
- private $timeout: ng.ITimeoutService
- private componentValid = true
- private cleanUpOn: () => void
- private unregisterComponentHandler!: () => void
- private unregisterDesktopObserver!: () => void
- private issueLoading = false
- private isDeprecated = false
- private deprecationMessage: string | undefined = undefined
- private deprecationMessageDismissed = false
- public reloading = false
- private expired = false
- private loading = false
- private didAttemptReload = false
- public error: 'offline-restricted' | 'url-missing' | undefined
- private loadTimeout: any
-
- /* @ngInject */
- constructor(
- $scope: ng.IScope,
- $rootScope: ng.IRootScopeService,
- $timeout: ng.ITimeoutService,
- ) {
- this.$rootScope = $rootScope;
- this.$timeout = $timeout;
- this.cleanUpOn = $scope.$on('ext-reload-complete', () => {
- this.reloadStatus(false);
- });
- /** To allow for registering events */
- this.onVisibilityChange = this.onVisibilityChange.bind(this);
- }
-
- $onDestroy() {
- if(this.application.componentManager) {
- /** Component manager Can be destroyed already via locking */
- this.application.componentManager.onComponentIframeDestroyed(this.component.uuid);
- if(this.templateComponent) {
- this.application.componentManager.removeTemporaryTemplateComponent(this.templateComponent);
- }
- }
- if(this.liveComponent) {
- this.liveComponent.deinit();
- }
- this.cleanUpOn();
- (this.cleanUpOn as any) = undefined;
- this.unregisterComponentHandler();
- (this.unregisterComponentHandler as any) = undefined;
- this.unregisterDesktopObserver();
- (this.unregisterDesktopObserver as any) = undefined;
- (this.templateComponent as any) = undefined;
- (this.liveComponent as any) = undefined;
- (this.application as any) = undefined;
- (this.onVisibilityChange as any) = undefined;
- this.onLoad = undefined;
- document.removeEventListener(
- VisibilityChangeKey,
- this.onVisibilityChange
- );
- }
-
- $onInit() {
- if(this.componentUuid) {
- this.liveComponent = new LiveItem(this.componentUuid, this.application);
- } else {
- this.application.componentManager.addTemporaryTemplateComponent(this.templateComponent);
- }
- this.registerComponentHandlers();
- this.registerPackageUpdateObserver();
- }
-
- get component() {
- return this.templateComponent || this.liveComponent?.item;
- }
-
- /** @template */
- public onIframeInit() {
- /** Perform in timeout required so that dynamic iframe id is set (based on ctrl values) */
- this.$timeout(() => {
- this.loadComponent();
- });
- }
-
- private loadComponent() {
- if (!this.component) {
- throw Error('Component view is missing component');
- }
- if (!this.component.active && !this.component.isEditor()) {
- /** Editors don't need to be active to be displayed */
- throw Error('Component view component must be active');
- }
- const iframe = this.application.componentManager!.iframeForComponent(
- this.component.uuid
- );
- if (!iframe) {
- return;
- }
- this.loading = true;
- if (this.loadTimeout) {
- this.$timeout.cancel(this.loadTimeout);
- }
- this.loadTimeout = this.$timeout(() => {
- this.handleIframeLoadTimeout();
- }, MaxLoadThreshold);
- iframe.onload = () => {
- this.reloadStatus();
- this.handleIframeLoad(iframe);
- };
- }
-
- private registerPackageUpdateObserver() {
- this.unregisterDesktopObserver = this.application.getDesktopService()
- .registerUpdateObserver((component: SNComponent) => {
- if (component.uuid === this.component.uuid && component.active) {
- this.reloadIframe();
- }
- });
- }
-
- private registerComponentHandlers() {
- this.unregisterComponentHandler = this.application.componentManager!.registerHandler({
- identifier: 'component-view-' + Math.random(),
- areas: [this.component.area],
- actionHandler: (component, action, data) => {
- switch (action) {
- case (ComponentAction.SetSize):
- this.application.componentManager!.handleSetSizeEvent(component, data);
- break;
- case (ComponentAction.KeyDown):
- this.application.io.handleComponentKeyDown(data.keyboardModifier);
- break;
- case (ComponentAction.KeyUp):
- this.application.io.handleComponentKeyUp(data.keyboardModifier);
- break;
- case (ComponentAction.Click):
- this.application.getAppState().notes.setContextMenuOpen(false);
- break;
- default:
- return;
- }
- }
- });
- }
-
- private reloadIframe() {
- this.$timeout(() => {
- this.reloading = true;
- this.$timeout(() => {
- this.reloading = false;
- });
- });
- }
-
- private dismissDeprecationMessage() {
- this.$timeout(() => {
- this.deprecationMessageDismissed = true;
- });
- }
-
- private onVisibilityChange() {
- if (document.visibilityState === 'hidden') {
- return;
- }
- if (this.issueLoading) {
- this.reloadIframe();
- }
- }
-
- public reloadStatus(doManualReload = true) {
- const component = this.component;
- const offlineRestricted = component.offlineOnly && !isDesktopApplication();
- const hasUrlError = function () {
- if (isDesktopApplication()) {
- return !component.local_url && !component.hasValidHostedUrl();
- } else {
- return !component.hasValidHostedUrl();
- }
- }();
- this.expired = component.valid_until && component.valid_until <= new Date();
- const readonlyState = this.application.componentManager!
- .getReadonlyStateForComponent(component);
- if (!readonlyState.lockReadonly) {
- this.application.componentManager!
- .setReadonlyStateForComponent(component, this.expired);
- }
- this.componentValid = !offlineRestricted && !hasUrlError;
- if (!this.componentValid) {
- this.loading = false;
- }
- if (offlineRestricted) {
- this.error = 'offline-restricted';
- } else if (hasUrlError) {
- this.error = 'url-missing';
- } else {
- this.error = undefined;
- }
- if (this.expired && doManualReload) {
- this.$rootScope.$broadcast(RootScopeMessages.ReloadExtendedData);
- }
- this.isDeprecated = component.isDeprecated;
- this.deprecationMessage = component.package_info.deprecation_message;
- }
-
- private async handleIframeLoadTimeout() {
- if (this.loading) {
- this.loading = false;
- this.issueLoading = true;
- if (!this.didAttemptReload) {
- this.didAttemptReload = true;
- this.reloadIframe();
- } else {
- document.addEventListener(
- VisibilityChangeKey,
- this.onVisibilityChange
- );
- }
- }
- }
-
- private async handleIframeLoad(iframe: HTMLIFrameElement) {
- let desktopError = false;
- if (isDesktopApplication()) {
- try {
- /** Accessing iframe.contentWindow.origin only allowed in desktop app. */
- if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') {
- desktopError = true;
- }
- // eslint-disable-next-line no-empty
- } catch (e) { }
- }
- this.$timeout.cancel(this.loadTimeout);
- await this.application.componentManager!.registerComponentWindow(
- this.component,
- iframe.contentWindow!
- );
- const avoidFlickerTimeout = 7;
- this.$timeout(() => {
- this.loading = false;
- // eslint-disable-next-line no-unneeded-ternary
- this.issueLoading = desktopError ? true : false;
- this.onLoad && this.onLoad(this.component!);
- }, avoidFlickerTimeout);
- }
-
- /** @template */
- public getUrl() {
- const url = this.application.componentManager!.urlForComponent(this.component);
- return url;
- }
-}
-
-export class ComponentView extends WebDirective {
- constructor() {
- super();
- this.restrict = 'E';
- this.template = template;
- this.scope = {
- componentUuid: '=',
- templateComponent: '=?',
- onLoad: '=?',
- application: '='
- };
- this.controller = ComponentViewCtrl;
- this.controllerAs = 'ctrl';
- this.bindToController = true;
- }
-}
diff --git a/app/assets/javascripts/directives/views/index.ts b/app/assets/javascripts/directives/views/index.ts
index 273a3e944..ee69ce822 100644
--- a/app/assets/javascripts/directives/views/index.ts
+++ b/app/assets/javascripts/directives/views/index.ts
@@ -1,6 +1,5 @@
export { ActionsMenu } from './actionsMenu';
export { ComponentModal } from './componentModal';
-export { ComponentView } from './componentView';
export { EditorMenu } from './editorMenu';
export { InputModal } from './inputModal';
export { MenuRow } from './menuRow';
diff --git a/app/assets/javascripts/messages.ts b/app/assets/javascripts/messages.ts
index 2dcecff72..8e7ede0ac 100644
--- a/app/assets/javascripts/messages.ts
+++ b/app/assets/javascripts/messages.ts
@@ -1,4 +1,3 @@
export enum RootScopeMessages {
- ReloadExtendedData = 'reload-ext-data',
NewUpdateAvailable = 'new-update-available'
}
diff --git a/app/assets/javascripts/preferences/PreferencesMenu.ts b/app/assets/javascripts/preferences/PreferencesMenu.ts
index fd07570ee..729dec93b 100644
--- a/app/assets/javascripts/preferences/PreferencesMenu.ts
+++ b/app/assets/javascripts/preferences/PreferencesMenu.ts
@@ -1,5 +1,10 @@
import { IconType } from '@/components/Icon';
-import { makeAutoObservable, observable } from 'mobx';
+import { action, makeAutoObservable, observable } from 'mobx';
+import { ExtensionsLatestVersions } from '@/preferences/panes/extensions-segments';
+import { ContentType, SNComponent } from '@node_modules/@standardnotes/snjs';
+import { WebApplication } from '@/ui_models/application';
+import { FeatureIdentifier } from '@node_modules/@standardnotes/features/dist/Domain/Feature/FeatureIdentifier';
+import { ComponentArea } from '@standardnotes/snjs';
const PREFERENCE_IDS = [
'general',
@@ -16,11 +21,15 @@ const PREFERENCE_IDS = [
export type PreferenceId = typeof PREFERENCE_IDS[number];
interface PreferencesMenuItem {
- readonly id: PreferenceId;
+ readonly id: PreferenceId | FeatureIdentifier;
readonly icon: IconType;
readonly label: string;
}
+interface SelectableMenuItem extends PreferencesMenuItem {
+ selected: boolean;
+}
+
/**
* Items are in order of appearance
*/
@@ -46,38 +55,93 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
];
export class PreferencesMenu {
- private _selectedPane: PreferenceId = 'account';
+ private _selectedPane: PreferenceId | FeatureIdentifier = 'account';
+ private _extensionPanes: SNComponent[] = [];
private _menu: PreferencesMenuItem[];
+ private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(new Map());
constructor(
+ private application: WebApplication,
private readonly _enableUnfinishedFeatures: boolean,
) {
this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS;
- makeAutoObservable(
+
+ this.loadExtensionsPanes();
+ this.loadLatestVersions();
+
+ makeAutoObservable(
this,
{
_twoFactorAuth: observable,
_selectedPane: observable,
+ _extensionPanes: observable.ref,
+ _extensionLatestVersions: observable.ref,
+ loadLatestVersions: action,
}
);
}
- get menuItems(): (PreferencesMenuItem & {
- selected: boolean;
- })[] {
- return this._menu.map((p) => ({
- ...p,
- selected: p.id === this._selectedPane,
+ private loadLatestVersions(): void {
+ ExtensionsLatestVersions.load(this.application).then(versions => {
+ this._extensionLatestVersions = versions;
+ });
+ }
+
+ get extensionsLatestVersions(): ExtensionsLatestVersions {
+ return this._extensionLatestVersions;
+ }
+
+ loadExtensionsPanes(): void {
+ this._extensionPanes = (this.application.getItems([
+ ContentType.ActionsExtension,
+ ContentType.Component,
+ ContentType.Theme,
+ ]) as SNComponent[])
+ .filter(extension => extension.area === ComponentArea.Modal && extension.package_info.identifier !== FeatureIdentifier.TwoFactorAuthManager);
+ }
+
+ get menuItems(): SelectableMenuItem[] {
+ const menuItems = this._menu.map((preference) => ({
+ ...preference,
+ selected: preference.id === this._selectedPane,
}));
+ const extensionsMenuItems: SelectableMenuItem[] = this._extensionPanes
+ .map(extension => {
+ return {
+ icon: 'window',
+ id: extension.package_info.identifier,
+ label: extension.name,
+ selected: extension.package_info.identifier === this._selectedPane
+ };
+ });
+
+ return menuItems.concat(extensionsMenuItems);
}
- get selectedPaneId(): PreferenceId {
- return (
- this._menu.find((item) => item.id === this._selectedPane)?.id ?? 'account'
- );
+ get selectedMenuItem(): PreferencesMenuItem | undefined {
+ return this._menu.find((item) => item.id === this._selectedPane);
}
- selectPane(key: PreferenceId): void {
+ get selectedExtension(): SNComponent | undefined {
+ return this._extensionPanes.find((extension) =>
+ extension.package_info.identifier === this._selectedPane);
+ }
+
+ get selectedPaneId(): PreferenceId | FeatureIdentifier {
+ if (this.selectedMenuItem != undefined) {
+ return this.selectedMenuItem.id;
+ }
+
+ if (this.selectedExtension != undefined) {
+ return this.selectedExtension.package_info.identifier;
+ }
+
+ return 'account';
+ }
+
+ selectPane(key: PreferenceId | FeatureIdentifier): void {
this._selectedPane = key;
}
}
diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx
index 561ff44d7..3bdb64e1b 100644
--- a/app/assets/javascripts/preferences/PreferencesView.tsx
+++ b/app/assets/javascripts/preferences/PreferencesView.tsx
@@ -9,6 +9,7 @@ import {
Security,
} from './panes';
import { observer } from 'mobx-react-lite';
+
import { PreferencesMenu } from './PreferencesMenu';
import { PreferencesMenuView } from './PreferencesMenuView';
import { WebApplication } from '@/ui_models/application';
@@ -16,6 +17,7 @@ import { MfaProps } from './panes/two-factor-auth/MfaProps';
import { AppState } from '@/ui_models/app_state';
import { useEffect, useMemo } from 'preact/hooks';
import { Extensions } from './panes/Extensions';
+import { ExtensionPane } from './panes/ExtensionPane';
interface PreferencesProps extends MfaProps {
application: WebApplication;
@@ -25,44 +27,64 @@ interface PreferencesProps extends MfaProps {
const PaneSelector: FunctionComponent<
PreferencesProps & { menu: PreferencesMenu }
-> = observer((props) => {
- switch (props.menu.selectedPaneId) {
- case 'general':
- return (
-
- );
- case 'account':
- return (
-
- );
- case 'appearance':
- return null;
- case 'security':
- return (
-
- );
- case 'extensions':
- return ;
- case 'listed':
- return ;
- case 'shortcuts':
- return null;
- case 'accessibility':
- return null;
- case 'get-free-month':
- return null;
- case 'help-feedback':
- return ;
- }
-});
+> = observer(
+ ({
+ menu,
+ appState,
+ application,
+ mfaProvider,
+ userProvider
+ }) => {
+ switch (menu.selectedPaneId) {
+ case 'general':
+ return (
+
+ );
+ case 'account':
+ return (
+
+ );
+ case 'appearance':
+ return null;
+ case 'security':
+ return (
+
+ );
+ case 'extensions':
+ return ;
+ case 'listed':
+ return ;
+ case 'shortcuts':
+ return null;
+ case 'accessibility':
+ return null;
+ case 'get-free-month':
+ return null;
+ case 'help-feedback':
+ return ;
+ default:
+ if (menu.selectedExtension != undefined) {
+ return (
+
+ );
+ } else {
+ return ;
+ }
+ }
+ });
const PreferencesCanvas: FunctionComponent<
PreferencesProps & { menu: PreferencesMenu }
@@ -75,9 +97,9 @@ const PreferencesCanvas: FunctionComponent<
export const PreferencesView: FunctionComponent = observer(
(props) => {
- const menu = useMemo(() => new PreferencesMenu(props.appState.enableUnfinishedFeatures), [
- props.appState.enableUnfinishedFeatures
- ]);
+ const menu = useMemo(
+ () => new PreferencesMenu(props.application, props.appState.enableUnfinishedFeatures),
+ [props.appState.enableUnfinishedFeatures, props.application]);
useEffect(() => {
menu.selectPane(props.appState.preferences.currentPane);
diff --git a/app/assets/javascripts/preferences/panes/ExtensionPane.tsx b/app/assets/javascripts/preferences/panes/ExtensionPane.tsx
new file mode 100644
index 000000000..ed8db4f35
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/ExtensionPane.tsx
@@ -0,0 +1,47 @@
+import { PreferencesGroup, PreferencesSegment } from "@/preferences/components";
+import { WebApplication } from "@/ui_models/application";
+import { SNComponent } from "@standardnotes/snjs/dist/@types";
+import { observer } from "mobx-react-lite";
+import { FunctionComponent } from "preact";
+import { ExtensionItem } from "./extensions-segments";
+import { ComponentView } from '@/components/ComponentView';
+import { AppState } from '@/ui_models/app_state';
+import { PreferencesMenu } from '@/preferences/PreferencesMenu';
+
+interface IProps {
+ application: WebApplication;
+ appState: AppState;
+ extension: SNComponent;
+ preferencesMenu: PreferencesMenu;
+}
+
+export const ExtensionPane: FunctionComponent = observer(
+ ({ extension, application, appState, preferencesMenu }) => {
+ const latestVersion = preferencesMenu.extensionsLatestVersions.getVersion(extension);
+
+ return (
+
+
+
+
+ application.deleteItem(extension).then(() => preferencesMenu.loadExtensionsPanes())}
+ toggleActivate={() => application.toggleComponent(extension).then(() => preferencesMenu.loadExtensionsPanes())}
+ latestVersion={latestVersion}
+ />
+
+
+
+
+
+
+
+ );
+ });
diff --git a/app/assets/javascripts/preferences/panes/Extensions.tsx b/app/assets/javascripts/preferences/panes/Extensions.tsx
index 7ce4a6295..3e51b1d39 100644
--- a/app/assets/javascripts/preferences/panes/Extensions.tsx
+++ b/app/assets/javascripts/preferences/panes/Extensions.tsx
@@ -9,9 +9,9 @@ import {
PreferencesPane,
PreferencesSegment,
} from '../components';
-import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments';
+import { ConfirmCustomExtension, ExtensionItem, ExtensionsLatestVersions } from './extensions-segments';
import { useEffect, useRef, useState } from 'preact/hooks';
-import { FeatureDescription } from '@standardnotes/features';
+import { observer } from 'mobx-react-lite';
const loadExtensions = (application: WebApplication) => application.getItems([
ContentType.ActionsExtension,
@@ -19,30 +19,14 @@ const loadExtensions = (application: WebApplication) => application.getItems([
ContentType.Theme,
]) as SNComponent[];
-function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map) {
- if (features == undefined) return;
- for (const feature of features) {
- versionMap.set(feature.identifier, feature.version);
- }
-}
-
-const loadLatestVersions = (application: WebApplication) => application.getAvailableSubscriptions()
- .then(subscriptions => {
- const versionMap: Map = new Map();
- collectFeatures(subscriptions?.CORE_PLAN?.features, versionMap);
- collectFeatures(subscriptions?.PLUS_PLAN?.features, versionMap);
- collectFeatures(subscriptions?.PRO_PLAN?.features, versionMap);
- return versionMap;
- });
-
export const Extensions: FunctionComponent<{
application: WebApplication
-}> = ({ application }) => {
+ extensionsLatestVersions: ExtensionsLatestVersions,
+}> = observer(({ application, extensionsLatestVersions }) => {
const [customUrl, setCustomUrl] = useState('');
const [confirmableExtension, setConfirmableExtension] = useState(undefined);
const [extensions, setExtensions] = useState(loadExtensions(application));
- const [latestVersions, setLatestVersions] = useState | undefined>(undefined);
const confirmableEnd = useRef(null);
@@ -52,12 +36,6 @@ export const Extensions: FunctionComponent<{
}
}, [confirmableExtension, confirmableEnd]);
- useEffect(() => {
- if (!latestVersions) {
- loadLatestVersions(application).then(versions => setLatestVersions(versions));
- }
- }, [latestVersions, application]);
-
const uninstallExtension = async (extension: SNComponent) => {
await application.deleteItem(extension);
setExtensions(loadExtensions(application));
@@ -94,12 +72,13 @@ export const Extensions: FunctionComponent<{
{
extensions
+ .filter(extension => !['modal', 'rooms'].includes(extension.area))
.sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase()))
.map((extension, i) => (
@@ -140,4 +119,4 @@ export const Extensions: FunctionComponent<{
);
-};
+});
diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx
index 15f6b7731..a3f0bd9fb 100644
--- a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx
+++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx
@@ -6,6 +6,7 @@ import { Switch } from "@/components/Switch";
import { WebApplication } from "@/ui_models/application";
import { useEffect, useRef, useState } from "preact/hooks";
import { Button } from "@/components/Button";
+import { RenameExtension } from "./RenameExtension";
const ExtensionVersions: FunctionComponent<{
installedVersion: string,
@@ -37,161 +38,108 @@ const UseHosted: FunctionComponent<{
);
-const RenameExtension: FunctionComponent<{
- extensionName: string, changeName: (newName: string) => void
-}> = ({ extensionName, changeName }) => {
- const [isRenaming, setIsRenaming] = useState(false);
- const [newExtensionName, setNewExtensionName] = useState
(extensionName);
-
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (isRenaming) {
- inputRef.current!.focus();
- }
- }, [inputRef, isRenaming]);
-
- const startRenaming = () => {
- setNewExtensionName(extensionName);
- setIsRenaming(true);
- };
-
- const cancelRename = () => {
- setNewExtensionName(extensionName);
- setIsRenaming(false);
- };
-
- const confirmRename = () => {
- if (newExtensionName == undefined || newExtensionName === '') {
- return;
- }
- changeName(newExtensionName);
- setIsRenaming(false);
- };
-
- return (
-
-
setNewExtensionName((input as HTMLInputElement)?.value)}
- />
-
- {isRenaming ?
- <>
-
Confirm
-
-
Cancel
- > :
-
Rename
- }
-
- );
-};
-
-export const ExtensionItem: FunctionComponent<{
+export interface ExtensionItemProps {
application: WebApplication,
extension: SNComponent,
first: boolean,
latestVersion: string | undefined,
uninstall: (extension: SNComponent) => void,
- toggleActivate: (extension: SNComponent) => void,
-}> = ({ application, extension, first, uninstall, toggleActivate, latestVersion }) => {
+ toggleActivate?: (extension: SNComponent) => void,
+}
+
+export const ExtensionItem: FunctionComponent =
+ ({ application, extension, first, uninstall, toggleActivate, latestVersion }) => {
const [autoupdateDisabled, setAutoupdateDisabled] = useState(extension.autoupdateDisabled ?? false);
const [offlineOnly, setOfflineOnly] = useState(extension.offlineOnly ?? false);
const [extensionName, setExtensionName] = useState(extension.name);
- const toggleAutoupdate = () => {
- const newAutoupdateValue = !autoupdateDisabled;
- setAutoupdateDisabled(newAutoupdateValue);
- application
- .changeAndSaveItem(extension.uuid, (m: any) => {
- if (m.content == undefined) m.content = {};
- m.content.autoupdateDisabled = newAutoupdateValue;
- })
- .then((item) => {
- const component = (item as SNComponent);
- setAutoupdateDisabled(component.autoupdateDisabled);
- })
- .catch(e => {
- console.error(e);
- });
+ const toggleAutoupdate = () => {
+ const newAutoupdateValue = !autoupdateDisabled;
+ setAutoupdateDisabled(newAutoupdateValue);
+ application
+ .changeAndSaveItem(extension.uuid, (m: any) => {
+ if (m.content == undefined) m.content = {};
+ m.content.autoupdateDisabled = newAutoupdateValue;
+ })
+ .then((item) => {
+ const component = (item as SNComponent);
+ setAutoupdateDisabled(component.autoupdateDisabled);
+ })
+ .catch(e => {
+ console.error(e);
+ });
+ };
+
+ const toggleOffllineOnly = () => {
+ const newOfflineOnly = !offlineOnly;
+ setOfflineOnly(newOfflineOnly);
+ application
+ .changeAndSaveItem(extension.uuid, (m: any) => {
+ if (m.content == undefined) m.content = {};
+ m.content.offlineOnly = newOfflineOnly;
+ })
+ .then((item) => {
+ const component = (item as SNComponent);
+ setOfflineOnly(component.offlineOnly);
+ })
+ .catch(e => {
+ console.error(e);
+ });
+ };
+
+ const changeExtensionName = (newName: string) => {
+ setExtensionName(newName);
+ application
+ .changeAndSaveItem(extension.uuid, (m: any) => {
+ if (m.content == undefined) m.content = {};
+ m.content.name = newName;
+ })
+ .then((item) => {
+ const component = (item as SNComponent);
+ setExtensionName(component.name);
+ });
+ };
+
+ const localInstallable = extension.package_info.download_url;
+
+ const isExternal = !extension.package_info.identifier.startsWith('org.standardnotes.');
+
+ const installedVersion = extension.package_info.version;
+
+ const isEditorOrTags = ['editor-stack', 'tags-list'].includes(extension.area);
+
+ return (
+
+ {first && <>
+ Extensions
+
+ >}
+
+
+
+
+
+
+ {localInstallable && }
+ {localInstallable && }
+
+ {isEditorOrTags || isExternal &&
+ <>
+
+
+ {isEditorOrTags && toggleActivate != undefined && (
+ <>
+ {extension.active ?
+
toggleActivate(extension)} /> :
+ toggleActivate(extension)} />
+ }
+
+ >
+ )}
+ {isExternal && uninstall(extension)} />}
+
+ >
+ }
+
+ );
};
-
- const toggleOffllineOnly = () => {
- const newOfflineOnly = !offlineOnly;
- setOfflineOnly(newOfflineOnly);
- application
- .changeAndSaveItem(extension.uuid, (m: any) => {
- if (m.content == undefined) m.content = {};
- m.content.offlineOnly = newOfflineOnly;
- })
- .then((item) => {
- const component = (item as SNComponent);
- setOfflineOnly(component.offlineOnly);
- })
- .catch(e => {
- console.error(e);
- });
- };
-
- const changeExtensionName = (newName: string) => {
- setExtensionName(newName);
- application
- .changeAndSaveItem(extension.uuid, (m: any) => {
- if (m.content == undefined) m.content = {};
- m.content.name = newName;
- })
- .then((item) => {
- const component = (item as SNComponent);
- setExtensionName(component.name);
- });
- };
-
- const localInstallable = extension.package_info.download_url;
-
- const isExternal = !extension.package_info.identifier.startsWith('org.standardnotes.');
-
- const installedVersion = extension.package_info.version;
-
- const isEditorOrTags = ['editor-stack', 'tags-list'].includes(extension.area);
-
- return (
-
- {first && <>
- Extensions
-
- >}
-
-
-
-
-
-
- {localInstallable && }
- {localInstallable && }
-
- {isEditorOrTags || isExternal &&
- <>
-
-
- {isEditorOrTags && (
- <>
- {extension.active ?
-
toggleActivate(extension)} /> :
- toggleActivate(extension)} />
- }
-
- >
- )}
- {isExternal && uninstall(extension)} />}
-
- >
- }
-
- );
-};
diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionsLatestVersions.ts b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionsLatestVersions.ts
new file mode 100644
index 000000000..0075d69e2
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionsLatestVersions.ts
@@ -0,0 +1,35 @@
+import { WebApplication } from "@/ui_models/application";
+import { FeatureDescription } from "@standardnotes/features";
+import { SNComponent } from "@standardnotes/snjs/dist/@types";
+import { makeAutoObservable, observable } from "mobx";
+
+export class ExtensionsLatestVersions {
+ static async load(application: WebApplication): Promise {
+ const map = await application.getAvailableSubscriptions()
+ .then(subscriptions => {
+ const versionMap: Map = new Map();
+ collectFeatures(subscriptions?.CORE_PLAN?.features as FeatureDescription[], versionMap);
+ collectFeatures(subscriptions?.PLUS_PLAN?.features as FeatureDescription[], versionMap);
+ collectFeatures(subscriptions?.PRO_PLAN?.features as FeatureDescription[], versionMap);
+ return versionMap;
+ });
+ return new ExtensionsLatestVersions(map);
+ }
+
+ constructor(private readonly latestVersionsMap: Map) {
+ makeAutoObservable(
+ this, { latestVersionsMap: observable.ref });
+ }
+
+ getVersion(extension: SNComponent): string | undefined {
+ return this.latestVersionsMap.get(extension.package_info.identifier);
+ }
+
+}
+
+function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map) {
+ if (features == undefined) return;
+ for (const feature of features) {
+ versionMap.set(feature.identifier, feature.version);
+ }
+}
diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/RenameExtension.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/RenameExtension.tsx
new file mode 100644
index 000000000..7356f7ad4
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/extensions-segments/RenameExtension.tsx
@@ -0,0 +1,58 @@
+import { FunctionComponent } from "preact";
+import { useState, useRef, useEffect } from "preact/hooks";
+
+export const RenameExtension: FunctionComponent<{
+ extensionName: string, changeName: (newName: string) => void
+}> = ({ extensionName, changeName }) => {
+ const [isRenaming, setIsRenaming] = useState(false);
+ const [newExtensionName, setNewExtensionName] = useState(extensionName);
+
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (isRenaming) {
+ inputRef.current!.focus();
+ }
+ }, [inputRef, isRenaming]);
+
+ const startRenaming = () => {
+ setNewExtensionName(extensionName);
+ setIsRenaming(true);
+ };
+
+ const cancelRename = () => {
+ setNewExtensionName(extensionName);
+ setIsRenaming(false);
+ };
+
+ const confirmRename = () => {
+ if (!newExtensionName) {
+ return;
+ }
+ changeName(newExtensionName);
+ setIsRenaming(false);
+ };
+
+ return (
+
+
setNewExtensionName((input as HTMLInputElement)?.value)}
+ />
+
+ {isRenaming ?
+ <>
+
Confirm
+
+
Cancel
+ > :
+
Rename
+ }
+
+ );
+};
diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/index.ts b/app/assets/javascripts/preferences/panes/extensions-segments/index.ts
index 20694952c..ada8156e9 100644
--- a/app/assets/javascripts/preferences/panes/extensions-segments/index.ts
+++ b/app/assets/javascripts/preferences/panes/extensions-segments/index.ts
@@ -1,2 +1,3 @@
export * from './ConfirmCustomExtension';
export * from './ExtensionItem';
+export * from './ExtensionsLatestVersions';
diff --git a/app/assets/javascripts/ui_models/app_state/account_menu_state.ts b/app/assets/javascripts/ui_models/app_state/account_menu_state.ts
index 8fe287989..7b8ede15b 100644
--- a/app/assets/javascripts/ui_models/app_state/account_menu_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/account_menu_state.ts
@@ -61,6 +61,7 @@ export class AccountMenuState {
setOtherSessionsSignOut: action,
setCurrentPane: action,
setEnableServerOption: action,
+ setServer: action,
notesAndTagsCount: computed,
});
diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts
index f486cdf28..d82f1df5d 100644
--- a/app/assets/javascripts/ui_models/app_state/app_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/app_state.ts
@@ -34,7 +34,7 @@ export enum AppStateEvent {
BeganBackupDownload,
EndedBackupDownload,
WindowDidFocus,
- WindowDidBlur,
+ WindowDidBlur
}
export type PanelResizedData = {
diff --git a/app/assets/javascripts/ui_models/app_state/preferences_state.ts b/app/assets/javascripts/ui_models/app_state/preferences_state.ts
index 9a4a0a961..1c556254d 100644
--- a/app/assets/javascripts/ui_models/app_state/preferences_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/preferences_state.ts
@@ -29,7 +29,7 @@ export class PreferencesState {
this.currentPane = 'account';
};
- get isOpen() {
+ get isOpen(): boolean {
return this._open;
}
}
diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug
index 8c7724f55..90ec90159 100644
--- a/app/assets/javascripts/views/editor/editor-view.pug
+++ b/app/assets/javascripts/views/editor/editor-view.pug
@@ -112,6 +112,7 @@
ng-if='self.state.editorComponent && !self.state.editorUnloading',
on-load='self.onEditorLoad',
application='self.application'
+ app-state='self.appState'
)
textarea#note-text-editor.editable.font-editor(
dir='auto',
@@ -168,4 +169,5 @@
manual-dealloc='true',
ng-show='!self.stackComponentHidden(component)',
application='self.application'
+ app-state='self.appState'
)
diff --git a/app/assets/javascripts/views/tags/tags-view.pug b/app/assets/javascripts/views/tags/tags-view.pug
index 600ab2760..a32bb709e 100644
--- a/app/assets/javascripts/views/tags/tags-view.pug
+++ b/app/assets/javascripts/views/tags/tags-view.pug
@@ -3,6 +3,7 @@
component-view.component-view(
component-uuid='self.component.uuid',
application='self.application'
+ app-state='self.appState'
)
#tags-content.content(ng-if='!(self.component && self.component.active)')
.tags-title-section.section-title-bar
diff --git a/app/assets/stylesheets/_preferences.scss b/app/assets/stylesheets/_preferences.scss
index bb9a3342f..6bddccf02 100644
--- a/app/assets/stylesheets/_preferences.scss
+++ b/app/assets/stylesheets/_preferences.scss
@@ -44,3 +44,9 @@
@extend .color-info;
}
}
+
+.preferences-extension-pane {
+ iframe {
+ height: 60vh;
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss
index 259767b39..8ac9088df 100644
--- a/app/assets/stylesheets/_sn.scss
+++ b/app/assets/stylesheets/_sn.scss
@@ -282,10 +282,18 @@
width: 6.5rem;
}
+.max-w-200 {
+ max-width: 50rem;
+}
+
.w-92 {
width: 23rem;
}
+.w-200 {
+ width: 50rem;
+}
+
.min-w-1 {
min-width: 0.25rem;
}
@@ -316,7 +324,7 @@
.min-w-70 {
min-width: 17.5rem;
-}
+}
.min-w-24 {
min-width: 6rem;
@@ -328,7 +336,7 @@
.min-w-90 {
min-width: 22.5rem;
-}
+}
.min-h-1px {
min-height: 1px;
@@ -549,7 +557,7 @@
.-z-index-1 {
z-index: -1;
}
-
+
.sn-component .btn-w-full {
width: 100%;
}
diff --git a/app/assets/templates/directives/component-modal.pug b/app/assets/templates/directives/component-modal.pug
index 150af89e4..8baf0f119 100644
--- a/app/assets/templates/directives/component-modal.pug
+++ b/app/assets/templates/directives/component-modal.pug
@@ -14,4 +14,5 @@
ng-if='ctrl.component.active'
component-uuid="ctrl.component.uuid",
application='ctrl.application'
+ app-state='self.appState'
)
diff --git a/app/assets/templates/directives/component-view.pug b/app/assets/templates/directives/component-view.pug
deleted file mode 100644
index 917f44a66..000000000
--- a/app/assets/templates/directives/component-view.pug
+++ /dev/null
@@ -1,90 +0,0 @@
-.sn-component(ng-if='ctrl.issueLoading')
- .sk-app-bar.no-edges.no-top-edge.dynamic-height
- .left
- .sk-app-bar-item
- .sk-label.warning There was an issue loading {{ctrl.component.name}}.
- .right
- .sk-app-bar-item(ng-click='ctrl.reloadIframe()')
- button.sn-button.small.info Reload
-.sn-component(ng-if='ctrl.expired')
- .sk-app-bar.no-edges.no-top-edge.dynamic-height
- .left
- .sk-app-bar-item
- .sk-app-bar-item-column
- .sk-circle.danger.small
- .sk-app-bar-item-column
- div
- a.sk-label.sk-base(
- href='https://dashboard.standardnotes.com',
- rel='noopener',
- target='_blank'
- )
- | Your Extended subscription expired on
- | {{ctrl.component.dateToLocalizedString(ctrl.component.valid_until)}}.
- .sk-p
- | Extensions are in a read-only state.
- .right
- .sk-app-bar-item(ng-click='ctrl.reloadStatus(true)')
- button.sn-button.small.info Reload
- .sk-app-bar-item
- .sk-app-bar-item-column
- a.sn-button.small.warning(
- href='https://standardnotes.com/help/41/expired',
- rel='noopener',
- target='_blank'
- ) Help
-.sn-component(ng-if='ctrl.isDeprecated && !ctrl.deprecationMessageDismissed')
- .sk-app-bar.no-edges.no-top-edge.dynamic-height
- .left
- .sk-app-bar-item
- .sk-label.warning {{ctrl.deprecationMessage || 'This extension is deprecated.'}}
- .right
- .sk-app-bar-item(ng-click='ctrl.dismissDeprecationMessage()')
- button.sn-button.small.info Dismiss
-
-.sn-component(ng-if="ctrl.error == 'offline-restricted'")
- .sk-panel.static
- .sk-panel-content
- .sk-panel-section.stretch
- .sk-panel-column
- .sk-h1.sk-bold You have restricted this extension to be used offline only.
- .sk-subtitle Offline extensions are not available in the Web app.
- .sk-panel-row
- .sk-panel-row
- .sk-panel-column
- .sk-p You can either:
- ul
- li.sk-p
- strong Enable the Hosted option
- | for this extension by opening the 'Extensions' menu and
- | toggling 'Use hosted when local is unavailable' under this
- | extension's options. Then press Reload below.
- li.sk-p
- strong Use the Desktop application.
- .sk-panel-row
- button.sn-button.small.info(
- ng-click='ctrl.reloadStatus()',
- ng-if='!ctrl.reloading'
- ) Reload
- .sk-spinner.info.small(ng-if='ctrl.reloading')
-.sn-component(ng-if="ctrl.error == 'url-missing'")
- .sk-panel.static
- .sk-panel-content
- .sk-panel-section.stretch
- .sk-panel-section-title This extension is not installed correctly.
- p Please uninstall {{ctrl.component.name}}, then re-install it.
- p
- | This issue can occur if you access Standard Notes using an older
- | version of the app.
- | Ensure you are running at least version 2.1 on all platforms.
-iframe(
- data-component-id='{{ctrl.component.uuid}}',
- frameborder='0',
- ng-init='ctrl.onIframeInit()'
- ng-attr-id='component-iframe-{{ctrl.component.uuid}}',
- ng-if='ctrl.component.uuid && !ctrl.reloading && ctrl.componentValid',
- ng-src='{{ctrl.getUrl() | trusted}}',
- sandbox='allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms allow-downloads'
- )
- | Loading
-.loading-overlay(ng-if='ctrl.loading')
diff --git a/app/assets/templates/directives/revision-preview-modal.pug b/app/assets/templates/directives/revision-preview-modal.pug
index ee0895408..2fe889748 100644
--- a/app/assets/templates/directives/revision-preview-modal.pug
+++ b/app/assets/templates/directives/revision-preview-modal.pug
@@ -33,4 +33,5 @@
ng-if="ctrl.state.editor",
template-component="ctrl.state.editor",
application='ctrl.application'
+ app-state='self.appState'
)
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index 7fc7f578b..a8c17ec4d 100755
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -17,9 +17,9 @@ case "$COMMAND" in
echo "Prestart Step 2/5 - Cleaning assets"
bundle exec rails assets:clobber
echo "Prestart Step 3/5 - Installing dependencies"
- npm install
+ yarn install --pure-lockfile
echo "Prestart Step 4/5 - Creating Webpack bundle"
- npm run bundle
+ yarn run bundle
echo "Prestart Step 5/5 - Compiling assets"
bundle exec rails assets:precompile
echo "Starting Server..."
diff --git a/package.json b/package.json
index a0838b0a6..35b426f6b 100644
--- a/package.json
+++ b/package.json
@@ -70,9 +70,9 @@
"@reach/checkbox": "^0.16.0",
"@reach/dialog": "^0.16.2",
"@reach/listbox": "^0.16.2",
- "@standardnotes/features": "1.7.2",
- "@standardnotes/sncrypto-web": "^1.5.3",
- "@standardnotes/snjs": "2.16.0",
+ "@standardnotes/features": "1.7.3",
+ "@standardnotes/sncrypto-web": "1.5.3",
+ "@standardnotes/snjs": "2.16.2",
"mobx": "^6.3.5",
"mobx-react-lite": "^3.2.1",
"preact": "^10.5.15",
diff --git a/public/robots.txt.development b/public/robots.txt.development
new file mode 100644
index 000000000..1f53798bb
--- /dev/null
+++ b/public/robots.txt.development
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/public/robots.txt b/public/robots.txt.production
similarity index 100%
rename from public/robots.txt
rename to public/robots.txt.production
diff --git a/yarn.lock b/yarn.lock
index ad005dabb..84f98e265 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2147,55 +2147,43 @@
prop-types "^15.7.2"
tslib "^2.3.0"
-"@standardnotes/auth@3.7.2":
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.2.tgz#de553ca38c64ae76b3ee3a3aa12ea20311030adb"
- integrity sha512-YED+iWX1FxMpn4UJ0Yo37/K0Py/xNYoqcFSlgEcXNorNllRHpLXGXKZ3ILAQVRa0R1oYXpmsthx4bjg2JSptiA==
+"@standardnotes/auth@^3.8.1":
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.8.1.tgz#4197fb2f7e223c6bd13a870a3feac3c73294fb3c"
+ integrity sha512-Q2/81dgFGIGuYlQ4VnSjGRsDB0Qw0tQP/qsiuV+DQj+wdp5Wy5WX3Q4g+p2PNvoyEAYgbuduEHZfWuTLAaIdyw==
dependencies:
- "@standardnotes/common" "^1.1.0"
+ "@standardnotes/common" "^1.2.1"
-"@standardnotes/auth@^3.7.0":
- version "3.7.1"
- resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.1.tgz#d0b1eb63f605e04ecb077fdb5ef83e3fe6db33f9"
- integrity sha512-xtjAvtikLW3Xv75X/kYA1KTm8FJVPPlXvl+ofnrf/ijkIaRkbUW/3TUhMES+G5CMiG2TZv6uVn32GqJipqgQQQ==
+"@standardnotes/common@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.1.tgz#9db212db86ccbf08b347da02549b3dbe4bedbb02"
+ integrity sha512-HilBxS50CBlC6TJvy1mrnhGVDzOH63M/Jf+hyMxQ0Vt1nYzpd0iyxVEUrgMh7ZiyO1b9CLnCDED99Jy9rnZWVQ==
+
+"@standardnotes/domain-events@^2.5.1":
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.5.1.tgz#e6433e940ae616683d1c24f76133c70755504c44"
+ integrity sha512-p0VB4Al/ZcVqcj9ztU7TNqzc3jjjG6/U7x9lBW/QURHxpB+PnwJq3kFU5V5JA9QpCOYlXLT71CMERMf/O5QX6g==
dependencies:
- "@standardnotes/common" "^1.1.0"
+ "@standardnotes/auth" "^3.8.1"
-"@standardnotes/common@1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.0.tgz#949c9d384c54fbabeacca9ea3f6485cbc78da4bf"
- integrity sha512-QiOAG858BcXUGSRjsmtk854/4OLyGkdcbvixia8Xcfv4d76iL/pQf7JFTDbanr9Ygodrc6B+h+NuzliO41COcg==
-
-"@standardnotes/common@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.1.0.tgz#5ffb0a50f9947471e236bb66d097f153ad9a148f"
- integrity sha512-Nm2IFWbMSfZDD7cnKtN+Gjic0f+PhPq/da/o4eOoUKg21VeOaQkTn+jlQKraKIs6Lmf+w9mmPNAgMc5o4hj7Lg==
-
-"@standardnotes/domain-events@2.1.0":
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.1.0.tgz#a5c4379983a728a738f145aa9e76f7640c7283a2"
- integrity sha512-8bCQk2V2fyWKalVWC9L8cuj2kuKLe+bTTp0xBVTDpDhWrGFzXfsI79AzWbOl/CLHJU/PWrXf1lvUgQwPwT+RlA==
+"@standardnotes/features@1.7.3", "@standardnotes/features@^1.7.3":
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.7.3.tgz#4872c837fd11d069a8a41941bb3e5f294fb13d9c"
+ integrity sha512-G9NACv8pfVOB9O9L1C+Yoh25vMWVFLfF0FKSK5jjm/THm/w3SiQ2K82BIGgoQGpVGGAPEPa3Ru+OCBs3w8u+Jg==
dependencies:
- "@standardnotes/auth" "^3.7.0"
+ "@standardnotes/common" "^1.2.1"
-"@standardnotes/features@1.7.2":
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.7.2.tgz#7a45a947f56c55d191614f7293af553c5209705a"
- integrity sha512-zFTHzYAC+08Lbeni5x3RalR5FT8qVORgv3T/z6/Ye4mGvDyXSAddgDPn+o/NmzirwBTpaF6ogSzwZocsElm8zg==
- dependencies:
- "@standardnotes/common" "^1.1.0"
+"@standardnotes/settings@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.1.tgz#4c7656ea86d784a2f77c70acc89face5d28da024"
+ integrity sha512-EhCDtQKcVzY6cJ6qXCkAiA3sJ3Wj/q0L0ZVYq+tCXd0jaxmZ8fSk5YNqdwJfjmNXsqtuh7xq6eA2dcXd1fD9VQ==
-"@standardnotes/settings@1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.0.tgz#d7936c788138265b0720085ca9e63358d3092459"
- integrity sha512-7ikL9BfgXPcLsTJKgCNuRCJN/rFeWreXNxC8M/rxGY+Yk0694WXYyM6jFY8Ry6yV9vLaVukS7Ov6acf+D4wrFg==
-
-"@standardnotes/sncrypto-common@1.5.2", "@standardnotes/sncrypto-common@^1.5.2":
+"@standardnotes/sncrypto-common@^1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.5.2.tgz#be9404689d94f953c68302609a4f76751eaa82cd"
integrity sha512-+OQ6gajTcVSHruw33T52MHyBDKL1vRCfQBXQn4tt4+bCfBAe+PFLkEQMHp35bg5twCfg9+wUf2KhmNNSNyBBZw==
-"@standardnotes/sncrypto-web@^1.5.3":
+"@standardnotes/sncrypto-web@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.5.3.tgz#b055bcac553914cbeebfa10e45f46fff817116c3"
integrity sha512-thyFc71cTJTfmLNPgT1hDMiMefZ1bgN0eTa22GEJSp4T41J/X9MldyP2dTmc7sHNM95TJlwzlIJ0iQtxFUE50w==
@@ -2204,17 +2192,17 @@
buffer "^6.0.3"
libsodium-wrappers "^0.7.9"
-"@standardnotes/snjs@2.16.0":
- version "2.16.0"
- resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.16.0.tgz#af1e427d8ed7f71b5019f7c8cca349422a885b02"
- integrity sha512-dAvFFRu9PuIbW4fF1hZEfh/DwADO2bZP1s24biYY1bfMOr9Z0131n2xIN/yUsA7zw0Sch6m1Nof8OCEZntX5xQ==
+"@standardnotes/snjs@2.16.2":
+ version "2.16.2"
+ resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.16.2.tgz#2958ec0a2f1724343de82204f905311d7c3ffb65"
+ integrity sha512-G9HNu1TsAnK0OeRo6IYvmIR/huKoNkB+qWDPuh2+p/pJjLrtO6SGrOD4cm4Mg/63t29g8wW8Za/6/tPJHZOFCg==
dependencies:
- "@standardnotes/auth" "3.7.2"
- "@standardnotes/common" "1.2.0"
- "@standardnotes/domain-events" "2.1.0"
- "@standardnotes/features" "1.7.2"
- "@standardnotes/settings" "1.2.0"
- "@standardnotes/sncrypto-common" "1.5.2"
+ "@standardnotes/auth" "^3.8.1"
+ "@standardnotes/common" "^1.2.1"
+ "@standardnotes/domain-events" "^2.5.1"
+ "@standardnotes/features" "^1.7.3"
+ "@standardnotes/settings" "^1.2.1"
+ "@standardnotes/sncrypto-common" "^1.5.2"
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
version "5.4.0"