From b7d10810a5ee19cd6fe68f8252c54aaf750e518c Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Thu, 2 Sep 2021 15:59:43 +0200 Subject: [PATCH 01/71] chore: update snjs, sncrypto-web and fix tsc complaints (#624) Co-authored-by: Mo Bitar Co-authored-by: Mo Bitar --- .../directives/views/componentView.ts | 2 +- .../javascripts/views/footer/footer_view.ts | 10 +-- package.json | 4 +- yarn.lock | 71 +++++++++++++------ 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/directives/views/componentView.ts b/app/assets/javascripts/directives/views/componentView.ts index 5df917399..67392b335 100644 --- a/app/assets/javascripts/directives/views/componentView.ts +++ b/app/assets/javascripts/directives/views/componentView.ts @@ -34,7 +34,7 @@ class ComponentViewCtrl implements ComponentViewScope { private unregisterDesktopObserver!: () => void private issueLoading = false private isDeprecated = false - private deprecationMessage = '' + private deprecationMessage: string | undefined = undefined private deprecationMessageDismissed = false public reloading = false private expired = false diff --git a/app/assets/javascripts/views/footer/footer_view.ts b/app/assets/javascripts/views/footer/footer_view.ts index 9764d3c2a..33d0f9da6 100644 --- a/app/assets/javascripts/views/footer/footer_view.ts +++ b/app/assets/javascripts/views/footer/footer_view.ts @@ -301,7 +301,7 @@ class FooterViewCtrl extends PureViewCtrl< CollectionSort.Title, 'asc', (theme: SNTheme) => { - return theme.package_info && theme.package_info.dock_icon; + return theme.package_info && theme.package_info.dock_icon != undefined; } ); @@ -540,9 +540,11 @@ class FooterViewCtrl extends PureViewCtrl< const id = 'dock-svg-' + shortcut.component.uuid; const element = document.getElementById(id)!; const parser = new DOMParser(); - const svg = shortcut.component.package_info.dock_icon.source; - const doc = parser.parseFromString(svg, 'image/svg+xml'); - element.appendChild(doc.documentElement); + const svg = shortcut.component.package_info.dock_icon?.source; + if (svg != undefined) { + const doc = parser.parseFromString(svg, 'image/svg+xml'); + element.appendChild(doc.documentElement); + } } selectShortcut(shortcut: DockShortcut) { diff --git a/package.json b/package.json index bf01da617..65963cf74 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ "@reach/alert-dialog": "^0.13.0", "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", - "@standardnotes/sncrypto-web": "1.2.10", - "@standardnotes/snjs": "2.7.23", + "@standardnotes/sncrypto-web": "1.5.2", + "@standardnotes/snjs": "2.11.2", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12" diff --git a/yarn.lock b/yarn.lock index 30920454c..165cc2889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2016,32 +2016,61 @@ resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.1.1.tgz#834701c2e14d31eb204bff90457fa05e9183464a" integrity sha512-E9zDYZ1gJkVZBEzd7a1L2haQ4GYeH1lUrY87UmDH1AMYUHW+c0SqZ71af1fBNqGzrx3EZSXk+Qzr7RyOa6N1Mw== -"@standardnotes/features@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.0.0.tgz#906af029b6e58241689ca37436982c37a888a418" - integrity sha512-PEQyP/p/TQLVcNYcbu9jEIWNRqBrFFG1Qyy8QIcvNUt5o4lpLZGEY1T+PJUsPSisnuKKNpQrgVLc9LjhUKpuYw== - -"@standardnotes/sncrypto-common@^1.2.7", "@standardnotes/sncrypto-common@^1.2.9": - version "1.2.9" - resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.2.9.tgz#5212a959e4ec563584e42480bfd39ef129c3cbdf" - integrity sha512-xJ5IUGOZztjSgNP/6XL+Ut5+q9UgSTv6xMtKkcQC5aJxCOkJy9u6RamPLdF00WQgwibxx2tu0e43bKUjTgzMig== - -"@standardnotes/sncrypto-web@1.2.10": - version "1.2.10" - resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.2.10.tgz#ddda0c8ec92754c1334d9a83be7b320433c55b79" - integrity sha512-mmHlXFFovBqOdctpMkyXTpK0zI/k4NuXoV5LRGLKIXUktT9/cZg1pgNNfGPFUP+g4C5M2qxeFmZKFh581ipolQ== +"@standardnotes/auth@^3.2.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.0.tgz#1193f0521bd3b1c3655685c27aff894b98c7f582" + integrity sha512-lHMRyVOxF9g11MH04wcbhpErPysL721Hgrzbi6slRDqIuA+2VpECIkbAhI5WpgP5K8RjO/kZB4l0tzcRI1Mg/g== dependencies: - "@standardnotes/sncrypto-common" "^1.2.7" + "@standardnotes/common" "^1.0.0" + +"@standardnotes/common@1.1.0", "@standardnotes/common@^1.0.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.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.0.0.tgz#6e537485054c6b32cb1d9408fab9cd132ed0fb65" + integrity sha512-4rMCGw2Gy1CLfi6SkpsH7QzbT/80g5gVGeicEHhHvLhMolAEsHXCGOjpw5/nA3YKl8B/U8s6wvaee2vIDejFLA== + dependencies: + "@standardnotes/auth" "^3.2.0" + +"@standardnotes/features@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.4.0.tgz#64f0149eb94bee8cb7e748ad2dc2f94f5c2932f3" + integrity sha512-PQEjDdFjJ+hvGz6rD6LATtlICxNr2YKKwht69qGlDqMPSXQaljrq9bocAdGKuy+YOu+aKFvrmRbezsBK9rRIdw== + dependencies: + "@standardnotes/common" "^1.0.0" + +"@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": + 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.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.5.2.tgz#b213063149b563a4f57f0144f3c9fe62ad0458c0" + integrity sha512-ltfc0EqeCAGvd0+q7iGOcIVNgbPLJHRw5RQaAGdFtAe98VEH3GcyUm9PxEnUN+Q1gk+HJ083gU6q2tJbo5L+3Q== + dependencies: + "@standardnotes/sncrypto-common" "^1.5.2" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.7.23": - version "2.7.23" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.23.tgz#fedc9c025301dbe20ed2d598fb378e36f90ff64e" - integrity sha512-eoEwKlV2PZcJXFbCt6bgovu9nldVoT7DPoterTBo/NZ4odRILOwxLA1SAgL5H5FYPb9NHkwaaCt9uTdIqdNYhA== +"@standardnotes/snjs@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.11.2.tgz#2ae172950316ee3aa7ad95d4fec4dc85d7ac1269" + integrity sha512-49NPflKrBcAkVcs7nGf7D/oatcPRPZz7+TKyi4s9Xsxf69EmHIQhVu9u+sCUCaUTq2j1sjQt9tyCacjche7p4w== dependencies: "@standardnotes/auth" "3.1.1" - "@standardnotes/features" "1.0.0" - "@standardnotes/sncrypto-common" "^1.2.9" + "@standardnotes/common" "1.1.0" + "@standardnotes/domain-events" "2.0.0" + "@standardnotes/features" "1.4.0" + "@standardnotes/settings" "1.2.0" + "@standardnotes/sncrypto-common" "1.5.2" "@svgr/babel-plugin-add-jsx-attribute@^5.4.0": version "5.4.0" From 041d437bd453950fc2033357780d5a437803dc27 Mon Sep 17 00:00:00 2001 From: Vardan Hakobyan Date: Mon, 6 Sep 2021 10:36:57 +0400 Subject: [PATCH 02/71] feat: pass web app version to snjs application (#623) * feat: store web version in local storage * feat: pass web app version to snjs application when creating WebApplication * refactor: pass version in application constructor, remove unnecessary method * refactor: move global variables declarations to separate files to avoid declaring them in all places where they are used * refactor: better way to use global variables * chore: add comment * chore: make global constants pascal case * chore: version bump for snjs * chore: yarn.lock with correct snjs version * chore: bump snjs version --- app/assets/javascripts/app.ts | 8 +++---- .../javascripts/services/errorReporting.ts | 4 ++-- .../javascripts/ui_models/application.ts | 2 ++ app/assets/javascripts/utils.ts | 9 +++----- app/assets/javascripts/version.ts | 8 +++++++ package.json | 2 +- yarn.lock | 22 +++++++++---------- 7 files changed, 30 insertions(+), 25 deletions(-) create mode 100644 app/assets/javascripts/version.ts diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index b493808ad..aeb0c2f5f 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -1,8 +1,5 @@ 'use strict'; -declare const __VERSION__: string; -declare const __WEB__: boolean; - import { SNLog } from '@standardnotes/snjs'; import angular from 'angular'; import { configRoutes } from './routes'; @@ -66,6 +63,7 @@ import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel'; import { IconDirective } from './components/Icon'; import { NoteTagsContainerDirective } from './components/NoteTagsContainer'; import { PreferencesDirective } from './preferences'; +import { AppVersion, IsWebPlatform } from '@/version'; function reloadHiddenFirefoxTab(): boolean { /** @@ -191,10 +189,10 @@ const startApplication: StartApplication = async function startApplication( }); }; -if (__WEB__) { +if (IsWebPlatform) { startApplication( (window as any)._default_sync_server, - new BrowserBridge(__VERSION__), + new BrowserBridge(AppVersion), (window as any)._websocket_url, ); } else { diff --git a/app/assets/javascripts/services/errorReporting.ts b/app/assets/javascripts/services/errorReporting.ts index c3cd66a66..16c70c867 100644 --- a/app/assets/javascripts/services/errorReporting.ts +++ b/app/assets/javascripts/services/errorReporting.ts @@ -3,8 +3,8 @@ import { isDesktopApplication, isDev } from '@/utils'; import { storage, StorageKey } from './localStorage'; import Bugsnag from '@bugsnag/js'; import { WebCrypto } from '../crypto'; +import { AppVersion } from '@/version'; -declare const __VERSION__: string; declare global { interface Window { // eslint-disable-next-line camelcase @@ -50,7 +50,7 @@ export function startErrorReporting(): void { Bugsnag.start({ apiKey: window._bugsnag_api_key, appType: isDesktopApplication() ? 'desktop' : 'web', - appVersion: __VERSION__, + appVersion: AppVersion, collectUserIp: false, autoTrackSessions: false, releaseStage: isDev ? 'development' : undefined, diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index 2226284f9..fba8edf08 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -24,6 +24,7 @@ import { IOService } from '@/services/ioService'; import { NativeExtManager } from '@/services/nativeExtManager'; import { StatusManager } from '@/services/statusManager'; import { ThemeManager } from '@/services/themeManager'; +import { AppVersion } from '@/version'; type WebServices = { appState: AppState; @@ -63,6 +64,7 @@ export class WebApplication extends SNApplication { identifier, [], defaultSyncServerHost, + AppVersion ); this.$compile = $compile; this.scope = scope; diff --git a/app/assets/javascripts/utils.ts b/app/assets/javascripts/utils.ts index c5f9b6be6..73d82cc26 100644 --- a/app/assets/javascripts/utils.ts +++ b/app/assets/javascripts/utils.ts @@ -1,4 +1,5 @@ import { Platform, platformFromString } from '@standardnotes/snjs'; +import { IsDesktopPlatform, IsWebPlatform } from '@/version'; declare const process: { env: { @@ -160,16 +161,12 @@ export async function preventRefreshing( } } -/** Platform-detection functions */ -declare const __WEB__: boolean; -declare const __DESKTOP__: boolean; - -if (!__WEB__ && !__DESKTOP__) { +if (!IsWebPlatform && !IsDesktopPlatform) { throw Error( 'Neither __WEB__ nor __DESKTOP__ is true. Check your configuration files.' ); } export function isDesktopApplication() { - return __DESKTOP__; + return IsDesktopPlatform; } diff --git a/app/assets/javascripts/version.ts b/app/assets/javascripts/version.ts new file mode 100644 index 000000000..ca108b555 --- /dev/null +++ b/app/assets/javascripts/version.ts @@ -0,0 +1,8 @@ +/** Declared in webpack config */ +declare const __VERSION__: string; +declare const __DESKTOP__: boolean; +declare const __WEB__: boolean; + +export const AppVersion = __VERSION__; +export const IsDesktopPlatform = __DESKTOP__; +export const IsWebPlatform = __WEB__; diff --git a/package.json b/package.json index 65963cf74..7fddaee76 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.5.2", - "@standardnotes/snjs": "2.11.2", + "@standardnotes/snjs": "2.12.1", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12" diff --git a/yarn.lock b/yarn.lock index 165cc2889..9d9d74f29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2023,7 +2023,7 @@ dependencies: "@standardnotes/common" "^1.0.0" -"@standardnotes/common@1.1.0", "@standardnotes/common@^1.0.0": +"@standardnotes/common@1.1.0", "@standardnotes/common@^1.0.0", "@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== @@ -2035,12 +2035,12 @@ dependencies: "@standardnotes/auth" "^3.2.0" -"@standardnotes/features@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.4.0.tgz#64f0149eb94bee8cb7e748ad2dc2f94f5c2932f3" - integrity sha512-PQEjDdFjJ+hvGz6rD6LATtlICxNr2YKKwht69qGlDqMPSXQaljrq9bocAdGKuy+YOu+aKFvrmRbezsBK9rRIdw== +"@standardnotes/features@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.0.tgz#91317255bbad376670fd81fa445abc2d14fd43d9" + integrity sha512-0gN1UKpX0LZxOk7HlnLxbl7N8Drvyw36zRErFn3pSetUNQpr92Pd6qTTNNflBBtY2pwzqoP4XVB/2kZRJFAo8w== dependencies: - "@standardnotes/common" "^1.0.0" + "@standardnotes/common" "^1.1.0" "@standardnotes/settings@1.2.0": version "1.2.0" @@ -2060,15 +2060,15 @@ "@standardnotes/sncrypto-common" "^1.5.2" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.11.2": - version "2.11.2" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.11.2.tgz#2ae172950316ee3aa7ad95d4fec4dc85d7ac1269" - integrity sha512-49NPflKrBcAkVcs7nGf7D/oatcPRPZz7+TKyi4s9Xsxf69EmHIQhVu9u+sCUCaUTq2j1sjQt9tyCacjche7p4w== +"@standardnotes/snjs@2.12.1": + version "2.12.1" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.12.1.tgz#4c2cac0e9f87d55d7d24a30f0fbe90d878869c7b" + integrity sha512-4ehV9Nviko0yLs8NUToSXQvOTcQs4XlU2e1sfSkvZZ+Z+iQZo/gR/Xc9gXhr/PvJpahecJIAnbcLpPlJ/DU9MQ== dependencies: "@standardnotes/auth" "3.1.1" "@standardnotes/common" "1.1.0" "@standardnotes/domain-events" "2.0.0" - "@standardnotes/features" "1.4.0" + "@standardnotes/features" "1.6.0" "@standardnotes/settings" "1.2.0" "@standardnotes/sncrypto-common" "1.5.2" From 84ba497857db1afbbbefa556fc44cf28d69027d9 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 6 Sep 2021 03:57:33 -0300 Subject: [PATCH 03/71] feat: subscription info in preferences --- .../preferences/panes/AccountPreferences.tsx | 3 +- .../panes/account/Subscription.tsx | 187 ++++++++++++++++++ .../preferences/panes/account/index.ts | 1 + app/assets/stylesheets/_sn.scss | 4 + 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/preferences/panes/account/Subscription.tsx diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 6b034d7c6..8dd126cba 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -1,4 +1,4 @@ -import { Sync } from '@/preferences/panes/account'; +import { Sync, Subscription } from '@/preferences/panes/account'; import { PreferencesPane } from '@/preferences/components'; import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; @@ -7,6 +7,7 @@ export const AccountPreferences = observer(({application}: {application: WebAppl return ( + ); }); diff --git a/app/assets/javascripts/preferences/panes/account/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/Subscription.tsx new file mode 100644 index 000000000..0a3c79bd4 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/Subscription.tsx @@ -0,0 +1,187 @@ +import { + PreferencesGroup, + PreferencesSegment, + Text, + Title, +} from '@/preferences/components'; +import { Button } from '@/components/Button'; +import { observer } from '@node_modules/mobx-react-lite'; +import { WebApplication } from '@/ui_models/application'; +import { useEffect, useState } from 'preact/hooks'; +import { GetSubscriptionResponse } from '@standardnotes/snjs/dist/@types/services/api/responses'; + +type Props = { + application: WebApplication; +}; + +enum PlanName { + CorePlan = 'CORE_PLAN', + PlusPlan = 'PLUS_PLAN', + ProPlan = 'PRO_PLAN', +} + +type Subscription = { + cancelled: boolean; + planName: PlanName; + endsAt: number; +}; + +type SubscriptionInformationProps = { + subscription?: Subscription; +}; + +type ValidSubscriptionProps = { + subscription: Subscription; +}; + +const mapPlanNameToString = (planName: PlanName) => { + switch (planName) { + case 'CORE_PLAN': + return 'Core'; + case 'PLUS_PLAN': + return 'Plus'; + case 'PRO_PLAN': + return 'Pro'; + default: + return ''; + } +}; + +const NoSubscription = () => ( + <> + You don't have a Standard Notes subscription yet. +
+
+ +); + +const ActiveSubscription = ({ subscription }: ValidSubscriptionProps) => ( + <> + + Your{' '} + + Standard Notes {mapPlanNameToString(subscription.planName)} + {' '} + subscription will be{' '} + + renewed on {new Date(subscription.endsAt).toLocaleString()} + + . + +
+
+ +); + +const CancelledSubscription = ({ subscription }: ValidSubscriptionProps) => ( + <> + + Your{' '} + + Standard Notes {mapPlanNameToString(subscription.planName)} + {' '} + subscription has been{' '} + + canceled but will remain valid until{' '} + {new Date(subscription.endsAt).toLocaleString()} + + . You may resubscribe below if you wish. + +
+
+ +); + +const SubscriptionInformation = ({ + subscription, +}: SubscriptionInformationProps) => { + const now = new Date().getTime(); + if (subscription && subscription.endsAt > now) { + return subscription.cancelled ? ( + + ) : ( + + ); + } + return ; +}; + +const Subscription = observer(({ application }: Props) => { + const [subscription, setSubscription] = + useState(undefined); + + useEffect(() => { + const getSubscription = async () => { + const result = await application.getUserSubscription(); + if (!result.error && result.data) { + const data = (result as GetSubscriptionResponse).data; + const subscription = data!.subscription; + setSubscription(subscription); + } + }; + getSubscription(); + }, [application]); + + return ( + + +
+
+ Subscription + +
+
+
+
+ ); +}); + +export default Subscription; diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts index 2225b5881..d30c31420 100644 --- a/app/assets/javascripts/preferences/panes/account/index.ts +++ b/app/assets/javascripts/preferences/panes/account/index.ts @@ -1 +1,2 @@ export { default as Sync } from './Sync'; +export { default as Subscription } from './Subscription'; diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 32baa1cb4..a8a4bed8b 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -149,3 +149,7 @@ .sn-title { @extend .font-bold; } + +.mr-3 { + margin-right: 0.75rem; +} From 377de393febd987f25e868c292c39d796df4320e Mon Sep 17 00:00:00 2001 From: Vardan Hakobyan Date: Mon, 6 Sep 2021 11:09:16 +0400 Subject: [PATCH 04/71] chore: version bump to 3.8.22 (#630) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fddaee76..5f7acfeaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.8.21", + "version": "3.8.22", "license": "AGPL-3.0-or-later", "repository": { "type": "git", From c55946cb54d87459b6d91c47fd93f987e5bc4a53 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 6 Sep 2021 10:49:17 -0300 Subject: [PATCH 05/71] fix: fix challenge modal submission debounce --- .../javascripts/views/challenge_modal/challenge_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx b/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx index 6530d7d71..105ca989c 100644 --- a/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx +++ b/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx @@ -178,7 +178,7 @@ class ChallengeModalCtrl extends PureViewCtrl { if (!this.validate()) { return; } - if (this.submitting) { + if (this.submitting || this.state.processing) { return; } this.submitting = true; From 1294b941175e72a7298c246d58966289cba2086c Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Mon, 6 Sep 2021 17:15:34 +0200 Subject: [PATCH 06/71] feat: integrate two factor authentication (#626) * feat: integrate SNJS MFA with web * fix: create rudimentary typings file for qrcode.react * chore: lint fixes * fix: address PR feedback * fix: address PR feedback * fix: address PR feedback 2 * fix: replace spread props on TwoFactorAuthWrapper component * chore: change null check to undefined check --- .../javascripts/@types/qrcode.react.d.ts | 1 + .../components/AccountMenu/PasscodeLock.tsx | 1 - .../javascripts/components/DecoratedInput.tsx | 5 + app/assets/javascripts/components/NoteTag.tsx | 2 +- .../components/NotesContextMenu.tsx | 2 +- app/assets/javascripts/messages.ts | 2 +- .../preferences/PreferencesView.tsx | 58 +++--- .../preferences/PreferencesViewWrapper.tsx | 24 +++ .../preferences/components/Content.tsx | 7 +- .../{Pane.tsx => PreferencesPane.tsx} | 16 +- .../preferences/components/index.ts | 2 +- app/assets/javascripts/preferences/index.ts | 6 +- .../preferences/panes/Security.tsx | 7 +- .../panes/two-factor-auth/MfaProps.ts | 17 ++ .../panes/two-factor-auth/SaveSecretKey.tsx | 2 +- .../panes/two-factor-auth/ScanQRCode.tsx | 7 +- .../two-factor-auth/TwoFactorActivation.ts | 121 +++++++++++++ .../TwoFactorActivationView.tsx | 10 +- .../panes/two-factor-auth/TwoFactorAuth.ts | 130 ++++++++++++++ .../two-factor-auth/TwoFactorAuthView.tsx | 26 ++- .../panes/two-factor-auth/TwoFactorDialog.tsx | 4 +- .../two-factor-auth/TwoFactorEnabledView.tsx | 45 ----- .../panes/two-factor-auth/Verification.tsx | 24 +-- .../panes/two-factor-auth/index.tsx | 11 +- .../panes/two-factor-auth/model.ts | 168 ------------------ app/assets/javascripts/typings/pug.d.ts | 2 +- app/assets/javascripts/typings/stylekit.d.ts | 2 +- .../ui_models/app_state/notes_state.ts | 2 +- .../javascripts/ui_models/component_group.ts | 2 - app/assets/javascripts/views/index.ts | 2 +- .../javascripts/views/tags/tags_view.ts | 1 - package.json | 3 +- yarn.lock | 16 +- 33 files changed, 411 insertions(+), 317 deletions(-) create mode 100644 app/assets/javascripts/@types/qrcode.react.d.ts create mode 100644 app/assets/javascripts/preferences/PreferencesViewWrapper.tsx rename app/assets/javascripts/preferences/components/{Pane.tsx => PreferencesPane.tsx} (77%) create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts delete mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorEnabledView.tsx delete mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/model.ts diff --git a/app/assets/javascripts/@types/qrcode.react.d.ts b/app/assets/javascripts/@types/qrcode.react.d.ts new file mode 100644 index 000000000..f997f01d9 --- /dev/null +++ b/app/assets/javascripts/@types/qrcode.react.d.ts @@ -0,0 +1 @@ +declare module 'qrcode.react'; diff --git a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx index 69d675e1a..95df944c4 100644 --- a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx +++ b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx @@ -40,7 +40,6 @@ const PasscodeLock = observer(({ const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); - const handleAddPassCode = () => { setShowPasscodeForm(true); setIsPasscodeFocused(true); diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index f649ab38f..97cccdad2 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -6,6 +6,7 @@ interface Props { left?: ComponentChild[]; right?: ComponentChild[]; text?: string; + onChange?: (text: string) => void; } /** @@ -17,6 +18,7 @@ export const DecoratedInput: FunctionalComponent = ({ left, right, text, + onChange, }) => { const base = 'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4'; @@ -34,6 +36,9 @@ export const DecoratedInput: FunctionalComponent = ({ className="w-full no-border color-black focus:shadow-none" disabled={disabled} value={text} + onChange={(e) => + onChange && onChange((e.target as HTMLInputElement).value) + } /> {right} diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index ba80ebe48..9a31fd1e6 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -54,7 +54,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => { const getTabIndex = () => { if (focusedTagUuid) { return focusedTagUuid === tag.uuid ? 0 : -1; - } + } if (autocompleteInputFocused) { return -1; } diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index 292e7265b..252516a98 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => { ); useCloseOnClickOutside( - contextMenuRef, + contextMenuRef, (open: boolean) => appState.notes.setContextMenuOpen(open) ); diff --git a/app/assets/javascripts/messages.ts b/app/assets/javascripts/messages.ts index 6f48d0777..2dcecff72 100644 --- a/app/assets/javascripts/messages.ts +++ b/app/assets/javascripts/messages.ts @@ -1,4 +1,4 @@ export enum RootScopeMessages { ReloadExtendedData = 'reload-ext-data', NewUpdateAvailable = 'new-update-available' -} \ No newline at end of file +} diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 239955ce5..b285c67fb 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -6,20 +6,25 @@ import { observer } from 'mobx-react-lite'; import { PreferencesMenu } from './preferences-menu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; +import { MfaProps } from './panes/two-factor-auth/MfaProps'; -const PaneSelector: FunctionComponent<{ - prefs: PreferencesMenu; +interface PreferencesProps extends MfaProps { application: WebApplication; -}> = observer(({ prefs: menu, application }) => { - switch (menu.selectedPaneId) { + closePreferences: () => void; +} + +const PaneSelector: FunctionComponent< + PreferencesProps & { menu: PreferencesMenu } +> = observer((props) => { + switch (props.menu.selectedPaneId) { case 'general': return null; case 'account': - return ; + return ; case 'appearance': return null; case 'security': - return ; + return ; case 'listed': return null; case 'shortcuts': @@ -33,23 +38,18 @@ const PaneSelector: FunctionComponent<{ } }); -const PreferencesCanvas: FunctionComponent<{ - preferences: PreferencesMenu; - application: WebApplication; -}> = observer(({ preferences: prefs, application }) => ( +const PreferencesCanvas: FunctionComponent< + PreferencesProps & { menu: PreferencesMenu } +> = observer((props) => (
- - + +
)); -const PreferencesView: FunctionComponent<{ - close: () => void; - application: WebApplication; -}> = observer( - ({ close, application }) => { - const prefs = new PreferencesMenu(); - +export const PreferencesView: FunctionComponent = observer( + (props) => { + const menu = new PreferencesMenu(); return (
@@ -58,30 +58,14 @@ const PreferencesView: FunctionComponent<{ Your preferences for Standard Notes { - close(); + props.closePreferences(); }} type="normal" icon="close" /> - +
); } ); - -export interface PreferencesWrapperProps { - appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; - application: WebApplication; -} - -export const PreferencesViewWrapper: FunctionComponent = - observer(({ appState, application }) => { - if (!appState.preferences.isOpen) return null; - return ( - appState.preferences.closePreferences()} - /> - ); - }); diff --git a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx new file mode 100644 index 000000000..ddf131524 --- /dev/null +++ b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx @@ -0,0 +1,24 @@ +import { FunctionComponent } from 'preact'; +import { observer } from 'mobx-react-lite'; +import { WebApplication } from '@/ui_models/application'; +import { PreferencesView } from './PreferencesView'; + +export interface PreferencesViewWrapperProps { + appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; + application: WebApplication; +} + +export const PreferencesViewWrapper: FunctionComponent = + observer(({ appState, application }) => { + if (!appState.preferences.isOpen) { + return null; + } + + return ( + appState.preferences.closePreferences()} + application={application} + mfaGateway={application} + /> + ); + }); diff --git a/app/assets/javascripts/preferences/components/Content.tsx b/app/assets/javascripts/preferences/components/Content.tsx index 5f3da04f3..84cdae4d1 100644 --- a/app/assets/javascripts/preferences/components/Content.tsx +++ b/app/assets/javascripts/preferences/components/Content.tsx @@ -8,9 +8,10 @@ export const Subtitle: FunctionComponent = ({ children }) => (

{children}

); -export const Text: FunctionComponent = ({ children }) => ( -

{children}

-); +export const Text: FunctionComponent<{ className?: string }> = ({ + children, + className = '', +}) =>

{children}

; const buttonClasses = `block bg-default color-text rounded border-solid \ border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \ diff --git a/app/assets/javascripts/preferences/components/Pane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx similarity index 77% rename from app/assets/javascripts/preferences/components/Pane.tsx rename to app/assets/javascripts/preferences/components/PreferencesPane.tsx index ab2638f03..ed8ec3295 100644 --- a/app/assets/javascripts/preferences/components/Pane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -14,14 +14,16 @@ export const PreferencesSegment: FunctionComponent = ({ children }) => ( export const PreferencesGroup: FunctionComponent = ({ children }) => (
- {!Array.isArray(children) + {Array.isArray(children) ? children - : children.map((c, i, arr) => ( - <> - {c} - - - ))} + .filter((child) => child != undefined && child !== '') + .map((child, i, arr) => ( + <> + {child} + + + )) + : children}
); diff --git a/app/assets/javascripts/preferences/components/index.ts b/app/assets/javascripts/preferences/components/index.ts index ab868c871..0c4046f06 100644 --- a/app/assets/javascripts/preferences/components/index.ts +++ b/app/assets/javascripts/preferences/components/index.ts @@ -1,3 +1,3 @@ export * from './Content'; export * from './MenuItem'; -export * from './Pane'; +export * from './PreferencesPane'; diff --git a/app/assets/javascripts/preferences/index.ts b/app/assets/javascripts/preferences/index.ts index 8849e4b8c..ba89cd2b5 100644 --- a/app/assets/javascripts/preferences/index.ts +++ b/app/assets/javascripts/preferences/index.ts @@ -1,9 +1,9 @@ import { toDirective } from '../components/utils'; import { PreferencesViewWrapper, - PreferencesWrapperProps, -} from './PreferencesView'; + PreferencesViewWrapperProps, +} from './PreferencesViewWrapper'; -export const PreferencesDirective = toDirective( +export const PreferencesDirective = toDirective( PreferencesViewWrapper ); diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index c344e0c85..f5b0d1ecc 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -1,9 +1,12 @@ import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; import { TwoFactorAuthWrapper } from './two-factor-auth'; +import { MfaProps } from './two-factor-auth/MfaProps'; -export const Security: FunctionComponent = () => ( +interface SecurityProps extends MfaProps {} + +export const Security: FunctionComponent = (props) => ( - + ); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts new file mode 100644 index 000000000..79e89298b --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts @@ -0,0 +1,17 @@ +export interface MfaGateway { + getUser(): { uuid: string; email: string } | undefined; + + isMfaActivated(): Promise; + + generateMfaSecret(): Promise; + + getOtpToken(secret: string): Promise; + + enableMfa(secret: string, otpToken: string): Promise; + + disableMfa(): Promise; +} + +export interface MfaProps { + mfaGateway: MfaGateway; +} diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx index 34e060b73..7244240ea 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -4,7 +4,7 @@ import { IconButton } from '@/components/IconButton'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { downloadSecretKey } from './download-secret-key'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorDialog, TwoFactorDialogLabel, diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx index 82c73769e..9d12d0de8 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx @@ -1,9 +1,12 @@ import { FunctionComponent } from 'preact'; import { observer } from 'mobx-react-lite'; + +import QRCode from 'qrcode.react'; + import { DecoratedInput } from '../../../components/DecoratedInput'; import { IconButton } from '../../../components/IconButton'; import { Button } from '@/components/Button'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorDialog, TwoFactorDialogLabel, @@ -35,7 +38,7 @@ export const ScanQRCode: FunctionComponent<{
- QR code +
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts new file mode 100644 index 000000000..1f756162a --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts @@ -0,0 +1,121 @@ +import { action, makeAutoObservable, observable, untracked } from 'mobx'; +import { MfaGateway } from './MfaProps'; + +type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification'; +type VerificationStatus = 'none' | 'invalid' | 'valid'; + +export class TwoFactorActivation { + public readonly type = 'two-factor-activation' as const; + + private _activationStep: ActivationStep; + + private _2FAVerification: VerificationStatus = 'none'; + + private inputSecretKey = ''; + private inputOtpToken = ''; + + constructor( + private mfaGateway: MfaGateway, + private readonly _secretKey: string, + private _cancelActivation: () => void, + private _enabled2FA: () => void + ) { + this._activationStep = 'scan-qr-code'; + + makeAutoObservable< + TwoFactorActivation, + | '_secretKey' + | '_authCode' + | '_step' + | '_enable2FAVerification' + | 'refreshOtp' + | 'inputOtpToken' + | 'inputSecretKey' + >( + this, + { + _secretKey: observable, + _authCode: observable, + _step: observable, + _enable2FAVerification: observable, + refreshOtp: action, + inputOtpToken: observable, + inputSecretKey: observable, + }, + { autoBind: true } + ); + } + + get secretKey(): string { + return this._secretKey; + } + + get activationStep(): ActivationStep { + return this._activationStep; + } + + get verificationStatus(): VerificationStatus { + return this._2FAVerification; + } + + get qrCode(): string { + const email = this.mfaGateway.getUser()!.email; + return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`; + } + + cancelActivation(): void { + this._cancelActivation(); + } + + openScanQRCode(): void { + const preconditions: ActivationStep[] = ['save-secret-key']; + if (preconditions.includes(this._activationStep)) { + this._activationStep = 'scan-qr-code'; + } + } + + openSaveSecretKey(): void { + const preconditions: ActivationStep[] = ['scan-qr-code', 'verification']; + if (preconditions.includes(this._activationStep)) { + this._activationStep = 'save-secret-key'; + } + } + + openVerification(): void { + this.inputOtpToken = ''; + this.inputSecretKey = ''; + const preconditions: ActivationStep[] = ['save-secret-key']; + if (preconditions.includes(this._activationStep)) { + this._activationStep = 'verification'; + this._2FAVerification = 'none'; + } + } + + setInputSecretKey(secretKey: string): void { + this.inputSecretKey = secretKey; + } + + setInputOtpToken(otpToken: string): void { + this.inputOtpToken = otpToken; + } + + enable2FA(): void { + if (this.inputSecretKey === this._secretKey) { + this.mfaGateway + .enableMfa(this.inputSecretKey, this.inputOtpToken) + .then( + action(() => { + this._2FAVerification = 'valid'; + this._enabled2FA(); + }) + ) + .catch( + action(() => { + this._2FAVerification = 'invalid'; + }) + ); + } else { + this._2FAVerification = 'invalid'; + } + } +} diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx index ef06f00ae..72d335f57 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx @@ -1,6 +1,6 @@ import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { SaveSecretKey } from './SaveSecretKey'; import { ScanQRCode } from './ScanQRCode'; import { Verification } from './Verification'; @@ -9,10 +9,12 @@ export const TwoFactorActivationView: FunctionComponent<{ activation: TwoFactorActivation; }> = observer(({ activation: act }) => ( <> - {act.step === 'scan-qr-code' && } + {act.activationStep === 'scan-qr-code' && } - {act.step === 'save-secret-key' && } + {act.activationStep === 'save-secret-key' && ( + + )} - {act.step === 'verification' && } + {act.activationStep === 'verification' && } )); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts new file mode 100644 index 000000000..44aeb360f --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts @@ -0,0 +1,130 @@ +import { action, makeAutoObservable, observable } from 'mobx'; +import { MfaGateway } from './MfaProps'; +import { TwoFactorActivation } from './TwoFactorActivation'; + +type TwoFactorStatus = + | 'two-factor-enabled' + | TwoFactorActivation + | 'two-factor-disabled'; + +export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' => + s === 'two-factor-disabled'; + +export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation => + (s as any).type === 'two-factor-activation'; + +export const is2FAEnabled = (s: TwoFactorStatus): s is 'two-factor-enabled' => + s === 'two-factor-enabled'; + +export class TwoFactorAuth { + private _status: TwoFactorStatus | 'fetching' = 'fetching'; + private _errorMessage: string | null; + + constructor(private readonly mfaGateway: MfaGateway) { + this._errorMessage = null; + + makeAutoObservable< + TwoFactorAuth, + '_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation' + >(this, { + _status: observable, + _errorMessage: observable, + deactivateMfa: action, + startActivation: action, + }); + } + + private startActivation(): void { + const setDisabled = action(() => (this._status = 'two-factor-disabled')); + const setEnabled = action(() => (this._status = 'two-factor-enabled')); + this.mfaGateway + .generateMfaSecret() + .then( + action((secret) => { + this._status = new TwoFactorActivation( + this.mfaGateway, + secret, + setDisabled, + setEnabled + ); + }) + ) + .catch( + action((e) => { + this.setError(e.message); + }) + ); + } + + private deactivate2FA(): void { + this.mfaGateway + .disableMfa() + .then( + action(() => { + this._status = 'two-factor-disabled'; + }) + ) + .catch( + action((e) => { + this.setError(e.message); + }) + ); + } + + private get isLoggedIn(): boolean { + return this.mfaGateway.getUser() != undefined; + } + + fetchStatus(): void { + this._status = 'fetching'; + + if (!this.isLoggedIn) { + this.setError('To enable 2FA, sign in or register for an account.'); + return; + } + + this.mfaGateway + .isMfaActivated() + .then( + action((active) => { + this._status = active ? 'two-factor-enabled' : 'two-factor-disabled'; + this.setError(null); + }) + ) + .catch( + action((e) => { + this._status = 'two-factor-disabled'; + this.setError(e.message); + }) + ); + } + + setError(errorMessage: string | null): void { + this._errorMessage = errorMessage; + } + + toggle2FA(): void { + if (!this.isLoggedIn) { + return; + } + + if (this._status === 'two-factor-disabled') { + return this.startActivation(); + } + + if (this._status === 'two-factor-enabled') { + return this.deactivate2FA(); + } + } + + get errorMessage(): string | null { + return this._errorMessage; + } + + get status(): TwoFactorStatus { + if (this._status === 'fetching') { + return 'two-factor-disabled'; + } + return this._status; + } +} diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index 4df5b0b39..d23c7ed77 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -12,9 +12,8 @@ import { is2FADisabled, is2FAEnabled, TwoFactorAuth, -} from './model'; +} from './TwoFactorAuth'; import { TwoFactorDisabledView } from './TwoFactorDisabledView'; -import { TwoFactorEnabledView } from './TwoFactorEnabledView'; import { TwoFactorActivationView } from './TwoFactorActivationView'; export const TwoFactorAuthView: FunctionComponent<{ @@ -28,6 +27,9 @@ export const TwoFactorAuthView: FunctionComponent<{ An extra layer of security when logging in to your account. + {auth.errorMessage != null && ( + {auth.errorMessage} + )}
- - {is2FAEnabled(auth.status) && ( - - )} - {is2FAActivation(auth.status) && ( - - )} + {is2FAActivation(auth.status) ? ( + + ) : null} - {!is2FAEnabled(auth.status) && } - + {!is2FAEnabled(auth.status) ? ( + + + + ) : null} )); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index f9ffe7757..8c0b45987 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -19,8 +19,8 @@ export const TwoFactorDialog: FunctionComponent<{ return ( {/* sn-component is focusable by default, but doesn't stretch to child width - resulting in a badly focused dialog. Utility classes are not available - at the sn-component level, only below it. tabIndex -1 disables focus + resulting in a badly focused dialog. Utility classes are not available + at the sn-component level, only below it. tabIndex -1 disables focus and enables it on the child component */}
= ({ secretKey, authCode }) => { - const download = ( - { - downloadSecretKey(secretKey); - }} - /> - ); - const copy = ( - { - navigator?.clipboard?.writeText(secretKey); - }} - /> - ); - const progress = ; - return ( -
-
- Secret Key - -
-
- Authentication Code - -
-
- ); -}; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx index 73795da72..f411a35b2 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx @@ -2,7 +2,7 @@ import { Button } from '@/components/Button'; import { DecoratedInput } from '@/components/DecoratedInput'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorDialog, TwoFactorDialogLabel, @@ -17,11 +17,7 @@ export const Verification: FunctionComponent<{ act.verificationStatus === 'invalid' ? 'border-dark-red' : ''; return ( - { - act.cancelActivation(); - }} - > + Step 3 of 3 - Verification @@ -30,20 +26,26 @@ export const Verification: FunctionComponent<{
・Enter your secret key:
- +
・Verify the authentication code generated by your authenticator app:
- +
{act.verificationStatus === 'invalid' && ( -
+
Incorrect credentials, please try again.
)} @@ -51,13 +53,13 @@ export const Verification: FunctionComponent<{ className="min-w-20" type="normal" label="Back" - onClick={() => act.openSaveSecretKey()} + onClick={act.openSaveSecretKey} />
- -); - -const ActiveSubscription = ({ - subscription, - availableSubscriptions, -}: ValidSubscriptionProps) => ( - <> - - Your{' '} - - Standard Notes {availableSubscriptions[subscription.planName]} - {' '} - subscription will be{' '} - - renewed on {new Date(subscription.endsAt).toLocaleString()} - - . - -
-
- -); - -const CancelledSubscription = ({ - subscription, - availableSubscriptions, -}: ValidSubscriptionProps) => ( - <> - - Your{' '} - - Standard Notes {availableSubscriptions[subscription.planName]} - {' '} - subscription has been{' '} - - canceled but will remain valid until{' '} - {new Date(subscription.endsAt).toLocaleString()} - - . You may resubscribe below if you wish. - -
-
- -); - -const SubscriptionInformation = ({ - subscription, - availableSubscriptions, -}: SubscriptionInformationProps) => { - const now = new Date().getTime(); - if (subscription && subscription.endsAt > now) { - return subscription.cancelled ? ( - - ) : ( - - ); - } - return ; -}; - -const Subscription = observer(({ application }: Props) => { - const [subscription, setSubscription] = - useState(undefined); - const [availableSubscriptions, setAvailableSubscriptions] = - useState({}); - const [error, setError] = useState(false); - - useEffect(() => { - const getSubscriptions = async () => { - try { - const result = await application.getSubscriptions(); - if (result.data) { - const data = (result as GetSubscriptionsResponse).data; - setAvailableSubscriptions(data!); - } else { - setError(true); - } - } catch (e) { - setError(true); - } - }; - const getSubscription = async () => { - try { - const result = await application.getUserSubscription(); - if (!result.error && result.data) { - const data = (result as GetSubscriptionResponse).data; - const subscription = data!.subscription; - setSubscription(subscription); - } else { - setError(true); - } - } catch (e) { - setError(true); - } - }; - getSubscriptions(); - getSubscription(); - }, [application]); - - return ( - - -
-
- Subscription - {error ? ( - 'No subscription information available.' - ) : ( - - )} -
-
-
-
- ); -}); - -export default Subscription; diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts index d30c31420..063947150 100644 --- a/app/assets/javascripts/preferences/panes/account/index.ts +++ b/app/assets/javascripts/preferences/panes/account/index.ts @@ -1,2 +1,2 @@ export { default as Sync } from './Sync'; -export { default as Subscription } from './Subscription'; +export { SubscriptionWrapper } from './subscription/SubscriptionWrapper'; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx new file mode 100644 index 000000000..eb5d984ad --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx @@ -0,0 +1,49 @@ +import { observer } from 'mobx-react-lite'; +import { SubscriptionState } from './subscription_state'; +import { Text } from '@/preferences/components'; +import { Button } from '@/components/Button'; + +type Props = { + subscriptionState: SubscriptionState; +}; + +export const ActiveSubscription = observer( + ({ subscriptionState }: Props) => { + const { userSubscription, userSubscriptionName } = subscriptionState; + return ( + <> + + Your{' '} + + Standard Notes{userSubscriptionName ? " " : ""}{userSubscriptionName} + {' '} + subscription will be{' '} + + renewed on {new Date(userSubscription!.endsAt).toLocaleString()} + + . + +
+
+ + ); + } +); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx new file mode 100644 index 000000000..84cf6639c --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx @@ -0,0 +1,50 @@ +import { observer } from 'mobx-react-lite'; +import { SubscriptionState } from './subscription_state'; +import { Text } from '@/preferences/components'; +import { Button } from '@/components/Button'; + +type Props = { + subscriptionState: SubscriptionState; +}; + +export const CancelledSubscription = observer( + ({ subscriptionState }: Props) => { + const { userSubscription, userSubscriptionName } = subscriptionState; + return ( + <> + + Your{' '} + + Standard NotesStandard Notes{userSubscriptionName ? " " : ""}{userSubscriptionName} + {' '} + subscription has been{' '} + + canceled but will remain valid until{' '} + {new Date(userSubscription!.endsAt).toLocaleString()} + + . You may resubscribe below if you wish. + +
+
+ + ); + } +); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx new file mode 100644 index 000000000..5042ea992 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx @@ -0,0 +1,23 @@ +import { FunctionalComponent } from "preact"; +import { Text } from '@/preferences/components'; +import { Button } from '@/components/Button'; + +export const NoSubscription: FunctionalComponent = () => ( + <> + You don't have a Standard Notes subscription yet. +
+
+ +); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx new file mode 100644 index 000000000..3ced9309e --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -0,0 +1,92 @@ +import { + PreferencesGroup, + PreferencesSegment, + Title, +} from '@/preferences/components'; +import { observer } from '@node_modules/mobx-react-lite'; +import { WebApplication } from '@/ui_models/application'; +import { useEffect, useState } from 'preact/hooks'; +import { + GetSubscriptionResponse, + GetSubscriptionsResponse, +} from '@standardnotes/snjs/dist/@types/services/api/responses'; +import { SubscriptionState } from './subscription_state'; +import { CancelledSubscription } from './CancelledSubscription'; +import { ActiveSubscription } from './ActiveSubscription'; +import { NoSubscription } from './NoSubscription'; + +type Props = { + application: WebApplication; + subscriptionState: SubscriptionState; +}; + +type SubscriptionInformationProps = { + subscriptionState: SubscriptionState; +}; + +const SubscriptionInformation = ({ + subscriptionState, +}: SubscriptionInformationProps) => { + const now = new Date().getTime(); + const { userSubscription } = subscriptionState; + + if (userSubscription && userSubscription.endsAt > now) { + return userSubscription.cancelled ? ( + + ) : ( + + ); + } + return ; +}; + +export const Subscription = observer(({ application, subscriptionState }: Props) => { + const [error, setError] = useState(false); + + useEffect(() => { + const getSubscriptions = async () => { + try { + const result = await application.getSubscriptions(); + if (result.data) { + const data = (result as GetSubscriptionsResponse).data; + subscriptionState.setAvailableSubscriptions(data!); + } + } catch (e) { + // Error in this call will only prevent the plan name from showing + } + }; + const getSubscription = async () => { + try { + const result = await application.getUserSubscription(); + if (!result.error && result.data) { + const data = (result as GetSubscriptionResponse).data; + const subscription = data!.subscription; + subscriptionState.setUserSubscription(subscription); + } else { + setError(true); + } + } catch (e) { + setError(true); + } + }; + getSubscriptions(); + getSubscription(); + }, [application, subscriptionState]); + + return ( + + +
+
+ Subscription + {error ? ( + 'No subscription information available.' + ) : ( + + )} +
+
+
+
+ ); +}); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx new file mode 100644 index 000000000..9a1c4033f --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx @@ -0,0 +1,20 @@ +import { WebApplication } from '@/ui_models/application'; +import { FunctionalComponent } from 'preact'; +import { Subscription } from './Subscription'; +import { SubscriptionState } from './subscription_state'; + +type Props = { + application: WebApplication; +}; + +export const SubscriptionWrapper: FunctionalComponent = ({ + application, +}) => { + const subscriptionState = new SubscriptionState(); + return ( + + ); +}; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx new file mode 100644 index 000000000..aaa2bab72 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx @@ -0,0 +1,51 @@ +import { action, computed, makeObservable, observable } from 'mobx'; + +type Subscription = { + planName: string; + cancelled: boolean; + endsAt: number; +}; + +type AvailableSubscriptions = { + [key: string]: { + name: string; + }; +}; + +export class SubscriptionState { + userSubscription: Subscription | undefined; + availableSubscriptions: AvailableSubscriptions | undefined; + + constructor() { + makeObservable(this, { + userSubscription: observable, + availableSubscriptions: observable, + + userSubscriptionName: computed, + + setUserSubscription: action, + setAvailableSubscriptions: action, + }); + } + + get userSubscriptionName(): string { + if ( + this.availableSubscriptions && + this.userSubscription && + this.availableSubscriptions[this.userSubscription.planName] + ) { + return this.availableSubscriptions[this.userSubscription.planName].name; + } + return ''; + } + + public setUserSubscription(subscription: Subscription): void { + this.userSubscription = subscription; + } + + public setAvailableSubscriptions( + subscriptions: AvailableSubscriptions + ): void { + this.availableSubscriptions = subscriptions; + } +} From 99ef854ae8b36a993583b7e29ffa6ca66a57d3c6 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 7 Sep 2021 12:06:25 -0300 Subject: [PATCH 09/71] fix: set mobx store as local state --- .../panes/account/subscription/SubscriptionWrapper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx index 9a1c4033f..9a19112e6 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx @@ -1,5 +1,6 @@ import { WebApplication } from '@/ui_models/application'; import { FunctionalComponent } from 'preact'; +import { useState } from 'preact/hooks'; import { Subscription } from './Subscription'; import { SubscriptionState } from './subscription_state'; @@ -10,7 +11,7 @@ type Props = { export const SubscriptionWrapper: FunctionalComponent = ({ application, }) => { - const subscriptionState = new SubscriptionState(); + const [subscriptionState] = useState(() => new SubscriptionState()); return ( Date: Tue, 7 Sep 2021 13:14:12 -0300 Subject: [PATCH 10/71] fix: fix state and add loading state --- .../subscription/CancelledSubscription.tsx | 2 +- .../account/subscription/Subscription.tsx | 78 +++++++++++-------- .../subscription/subscription_state.tsx | 6 +- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx index 84cf6639c..d3742e65c 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx @@ -15,7 +15,7 @@ export const CancelledSubscription = observer( Your{' '} - Standard NotesStandard Notes{userSubscriptionName ? " " : ""}{userSubscriptionName} + Standard Notes{userSubscriptionName ? " " : ""}{userSubscriptionName} {' '} subscription has been{' '} diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx index 3ced9309e..df5b3f9fc 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -5,7 +5,7 @@ import { } from '@/preferences/components'; import { observer } from '@node_modules/mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; -import { useEffect, useState } from 'preact/hooks'; +import { useCallback, useEffect, useState } from 'preact/hooks'; import { GetSubscriptionResponse, GetSubscriptionsResponse, @@ -14,6 +14,8 @@ import { SubscriptionState } from './subscription_state'; import { CancelledSubscription } from './CancelledSubscription'; import { ActiveSubscription } from './ActiveSubscription'; import { NoSubscription } from './NoSubscription'; +import { Text } from '@/preferences/components'; +import { FunctionalComponent } from 'preact'; type Props = { application: WebApplication; @@ -24,7 +26,7 @@ type SubscriptionInformationProps = { subscriptionState: SubscriptionState; }; -const SubscriptionInformation = ({ +const SubscriptionInformation = observer(({ subscriptionState, }: SubscriptionInformationProps) => { const now = new Date().getTime(); @@ -38,41 +40,53 @@ const SubscriptionInformation = ({ ); } return ; -}; +}); -export const Subscription = observer(({ application, subscriptionState }: Props) => { +export const Subscription: FunctionalComponent = ({ application, subscriptionState }) => { + const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - useEffect(() => { - const getSubscriptions = async () => { - try { - const result = await application.getSubscriptions(); - if (result.data) { - const data = (result as GetSubscriptionsResponse).data; - subscriptionState.setAvailableSubscriptions(data!); - } - } catch (e) { - // Error in this call will only prevent the plan name from showing + const getSubscriptions = useCallback(async () => { + try { + const result = await application.getSubscriptions(); + if (result.data) { + const data = (result as GetSubscriptionsResponse).data; + subscriptionState.setAvailableSubscriptions(data!); } - }; - const getSubscription = async () => { - try { - const result = await application.getUserSubscription(); - if (!result.error && result.data) { - const data = (result as GetSubscriptionResponse).data; - const subscription = data!.subscription; - subscriptionState.setUserSubscription(subscription); - } else { - setError(true); - } - } catch (e) { + } catch (e) { + // Error in this call will only prevent the plan name from showing + } + }, [application, subscriptionState]); + + const getSubscription = useCallback(async () => { + try { + const result = await application.getUserSubscription(); + if (!result.error && result.data) { + const data = (result as GetSubscriptionResponse).data; + const subscription = data!.subscription; + subscriptionState.setUserSubscription(subscription); + } else { setError(true); } - }; - getSubscriptions(); - getSubscription(); + } catch (e) { + setError(true); + } }, [application, subscriptionState]); + const getSubscriptionInfo = useCallback(async () => { + setLoading(true); + try { + await getSubscription(); + await getSubscriptions(); + } finally { + setLoading(false); + } + }, [getSubscription, getSubscriptions]); + + useEffect(() => { + getSubscriptionInfo(); + }, [getSubscriptionInfo]); + return ( @@ -80,7 +94,9 @@ export const Subscription = observer(({ application, subscriptionState }: Props)
Subscription {error ? ( - 'No subscription information available.' + No subscription information available. + ) : loading ? ( + Loading subscription information... ) : ( )} @@ -89,4 +105,4 @@ export const Subscription = observer(({ application, subscriptionState }: Props) ); -}); +}; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx index aaa2bab72..f5a9a0a74 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx @@ -13,8 +13,8 @@ type AvailableSubscriptions = { }; export class SubscriptionState { - userSubscription: Subscription | undefined; - availableSubscriptions: AvailableSubscriptions | undefined; + userSubscription: Subscription | undefined = undefined; + availableSubscriptions: AvailableSubscriptions | undefined = undefined; constructor() { makeObservable(this, { @@ -40,7 +40,9 @@ export class SubscriptionState { } public setUserSubscription(subscription: Subscription): void { + console.log('set subscription in state', subscription); this.userSubscription = subscription; + console.log(this.userSubscription); } public setAvailableSubscriptions( From dee0c74ebb238a4dd44e746242cfb457f49a3131 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 7 Sep 2021 14:37:37 -0300 Subject: [PATCH 11/71] Update app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx Co-authored-by: Mo Bitar --- .../panes/account/subscription/subscription_state.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx index f5a9a0a74..fa09dfd0b 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx @@ -40,7 +40,6 @@ export class SubscriptionState { } public setUserSubscription(subscription: Subscription): void { - console.log('set subscription in state', subscription); this.userSubscription = subscription; console.log(this.userSubscription); } From 2696f5b735db370e0efa10d60a8f2993d04b9510 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 7 Sep 2021 15:11:27 -0300 Subject: [PATCH 12/71] refactor: merge active and cancelled components into one --- .../subscription/ActiveSubscription.tsx | 49 ----------- .../subscription/CancelledSubscription.tsx | 50 ------------ .../account/subscription/Subscription.tsx | 39 +++------ .../subscription/SubscriptionInformation.tsx | 81 +++++++++++++++++++ .../subscription/subscription_state.tsx | 1 - 5 files changed, 94 insertions(+), 126 deletions(-) delete mode 100644 app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx delete mode 100644 app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx create mode 100644 app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx diff --git a/app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx deleted file mode 100644 index eb5d984ad..000000000 --- a/app/assets/javascripts/preferences/panes/account/subscription/ActiveSubscription.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { SubscriptionState } from './subscription_state'; -import { Text } from '@/preferences/components'; -import { Button } from '@/components/Button'; - -type Props = { - subscriptionState: SubscriptionState; -}; - -export const ActiveSubscription = observer( - ({ subscriptionState }: Props) => { - const { userSubscription, userSubscriptionName } = subscriptionState; - return ( - <> - - Your{' '} - - Standard Notes{userSubscriptionName ? " " : ""}{userSubscriptionName} - {' '} - subscription will be{' '} - - renewed on {new Date(userSubscription!.endsAt).toLocaleString()} - - . - -
-
- - ); - } -); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx deleted file mode 100644 index d3742e65c..000000000 --- a/app/assets/javascripts/preferences/panes/account/subscription/CancelledSubscription.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { SubscriptionState } from './subscription_state'; -import { Text } from '@/preferences/components'; -import { Button } from '@/components/Button'; - -type Props = { - subscriptionState: SubscriptionState; -}; - -export const CancelledSubscription = observer( - ({ subscriptionState }: Props) => { - const { userSubscription, userSubscriptionName } = subscriptionState; - return ( - <> - - Your{' '} - - Standard Notes{userSubscriptionName ? " " : ""}{userSubscriptionName} - {' '} - subscription has been{' '} - - canceled but will remain valid until{' '} - {new Date(userSubscription!.endsAt).toLocaleString()} - - . You may resubscribe below if you wish. - -
-
- - ); - } -); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx index df5b3f9fc..33dff200b 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -3,7 +3,6 @@ import { PreferencesSegment, Title, } from '@/preferences/components'; -import { observer } from '@node_modules/mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; import { useCallback, useEffect, useState } from 'preact/hooks'; import { @@ -11,41 +10,25 @@ import { GetSubscriptionsResponse, } from '@standardnotes/snjs/dist/@types/services/api/responses'; import { SubscriptionState } from './subscription_state'; -import { CancelledSubscription } from './CancelledSubscription'; -import { ActiveSubscription } from './ActiveSubscription'; +import { SubscriptionInformation } from './SubscriptionInformation'; import { NoSubscription } from './NoSubscription'; import { Text } from '@/preferences/components'; -import { FunctionalComponent } from 'preact'; +import { observer } from 'mobx-react-lite'; type Props = { application: WebApplication; subscriptionState: SubscriptionState; }; -type SubscriptionInformationProps = { - subscriptionState: SubscriptionState; -}; - -const SubscriptionInformation = observer(({ +export const Subscription = observer(({ + application, subscriptionState, -}: SubscriptionInformationProps) => { - const now = new Date().getTime(); - const { userSubscription } = subscriptionState; - - if (userSubscription && userSubscription.endsAt > now) { - return userSubscription.cancelled ? ( - - ) : ( - - ); - } - return ; -}); - -export const Subscription: FunctionalComponent = ({ application, subscriptionState }) => { +}: Props) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const { userSubscription } = subscriptionState; + const getSubscriptions = useCallback(async () => { try { const result = await application.getSubscriptions(); @@ -87,6 +70,8 @@ export const Subscription: FunctionalComponent = ({ application, subscrip getSubscriptionInfo(); }, [getSubscriptionInfo]); + const now = new Date().getTime(); + return ( @@ -97,12 +82,14 @@ export const Subscription: FunctionalComponent = ({ application, subscrip No subscription information available. ) : loading ? ( Loading subscription information... - ) : ( + ) : userSubscription && userSubscription.endsAt > now ? ( + ) : ( + )}
); -}; +}); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx new file mode 100644 index 000000000..eb97147a5 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx @@ -0,0 +1,81 @@ +import { observer } from 'mobx-react-lite'; +import { SubscriptionState } from './subscription_state'; +import { Text } from '@/preferences/components'; +import { Button } from '@/components/Button'; + +type Props = { + subscriptionState: SubscriptionState; +}; + +const StatusText = observer(({ subscriptionState }: Props) => { + const { userSubscription, userSubscriptionName } = subscriptionState; + + return userSubscription!.cancelled ? ( + + Your{' '} + + Standard Notes{userSubscriptionName ? ' ' : ''} + {userSubscriptionName} + {' '} + subscription has been{' '} + + canceled but will remain valid until{' '} + {new Date(userSubscription!.endsAt).toLocaleString()} + + . You may resubscribe below if you wish. + + ) : ( + + Your{' '} + + Standard Notes{userSubscriptionName ? ' ' : ''} + {userSubscriptionName} + {' '} + subscription will be{' '} + + renewed on {new Date(userSubscription!.endsAt).toLocaleString()} + + . + + ); +}); + +const PrimaryButton = observer(({ subscriptionState }: Props) => { + const { userSubscription } = subscriptionState; + + return ( + @@ -95,7 +95,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { } else { application.signOut(); } - close(); + closeDialog(); }} > {application.hasAccount() diff --git a/app/assets/javascripts/components/ConfirmationDialog.tsx b/app/assets/javascripts/components/ConfirmationDialog.tsx new file mode 100644 index 000000000..78114ed66 --- /dev/null +++ b/app/assets/javascripts/components/ConfirmationDialog.tsx @@ -0,0 +1,35 @@ +import { ComponentChildren, FunctionComponent } from 'preact'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { useRef } from 'preact/hooks'; + +export const ConfirmationDialog: FunctionComponent<{ + title: string | ComponentChildren; +}> = ({ title, children }) => { + const ldRef = useRef(); + + return ( + + {/* sn-component is focusable by default, but doesn't stretch to child width + resulting in a badly focused dialog. Utility classes are not available + at the sn-component level, only below it. tabIndex -1 disables focus + and enables it on the child component */} +
+
+ {title} +
+ + + {children} + +
+
+ + ); +}; diff --git a/app/assets/javascripts/components/OtherSessionsLogout.tsx b/app/assets/javascripts/components/OtherSessionsLogout.tsx new file mode 100644 index 000000000..a6da7ef9f --- /dev/null +++ b/app/assets/javascripts/components/OtherSessionsLogout.tsx @@ -0,0 +1,80 @@ +import { useRef, useState } from 'preact/hooks'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +type Props = { + application: WebApplication; + appState: AppState; +}; + +export const OtherSessionsLogoutContainer = observer((props: Props) => { + if (!props.appState.accountMenu.otherSessionsLogOut) { + return null; + } + return ; +}); + +const ConfirmOtherSessionsLogout = observer( + ({ application, appState }: Props) => { + + const cancelRef = useRef(); + function closeDialog() { + appState.accountMenu.setOtherSessionsLogout(false); + } + + return ( + +
+
+
+
+
+ + End all other sessions? + + +

+ This action will sign out all other devices signed into your account, + and remove your data from those devices when they next regain connection + to the internet. You may sign back in on those devices at any time. +

+
+
+ + +
+
+
+
+
+
+
+ ); + } +); diff --git a/app/assets/javascripts/components/SessionsModal.tsx b/app/assets/javascripts/components/SessionsModal.tsx index 875f25f9c..e06c7df7b 100644 --- a/app/assets/javascripts/components/SessionsModal.tsx +++ b/app/assets/javascripts/components/SessionsModal.tsx @@ -26,12 +26,12 @@ type Session = RemoteSession & { function useSessions( application: SNApplication ): [ - Session[], - () => void, - boolean, - (uuid: UuidString) => Promise, - string -] { + Session[], + () => void, + boolean, + (uuid: UuidString) => Promise, + string + ] { const [sessions, setSessions] = useState([]); const [lastRefreshDate, setLastRefreshDate] = useState(Date.now()); const [refreshing, setRefreshing] = useState(true); @@ -240,7 +240,7 @@ const SessionsModal: FunctionComponent<{ ); }; -const Sessions: FunctionComponent<{ +export const Sessions: FunctionComponent<{ appState: AppState; application: WebApplication; }> = observer(({ appState, application }) => { diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 34ee084af..9ed5cf152 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -7,9 +7,11 @@ import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; +import { AppState } from '@/ui_models/app_state'; interface PreferencesProps extends MfaProps { application: WebApplication; + appState: AppState; closePreferences: () => void; } @@ -20,7 +22,12 @@ const PaneSelector: FunctionComponent< case 'general': return null; case 'account': - return ; + return ( + + ); case 'appearance': return null; case 'security': diff --git a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx index cd4007990..03cc8623d 100644 --- a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx +++ b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx @@ -2,9 +2,10 @@ import { FunctionComponent } from 'preact'; import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; import { PreferencesView } from './PreferencesView'; +import { AppState } from '@/ui_models/app_state'; export interface PreferencesViewWrapperProps { - appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; + appState: AppState; application: WebApplication; } @@ -18,6 +19,7 @@ export const PreferencesViewWrapper: FunctionComponent appState.preferences.closePreferences()} application={application} + appState={appState} mfaProvider={application} userProvider={application} /> diff --git a/app/assets/javascripts/preferences/components/PreferencesGroup.tsx b/app/assets/javascripts/preferences/components/PreferencesGroup.tsx new file mode 100644 index 000000000..51dbd196a --- /dev/null +++ b/app/assets/javascripts/preferences/components/PreferencesGroup.tsx @@ -0,0 +1,24 @@ +import { FunctionComponent } from 'preact'; +import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; + +const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({ + index, + length, +}) => (index < length - 1 ? : null); + +export const PreferencesGroup: FunctionComponent = ({ children }) => ( +
+ {Array.isArray(children) + ? children + .filter( + (child) => child != undefined && child !== '' && child !== false + ) + .map((child, i, arr) => ( + <> + {child} + + + )) + : children} +
+); diff --git a/app/assets/javascripts/preferences/components/PreferencesPane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx index 722a02ec4..bfe2e4572 100644 --- a/app/assets/javascripts/preferences/components/PreferencesPane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -1,43 +1,18 @@ import { FunctionComponent } from 'preact'; -import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; - -const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({ - index, - length, -}) => (index < length - 1 ? : null); - -export const PreferencesSegment: FunctionComponent = ({ children }) => ( -
{children}
-); - -export const PreferencesGroup: FunctionComponent = ({ children }) => ( -
- {Array.isArray(children) - ? children - .filter( - (child) => child != undefined && child !== '' && child !== false - ) - .map((child, i, arr) => ( - <> - {child} - - - )) - : children} -
-); export const PreferencesPane: FunctionComponent = ({ children }) => (
{children != undefined && Array.isArray(children) - ? children.map((child, idx, arr) => ( - <> - {child} - {idx < arr.length - 1 ?
: undefined} - - )) + ? children + .filter((child) => child != undefined) + .map((child) => ( + <> + {child} +
+ + )) : children}
diff --git a/app/assets/javascripts/preferences/components/PreferencesSegment.tsx b/app/assets/javascripts/preferences/components/PreferencesSegment.tsx new file mode 100644 index 000000000..dad798f5c --- /dev/null +++ b/app/assets/javascripts/preferences/components/PreferencesSegment.tsx @@ -0,0 +1,5 @@ +import { FunctionComponent } from 'preact'; + +export const PreferencesSegment: FunctionComponent = ({ children }) => ( +
{children}
+); diff --git a/app/assets/javascripts/preferences/components/index.ts b/app/assets/javascripts/preferences/components/index.ts index 0c4046f06..04139c83d 100644 --- a/app/assets/javascripts/preferences/components/index.ts +++ b/app/assets/javascripts/preferences/components/index.ts @@ -1,3 +1,5 @@ export * from './Content'; export * from './MenuItem'; export * from './PreferencesPane'; +export * from './PreferencesGroup'; +export * from './PreferencesSegment'; diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 49eb88a7d..1af8bc6f3 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -1,17 +1,28 @@ -import { Sync, SubscriptionWrapper, Credentials } from '@/preferences/panes/account'; +import { + Sync, + SubscriptionWrapper, + Credentials, + LogOutWrapper, +} from '@/preferences/panes/account'; import { PreferencesPane } from '@/preferences/components'; import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; -} -export const AccountPreferences = observer(({application}: Props) => { - return ( - - - - - - ); -}); + appState: AppState; +}; + +export const AccountPreferences = observer( + ({ application, appState }: Props) => { + return ( + + + + + + + ); + } +); diff --git a/app/assets/javascripts/preferences/panes/account/LogOutView.tsx b/app/assets/javascripts/preferences/panes/account/LogOutView.tsx new file mode 100644 index 000000000..a3f1219f9 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/LogOutView.tsx @@ -0,0 +1,103 @@ +import { Button } from '@/components/Button'; +import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; +import { OtherSessionsLogoutContainer } from '@/components/OtherSessionsLogout'; +import { + PreferencesGroup, + PreferencesSegment, + Subtitle, + Text, + Title, +} from '@/preferences/components'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +const LogOutView: FunctionComponent<{ + application: WebApplication; + appState: AppState; +}> = observer(({ application, appState }) => { + + return ( + <> + + + Log out +
+ Other devices + Want to log out on all devices except this one? +
+
+
+ + + This device + This will delete all local items and preferences. +
+
+ )} + +
+ + + {hasPasscode && ( + <> +
+ + + Autolock + The autolock timer begins when the window or tab loses focus. +
+ {passcodeAutoLockOptions.map(option => { + return ( + selectAutoLockInterval(option.value)}> + {option.label} + + ); + })} +
+ +
+
+ + )} + + ); +}); diff --git a/app/assets/javascripts/preferences/panes/security-segments/index.ts b/app/assets/javascripts/preferences/panes/security-segments/index.ts new file mode 100644 index 000000000..f9a8b3630 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/security-segments/index.ts @@ -0,0 +1,2 @@ +export * from './Encryption'; +export * from './PasscodeLock'; diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 4b4d6756a..6a79eec95 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -214,6 +214,10 @@ min-height: 0.75rem; } +.min-h-4 { + min-height: 1rem; +} + .min-h-6 { min-height: 1.5rem; } From 5f65d2eca76cf5925aff309e54a73a1760345061 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Thu, 23 Sep 2021 17:34:59 +0200 Subject: [PATCH 27/71] feat: implement Protections in prefs (#645) --- .../preferences/panes/Security.tsx | 3 +- .../panes/security-segments/Protections.tsx | 93 +++++++++++++++++++ .../panes/security-segments/index.ts | 1 + 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/preferences/panes/security-segments/Protections.tsx diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index 13d7ba2b0..900940d7a 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -2,7 +2,7 @@ import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; -import { Encryption, PasscodeLock } from './security-segments'; +import { Encryption, PasscodeLock, Protections } from './security-segments'; import { TwoFactorAuthWrapper } from './two-factor-auth'; import { MfaProps } from './two-factor-auth/MfaProps'; @@ -14,6 +14,7 @@ interface SecurityProps extends MfaProps { export const Security: FunctionComponent = (props) => ( + = ({ application }) => { + const enableProtections = () => { + application.clearProtectionSession(); + }; + + const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources()); + + const getProtectionsDisabledUntil = useCallback((): string | null => { + const protectionExpiry = application.getProtectionSessionExpiryDate(); + const now = new Date(); + if (protectionExpiry > now) { + let f: Intl.DateTimeFormat; + if (isSameDay(protectionExpiry, now)) { + f = new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + minute: 'numeric' + }); + } else { + f = new Intl.DateTimeFormat(undefined, { + weekday: 'long', + day: 'numeric', + month: 'short', + hour: 'numeric', + minute: 'numeric' + }); + } + + return f.format(protectionExpiry); + } + return null; + }, [application]); + + const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); + + useEffect(() => { + const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( + async () => { + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); + }, + ApplicationEvent.ProtectionSessionExpiryDateChanged + ); + + const removeKeyStatusChangedObserver = application.addEventObserver( + async () => { + setHasProtections(application.hasProtectionSources()); + }, + ApplicationEvent.KeyStatusChanged + ); + + return () => { + removeProtectionSessionExpiryDateChangedObserver(); + removeKeyStatusChangedObserver(); + }; + }, [application, getProtectionsDisabledUntil]); + + if (!hasProtections) { + return null; + } + + return ( + + + Protections + {protectionsDisabledUntil + ? Protections are disabled until {protectionsDisabledUntil}. + : Protections are enabled. + } + + Actions like viewing protected notes, exporting decrypted backups, + or revoking an active session, require additional authentication + like entering your account password or application passcode. + + {protectionsDisabledUntil && +
+
+ Standard Notes is free on every platform, and comes standard with sync and encryption. +
+ + + ); + }); diff --git a/app/assets/javascripts/preferences/panes/account/Credentials.tsx b/app/assets/javascripts/preferences/panes/account/Credentials.tsx index 4ba981a53..ff7e09290 100644 --- a/app/assets/javascripts/preferences/panes/account/Credentials.tsx +++ b/app/assets/javascripts/preferences/panes/account/Credentials.tsx @@ -7,12 +7,13 @@ import { dateToLocalizedString } from '@/utils'; import { useState } from 'preact/hooks'; import { ChangeEmail } from '@/preferences/panes/account/changeEmail'; import { PasswordWizardType } from '@/types'; +import { FunctionComponent } from 'preact'; type Props = { application: WebApplication; }; -export const Credentials = observer(({ application }: Props) => { +export const Credentials: FunctionComponent = observer(({ application }: Props) => { const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] = useState(false); const user = application.getUser(); diff --git a/app/assets/javascripts/preferences/panes/account/Sync.tsx b/app/assets/javascripts/preferences/panes/account/Sync.tsx index 192b18022..89d730cb6 100644 --- a/app/assets/javascripts/preferences/panes/account/Sync.tsx +++ b/app/assets/javascripts/preferences/panes/account/Sync.tsx @@ -6,12 +6,13 @@ import { useState } from '@node_modules/preact/hooks'; import { dateToLocalizedString } from '@/utils'; import { observer } from '@node_modules/mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; +import { FunctionComponent } from 'preact'; type Props = { application: WebApplication; }; -export const Sync = observer(({ application }: Props) => { +export const Sync: FunctionComponent = observer(({ application }: Props) => { const formatLastSyncDate = (lastUpdatedDate: Date) => { return dateToLocalizedString(lastUpdatedDate); }; diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts index 2fd73c04c..51ad45922 100644 --- a/app/assets/javascripts/preferences/panes/account/index.ts +++ b/app/assets/javascripts/preferences/panes/account/index.ts @@ -2,3 +2,4 @@ export { SubscriptionWrapper } from './subscription/SubscriptionWrapper'; export { Sync } from './Sync'; export { Credentials } from './Credentials'; export { LogOutWrapper } from './LogOutView'; +export { Authentication } from './Authentication'; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx index 4f159cb0c..a063d1a06 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -10,13 +10,14 @@ import { SubscriptionInformation } from './SubscriptionInformation'; import { NoSubscription } from './NoSubscription'; import { Text } from '@/preferences/components'; import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; type Props = { application: WebApplication; subscriptionState: SubscriptionState; }; -export const Subscription = observer(({ +export const Subscription: FunctionComponent = observer(({ application, subscriptionState, }: Props) => { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 6a79eec95..c066f10b6 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -247,6 +247,11 @@ padding-right: 2.25rem; } +.px-12 { + padding-left: 3rem; + padding-right: 3rem; +} + .py-9 { padding-top: 2.25rem; padding-bottom: 2.25rem; From 3867f625f1ef524b8039b25ffcb09738befd5115 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Tue, 28 Sep 2021 18:08:27 +0200 Subject: [PATCH 32/71] feat(preferences): error reporting segment (#652) * feat(preferences): error reporting segment * feat(preferences): move error reporting to general pane --- .../preferences/PreferencesView.tsx | 6 +- .../components/PreferencesGroup.tsx | 20 ++--- .../components/PreferencesPane.tsx | 6 -- .../javascripts/preferences/panes/General.tsx | 16 ++++ .../panes/general-segments/ErrorReporting.tsx | 86 +++++++++++++++++++ .../panes/general-segments/index.ts | 1 + .../javascripts/preferences/panes/index.ts | 1 + .../panes/security-segments/Protections.tsx | 1 - 8 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 app/assets/javascripts/preferences/panes/General.tsx create mode 100644 app/assets/javascripts/preferences/panes/general-segments/ErrorReporting.tsx create mode 100644 app/assets/javascripts/preferences/panes/general-segments/index.ts diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index fc5c328a2..d9059a440 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -1,14 +1,14 @@ import { RoundIconButton } from '@/components/RoundIconButton'; import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; -import { AccountPreferences, HelpAndFeedback, Security } from './panes'; +import { AccountPreferences, General, HelpAndFeedback, Security } from './panes'; import { observer } from 'mobx-react-lite'; import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { AppState } from '@/ui_models/app_state'; -import { useCallback, useEffect } from 'preact/hooks'; +import { useEffect } from 'preact/hooks'; interface PreferencesProps extends MfaProps { application: WebApplication; @@ -21,7 +21,7 @@ const PaneSelector: FunctionComponent< > = observer((props) => { switch (props.menu.selectedPaneId) { case 'general': - return null; + return case 'account': return ( = ({ }) => (index < length - 1 ? : null); export const PreferencesGroup: FunctionComponent = ({ children }) => ( -
+
{Array.isArray(children) ? children - .filter( - (child) => child != undefined && child !== '' && child !== false - ) - .map((child, i, arr) => ( - <> - {child} - - - )) + .filter( + (child) => child != undefined && child !== '' && child !== false + ) + .map((child, i, arr) => ( + <> + {child} + + + )) : children}
); diff --git a/app/assets/javascripts/preferences/components/PreferencesPane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx index 257ce4f73..f1a136884 100644 --- a/app/assets/javascripts/preferences/components/PreferencesPane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -7,12 +7,6 @@ export const PreferencesPane: FunctionComponent = ({ children }) => ( {children != undefined && Array.isArray(children) ? children .filter((child) => child != undefined) - .map((child) => ( - <> - {child} -
- - )) : children}
diff --git a/app/assets/javascripts/preferences/panes/General.tsx b/app/assets/javascripts/preferences/panes/General.tsx new file mode 100644 index 000000000..2598b7a98 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/General.tsx @@ -0,0 +1,16 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { FunctionComponent } from 'preact'; +import { PreferencesPane } from '../components'; +import { ErrorReporting } from './general-segments'; + +interface GeneralProps { + appState: AppState; + application: WebApplication; +} + +export const General: FunctionComponent = (props) => ( + + + +); diff --git a/app/assets/javascripts/preferences/panes/general-segments/ErrorReporting.tsx b/app/assets/javascripts/preferences/panes/general-segments/ErrorReporting.tsx new file mode 100644 index 000000000..5b4d7f043 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/general-segments/ErrorReporting.tsx @@ -0,0 +1,86 @@ +import { useState } from 'preact/hooks'; +import { storage, StorageKey } from '@Services/localStorage'; +import { disableErrorReporting, enableErrorReporting, errorReportingId } from '@Services/errorReporting'; +import { alertDialog } from '@Services/alertService'; +import { observer } from 'mobx-react-lite'; +import { AppState } from '@/ui_models/app_state'; +import { FunctionComponent } from 'preact'; +import { PreferencesGroup, PreferencesSegment, Title, Text } from '@/preferences/components'; +import { Switch } from '@/components/Switch'; + +type Props = { + appState: AppState; +} + +export const ErrorReporting: FunctionComponent = observer(({ appState }: Props) => { + const [isErrorReportingEnabled] = useState(() => storage.get(StorageKey.DisableErrorReporting) === false); + const [errorReportingIdValue] = useState(() => errorReportingId()); + + const toggleErrorReportingEnabled = () => { + if (isErrorReportingEnabled) { + disableErrorReporting(); + } else { + enableErrorReporting(); + } + if (!appState.sync.inProgress) { + window.location.reload(); + } + }; + + const openErrorReportingDialog = () => { + alertDialog({ + title: 'Data sent during automatic error reporting', + text: ` + We use Bugsnag + to automatically report errors that occur while the app is running. See + + this article, paragraph 'Browser' under 'Sending diagnostic data', + + to see what data is included in error reports. +

+ Error reports never include IP addresses and are fully + anonymized. We use error reports to be alerted when something in our + code is causing unexpected errors and crashes in your application + experience. + ` + }); + }; + + return ( + + + +
+
+ Error Reporting + + Help us improve Standard Notes by automatically submitting + anonymized error reports. + +
+
+ +
+
+
+ + {errorReportingIdValue && ( + <> + + Your random identifier is {errorReportingIdValue} + + + Disabling error reporting will remove that identifier from your + local storage, and a new identifier will be created should you + decide to enable error reporting again in the future. + + + )} + + + What data is being sent? + + + + ); +}); diff --git a/app/assets/javascripts/preferences/panes/general-segments/index.ts b/app/assets/javascripts/preferences/panes/general-segments/index.ts new file mode 100644 index 000000000..619f11e36 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/general-segments/index.ts @@ -0,0 +1 @@ +export * from './ErrorReporting'; diff --git a/app/assets/javascripts/preferences/panes/index.ts b/app/assets/javascripts/preferences/panes/index.ts index 741508ad0..da5721469 100644 --- a/app/assets/javascripts/preferences/panes/index.ts +++ b/app/assets/javascripts/preferences/panes/index.ts @@ -1,3 +1,4 @@ export * from './HelpFeedback'; export * from './Security'; export * from './AccountPreferences'; +export * from './General'; diff --git a/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx b/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx index ae1d3a9e5..2cf2caabb 100644 --- a/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx +++ b/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx @@ -6,7 +6,6 @@ import { ApplicationEvent } from '@standardnotes/snjs'; import { isSameDay } from '@/utils'; import { PreferencesGroup, PreferencesSegment, Title, Text } from '@/preferences/components'; import { Button } from '@/components/Button'; -import { Switch } from '@/components/Switch'; type Props = { application: WebApplication; From 0e5692d8ab637c811d029edf1b89dfca4999da93 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Wed, 29 Sep 2021 17:52:27 +0200 Subject: [PATCH 33/71] feat: slim account menu and icon buttons (#655) * feat: footer icon for preferences menu * feat: account footer icon * feat: account menu icon and slimmed down account menu --- app/assets/icons/ic-account-circle.svg | 3 +++ .../components/AccountMenu/User.tsx | 25 +++-------------- .../components/AccountMenu/index.tsx | 17 ++---------- app/assets/javascripts/components/Icon.tsx | 4 ++- .../javascripts/views/footer/footer-view.pug | 27 ++++++++++++------- app/assets/stylesheets/_footer.scss | 11 -------- app/assets/stylesheets/_main.scss | 2 +- app/assets/stylesheets/_sn.scss | 11 ++++++++ 8 files changed, 42 insertions(+), 58 deletions(-) create mode 100644 app/assets/icons/ic-account-circle.svg diff --git a/app/assets/icons/ic-account-circle.svg b/app/assets/icons/ic-account-circle.svg new file mode 100644 index 000000000..53abd5a94 --- /dev/null +++ b/app/assets/icons/ic-account-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/components/AccountMenu/User.tsx b/app/assets/javascripts/components/AccountMenu/User.tsx index 07e5f58ac..1dc2b2af4 100644 --- a/app/assets/javascripts/components/AccountMenu/User.tsx +++ b/app/assets/javascripts/components/AccountMenu/User.tsx @@ -1,6 +1,5 @@ import { observer } from 'mobx-react-lite'; import { AppState } from '@/ui_models/app_state'; -import { PasswordWizardType } from '@/types'; import { WebApplication } from '@/ui_models/application'; import { User } from '@standardnotes/snjs/dist/@types/services/api/responses'; @@ -10,22 +9,12 @@ type Props = { } const User = observer(({ - appState, - application, - }: Props) => { - const { server, closeAccountMenu } = appState.accountMenu; + appState, + application, +}: Props) => { + const { server } = appState.accountMenu; const user = application.getUser(); - const openPasswordWizard = () => { - closeAccountMenu(); - application.presentPasswordWizard(PasswordWizardType.ChangePassword); - }; - - const openSessionsModal = () => { - closeAccountMenu(); - appState.openSessionsModal(); - }; - return (
{appState.sync.errorMessage && ( @@ -56,12 +45,6 @@ const User = observer(({
); }); diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index cf8c45b2f..0213e5c5b 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -51,25 +51,12 @@ const AccountMenu = observer(({ application, appState }: Props) => { application={application} appState={appState} /> - {!showLogin && !showRegister && ( + {!showLogin && !showRegister && user && (
- {user && ( - - )} - - - - -
)}
diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index bbd43d7f0..e72804ae2 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -28,6 +28,7 @@ import DownloadIcon from '../../icons/ic-download.svg'; import InfoIcon from '../../icons/ic-info.svg'; import CheckIcon from '../../icons/ic-check.svg'; import CheckBoldIcon from '../../icons/ic-check-bold.svg'; +import AccountCircleIcon from '../../icons/ic-account-circle.svg'; import { toDirective } from './utils'; import { FunctionalComponent } from 'preact'; @@ -61,7 +62,8 @@ const ICONS = { download: DownloadIcon, info: InfoIcon, check: CheckIcon, - "check-bold": CheckBoldIcon, + 'check-bold': CheckBoldIcon, + 'account-circle': AccountCircleIcon, }; export type IconType = keyof typeof ICONS; diff --git a/app/assets/javascripts/views/footer/footer-view.pug b/app/assets/javascripts/views/footer/footer-view.pug index 6970a442d..94bbb985f 100644 --- a/app/assets/javascripts/views/footer/footer-view.pug +++ b/app/assets/javascripts/views/footer/footer-view.pug @@ -1,28 +1,37 @@ .sn-component #footer-bar.sk-app-bar.no-edges.no-bottom-edge .left - .sk-app-bar-item( + .sk-app-bar-item.ml-0( click-outside='ctrl.clickOutsideAccountMenu()', is-open='ctrl.showAccountMenu', ng-click='ctrl.accountMenuPressed()' - ) - .sk-app-bar-item-column - .sk-circle.small( - ng-class="ctrl.hasError ? 'danger' : (ctrl.user ? 'info' : 'neutral')" + ) + .w-8.h-8.flex.items-center.justify-center.cursor-pointer.rounded-full( + ng-class="ctrl.showAccountMenu ? 'bg-border' : '' " + ) + .w-5.h-5( + ng-class="ctrl.hasError ? 'danger' : (ctrl.user ? 'info' : 'neutral')" ) - .sk-app-bar-item-column - .sk-label.title(ng-class='{red: ctrl.hasError}') Account + icon( + type="account-circle" + class-name="hover:color-info w-5 h-5 max-h-5" + ) account-menu( ng-click='$event.stopPropagation()', app-state='ctrl.appState' application='ctrl.application' ng-if='ctrl.showAccountMenu', ) - .sk-app-bar-item( + .sk-app-bar-item.ml-0-important( ng-click='ctrl.clickPreferences()' ng-if='ctrl.appState.enableUnfinishedFeatures' ) - .sk-label.title Preferences + .w-8.h-8.flex.items-center.justify-center.cursor-pointer + .h-5 + icon( + type="tune" + class-name="rounded hover:color-info" + ) .sk-app-bar-item a.no-decoration.sk-label.title( href='https://standardnotes.com/help', diff --git a/app/assets/stylesheets/_footer.scss b/app/assets/stylesheets/_footer.scss index eb2d65cb5..5b813b3e9 100644 --- a/app/assets/stylesheets/_footer.scss +++ b/app/assets/stylesheets/_footer.scss @@ -34,17 +34,6 @@ border-bottom: 2px solid var(--sn-stylekit-info-color); } } - - svg { - width: 12px; - height: 12px; - fill: var(--sn-stylekit-secondary-foreground-color); - - &:hover { - fill: var(--sn-stylekit-info-color) !important; - color: var(--sn-stylekit-info-color) !important; - } - } } #account-switcher-icon { diff --git a/app/assets/stylesheets/_main.scss b/app/assets/stylesheets/_main.scss index f57cc05db..b54211c3a 100644 --- a/app/assets/stylesheets/_main.scss +++ b/app/assets/stylesheets/_main.scss @@ -115,7 +115,7 @@ p { background-color: var(--sn-stylekit-background-color); } -$footer-height: 32px; +$footer-height: 2rem; #resizer-overlay { position: absolute; diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index c066f10b6..1c853c81e 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -154,6 +154,13 @@ @extend .font-bold; } +.ml-0-important { + margin-left: 0rem !important; +} + +.ml-3 { + margin-left: 0.75rem; +} .ml-4 { margin-left: 1rem; } @@ -222,6 +229,10 @@ min-height: 1.5rem; } +.max-h-5 { + max-height: 1.25rem; +} + .border-danger { border-color: var(--sn-stylekit-danger-color); } From e72d7372204756e2513b5d38e2b9e32ce4fe3abb Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Wed, 29 Sep 2021 21:32:30 +0530 Subject: [PATCH 34/71] feat: Add Listed pane in preferences (#651) * feat: Add Listed pane in preferences * feat: Add list of blogs in Listed preferences feat: Allow custom classnames in LinkButton feat: Add mt-0 class * fix: Don't show non-Listed Action Extensions * fix: Use streamItems() * fix: Re-render UI when item is deleted * feat: Remove hardcoded margin-top for LinkButton * fix: Fix ESLint exhaustive-deps error * fix: Use useCallback hook feat: Disconnect shows state "Disconnecting..." when deleting item * fix: Remove unused imports * fix: Simplify disconnect function fix: Use key in the correct place * feat: Add confirmation dialog when deleting a blog feat: Show Blog/Blogs in the title depending on the number of items * style: Revert file to original formatting * refactor: Use preact instead of react refactor: Use FunctionalComponent type * feat: Show alert when disconnecting errors out fix: Set state to false even if errors refactor: Use ternary operator for Getting Started section * feat: Load Listed blog actions asynchronously * feat: Only fetch actions if not already available * refactor: Use async/await for disconnecting Co-authored-by: Mo Bitar --- .../preferences/PreferencesView.tsx | 4 +- .../preferences/components/Content.tsx | 13 +- .../preferences/panes/HelpFeedback.tsx | 10 +- .../javascripts/preferences/panes/Listed.tsx | 116 ++++++++++++++++++ .../javascripts/preferences/panes/index.ts | 1 + .../preferences/panes/listed/BlogItem.tsx | 110 +++++++++++++++++ 6 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/preferences/panes/Listed.tsx create mode 100644 app/assets/javascripts/preferences/panes/listed/BlogItem.tsx diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index d9059a440..74839c310 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -1,7 +1,7 @@ import { RoundIconButton } from '@/components/RoundIconButton'; import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; -import { AccountPreferences, General, HelpAndFeedback, Security } from './panes'; +import { AccountPreferences, HelpAndFeedback, Listed, General, Security } from './panes'; import { observer } from 'mobx-react-lite'; import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; @@ -41,7 +41,7 @@ const PaneSelector: FunctionComponent< /> ); case 'listed': - return null; + return ; case 'shortcuts': return null; case 'accessibility': diff --git a/app/assets/javascripts/preferences/components/Content.tsx b/app/assets/javascripts/preferences/components/Content.tsx index a754c3d4d..fde262846 100644 --- a/app/assets/javascripts/preferences/components/Content.tsx +++ b/app/assets/javascripts/preferences/components/Content.tsx @@ -14,14 +14,15 @@ export const Text: FunctionComponent<{ className?: string }> = ({ }) =>

{children}

; const buttonClasses = `block bg-default color-text rounded border-solid \ -border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \ +border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content \ focus:bg-contrast hover:bg-contrast `; -export const LinkButton: FunctionComponent<{ label: string; link: string }> = ({ - label, - link, -}) => ( - +export const LinkButton: FunctionComponent<{ + label: string; + link: string; + className?: string; +}> = ({ label, link, className }) => ( + {label} ); diff --git a/app/assets/javascripts/preferences/panes/HelpFeedback.tsx b/app/assets/javascripts/preferences/panes/HelpFeedback.tsx index 58063a33d..d2a1d0d19 100644 --- a/app/assets/javascripts/preferences/panes/HelpFeedback.tsx +++ b/app/assets/javascripts/preferences/panes/HelpFeedback.tsx @@ -52,7 +52,11 @@ export const HelpAndFeedback: FunctionComponent = () => ( Can’t find your question here? - + @@ -68,6 +72,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( before advocating for a feature request. @@ -82,6 +87,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( group for discussions on security, themes, editors and more. @@ -93,7 +99,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( Send an email to help@standardnotes.com and we’ll sort it out. - + diff --git a/app/assets/javascripts/preferences/panes/Listed.tsx b/app/assets/javascripts/preferences/panes/Listed.tsx new file mode 100644 index 000000000..d1254c2af --- /dev/null +++ b/app/assets/javascripts/preferences/panes/Listed.tsx @@ -0,0 +1,116 @@ +import { + PreferencesGroup, + PreferencesPane, + PreferencesSegment, + Title, + Subtitle, + Text, + LinkButton, +} from '../components'; +import { observer } from 'mobx-react-lite'; +import { WebApplication } from '@/ui_models/application'; +import { ContentType, SNComponent } from '@standardnotes/snjs'; +import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item'; +import { useCallback, useEffect, useState } from 'preact/hooks'; +import { BlogItem } from './listed/BlogItem'; + +type Props = { + application: WebApplication; +}; + +export const Listed = observer(({ application }: Props) => { + const [items, setItems] = useState([]); + const [isDeleting, setIsDeleting] = useState(false); + + const reloadItems = useCallback(() => { + const components = application + .getItems(ContentType.ActionsExtension) + .filter( + (item) => (item as SNComponent).package_info?.name === 'Listed' + ) as SNComponent[]; + setItems(components); + }, [application]); + + useEffect(() => { + reloadItems(); + }, [reloadItems]); + + const disconnectListedBlog = (item: SNItem) => { + return new Promise((resolve, reject) => { + setIsDeleting(true); + application + .deleteItem(item) + .then(() => { + reloadItems(); + setIsDeleting(false); + resolve(true); + }) + .catch((err) => { + application.alertService.alert(err); + setIsDeleting(false); + console.error(err); + reject(err); + }); + }); + }; + + return ( + + {items.length > 0 && ( + + + + Your {items.length === 1 ? 'Blog' : 'Blogs'} on Listed + +
+ {items.map((item, index, array) => { + return ( + + ); + })} + + + )} + + + About Listed +
+ What is Listed? + + Listed is a free blogging platform that allows you to create a + public journal published directly from your notes.{' '} + + Learn more + + + + {items.length === 0 ? ( + + How to get started? + + First, you’ll need to sign up for Listed. Once you have your + Listed account, follow the instructions to connect it with your + Standard Notes account. + + + + ) : null} + + + ); +}); diff --git a/app/assets/javascripts/preferences/panes/index.ts b/app/assets/javascripts/preferences/panes/index.ts index da5721469..56e97df76 100644 --- a/app/assets/javascripts/preferences/panes/index.ts +++ b/app/assets/javascripts/preferences/panes/index.ts @@ -1,4 +1,5 @@ export * from './HelpFeedback'; export * from './Security'; export * from './AccountPreferences'; +export * from './Listed'; export * from './General'; diff --git a/app/assets/javascripts/preferences/panes/listed/BlogItem.tsx b/app/assets/javascripts/preferences/panes/listed/BlogItem.tsx new file mode 100644 index 000000000..804a9c813 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/listed/BlogItem.tsx @@ -0,0 +1,110 @@ +import { Button } from '@/components/Button'; +import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; +import { LinkButton, Subtitle } from '@/preferences/components'; +import { WebApplication } from '@/ui_models/application'; +import { + Action, + ButtonType, + SNActionsExtension, + SNComponent, + SNItem, +} from '@standardnotes/snjs'; +import { FunctionalComponent } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; + +type Props = { + item: SNComponent; + showSeparator: boolean; + disabled: boolean; + disconnect: (item: SNItem) => Promise; + application: WebApplication; +}; + +export const BlogItem: FunctionalComponent = ({ + item, + showSeparator, + disabled, + disconnect, + application, +}) => { + const [actions, setActions] = useState([]); + const [isLoadingActions, setIsLoadingActions] = useState(false); + const [isDisconnecting, setIsDisconnecting] = useState(false); + + useEffect(() => { + const loadActions = async () => { + setIsLoadingActions(true); + application.actionsManager + .loadExtensionInContextOfItem(item as SNActionsExtension, item) + .then((extension) => { + setActions(extension?.actions); + }) + .catch((err) => application.alertService.alert(err)) + .finally(() => { + setIsLoadingActions(false); + }); + }; + if (!actions || actions.length === 0) loadActions(); + }, [application.actionsManager, application.alertService, item, actions]); + + const handleDisconnect = () => { + setIsDisconnecting(true); + application.alertService + .confirm( + 'Disconnecting will result in loss of access to your blog. Ensure your Listed author key is backed up before uninstalling.', + `Disconnect blog "${item?.name}"?`, + 'Disconnect', + ButtonType.Danger + ) + .then(async (shouldDisconnect) => { + if (shouldDisconnect) { + await disconnect(item as SNItem); + } + }) + .catch((err) => { + console.error(err); + application.alertService.alert(err); + }) + .finally(() => { + setIsDisconnecting(false); + }); + }; + + return ( + <> + {item?.name} +
+ {isLoadingActions ? ( +
+ ) : null} + {actions && actions?.length > 0 ? ( + <> + action.label === 'Open Blog') + ?.url || '' + } + /> + action.label === 'Settings') + ?.url || '' + } + /> +
+ {showSeparator && } + + ); +}; From 091842555f78764e48ba033d38401d382935ee7d Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Wed, 29 Sep 2021 18:08:43 +0200 Subject: [PATCH 35/71] fix: use theme-modifiable colors (#658) --- app/assets/icons/ic-check-bold.svg | 2 +- app/assets/icons/ic-check.svg | 2 +- app/assets/javascripts/components/Button.tsx | 4 ++-- .../javascripts/components/DecoratedInput.tsx | 6 +++--- app/assets/javascripts/components/Input.tsx | 4 ++-- .../javascripts/components/shared/ModalDialog.tsx | 14 +++++++------- .../javascripts/preferences/components/Content.tsx | 2 +- .../preferences/components/MenuItem.tsx | 2 +- .../preferences/components/PreferencesPane.tsx | 2 +- .../panes/security-segments/Encryption.tsx | 2 +- .../panes/two-factor-auth/AuthAppInfoPopup.tsx | 7 +++---- .../preferences/panes/two-factor-auth/Bullet.tsx | 2 +- app/assets/stylesheets/_sn.scss | 12 ++++++++++++ 13 files changed, 36 insertions(+), 25 deletions(-) diff --git a/app/assets/icons/ic-check-bold.svg b/app/assets/icons/ic-check-bold.svg index 82c81e896..01df89c32 100644 --- a/app/assets/icons/ic-check-bold.svg +++ b/app/assets/icons/ic-check-bold.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-check.svg b/app/assets/icons/ic-check.svg index 93a4a4a42..ff64da1ba 100644 --- a/app/assets/icons/ic-check.svg +++ b/app/assets/icons/ic-check.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx index 484983dde..6737c971e 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/components/Button.tsx @@ -9,9 +9,9 @@ const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`; type ButtonType = 'normal' | 'primary' | 'danger'; const buttonClasses: { [type in ButtonType]: string } = { - normal: `${baseClass} bg-default color-text border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, + normal: `${baseClass} bg-default color-text border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, primary: `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 focus:brightness-130`, - danger: `${baseClass} bg-default color-danger border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, + danger: `${baseClass} bg-default color-danger border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, }; export const Button: FunctionComponent<{ diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index 9caeebfb1..fe3b67178 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -28,13 +28,13 @@ export const DecoratedInput: FunctionalComponent = ({ autocomplete = false, }) => { const baseClasses = - 'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center'; + 'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center bg-contrast'; const stateClasses = disabled - ? 'no-border bg-grey-5' + ? 'no-border' : 'border-solid border-1 border-gray-300'; const classes = `${baseClasses} ${stateClasses} ${className}`; - const inputBaseClasses = 'w-full no-border color-black focus:shadow-none'; + const inputBaseClasses = 'w-full no-border color-text focus:shadow-none bg-contrast'; const inputStateClasses = disabled ? 'overflow-ellipsis' : ''; return (
diff --git a/app/assets/javascripts/components/Input.tsx b/app/assets/javascripts/components/Input.tsx index 0955b632c..39fac5e08 100644 --- a/app/assets/javascripts/components/Input.tsx +++ b/app/assets/javascripts/components/Input.tsx @@ -11,9 +11,9 @@ export const Input: FunctionalComponent = ({ disabled = false, text, }) => { - const base = `rounded py-1.5 px-3 text-input my-1 h-8`; + const base = `rounded py-1.5 px-3 text-input my-1 h-8 bg-contrast`; const stateClasses = disabled - ? 'no-border bg-grey-5' + ? 'no-border' : 'border-solid border-1 border-gray-300'; const classes = `${base} ${stateClasses} ${className}`; return ( diff --git a/app/assets/javascripts/components/shared/ModalDialog.tsx b/app/assets/javascripts/components/shared/ModalDialog.tsx index c9eac7a8c..b7abb82a2 100644 --- a/app/assets/javascripts/components/shared/ModalDialog.tsx +++ b/app/assets/javascripts/components/shared/ModalDialog.tsx @@ -33,11 +33,11 @@ export const ModalDialogLabel: FunctionComponent<{ }> = ({ children, closeDialog }) => (
-
{children}
+
{children}
closeDialog()} /> @@ -61,11 +61,11 @@ export const ModalDialogButtons: FunctionComponent = ({ children }) => (
{children != undefined && Array.isArray(children) ? children.map((child, idx, arr) => ( - <> - {child} - {idx < arr.length - 1 ?
: undefined} - - )) + <> + {child} + {idx < arr.length - 1 ?
: undefined} + + )) : children}
diff --git a/app/assets/javascripts/preferences/components/Content.tsx b/app/assets/javascripts/preferences/components/Content.tsx index fde262846..1b5bcc646 100644 --- a/app/assets/javascripts/preferences/components/Content.tsx +++ b/app/assets/javascripts/preferences/components/Content.tsx @@ -15,7 +15,7 @@ export const Text: FunctionComponent<{ className?: string }> = ({ const buttonClasses = `block bg-default color-text rounded border-solid \ border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content \ -focus:bg-contrast hover:bg-contrast `; +focus:bg-contrast hover:bg-contrast border-neutral`; export const LinkButton: FunctionComponent<{ label: string; diff --git a/app/assets/javascripts/preferences/components/MenuItem.tsx b/app/assets/javascripts/preferences/components/MenuItem.tsx index fefc383ab..66b34d288 100644 --- a/app/assets/javascripts/preferences/components/MenuItem.tsx +++ b/app/assets/javascripts/preferences/components/MenuItem.tsx @@ -15,7 +15,7 @@ export const MenuItem: FunctionComponent = ({ onClick, }) => (
{ e.preventDefault(); onClick(); diff --git a/app/assets/javascripts/preferences/components/PreferencesPane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx index f1a136884..3cea1daa1 100644 --- a/app/assets/javascripts/preferences/components/PreferencesPane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -1,7 +1,7 @@ import { FunctionComponent } from 'preact'; export const PreferencesPane: FunctionComponent = ({ children }) => ( -
+
{children != undefined && Array.isArray(children) diff --git a/app/assets/javascripts/preferences/panes/security-segments/Encryption.tsx b/app/assets/javascripts/preferences/panes/security-segments/Encryption.tsx index b4c9740fe..102af157c 100644 --- a/app/assets/javascripts/preferences/panes/security-segments/Encryption.tsx +++ b/app/assets/javascripts/preferences/panes/security-segments/Encryption.tsx @@ -15,7 +15,7 @@ const EncryptionEnabled: FunctionComponent<{ appState: AppState }> = observer(({ const archived = formatCount(count.archived, 'archived notes'); const deleted = formatCount(count.deleted, 'trashed notes'); - const checkIcon = ; + const checkIcon = ; const noteIcon = ; const tagIcon = ; const archiveIcon = ; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx index 612179c5d..2cfa766d0 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx @@ -16,9 +16,8 @@ const DisclosureIconButton: FunctionComponent<{ @@ -58,7 +57,7 @@ export const AuthAppInfoTooltip: FunctionComponent = () => { />
Some apps, like Google Authenticator, do not back up and restore diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/Bullet.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/Bullet.tsx index 86402fb3d..311e80517 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/Bullet.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Bullet.tsx @@ -3,5 +3,5 @@ import { FunctionComponent } from 'preact'; export const Bullet: FunctionComponent<{ className?: string }> = ({ className = '', }) => ( -
+
); diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 1c853c81e..6ecdf4abe 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -237,6 +237,14 @@ border-color: var(--sn-stylekit-danger-color); } +.bg-inverted-default { + background-color: var(--sn-stylekit-contrast-foreground-color); +} + +.color-inverted-default { + color: var(--sn-stylekit-background-color); +} + .pt-1 { padding-top: 0.25rem; } @@ -267,3 +275,7 @@ padding-top: 2.25rem; padding-bottom: 2.25rem; } + +.select-none { + user-select: none; +} \ No newline at end of file From 7dbfa2fde01fc1078458eec2f764c9936ef10f6f Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Wed, 29 Sep 2021 23:21:48 +0530 Subject: [PATCH 36/71] feat(preferences): Tools segment (#657) --- .../javascripts/preferences/panes/General.tsx | 2 + .../panes/general-segments/Tools.tsx | 86 ++++++++++++ .../panes/general-segments/index.ts | 1 + .../javascripts/views/editor/editor-view.pug | 36 ----- .../javascripts/views/editor/editor_view.ts | 126 +++++++----------- 5 files changed, 140 insertions(+), 111 deletions(-) create mode 100644 app/assets/javascripts/preferences/panes/general-segments/Tools.tsx diff --git a/app/assets/javascripts/preferences/panes/General.tsx b/app/assets/javascripts/preferences/panes/General.tsx index 2598b7a98..b4fa9ad8e 100644 --- a/app/assets/javascripts/preferences/panes/General.tsx +++ b/app/assets/javascripts/preferences/panes/General.tsx @@ -3,6 +3,7 @@ import { AppState } from '@/ui_models/app_state'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; import { ErrorReporting } from './general-segments'; +import { Tools } from './general-segments/Tools'; interface GeneralProps { appState: AppState; @@ -11,6 +12,7 @@ interface GeneralProps { export const General: FunctionComponent = (props) => ( + ); diff --git a/app/assets/javascripts/preferences/panes/general-segments/Tools.tsx b/app/assets/javascripts/preferences/panes/general-segments/Tools.tsx new file mode 100644 index 000000000..3cd46b114 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/general-segments/Tools.tsx @@ -0,0 +1,86 @@ +import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; +import { Switch } from '@/components/Switch'; +import { + PreferencesGroup, + PreferencesSegment, + Subtitle, + Text, + Title, +} from '@/preferences/components'; +import { WebApplication } from '@/ui_models/application'; +import { PrefKey } from '@standardnotes/snjs'; +import { observer } from 'mobx-react-lite'; +import { FunctionalComponent } from 'preact'; +import { useState } from 'preact/hooks'; + +type Props = { + application: WebApplication; +}; + +export const Tools: FunctionalComponent = observer( + ({ application }: Props) => { + const [monospaceFont, setMonospaceFont] = useState(() => + application.getPreference(PrefKey.EditorMonospaceEnabled) + ); + const [marginResizers, setMarginResizers] = useState(() => + application.getPreference(PrefKey.EditorResizersEnabled) + ); + const [spellcheck, setSpellcheck] = useState(() => + application.getPreference(PrefKey.EditorSpellcheck) + ); + + const toggleMonospaceFont = () => { + setMonospaceFont(!monospaceFont); + application.setPreference(PrefKey.EditorMonospaceEnabled, !monospaceFont); + }; + + const toggleMarginResizers = () => { + setMarginResizers(!marginResizers); + application.setPreference(PrefKey.EditorResizersEnabled, !marginResizers); + }; + + const toggleSpellcheck = () => { + setSpellcheck(!spellcheck); + application.setPreference(PrefKey.EditorSpellcheck, !spellcheck); + }; + + return ( + + + Tools +
+
+
+ Monospace Font + Toggles the font style in the Plain Text editor. +
+ +
+ +
+
+ Margin Resizers + Allows left and right editor margins to be resized. +
+ +
+ +
+
+ Spellcheck + + May degrade performance, especially with long notes. Available + in the Plain Text editor and most specialty editors. + +
+ +
+
+
+
+ ); + } +); diff --git a/app/assets/javascripts/preferences/panes/general-segments/index.ts b/app/assets/javascripts/preferences/panes/general-segments/index.ts index 619f11e36..dd28d7a2c 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/index.ts +++ b/app/assets/javascripts/preferences/panes/general-segments/index.ts @@ -1 +1,2 @@ export * from './ErrorReporting'; +export * from './Tools'; diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index d0fe2249c..8c7724f55 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -59,42 +59,6 @@ .sn-component(ng-if='self.note') #editor-menu-bar.sk-app-bar.no-edges .left - .sk-app-bar-item( - click-outside=`self.setMenuState('showOptionsMenu', false)`, - is-open='self.state.showOptionsMenu', - ng-class="{'selected' : self.state.showOptionsMenu}", - ng-click="self.toggleMenu('showOptionsMenu')" - ) - .sk-label Options - .sk-menu-panel.dropdown-menu(ng-if='self.state.showOptionsMenu') - .sk-menu-panel-section - .sk-menu-panel-header - .sk-menu-panel-header-title Global Display - menu-row( - action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMonospace)" - circle="self.state.monospaceFont ? 'success' : 'neutral'", - desc="'Toggles the font style for the default editor'", - disabled='self.state.editorComponent', - label="'Monospace Font'", - subtitle="self.state.editorComponent ? 'Not available with editor extensions' : null" - ) - menu-row( - action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeySpellcheck)" - circle="self.state.spellcheck ? 'success' : 'neutral'", - desc="'Toggles spellcheck for the default editor'", - disabled='self.state.editorComponent', - label="'Spellcheck'", - subtitle=` - self.state.editorComponent - ? 'Not available with editor extensions' - : (self.state.isDesktop ? 'May degrade editor performance' : null) - `) - menu-row( - action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMarginResizers)" - circle="self.state.marginResizersEnabled ? 'success' : 'neutral'", - desc="'Allows for editor left and right margins to be resized'", - label="'Margin Resizers'" - ) .sk-app-bar-item( click-outside=`self.setMenuState('showEditorMenu', false)` is-open='self.state.showEditorMenu', diff --git a/app/assets/javascripts/views/editor/editor_view.ts b/app/assets/javascripts/views/editor/editor_view.ts index 2c3e346af..9d6c282d8 100644 --- a/app/assets/javascripts/views/editor/editor_view.ts +++ b/app/assets/javascripts/views/editor/editor_view.ts @@ -1,6 +1,4 @@ -import { - STRING_SAVING_WHILE_DOCUMENT_HIDDEN, -} from './../../strings'; +import { STRING_SAVING_WHILE_DOCUMENT_HIDDEN } from './../../strings'; import { Editor } from '@/ui_models/editor'; import { WebApplication } from '@/ui_models/application'; import { PanelPuppet, WebDirective } from '@/types'; @@ -61,7 +59,6 @@ type EditorState = { isDesktop?: boolean; syncTakingTooLong: boolean; showActionsMenu: boolean; - showOptionsMenu: boolean; showEditorMenu: boolean; showHistoryMenu: boolean; spellcheck: boolean; @@ -202,7 +199,7 @@ class EditorViewCtrl extends PureViewCtrl { }); this.autorun(() => { this.setState({ - showProtectedWarning: this.appState.notes.showProtectedWarning + showProtectedWarning: this.appState.notes.showProtectedWarning, }); }); } @@ -216,7 +213,6 @@ class EditorViewCtrl extends PureViewCtrl { spellcheck: true, syncTakingTooLong: false, showActionsMenu: false, - showOptionsMenu: false, showEditorMenu: false, showHistoryMenu: false, noteStatus: undefined, @@ -272,11 +268,11 @@ class EditorViewCtrl extends PureViewCtrl { async handleEditorNoteChange() { this.cancelPendingSetStatus(); const note = this.editor.note; - const showProtectedWarning = note.protected && !this.application.hasProtectionSources(); + const showProtectedWarning = + note.protected && !this.application.hasProtectionSources(); this.setShowProtectedWarning(showProtectedWarning); await this.setState({ showActionsMenu: false, - showOptionsMenu: false, showEditorMenu: false, showHistoryMenu: false, noteStatus: undefined, @@ -364,12 +360,7 @@ class EditorViewCtrl extends PureViewCtrl { } closeAllMenus(exclude?: string) { - const allMenus = [ - 'showOptionsMenu', - 'showEditorMenu', - 'showActionsMenu', - 'showHistoryMenu', - ]; + const allMenus = ['showEditorMenu', 'showActionsMenu', 'showHistoryMenu']; const menuState: any = {}; for (const candidate of allMenus) { if (candidate !== exclude) { @@ -591,7 +582,7 @@ class EditorViewCtrl extends PureViewCtrl { } clickedTextArea() { - this.setMenuState('showOptionsMenu', false); + this.closeAllMenus(); } // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -607,12 +598,6 @@ class EditorViewCtrl extends PureViewCtrl { this.lastEditorFocusEventSource = undefined; } - selectedMenuItem(hide: boolean) { - if (hide) { - this.setMenuState('showOptionsMenu', false); - } - } - setShowProtectedWarning(show: boolean) { this.appState.notes.setShowProtectedWarning(show); } @@ -757,13 +742,10 @@ class EditorViewCtrl extends PureViewCtrl { /** @components */ registerComponentHandler() { - this.unregisterComponent = this.application.componentManager!.registerHandler( - { + this.unregisterComponent = + this.application.componentManager!.registerHandler({ identifier: 'editor', - areas: [ - ComponentArea.EditorStack, - ComponentArea.Editor, - ], + areas: [ComponentArea.EditorStack, ComponentArea.Editor], contextRequestHandler: (componentUuid) => { const currentEditor = this.state.editorComponent; if ( @@ -778,8 +760,7 @@ class EditorViewCtrl extends PureViewCtrl { this.closeAllMenus(); } }, - } - ); + }); } async reloadStackComponents() { @@ -809,9 +790,8 @@ class EditorViewCtrl extends PureViewCtrl { } async toggleStackComponentForCurrentItem(component: SNComponent) { - const hidden = this.application.componentManager!.isComponentHidden( - component - ); + const hidden = + this.application.componentManager!.isComponentHidden(component); if (hidden || !component.active) { this.application.componentManager!.setComponentHidden(component, false); await this.associateComponentWithCurrentNote(component); @@ -844,16 +824,14 @@ class EditorViewCtrl extends PureViewCtrl { } registerKeyboardShortcuts() { - this.removeTrashKeyObserver = this.application - .io - .addKeyObserver({ - key: KeyboardKey.Backspace, - notTags: ['INPUT', 'TEXTAREA'], - modifiers: [KeyboardModifier.Meta], - onKeyDown: () => { - this.deleteNote(false); - }, - }); + this.removeTrashKeyObserver = this.application.io.addKeyObserver({ + key: KeyboardKey.Backspace, + notTags: ['INPUT', 'TEXTAREA'], + modifiers: [KeyboardModifier.Meta], + onKeyDown: () => { + this.deleteNote(false); + }, + }); } setScrollPosition() { @@ -883,39 +861,37 @@ class EditorViewCtrl extends PureViewCtrl { const editor = document.getElementById( ElementIds.NoteTextEditor )! as HTMLInputElement; - this.removeTabObserver = this.application - .io - .addKeyObserver({ - element: editor, - key: KeyboardKey.Tab, - onKeyDown: (event) => { - if (document.hidden || this.note.locked || event.shiftKey) { - return; - } - event.preventDefault(); - /** Using document.execCommand gives us undo support */ - const insertSuccessful = document.execCommand( - 'insertText', - false, - '\t' - ); - if (!insertSuccessful) { - /** document.execCommand works great on Chrome/Safari but not Firefox */ - const start = editor.selectionStart!; - const end = editor.selectionEnd!; - const spaces = ' '; - /** Insert 4 spaces */ - editor.value = - editor.value.substring(0, start) + - spaces + - editor.value.substring(end); - /** Place cursor 4 spaces away from where the tab key was pressed */ - editor.selectionStart = editor.selectionEnd = start + 4; - } - this.editorValues.text = editor.value; - this.save(this.note, copyEditorValues(this.editorValues), true); - }, - }); + this.removeTabObserver = this.application.io.addKeyObserver({ + element: editor, + key: KeyboardKey.Tab, + onKeyDown: (event) => { + if (document.hidden || this.note.locked || event.shiftKey) { + return; + } + event.preventDefault(); + /** Using document.execCommand gives us undo support */ + const insertSuccessful = document.execCommand( + 'insertText', + false, + '\t' + ); + if (!insertSuccessful) { + /** document.execCommand works great on Chrome/Safari but not Firefox */ + const start = editor.selectionStart!; + const end = editor.selectionEnd!; + const spaces = ' '; + /** Insert 4 spaces */ + editor.value = + editor.value.substring(0, start) + + spaces + + editor.value.substring(end); + /** Place cursor 4 spaces away from where the tab key was pressed */ + editor.selectionStart = editor.selectionEnd = start + 4; + } + this.editorValues.text = editor.value; + this.save(this.note, copyEditorValues(this.editorValues), true); + }, + }); editor.addEventListener('scroll', this.setScrollPosition); editor.addEventListener('input', this.resetScrollPosition); From 0a3521a25cda2bba66a762f0408596c0a90d39e1 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 30 Sep 2021 08:56:25 -0500 Subject: [PATCH 37/71] deps: snjs@2.14.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab5ad7515..442f0aae2 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.5.2", "@standardnotes/features": "1.6.1", - "@standardnotes/snjs": "2.14.6", + "@standardnotes/snjs": "2.14.8", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12", From b4fcc993aa6d994efadff8a7401d5515eb43fa1b Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 30 Sep 2021 20:50:44 +0530 Subject: [PATCH 38/71] feat(preferences): Defaults segment feat: Dropdown component --- .../javascripts/components/Dropdown.tsx | 45 ++++++++++ .../javascripts/preferences/panes/General.tsx | 4 +- .../panes/general-segments/Defaults.tsx | 59 +++++++++++++ .../panes/general-segments/index.ts | 1 + package.json | 3 +- yarn.lock | 83 ++++++++++++++++++- 6 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/components/Dropdown.tsx create mode 100644 app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx new file mode 100644 index 000000000..d9667c310 --- /dev/null +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -0,0 +1,45 @@ +import { Listbox, ListboxOption } from '@reach/listbox'; +import VisuallyHidden from '@reach/visually-hidden'; +import { FunctionComponent } from 'preact'; +import { IconType } from './Icon'; +import '@reach/listbox/styles.css'; +import { useState } from 'preact/hooks'; + +export type DropdownItem = { + icon?: IconType; + label: string; + value: string; +}; + +type Props = { + id: string; + srLabel: string; + items: DropdownItem[]; + defaultValue: string; +}; + +export const Dropdown: FunctionComponent = ({ + id, + srLabel, + items, + defaultValue, +}) => { + const [value, setValue] = useState(defaultValue); + + const labelId = `${id}-label`; + + return ( + <> + {srLabel} + setValue(value)} + aria-labelledby={labelId} + > + {items.map((item) => ( + {item.label} + ))} + + + ); +}; diff --git a/app/assets/javascripts/preferences/panes/General.tsx b/app/assets/javascripts/preferences/panes/General.tsx index b4fa9ad8e..826485fef 100644 --- a/app/assets/javascripts/preferences/panes/General.tsx +++ b/app/assets/javascripts/preferences/panes/General.tsx @@ -2,8 +2,7 @@ import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; -import { ErrorReporting } from './general-segments'; -import { Tools } from './general-segments/Tools'; +import { ErrorReporting, Tools, Defaults } from './general-segments'; interface GeneralProps { appState: AppState; @@ -13,6 +12,7 @@ interface GeneralProps { export const General: FunctionComponent = (props) => ( + ); diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx new file mode 100644 index 000000000..7f492b854 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -0,0 +1,59 @@ +import { Dropdown, DropdownItem } from '@/components/Dropdown'; +import { + PreferencesGroup, + PreferencesSegment, + Subtitle, + Text, + Title, +} from '@/preferences/components'; +import { WebApplication } from '@/ui_models/application'; +import { ComponentArea } from '@standardnotes/snjs'; +import { FunctionComponent } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; + +type Props = { + application: WebApplication; +}; + +export const Defaults: FunctionComponent = ({ application }) => { + const [editorItems, setEditorItems] = useState([]); + + useEffect(() => { + const editors = application.componentManager + .componentsForArea(ComponentArea.Editor) + .map((editor) => { + return { + label: editor.name, + value: editor.package_info.identifier, + }; + }); + + setEditorItems([ + { + label: 'Plain Editor', + value: 'plain-editor', + }, + ...editors, + ]); + }, [application]); + + return ( + + + Defaults +
+ Default Editor + New notes will be created using this editor. +
+ +
+
+
+
+ ); +}; diff --git a/app/assets/javascripts/preferences/panes/general-segments/index.ts b/app/assets/javascripts/preferences/panes/general-segments/index.ts index dd28d7a2c..42a057b5d 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/index.ts +++ b/app/assets/javascripts/preferences/panes/general-segments/index.ts @@ -1,2 +1,3 @@ export * from './ErrorReporting'; export * from './Tools'; +export * from './Defaults'; diff --git a/package.json b/package.json index 442f0aae2..d0fe0b065 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,9 @@ "@reach/alert-dialog": "^0.13.0", "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", - "@standardnotes/sncrypto-web": "1.5.2", + "@reach/listbox": "^0.16.1", "@standardnotes/features": "1.6.1", + "@standardnotes/sncrypto-web": "1.5.2", "@standardnotes/snjs": "2.14.8", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index 2b15bab0f..62314ca67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1834,6 +1834,14 @@ "@reach/utils" "0.15.2" tslib "^2.3.0" +"@reach/auto-id@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.16.0.tgz#dfabc3227844e8c04f8e6e45203a8e14a8edbaed" + integrity sha512-5ssbeP5bCkM39uVsfQCwBBL+KT8YColdnMN5/Eto6Rj7929ql95R3HZUOkKIvj7mgPtEb60BLQxd1P3o6cjbmg== + dependencies: + "@reach/utils" "0.16.0" + tslib "^2.3.0" + "@reach/checkbox@^0.13.2": version "0.13.2" resolved "https://registry.yarnpkg.com/@reach/checkbox/-/checkbox-0.13.2.tgz#b972b922cf6cea0c2bbabca0129c58307b566a4e" @@ -1853,6 +1861,14 @@ "@reach/utils" "0.15.2" tslib "^2.3.0" +"@reach/descendants@0.16.1": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.16.1.tgz#fa3d89c0503565369707f32985d87eef61985d9f" + integrity sha512-3WZgRnD9O4EORKE31rrduJDiPFNMOjUkATx0zl192ZxMq3qITe4tUj70pS5IbJl/+v9zk78JwyQLvA1pL7XAPA== + dependencies: + "@reach/utils" "0.16.0" + tslib "^2.3.0" + "@reach/dialog@0.13.0", "@reach/dialog@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@reach/dialog/-/dialog-0.13.0.tgz#2110725c3b8a3c64685834cdc9f3ce5c15617809" @@ -1887,6 +1903,18 @@ "@reach/utils" "0.15.2" prop-types "^15.7.2" +"@reach/listbox@^0.16.1": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@reach/listbox/-/listbox-0.16.1.tgz#8a4a13cf171e9ba3118d2e6e72f3e7f17f4c2f80" + integrity sha512-2KQTYmxKvZW0XdBWGoV7E2gUWWBINJmfj2MDRmWbRNdTjsF9w3mTb09buTWQ2sznzF7DrLrwiBFv4b2egMfKOw== + dependencies: + "@reach/auto-id" "0.16.0" + "@reach/descendants" "0.16.1" + "@reach/machine" "0.16.0" + "@reach/popover" "0.16.0" + "@reach/utils" "0.16.0" + prop-types "^15.7.2" + "@reach/machine@0.13.2": version "0.13.2" resolved "https://registry.yarnpkg.com/@reach/machine/-/machine-0.13.2.tgz#744302f5ce2d4e5fd0527ae0baa60d325b2325d8" @@ -1905,6 +1933,15 @@ "@xstate/fsm" "1.4.0" tslib "^2.3.0" +"@reach/machine@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@reach/machine/-/machine-0.16.0.tgz#0504ba47ac09ed495bd341bf5fdd6625bcade0e3" + integrity sha512-c8SRQz2xGtg5M9aXuuM5pFgaV1ZW5/nyMIYpZzBwHUlNFKGO+VBhwedbnqUxO0yLcbgl3wPvjPh740O3YjqiHg== + dependencies: + "@reach/utils" "0.16.0" + "@xstate/fsm" "1.4.0" + tslib "^2.3.0" + "@reach/menu-button@^0.15.1": version "0.15.2" resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.15.2.tgz#00f91402be3ff23d3b4cfe377529f4f9e82a78fe" @@ -1934,6 +1971,17 @@ tabbable "^4.0.0" tslib "^2.3.0" +"@reach/popover@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.16.0.tgz#82c5ab96a88c49e2451a9c04b2d4392a9055f623" + integrity sha512-xmgiSyQwfshMkMNu6URbGrjjDTD3dnAITojvgEqfEtV1chDYqktKdDbIPrq+UGI54ey/IxbRpVzKcIjXiKoMmA== + dependencies: + "@reach/portal" "0.16.0" + "@reach/rect" "0.16.0" + "@reach/utils" "0.16.0" + tabbable "^4.0.0" + tslib "^2.3.0" + "@reach/portal@0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.13.0.tgz#bed220d41097deb1454a7928b22529ba10d3ea2b" @@ -1950,6 +1998,14 @@ "@reach/utils" "0.15.2" tslib "^2.3.0" +"@reach/portal@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.16.0.tgz#1544531d978b770770b718b2872b35652a11e7e3" + integrity sha512-vXJ0O9T+72HiSEWHPs2cx7YbSO7pQsTMhgqPc5aaddIYpo2clJx1PnYuS0lSNlVaDO0IxQhwYq43evXaXnmviw== + dependencies: + "@reach/utils" "0.16.0" + tslib "^2.3.0" + "@reach/rect@0.15.2": version "0.15.2" resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.15.2.tgz#734e3f17a499d6e22bd2ea95856c801c41ed66fd" @@ -1961,6 +2017,17 @@ tiny-warning "^1.0.3" tslib "^2.3.0" +"@reach/rect@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.16.0.tgz#78cf6acefe2e83d3957fa84f938f6e1fc5700f16" + integrity sha512-/qO9jQDzpOCdrSxVPR6l674mRHNTqfEjkaxZHluwJ/2qGUtYsA0GSZiF/+wX/yOWeBif1ycxJDa6HusAMJZC5Q== + dependencies: + "@reach/observe-rect" "1.2.0" + "@reach/utils" "0.16.0" + prop-types "^15.7.2" + tiny-warning "^1.0.3" + tslib "^2.3.0" + "@reach/utils@0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.13.0.tgz#2da775a910d8894bb34e1e94fe95842674f71844" @@ -1996,6 +2063,14 @@ tiny-warning "^1.0.3" tslib "^2.3.0" +"@reach/utils@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.16.0.tgz#5b0777cf16a7cab1ddd4728d5d02762df0ba84ce" + integrity sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q== + dependencies: + tiny-warning "^1.0.3" + tslib "^2.3.0" + "@reach/visually-hidden@0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.13.0.tgz#cace36d9bb80ffb797374fcaea989391b881038f" @@ -2069,10 +2144,10 @@ "@standardnotes/sncrypto-common" "^1.5.2" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.14.6": - version "2.14.6" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.6.tgz#fb3f625ec6f22bbee543ccb6fc69e177311c1a4b" - integrity sha512-/PfuyOv2u4Km29yQi2JXYYUteFmHmLYnbQhk96wYHbCwBiNmIyVdSp0VaA4XgVIuZq32QyhhTayjkexq5Huw/Q== +"@standardnotes/snjs@2.14.8": + version "2.14.8" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.8.tgz#ebd89734d7e846ade96b7d0d81a465418147337d" + integrity sha512-+nfo1civqJlHfrcDeKNJfp2t6o4gs1pkDbjPzT3vULhZUWbOwCeuF1jXHosCxpjnhd+5Wa/oG2KcbKKsqlrrfw== dependencies: "@standardnotes/auth" "3.7.2" "@standardnotes/common" "1.1.0" From 6d47821b8f967f52fea3e43bdf2f4102257928b4 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 00:28:03 +0530 Subject: [PATCH 39/71] feat: Add editor icons --- app/assets/icons/ic-authenticator.svg | 3 +++ app/assets/icons/ic-code.svg | 3 +++ app/assets/icons/ic-markdown.svg | 3 +++ app/assets/icons/ic-menu-arrow-down.svg | 4 ++++ app/assets/icons/ic-spreadsheets.svg | 3 +++ app/assets/icons/ic-tasks.svg | 3 +++ app/assets/icons/ic-text-paragraph.svg | 4 ++++ 7 files changed, 23 insertions(+) create mode 100644 app/assets/icons/ic-authenticator.svg create mode 100644 app/assets/icons/ic-code.svg create mode 100644 app/assets/icons/ic-markdown.svg create mode 100644 app/assets/icons/ic-menu-arrow-down.svg create mode 100644 app/assets/icons/ic-spreadsheets.svg create mode 100644 app/assets/icons/ic-tasks.svg create mode 100644 app/assets/icons/ic-text-paragraph.svg diff --git a/app/assets/icons/ic-authenticator.svg b/app/assets/icons/ic-authenticator.svg new file mode 100644 index 000000000..9a1193919 --- /dev/null +++ b/app/assets/icons/ic-authenticator.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-code.svg b/app/assets/icons/ic-code.svg new file mode 100644 index 000000000..4a871e270 --- /dev/null +++ b/app/assets/icons/ic-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-markdown.svg b/app/assets/icons/ic-markdown.svg new file mode 100644 index 000000000..bceed54b3 --- /dev/null +++ b/app/assets/icons/ic-markdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-menu-arrow-down.svg b/app/assets/icons/ic-menu-arrow-down.svg new file mode 100644 index 000000000..be1d6a489 --- /dev/null +++ b/app/assets/icons/ic-menu-arrow-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-spreadsheets.svg b/app/assets/icons/ic-spreadsheets.svg new file mode 100644 index 000000000..70f175be2 --- /dev/null +++ b/app/assets/icons/ic-spreadsheets.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-tasks.svg b/app/assets/icons/ic-tasks.svg new file mode 100644 index 000000000..0f8ef0587 --- /dev/null +++ b/app/assets/icons/ic-tasks.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/ic-text-paragraph.svg b/app/assets/icons/ic-text-paragraph.svg new file mode 100644 index 000000000..4f43cdc0c --- /dev/null +++ b/app/assets/icons/ic-text-paragraph.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From a98409a95ff6a571d0c0c12f2d0196444758718a Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 00:29:32 +0530 Subject: [PATCH 40/71] feat: Dropdown component feat: Add editor icons feat: Implement default editor selection --- .../javascripts/components/Dropdown.tsx | 77 +++++++++++++--- app/assets/javascripts/components/Icon.tsx | 14 +++ .../panes/general-segments/Defaults.tsx | 90 ++++++++++++++++++- app/assets/stylesheets/_reach-sub.scss | 3 +- app/assets/stylesheets/_sn.scss | 35 +++++++- 5 files changed, 204 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index d9667c310..54b3b64b5 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -1,7 +1,14 @@ -import { Listbox, ListboxOption } from '@reach/listbox'; +import { + ListboxArrow, + ListboxButton, + ListboxInput, + ListboxList, + ListboxOption, + ListboxPopover, +} from '@reach/listbox'; import VisuallyHidden from '@reach/visually-hidden'; import { FunctionComponent } from 'preact'; -import { IconType } from './Icon'; +import { IconType, Icon } from './Icon'; import '@reach/listbox/styles.css'; import { useState } from 'preact/hooks'; @@ -11,35 +18,85 @@ export type DropdownItem = { value: string; }; -type Props = { +type DropdownProps = { id: string; srLabel: string; items: DropdownItem[]; defaultValue: string; + onChange: (value: string) => void; }; -export const Dropdown: FunctionComponent = ({ +type ListboxButtonProps = { + value: string | null; + label: string; + isExpanded: boolean; +}; + +const customDropdownButton: FunctionComponent = ({ + label, + isExpanded, +}) => ( + <> +
{label}
+ + + + +); + +export const Dropdown: FunctionComponent = ({ id, srLabel, items, defaultValue, + onChange, }) => { const [value, setValue] = useState(defaultValue); const labelId = `${id}-label`; + const handleChange = (value: string) => { + setValue(value); + onChange(value); + }; + return ( <> {srLabel} - setValue(value)} + onChange={handleChange} aria-labelledby={labelId} > - {items.map((item) => ( - {item.label} - ))} - + + +
+ + {items.map((item) => ( + + {item.icon ? ( +
+ +
+ ) : null} +
{item.label}
+
+ ))} +
+
+
+ ); }; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index e72804ae2..9e2ca1b1a 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -1,4 +1,5 @@ import PencilOffIcon from '../../icons/ic-pencil-off.svg'; +import PlainTextIcon from '../../icons/ic-text-paragraph.svg'; import RichTextIcon from '../../icons/ic-text-rich.svg'; import TrashIcon from '../../icons/ic-trash.svg'; import PinIcon from '../../icons/ic-pin.svg'; @@ -13,6 +14,12 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg'; import TrashSweepIcon from '../../icons/ic-trash-sweep.svg'; import MoreIcon from '../../icons/ic-more.svg'; import TuneIcon from '../../icons/ic-tune.svg'; +import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg'; +import AuthenticatorIcon from '../../icons/ic-authenticator.svg'; +import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg'; +import TasksIcon from '../../icons/ic-tasks.svg'; +import MarkdownIcon from '../../icons/ic-markdown.svg'; +import CodeIcon from '../../icons/ic-code.svg'; import AccessibilityIcon from '../../icons/ic-accessibility.svg'; import HelpIcon from '../../icons/ic-help.svg'; @@ -35,7 +42,13 @@ import { FunctionalComponent } from 'preact'; const ICONS = { 'pencil-off': PencilOffIcon, + 'plain-text': PlainTextIcon, 'rich-text': RichTextIcon, + code: CodeIcon, + markdown: MarkdownIcon, + authenticator: AuthenticatorIcon, + spreadsheets: SpreadsheetsIcon, + tasks: TasksIcon, trash: TrashIcon, pin: PinIcon, unpin: UnpinIcon, @@ -64,6 +77,7 @@ const ICONS = { check: CheckIcon, 'check-bold': CheckBoldIcon, 'account-circle': AccountCircleIcon, + 'menu-arrow-down': MenuArrowDownIcon, }; export type IconType = keyof typeof ICONS; diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index 7f492b854..5ebb8725f 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -1,4 +1,5 @@ import { Dropdown, DropdownItem } from '@/components/Dropdown'; +import { IconType } from '@/components/Icon'; import { PreferencesGroup, PreferencesSegment, @@ -7,7 +8,11 @@ import { Title, } from '@/preferences/components'; import { WebApplication } from '@/ui_models/application'; -import { ComponentArea } from '@standardnotes/snjs'; +import { + ComponentArea, + ComponentMutator, + SNComponent, +} from '@standardnotes/snjs'; import { FunctionComponent } from 'preact'; import { useEffect, useState } from 'preact/hooks'; @@ -15,21 +20,85 @@ type Props = { application: WebApplication; }; +const getEditorIconType = (name: string): IconType | null => { + switch (name) { + case 'Bold Editor': + case 'Plus Editor': + return 'rich-text'; + case 'TokenVault': + return 'authenticator'; + case 'Secure Spreadsheets': + return 'spreadsheets'; + case 'Task Editor': + return 'tasks'; + case 'Code Editor': + return 'code'; + } + if (name.includes('Markdown')) { + return 'markdown'; + } + return null; +}; + +const makeEditorDefault = ( + application: WebApplication, + component: SNComponent, + currentDefault: SNComponent +) => { + if (currentDefault) { + application.changeItem(currentDefault.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); + } + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = true; + }); +}; + +const removeEditorDefault = ( + application: WebApplication, + component: SNComponent +) => { + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); +}; + +const getDefaultEditor = (application: WebApplication) => { + return application.componentManager + .componentsForArea(ComponentArea.Editor) + .filter((e) => e.isDefaultEditor())[0]; +}; + export const Defaults: FunctionComponent = ({ application }) => { const [editorItems, setEditorItems] = useState([]); + const [defaultEditorValue] = useState( + () => + getDefaultEditor(application)?.package_info?.identifier || 'plain-editor' + ); useEffect(() => { const editors = application.componentManager .componentsForArea(ComponentArea.Editor) + .sort((a, b) => { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }) .map((editor) => { + const iconType = getEditorIconType(editor.name); + return { label: editor.name, value: editor.package_info.identifier, + ...(iconType ? { icon: iconType } : null), }; }); setEditorItems([ { + icon: 'plain-text', label: 'Plain Editor', value: 'plain-editor', }, @@ -37,6 +106,22 @@ export const Defaults: FunctionComponent = ({ application }) => { ]); }, [application]); + const setDefaultEditor = (value: string) => { + const editors = application.componentManager.componentsForArea( + ComponentArea.Editor + ); + const currentDefault = getDefaultEditor(application); + + if (value !== 'plain-editor') { + const editorComponent = editors.filter( + (e) => e.package_info.identifier === value + )[0]; + makeEditorDefault(application, editorComponent, currentDefault); + } else { + removeEditorDefault(application, currentDefault); + } + }; + return ( @@ -49,7 +134,8 @@ export const Defaults: FunctionComponent = ({ application }) => { id="def-editor-dropdown" srLabel="Select the default editor" items={editorItems} - defaultValue="plain-editor" + defaultValue={defaultEditorValue} + onChange={setDefaultEditor} />
diff --git a/app/assets/stylesheets/_reach-sub.scss b/app/assets/stylesheets/_reach-sub.scss index fcb0da138..3d209c899 100644 --- a/app/assets/stylesheets/_reach-sub.scss +++ b/app/assets/stylesheets/_reach-sub.scss @@ -9,14 +9,13 @@ } [data-reach-dialog-overlay]::before { background-color: var(--sn-stylekit-contrast-background-color); - content: ""; + content: ''; position: fixed; top: 0px; right: 0px; bottom: 0px; left: 0px; opacity: 0.75; - } [data-reach-dialog-content] { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 6ecdf4abe..39bdc30ca 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -53,6 +53,29 @@ } } +.sn-dropdown-popover { + z-index: 3001; +} + +.sn-dropdown-button { + @extend .rounded; + @extend .px-3\.5; + @extend .py-1\.75; + @extend .fit-content; + @extend .bg-default; + @extend .text-input; + @extend .color-text; + @extend .border-neutral; + @extend .border-solid; + @extend .border-gray-300; + @extend .border-1; + @extend .min-w-42; +} + +.sn-dropdown-arrow { + @extend .flex; +} + /** Lesser specificity will give priority to reach's styles */ [data-reach-custom-checkbox-container].sn-switch { @extend .duration-150; @@ -114,6 +137,16 @@ &.sn-dropdown-item--no-icon { @extend .py-2; } + + &[data-current-nav] { + @extend .bg-contrast; + @extend .hover\:color-text; + } + + &[data-current-selected] { + background-color: var(--sn-stylekit-info-backdrop-color); + @extend .color-info; + } } .sn-tag { @@ -278,4 +311,4 @@ .select-none { user-select: none; -} \ No newline at end of file +} From 1a92fffcd46941de944f8416060c27e6624a157d Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 11:46:56 +0530 Subject: [PATCH 41/71] feat: Remove Set Default & Undefault buttons --- .../directives/views/editorMenu.ts | 70 ++++--------------- .../templates/directives/editor-menu.pug | 4 -- 2 files changed, 15 insertions(+), 59 deletions(-) diff --git a/app/assets/javascripts/directives/views/editorMenu.ts b/app/assets/javascripts/directives/views/editorMenu.ts index 3f2d0244e..44427d675 100644 --- a/app/assets/javascripts/directives/views/editorMenu.ts +++ b/app/assets/javascripts/directives/views/editorMenu.ts @@ -4,34 +4,30 @@ import { SNComponent, SNItem, ComponentArea } from '@standardnotes/snjs'; import { isDesktopApplication } from '@/utils'; import template from '%/directives/editor-menu.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; -import { ComponentMutator } from '@standardnotes/snjs'; interface EditorMenuScope { - callback: (component: SNComponent) => void - selectedEditorUuid: string - currentItem: SNItem - application: WebApplication + callback: (component: SNComponent) => void; + selectedEditorUuid: string; + currentItem: SNItem; + application: WebApplication; } class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope { - - callback!: () => (component: SNComponent) => void - selectedEditorUuid!: string - currentItem!: SNItem - application!: WebApplication + callback!: () => (component: SNComponent) => void; + selectedEditorUuid!: string; + currentItem!: SNItem; + application!: WebApplication; /* @ngInject */ - constructor( - $timeout: ng.ITimeoutService, - ) { + constructor($timeout: ng.ITimeoutService) { super($timeout); this.state = { - isDesktop: isDesktopApplication() + isDesktop: isDesktopApplication(), }; } public isEditorSelected(editor: SNComponent) { - if(!this.selectedEditorUuid) { + if (!this.selectedEditorUuid) { return false; } return this.selectedEditorUuid === editor.uuid; @@ -43,14 +39,15 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope { $onInit() { super.$onInit(); - const editors = this.application.componentManager!.componentsForArea(ComponentArea.Editor) + const editors = this.application + .componentManager!.componentsForArea(ComponentArea.Editor) .sort((a, b) => { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; }); const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0]; this.setState({ editors: editors, - defaultEditor: defaultEditor + defaultEditor: defaultEditor, }); } @@ -67,46 +64,9 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope { }); } - toggleDefaultForEditor(editor: SNComponent) { - if (this.state.defaultEditor === editor) { - this.removeEditorDefault(editor); - } else { - this.makeEditorDefault(editor); - } - } - offlineAvailableForComponent(component: SNComponent) { return component.local_url && this.state.isDesktop; } - - makeEditorDefault(component: SNComponent) { - const currentDefault = this.application.componentManager! - .componentsForArea(ComponentArea.Editor) - .filter((e) => e.isDefaultEditor())[0]; - if (currentDefault) { - this.application.changeItem(currentDefault.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); - } - this.application.changeAndSaveItem(component.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = true; - }); - this.setState({ - defaultEditor: component - }); - } - - removeEditorDefault(component: SNComponent) { - this.application.changeAndSaveItem(component.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); - this.setState({ - defaultEditor: null - }); - } } export class EditorMenu extends WebDirective { @@ -121,7 +81,7 @@ export class EditorMenu extends WebDirective { callback: '&', selectedEditorUuid: '=', currentItem: '=', - application: '=' + application: '=', }; } } diff --git a/app/assets/templates/directives/editor-menu.pug b/app/assets/templates/directives/editor-menu.pug index 67710575f..7d278faca 100644 --- a/app/assets/templates/directives/editor-menu.pug +++ b/app/assets/templates/directives/editor-menu.pug @@ -11,11 +11,7 @@ menu-row( ng-repeat='editor in self.state.editors track by editor.uuid' action='self.selectComponent(editor)', - button-action='self.toggleDefaultForEditor(editor)', - button-class="self.isEditorSelected(editor) ? 'warning' : 'info'", - button-text="self.isEditorDefault(editor) ? 'Undefault' : 'Set Default'", circle="self.isEditorSelected(editor) && 'success'", - has-button='self.isEditorSelected(editor) || isEditorDefault(editor)', label='editor.name', subtitle="self.isEditorSelected(editor) && 'Version ' + editor.package_info.version", ) From 47b49aa7a978896ccd20ddc5b350ccfa9405d837 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 13:06:00 +0530 Subject: [PATCH 42/71] feat: Add support for icons in Dropdown --- .../javascripts/components/Dropdown.tsx | 24 ++++++++++++++++--- app/assets/stylesheets/_sn.scss | 11 ++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index 54b3b64b5..093914de7 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -27,6 +27,7 @@ type DropdownProps = { }; type ListboxButtonProps = { + icon?: IconType; value: string | null; label: string; isExpanded: boolean; @@ -35,12 +36,20 @@ type ListboxButtonProps = { const customDropdownButton: FunctionComponent = ({ label, isExpanded, + icon, }) => ( <> -
{label}
+
+ {icon ? ( +
+ +
+ ) : null} +
{label}
+
@@ -74,7 +83,16 @@ export const Dropdown: FunctionComponent = ({ > { + const current = items.find((item) => item.value === value); + const icon = current ? current?.icon : null; + return customDropdownButton({ + value, + label, + isExpanded, + ...(icon ? { icon } : null), + }); + }} />
diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 39bdc30ca..a17f5fbf0 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -69,11 +69,20 @@ @extend .border-solid; @extend .border-gray-300; @extend .border-1; - @extend .min-w-42; + @extend .min-w-55; +} + +.sn-dropdown-button-label { + @extend .flex; + @extend .items-center; } .sn-dropdown-arrow { @extend .flex; + + &.sn-dropdown-arrow-flipped { + transform: rotate(180deg); + } } /** Lesser specificity will give priority to reach's styles */ From b165be2928f87640066fba67f7f8104440f89c14 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 13:23:52 +0530 Subject: [PATCH 43/71] fix: Fix dropdown list colors --- app/assets/stylesheets/_sn.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index a17f5fbf0..e1ff02453 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -55,6 +55,10 @@ .sn-dropdown-popover { z-index: 3001; + + &[data-reach-listbox-popover] { + background: var(--sn-stylekit-background-color); + } } .sn-dropdown-button { @@ -147,6 +151,10 @@ @extend .py-2; } + .sn-dropdown-popover & { + @extend .bg-default; + } + &[data-current-nav] { @extend .bg-contrast; @extend .hover\:color-text; From db888c4e0246346e55c794e0fbff1babfda3b604 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 20:04:39 +0530 Subject: [PATCH 44/71] fix: Fix dropdown item colors --- app/assets/javascripts/components/Dropdown.tsx | 1 - app/assets/stylesheets/_sn.scss | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index 093914de7..46a65a909 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -9,7 +9,6 @@ import { import VisuallyHidden from '@reach/visually-hidden'; import { FunctionComponent } from 'preact'; import { IconType, Icon } from './Icon'; -import '@reach/listbox/styles.css'; import { useState } from 'preact/hooks'; export type DropdownItem = { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index e1ff02453..d42e5bdaa 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -156,6 +156,7 @@ } &[data-current-nav] { + color: var(--sn-stylekit-contrast-foreground-color); @extend .bg-contrast; @extend .hover\:color-text; } From b1d95808d13dc3f903ed2ce0fb55651b7731451b Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 20:07:33 +0530 Subject: [PATCH 45/71] fix: Selected item bg color --- app/assets/stylesheets/_sn.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index d42e5bdaa..76e54101b 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -161,7 +161,7 @@ @extend .hover\:color-text; } - &[data-current-selected] { + .sn-dropdown-popover &[data-current-selected] { background-color: var(--sn-stylekit-info-backdrop-color); @extend .color-info; } From f6c019b63c7ab5ac9ba2d030dabad3e08b1baf6d Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 20:38:58 +0530 Subject: [PATCH 46/71] feat: Remove references of default editor --- app/assets/javascripts/directives/views/editorMenu.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/assets/javascripts/directives/views/editorMenu.ts b/app/assets/javascripts/directives/views/editorMenu.ts index 44427d675..2c41dc302 100644 --- a/app/assets/javascripts/directives/views/editorMenu.ts +++ b/app/assets/javascripts/directives/views/editorMenu.ts @@ -33,10 +33,6 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope { return this.selectedEditorUuid === editor.uuid; } - public isEditorDefault(editor: SNComponent) { - return this.state.defaultEditor?.uuid === editor.uuid; - } - $onInit() { super.$onInit(); const editors = this.application @@ -44,10 +40,8 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope { .sort((a, b) => { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; }); - const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0]; this.setState({ editors: editors, - defaultEditor: defaultEditor, }); } From c48d9f22302dee82c20734f36a42adbd805cf556 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 20:39:52 +0530 Subject: [PATCH 47/71] refactor: Use identifier instead of name feat: Create EditorIdentifier enum --- .../panes/general-segments/Defaults.tsx | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index 5ebb8725f..e6e8f6127 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -20,23 +20,39 @@ type Props = { application: WebApplication; }; -const getEditorIconType = (name: string): IconType | null => { - switch (name) { - case 'Bold Editor': - case 'Plus Editor': +enum EditorIdentifier { + PlainEditor = 'plain-editor', + BoldEditor = 'org.standardnotes.bold-editor', + CodeEditor = 'org.standardnotes.code-editor', + MarkdownBasic = 'org.standardnotes.simple-markdown-editor', + MarkdownMath = 'org.standardnotes.fancy-markdown-editor', + MarkdownMinimist = 'org.standardnotes.minimal-markdown-editor', + MarkdownPro = 'org.standardnotes.advanced-markdown-editor', + PlusEditor = 'org.standardnotes.plus-editor', + SecureSpreadsheets = 'org.standardnotes.standard-sheets', + TaskEditor = 'org.standardnotes.simple-task-editor', + TokenVault = 'org.standardnotes.token-vault', +} + +const getEditorIconType = (identifier: string): IconType | null => { + switch (identifier) { + case EditorIdentifier.BoldEditor: + case EditorIdentifier.PlusEditor: return 'rich-text'; - case 'TokenVault': + case EditorIdentifier.MarkdownBasic: + case EditorIdentifier.MarkdownMath: + case EditorIdentifier.MarkdownMinimist: + case EditorIdentifier.MarkdownPro: + return 'markdown'; + case EditorIdentifier.TokenVault: return 'authenticator'; - case 'Secure Spreadsheets': + case EditorIdentifier.SecureSpreadsheets: return 'spreadsheets'; - case 'Task Editor': + case EditorIdentifier.TaskEditor: return 'tasks'; - case 'Code Editor': + case EditorIdentifier.CodeEditor: return 'code'; } - if (name.includes('Markdown')) { - return 'markdown'; - } return null; }; @@ -87,11 +103,12 @@ export const Defaults: FunctionComponent = ({ application }) => { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; }) .map((editor) => { - const iconType = getEditorIconType(editor.name); + const identifier = editor.package_info.identifier; + const iconType = getEditorIconType(identifier); return { label: editor.name, - value: editor.package_info.identifier, + value: identifier, ...(iconType ? { icon: iconType } : null), }; }); From b4b1e4260668d40372345603cfb2c658050457d0 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 20:46:00 +0530 Subject: [PATCH 48/71] refactor: Use enum instead of string --- .../preferences/panes/general-segments/Defaults.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index e6e8f6127..f3f15acc3 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -93,7 +93,8 @@ export const Defaults: FunctionComponent = ({ application }) => { const [editorItems, setEditorItems] = useState([]); const [defaultEditorValue] = useState( () => - getDefaultEditor(application)?.package_info?.identifier || 'plain-editor' + getDefaultEditor(application)?.package_info?.identifier || + EditorIdentifier.PlainEditor ); useEffect(() => { @@ -117,7 +118,7 @@ export const Defaults: FunctionComponent = ({ application }) => { { icon: 'plain-text', label: 'Plain Editor', - value: 'plain-editor', + value: EditorIdentifier.PlainEditor, }, ...editors, ]); @@ -129,7 +130,7 @@ export const Defaults: FunctionComponent = ({ application }) => { ); const currentDefault = getDefaultEditor(application); - if (value !== 'plain-editor') { + if (value !== EditorIdentifier.PlainEditor) { const editorComponent = editors.filter( (e) => e.package_info.identifier === value )[0]; From e71c5cc85d41f080894f8725f90b6947ba5c94d5 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 20:50:29 +0530 Subject: [PATCH 49/71] feat: Use lighter border on Dropdown --- app/assets/stylesheets/_sn.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 76e54101b..3567bff84 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -69,7 +69,6 @@ @extend .bg-default; @extend .text-input; @extend .color-text; - @extend .border-neutral; @extend .border-solid; @extend .border-gray-300; @extend .border-1; From 7aea7f330ca152b818ada0326593bd36868b0730 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 21:07:26 +0530 Subject: [PATCH 50/71] refactor: Sort editor array after concat --- .../panes/general-segments/Defaults.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index f3f15acc3..6c3bb2423 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -100,9 +100,6 @@ export const Defaults: FunctionComponent = ({ application }) => { useEffect(() => { const editors = application.componentManager .componentsForArea(ComponentArea.Editor) - .sort((a, b) => { - return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; - }) .map((editor) => { const identifier = editor.package_info.identifier; const iconType = getEditorIconType(identifier); @@ -112,16 +109,19 @@ export const Defaults: FunctionComponent = ({ application }) => { value: identifier, ...(iconType ? { icon: iconType } : null), }; + }) + .concat([ + { + icon: 'plain-text', + label: 'Plain Editor', + value: EditorIdentifier.PlainEditor, + }, + ]) + .sort((a, b) => { + return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1; }); - setEditorItems([ - { - icon: 'plain-text', - label: 'Plain Editor', - value: EditorIdentifier.PlainEditor, - }, - ...editors, - ]); + setEditorItems(editors); }, [application]); const setDefaultEditor = (value: string) => { From ce1c51905304af72072b49820c76f2dc09b5949e Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 21:15:38 +0530 Subject: [PATCH 51/71] refactor: Move functions inside component refactor: Remove application param as it is already present in component props --- .../panes/general-segments/Defaults.tsx | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index 6c3bb2423..a3620b812 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -56,47 +56,43 @@ const getEditorIconType = (identifier: string): IconType | null => { return null; }; -const makeEditorDefault = ( - application: WebApplication, - component: SNComponent, - currentDefault: SNComponent -) => { - if (currentDefault) { - application.changeItem(currentDefault.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); - } - application.changeAndSaveItem(component.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = true; - }); -}; - -const removeEditorDefault = ( - application: WebApplication, - component: SNComponent -) => { - application.changeAndSaveItem(component.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); -}; - -const getDefaultEditor = (application: WebApplication) => { - return application.componentManager - .componentsForArea(ComponentArea.Editor) - .filter((e) => e.isDefaultEditor())[0]; -}; - export const Defaults: FunctionComponent = ({ application }) => { const [editorItems, setEditorItems] = useState([]); const [defaultEditorValue] = useState( () => - getDefaultEditor(application)?.package_info?.identifier || + getDefaultEditor()?.package_info?.identifier || EditorIdentifier.PlainEditor ); + const makeEditorDefault = ( + component: SNComponent, + currentDefault: SNComponent + ) => { + if (currentDefault) { + application.changeItem(currentDefault.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); + } + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = true; + }); + }; + + const removeEditorDefault = (component: SNComponent) => { + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); + }; + + const getDefaultEditor = () => { + return application.componentManager + .componentsForArea(ComponentArea.Editor) + .filter((e) => e.isDefaultEditor())[0]; + }; + useEffect(() => { const editors = application.componentManager .componentsForArea(ComponentArea.Editor) @@ -128,15 +124,15 @@ export const Defaults: FunctionComponent = ({ application }) => { const editors = application.componentManager.componentsForArea( ComponentArea.Editor ); - const currentDefault = getDefaultEditor(application); + const currentDefault = getDefaultEditor(); if (value !== EditorIdentifier.PlainEditor) { const editorComponent = editors.filter( (e) => e.package_info.identifier === value )[0]; - makeEditorDefault(application, editorComponent, currentDefault); + makeEditorDefault(editorComponent, currentDefault); } else { - removeEditorDefault(application, currentDefault); + removeEditorDefault(currentDefault); } }; From 24289a9322f7c74962818561630625e124ea41a1 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 1 Oct 2021 21:33:40 +0530 Subject: [PATCH 52/71] Revert "refactor: Move functions inside component" This reverts commit ce1c51905304af72072b49820c76f2dc09b5949e. --- .../panes/general-segments/Defaults.tsx | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index a3620b812..6c3bb2423 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -56,43 +56,47 @@ const getEditorIconType = (identifier: string): IconType | null => { return null; }; +const makeEditorDefault = ( + application: WebApplication, + component: SNComponent, + currentDefault: SNComponent +) => { + if (currentDefault) { + application.changeItem(currentDefault.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); + } + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = true; + }); +}; + +const removeEditorDefault = ( + application: WebApplication, + component: SNComponent +) => { + application.changeAndSaveItem(component.uuid, (m) => { + const mutator = m as ComponentMutator; + mutator.defaultEditor = false; + }); +}; + +const getDefaultEditor = (application: WebApplication) => { + return application.componentManager + .componentsForArea(ComponentArea.Editor) + .filter((e) => e.isDefaultEditor())[0]; +}; + export const Defaults: FunctionComponent = ({ application }) => { const [editorItems, setEditorItems] = useState([]); const [defaultEditorValue] = useState( () => - getDefaultEditor()?.package_info?.identifier || + getDefaultEditor(application)?.package_info?.identifier || EditorIdentifier.PlainEditor ); - const makeEditorDefault = ( - component: SNComponent, - currentDefault: SNComponent - ) => { - if (currentDefault) { - application.changeItem(currentDefault.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); - } - application.changeAndSaveItem(component.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = true; - }); - }; - - const removeEditorDefault = (component: SNComponent) => { - application.changeAndSaveItem(component.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); - }; - - const getDefaultEditor = () => { - return application.componentManager - .componentsForArea(ComponentArea.Editor) - .filter((e) => e.isDefaultEditor())[0]; - }; - useEffect(() => { const editors = application.componentManager .componentsForArea(ComponentArea.Editor) @@ -124,15 +128,15 @@ export const Defaults: FunctionComponent = ({ application }) => { const editors = application.componentManager.componentsForArea( ComponentArea.Editor ); - const currentDefault = getDefaultEditor(); + const currentDefault = getDefaultEditor(application); if (value !== EditorIdentifier.PlainEditor) { const editorComponent = editors.filter( (e) => e.package_info.identifier === value )[0]; - makeEditorDefault(editorComponent, currentDefault); + makeEditorDefault(application, editorComponent, currentDefault); } else { - removeEditorDefault(currentDefault); + removeEditorDefault(application, currentDefault); } }; From d89f6bfac189c5c53fd9564b4041306e79e4e966 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Mon, 4 Oct 2021 11:04:08 +0200 Subject: [PATCH 53/71] fix: remove unused vars in account menu (#659) --- app/assets/javascripts/components/AccountMenu/index.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index 0213e5c5b..14bdacecf 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -6,11 +6,6 @@ import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; import Authentication from '@/components/AccountMenu/Authentication'; import Footer from '@/components/AccountMenu/Footer'; import User from '@/components/AccountMenu/User'; -import Encryption from '@/components/AccountMenu/Encryption'; -import Protections from '@/components/AccountMenu/Protections'; -import PasscodeLock from '@/components/AccountMenu/PasscodeLock'; -import DataBackup from '@/components/AccountMenu/DataBackup'; -import ErrorReporting from '@/components/AccountMenu/ErrorReporting'; import { useEffect } from 'preact/hooks'; type Props = { From fc226043fac6bf7c24e61fb86b1a06ead7c07a52 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 4 Oct 2021 19:00:57 +0530 Subject: [PATCH 54/71] Update app/assets/javascripts/components/Dropdown.tsx Co-authored-by: Antonella Sgarlatta --- app/assets/javascripts/components/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index 46a65a909..8169c43f0 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -32,7 +32,7 @@ type ListboxButtonProps = { isExpanded: boolean; }; -const customDropdownButton: FunctionComponent = ({ +const CustomDropdownButton: FunctionComponent = ({ label, isExpanded, icon, From 170234ce843ba1ca59faf05a02e006cc7987a147 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 4 Oct 2021 19:12:11 +0530 Subject: [PATCH 55/71] refactor: Use PascalCase naming --- app/assets/javascripts/components/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index 8169c43f0..cfb9c7839 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -85,7 +85,7 @@ export const Dropdown: FunctionComponent = ({ children={({ value, label, isExpanded }) => { const current = items.find((item) => item.value === value); const icon = current ? current?.icon : null; - return customDropdownButton({ + return CustomDropdownButton({ value, label, isExpanded, From 980ab9358df2f1368c8449eebc531ae4500b3fa8 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 4 Oct 2021 19:12:37 +0530 Subject: [PATCH 56/71] refactor: Use removeEditorDefault --- .../preferences/panes/general-segments/Defaults.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index 6c3bb2423..6231e74b8 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -62,10 +62,7 @@ const makeEditorDefault = ( currentDefault: SNComponent ) => { if (currentDefault) { - application.changeItem(currentDefault.uuid, (m) => { - const mutator = m as ComponentMutator; - mutator.defaultEditor = false; - }); + removeEditorDefault(application, currentDefault); } application.changeAndSaveItem(component.uuid, (m) => { const mutator = m as ComponentMutator; From f233c87b0f89c12414ffb180506d14e44870c042 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 4 Oct 2021 20:22:19 +0530 Subject: [PATCH 57/71] refactor: Change srLabel to label --- app/assets/javascripts/components/Dropdown.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index cfb9c7839..e06994bae 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -19,7 +19,7 @@ export type DropdownItem = { type DropdownProps = { id: string; - srLabel: string; + label: string; items: DropdownItem[]; defaultValue: string; onChange: (value: string) => void; @@ -58,7 +58,7 @@ const CustomDropdownButton: FunctionComponent = ({ export const Dropdown: FunctionComponent = ({ id, - srLabel, + label, items, defaultValue, onChange, @@ -74,7 +74,7 @@ export const Dropdown: FunctionComponent = ({ return ( <> - {srLabel} + {label} Date: Mon, 4 Oct 2021 20:25:09 +0530 Subject: [PATCH 58/71] refactor: Fix label prop --- .../javascripts/preferences/panes/general-segments/Defaults.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx index 6231e74b8..51a6062d1 100644 --- a/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/preferences/panes/general-segments/Defaults.tsx @@ -147,7 +147,7 @@ export const Defaults: FunctionComponent = ({ application }) => {
Date: Mon, 4 Oct 2021 17:52:33 +0200 Subject: [PATCH 59/71] fix: adjust footer element height so it doesn't produce a scrollbar (#663) --- app/assets/javascripts/views/footer/footer-view.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/views/footer/footer-view.pug b/app/assets/javascripts/views/footer/footer-view.pug index 94bbb985f..a8d7183bf 100644 --- a/app/assets/javascripts/views/footer/footer-view.pug +++ b/app/assets/javascripts/views/footer/footer-view.pug @@ -6,7 +6,7 @@ is-open='ctrl.showAccountMenu', ng-click='ctrl.accountMenuPressed()' ) - .w-8.h-8.flex.items-center.justify-center.cursor-pointer.rounded-full( + .w-8.h-full.flex.items-center.justify-center.cursor-pointer.rounded-full( ng-class="ctrl.showAccountMenu ? 'bg-border' : '' " ) .w-5.h-5( @@ -26,7 +26,7 @@ ng-click='ctrl.clickPreferences()' ng-if='ctrl.appState.enableUnfinishedFeatures' ) - .w-8.h-8.flex.items-center.justify-center.cursor-pointer + .w-8.h-full.flex.items-center.justify-center.cursor-pointer .h-5 icon( type="tune" From e134598c20ba6fe3c73936813312759f0fb77bfa Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 4 Oct 2021 17:03:20 -0300 Subject: [PATCH 60/71] feat: open purchase flow --- .../account/subscription/NoSubscription.tsx | 78 ++++++++++++++----- .../account/subscription/Subscription.tsx | 2 +- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx index 5042ea992..c0e901a7b 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx @@ -1,23 +1,63 @@ import { FunctionalComponent } from "preact"; import { Text } from '@/preferences/components'; import { Button } from '@/components/Button'; +import { WebApplication } from "@/ui_models/application"; +import { useState } from "preact/hooks"; +import { isDesktopApplication } from "@/utils"; -export const NoSubscription: FunctionalComponent = () => ( - <> - You don't have a Standard Notes subscription yet. -
-
- -); +export const NoSubscription: FunctionalComponent<{ + application: WebApplication; +}> = ({ application }) => { + const [isLoadingPurchaseFlow, setIsLoadingPurchaseFlow] = useState(false); + const [purchaseFlowError, setPurchaseFlowError] = useState(undefined); + + const onPurchaseClick = async () => { + const errorMessage = 'There was an error when attempting to redirect you to the subscription page.'; + setIsLoadingPurchaseFlow(true); + try { + const url = await application.getPurchaseFlowUrl(); + if (url) { + const currentUrl = window.location.href; + const successUrl = isDesktopApplication() ? `standardnotes://${currentUrl}` : currentUrl; + console.log(successUrl); + window.location.assign(`${url}&success_url=${successUrl}`); + } else { + setPurchaseFlowError(errorMessage); + } + } catch (e) { + setPurchaseFlowError(errorMessage); + } finally { + setIsLoadingPurchaseFlow(false); + } + }; + + return ( + <> + You don't have a Standard Notes subscription yet. + {isLoadingPurchaseFlow && ( + + Redirecting you to the subscription page... + + )} + {purchaseFlowError && ( + + {purchaseFlowError} + + )} +
+
+ + ); +}; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx index a063d1a06..0e9ce9126 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -77,7 +77,7 @@ export const Subscription: FunctionComponent = observer(({ ) : userSubscription && userSubscription.endsAt > now ? ( ) : ( - + )}
From 3cba208b0703e7656a335f94f34e243313e5fa0f Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 4 Oct 2021 17:10:20 -0300 Subject: [PATCH 61/71] feat: enable websocket connection --- app/assets/javascripts/ui_models/application.ts | 2 ++ app/assets/javascripts/ui_models/application_group.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index 99161903f..8e219e130 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -55,6 +55,7 @@ export class WebApplication extends SNApplication { scope: angular.IScope, defaultSyncServerHost: string, public bridge: Bridge, + webSocketUrl: string, ) { super( bridge.environment, @@ -67,6 +68,7 @@ export class WebApplication extends SNApplication { defaultSyncServerHost, AppVersion, isDev, + webSocketUrl, ); this.$compile = $compile; this.scope = scope; diff --git a/app/assets/javascripts/ui_models/application_group.ts b/app/assets/javascripts/ui_models/application_group.ts index 795e7530a..283c43397 100644 --- a/app/assets/javascripts/ui_models/application_group.ts +++ b/app/assets/javascripts/ui_models/application_group.ts @@ -64,6 +64,7 @@ export class ApplicationGroup extends SNApplicationGroup { scope, this.defaultSyncServerHost, this.bridge, + this.webSocketUrl, ); const appState = new AppState( this.$rootScope, From aa40354a311d2a5ee2563d0bea3145dd31bb0328 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 4 Oct 2021 20:34:08 -0300 Subject: [PATCH 62/71] chore(version-snjs): 2.14.11 --- package.json | 2 +- yarn.lock | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 442f0aae2..909ca1ec5 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.5.2", "@standardnotes/features": "1.6.1", - "@standardnotes/snjs": "2.14.8", + "@standardnotes/snjs": "2.14.11", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12", diff --git a/yarn.lock b/yarn.lock index 2b15bab0f..3402204fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2025,7 +2025,12 @@ dependencies: "@standardnotes/common" "^1.1.0" -"@standardnotes/common@1.1.0", "@standardnotes/common@^1.1.0": +"@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== @@ -2069,13 +2074,13 @@ "@standardnotes/sncrypto-common" "^1.5.2" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.14.6": - version "2.14.6" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.6.tgz#fb3f625ec6f22bbee543ccb6fc69e177311c1a4b" - integrity sha512-/PfuyOv2u4Km29yQi2JXYYUteFmHmLYnbQhk96wYHbCwBiNmIyVdSp0VaA4XgVIuZq32QyhhTayjkexq5Huw/Q== +"@standardnotes/snjs@2.14.11": + version "2.14.11" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.11.tgz#e80a23df763c44d463fe6b73ea0b12522afad9e7" + integrity sha512-L33rhU/NK2ev+o8QXWbVT7jZtVwIwvNwT46N/3Lv5RQXkY7wa1yxO08NHt4BfHqrmXI3mF5qnBH3mcYoOtBY7A== dependencies: "@standardnotes/auth" "3.7.2" - "@standardnotes/common" "1.1.0" + "@standardnotes/common" "1.2.0" "@standardnotes/domain-events" "2.1.0" "@standardnotes/features" "1.6.2" "@standardnotes/settings" "1.2.0" From 1190ebbe41e69e05424e5ef736d463b61a91d9fc Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 6 Oct 2021 16:35:43 -0300 Subject: [PATCH 63/71] fix: use enable unfinished features env var to set enableV4 --- app/assets/javascripts/app.ts | 3 +++ app/assets/javascripts/startApplication.ts | 1 + app/assets/javascripts/ui_models/application.ts | 3 ++- app/assets/javascripts/ui_models/application_group.ts | 4 +++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index aeb0c2f5f..6b43d3aa0 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -89,6 +89,7 @@ function reloadHiddenFirefoxTab(): boolean { const startApplication: StartApplication = async function startApplication( defaultSyncServerHost: string, bridge: Bridge, + enableUnfinishedFeatures: boolean, webSocketUrl: string, ) { if (reloadHiddenFirefoxTab()) { @@ -107,6 +108,7 @@ const startApplication: StartApplication = async function startApplication( .constant('bridge', bridge) .constant('defaultSyncServerHost', defaultSyncServerHost) .constant('appVersion', bridge.appVersion) + .constant('enableUnfinishedFeatures', enableUnfinishedFeatures) .constant('webSocketUrl', webSocketUrl); // Controllers @@ -194,6 +196,7 @@ if (IsWebPlatform) { (window as any)._default_sync_server, new BrowserBridge(AppVersion), (window as any)._websocket_url, + (window as any)._enable_unfinished_features, ); } else { (window as any).startApplication = startApplication; diff --git a/app/assets/javascripts/startApplication.ts b/app/assets/javascripts/startApplication.ts index a3f5b1668..334973948 100644 --- a/app/assets/javascripts/startApplication.ts +++ b/app/assets/javascripts/startApplication.ts @@ -3,5 +3,6 @@ import { Bridge } from "./services/bridge"; export type StartApplication = ( defaultSyncServerHost: string, bridge: Bridge, + enableUnfinishedFeatures: boolean, webSocketUrl: string, ) => Promise; diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index 8e219e130..a11dd17d4 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -55,6 +55,7 @@ export class WebApplication extends SNApplication { scope: angular.IScope, defaultSyncServerHost: string, public bridge: Bridge, + enableUnfinishedFeatures: boolean, webSocketUrl: string, ) { super( @@ -67,7 +68,7 @@ export class WebApplication extends SNApplication { [], defaultSyncServerHost, AppVersion, - isDev, + enableUnfinishedFeatures, webSocketUrl, ); this.$compile = $compile; diff --git a/app/assets/javascripts/ui_models/application_group.ts b/app/assets/javascripts/ui_models/application_group.ts index 283c43397..b9230eb1f 100644 --- a/app/assets/javascripts/ui_models/application_group.ts +++ b/app/assets/javascripts/ui_models/application_group.ts @@ -29,7 +29,8 @@ export class ApplicationGroup extends SNApplicationGroup { $timeout: ng.ITimeoutService, private defaultSyncServerHost: string, private bridge: Bridge, - private webSocketUrl: string + private enableUnfinishedFeatures: boolean, + private webSocketUrl: string, ) { super(new WebDeviceInterface($timeout, bridge)); this.$compile = $compile; @@ -64,6 +65,7 @@ export class ApplicationGroup extends SNApplicationGroup { scope, this.defaultSyncServerHost, this.bridge, + this.enableUnfinishedFeatures, this.webSocketUrl, ); const appState = new AppState( From 92699d23f4d7e241836ac0ad52e5bdaee0b6155e Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 7 Oct 2021 08:08:38 -0500 Subject: [PATCH 64/71] fix: func param order --- app/assets/javascripts/app.ts | 8 ++++---- .../panes/account/subscription/NoSubscription.tsx | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 6b43d3aa0..971f793eb 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -90,7 +90,7 @@ const startApplication: StartApplication = async function startApplication( defaultSyncServerHost: string, bridge: Bridge, enableUnfinishedFeatures: boolean, - webSocketUrl: string, + webSocketUrl: string ) { if (reloadHiddenFirefoxTab()) { return; @@ -193,10 +193,10 @@ const startApplication: StartApplication = async function startApplication( if (IsWebPlatform) { startApplication( - (window as any)._default_sync_server, + (window as any)._default_sync_server as string, new BrowserBridge(AppVersion), - (window as any)._websocket_url, - (window as any)._enable_unfinished_features, + (window as any)._enable_unfinished_features as boolean, + (window as any)._websocket_url as string, ); } else { (window as any).startApplication = startApplication; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx index c0e901a7b..071331823 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx @@ -19,7 +19,6 @@ export const NoSubscription: FunctionalComponent<{ if (url) { const currentUrl = window.location.href; const successUrl = isDesktopApplication() ? `standardnotes://${currentUrl}` : currentUrl; - console.log(successUrl); window.location.assign(`${url}&success_url=${successUrl}`); } else { setPurchaseFlowError(errorMessage); From 7b6c99d1883ab9a11d776dd5afa57ad77065daff Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Fri, 8 Oct 2021 09:20:46 +0200 Subject: [PATCH 65/71] feat: extension manager in preferences (#670) * feat: add extensions pane * fix: rename extensions folder for MacOS compatibility * feat: extension toggles and uninstall * feat: implement extension renaming, activation, deactivation and UI/UX fixes * feat(preferences): improve extension item design * feat(preferences): hide custom extension input when installation confirmed --- .../preferences/PreferencesMenu.ts | 4 +- .../preferences/PreferencesView.tsx | 3 + .../preferences/panes/Extensions.tsx | 112 ++++++++++ .../ConfirmCustomExtension.tsx | 80 +++++++ .../extensions-segments/ExtensionItem.tsx | 195 ++++++++++++++++++ .../panes/extensions-segments/index.ts | 2 + 6 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/preferences/panes/Extensions.tsx create mode 100644 app/assets/javascripts/preferences/panes/extensions-segments/ConfirmCustomExtension.tsx create mode 100644 app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx create mode 100644 app/assets/javascripts/preferences/panes/extensions-segments/index.ts diff --git a/app/assets/javascripts/preferences/PreferencesMenu.ts b/app/assets/javascripts/preferences/PreferencesMenu.ts index 2d1c75856..d538eb404 100644 --- a/app/assets/javascripts/preferences/PreferencesMenu.ts +++ b/app/assets/javascripts/preferences/PreferencesMenu.ts @@ -6,6 +6,7 @@ const PREFERENCE_IDS = [ 'account', 'appearance', 'security', + 'extensions', 'listed', 'shortcuts', 'accessibility', @@ -28,6 +29,7 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ { id: 'account', label: 'Account', icon: 'user' }, { id: 'appearance', label: 'Appearance', icon: 'themes' }, { id: 'security', label: 'Security', icon: 'security' }, + { id: 'extensions', label: 'Extensions', icon: 'tune' }, { id: 'listed', label: 'Listed', icon: 'listed' }, { id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' }, { id: 'accessibility', label: 'Accessibility', icon: 'accessibility' }, @@ -65,7 +67,7 @@ export class PreferencesMenu { ); } - selectPane(key: PreferenceId) { + selectPane(key: PreferenceId): void { this._selectedPane = key; } } diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 74839c310..e6b2b47bd 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -9,6 +9,7 @@ import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { AppState } from '@/ui_models/app_state'; import { useEffect } from 'preact/hooks'; +import { Extensions } from './panes/Extensions'; interface PreferencesProps extends MfaProps { application: WebApplication; @@ -40,6 +41,8 @@ const PaneSelector: FunctionComponent< application={props.application} /> ); + case 'extensions': + return ; case 'listed': return ; case 'shortcuts': diff --git a/app/assets/javascripts/preferences/panes/Extensions.tsx b/app/assets/javascripts/preferences/panes/Extensions.tsx new file mode 100644 index 000000000..185cdb31e --- /dev/null +++ b/app/assets/javascripts/preferences/panes/Extensions.tsx @@ -0,0 +1,112 @@ +import { ContentType, SNComponent } from '@standardnotes/snjs'; +import { Button } from '@/components/Button'; +import { DecoratedInput } from '@/components/DecoratedInput'; +import { WebApplication } from '@/ui_models/application'; +import { FunctionComponent } from 'preact'; +import { + Title, + PreferencesGroup, + PreferencesPane, + PreferencesSegment, +} from '../components'; +import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments'; +import { useEffect, useRef, useState } from 'preact/hooks'; + +const loadExtensions = (application: WebApplication) => application.getItems([ + ContentType.ActionsExtension, + ContentType.Component, + ContentType.Theme, +]) as SNComponent[]; + +export const Extensions: FunctionComponent<{ + application: WebApplication +}> = ({ application }) => { + + const [customUrl, setCustomUrl] = useState(''); + const [confirmableExtension, setConfirmableExtension] = useState(undefined); + const [extensions, setExtensions] = useState(loadExtensions(application)); + + const confirmableEnd = useRef(null); + + useEffect(() => { + if (confirmableExtension) { + confirmableEnd.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [confirmableExtension, confirmableEnd]); + + const uninstallExtension = async (extension: SNComponent) => { + await application.deleteItem(extension); + setExtensions(loadExtensions(application)); + }; + + const submitExtensionUrl = async (url: string) => { + const component = await application.downloadExternalFeature(url); + if (component) { + setConfirmableExtension(component); + } + }; + + const handleConfirmExtensionSubmit = async (confirm: boolean) => { + if (confirm) { + confirmExtension(); + } + setConfirmableExtension(undefined); + setCustomUrl(''); + }; + + const confirmExtension = async () => { + await application.insertItem(confirmableExtension as SNComponent); + setExtensions(loadExtensions(application)); + }; + + const toggleActivateExtension = (extension: SNComponent) => { + application.toggleComponent(extension); + setExtensions(loadExtensions(application)); + }; + + return ( + + + { + extensions + .filter(extension => extension.package_info.identifier !== 'org.standardnotes.extensions-manager') + .sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase())) + .map((extension, i) => ( + + )) + } + + + + {!confirmableExtension && + + Install Custom Extension +
+ { setCustomUrl(value); }} + /> +
+
+ + + ); +}; diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx new file mode 100644 index 000000000..d6d535bb3 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx @@ -0,0 +1,195 @@ +import { FunctionComponent } from "preact"; +import { SNComponent } from "@standardnotes/snjs"; + +import { PreferencesSegment, Subtitle, Title } from "@/preferences/components"; +import { Switch } from "@/components/Switch"; +import { WebApplication } from "@/ui_models/application"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { Button } from "@/components/Button"; + +const ExtensionVersions: FunctionComponent<{ + extension: SNComponent +}> = ({ extension }) => { + return ( +
+
+ Installed version {extension.package_info.version} +
+
+ ); +}; + +const AutoUpdateLocal: FunctionComponent<{ + autoupdateDisabled: boolean, + toggleAutoupdate: () => void +}> = ({ autoupdateDisabled, toggleAutoupdate }) => ( +
+ Autoupdate local installation + +
+); + +const UseHosted: FunctionComponent<{ + offlineOnly: boolean, toggleOfllineOnly: () => void +}> = ({ offlineOnly, toggleOfllineOnly }) => ( +
+ Use hosted when local is unavailable + +
+); + +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<{ + application: WebApplication, + extension: SNComponent, + first: boolean, + uninstall: (extension: SNComponent) => void, + toggleActivate: (extension: SNComponent) => void, +}> = ({ application, extension, first, uninstall, toggleActivate }) => { + 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 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 isEditorOrTags = ['editor-stack', 'tags-list'].includes(extension.area); + + return ( + + {first && <> + Extensions +
+ } + + +
+ + + + {localInstallable && } + {localInstallable && } + + {isEditorOrTags || isExternal && + <> +
+
+ {isEditorOrTags && ( + <> + {extension.active ? +
+ + } + + ); +}; diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/index.ts b/app/assets/javascripts/preferences/panes/extensions-segments/index.ts new file mode 100644 index 000000000..20694952c --- /dev/null +++ b/app/assets/javascripts/preferences/panes/extensions-segments/index.ts @@ -0,0 +1,2 @@ +export * from './ConfirmCustomExtension'; +export * from './ExtensionItem'; From f1122f292ecf8f44a1b8f4a711bc5d56a0069cdf Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 8 Oct 2021 21:48:31 +0530 Subject: [PATCH 66/71] feat: New account menu and text input with icon & toggle (#665) * feat: Add new icons * Revert "feat: Add new icons" This reverts commit 0acb403fe846dbb2e48fd22de35c3568c3cb4453. * feat: Add new icons for account menu * feat: Add new Icons * feat: Add "currentPane" state to prefs view * feat: Update account menu to new design * feat: Add input component with icon & toggle * fix: sync icon & function * fix: Fix eye icon * feat: Create re-usable checkbox feat: Add "merge local" option * feat: Allow using className on IconButton * feat: Add disabled state on input feat: Make toggle circle * refactor: Move checkbox to components * feat: Handle invalid email/password error * feat: Implement new design for Create Account * feat: Implement new account menu design * feat: Add disabled option to IconButton * feat: Set account menu pane from other component * feat: Add 2fa account menu pane feat: Add lock icon * feat: Remove unnecessary 2FA menu pane feat: Reset current menu pane on clickOutside * feat: Change "Log in" to "Sign in" * feat: Remove sync from footer * feat: Change "Login" to "Sign in" feat: Add spinner to "Syncing..." refactor: Use then-catch-finally for sync * feat: Use common enableCustomServer state * feat: Animate account menu closing * fix: Reset menu pane only after it's closed * feat: Add keyDown handler to InputWithIcon * feat: Handle Enter press in inputs * Update app/assets/javascripts/components/InputWithIcon.tsx Co-authored-by: Antonella Sgarlatta * Update app/assets/javascripts/components/InputWithIcon.tsx Co-authored-by: Antonella Sgarlatta * refactor: Use server state from AccountMenuState * Update app/assets/javascripts/components/AccountMenu/CreateAccount.tsx Co-authored-by: Antonella Sgarlatta * Update app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx Co-authored-by: Antonella Sgarlatta * feat: Use common AdvancedOptions * feat: Add "eye-off" icon and toggle state * feat: Allow undefined values * refactor: Remove enableCustomServer state * feat: Persist server option state * feat: Add bottom-100 and cursor-auto util classes refactor: Use bottom-100 and cursor-auto classes * refactor: Invert ternary operator * refactor: Remove unused imports * refactor: Use toggled as prop instead of state * refactor: Change "Log in/out" to "Sign in/out" * refactor: Change "Login" to "Sign in" * refactor: Remove hardcoded width/height * refactor: Use success class * feat: Remove hardcoded width & height from svg * fix: Fix chevron-down icon Co-authored-by: Antonella Sgarlatta Co-authored-by: Antonella Sgarlatta --- app/assets/icons/ic-accessibility.svg | 2 +- app/assets/icons/ic-arrow-left.svg | 4 + app/assets/icons/ic-check-circle.svg | 4 + app/assets/icons/ic-chevron-down.svg | 4 + app/assets/icons/ic-cloud-off.svg | 4 + app/assets/icons/ic-copy.svg | 2 +- app/assets/icons/ic-download.svg | 2 +- app/assets/icons/ic-email.svg | 4 + app/assets/icons/ic-eye-off.svg | 4 + app/assets/icons/ic-eye.svg | 4 + app/assets/icons/ic-help.svg | 2 +- app/assets/icons/ic-info.svg | 2 +- app/assets/icons/ic-keyboard.svg | 2 +- app/assets/icons/ic-listed.svg | 2 +- app/assets/icons/ic-lock.svg | 4 + app/assets/icons/ic-security.svg | 2 +- app/assets/icons/ic-server.svg | 4 + app/assets/icons/ic-settings.svg | 2 +- app/assets/icons/ic-signin.svg | 4 + app/assets/icons/ic-signout.svg | 4 + app/assets/icons/ic-star.svg | 2 +- app/assets/icons/ic-sync.svg | 4 + app/assets/icons/ic-themes.svg | 2 +- app/assets/icons/ic-user.svg | 2 +- .../AccountMenu/AdvancedOptions.tsx | 73 ++++++ .../components/AccountMenu/Authentication.tsx | 147 +++++++----- .../AccountMenu/ConfirmPassword.tsx | 172 +++++++++++++ .../components/AccountMenu/CreateAccount.tsx | 137 +++++++++++ .../components/AccountMenu/Footer.tsx | 41 ++-- .../AccountMenu/GeneralAccountMenu.tsx | 166 +++++++++++++ .../components/AccountMenu/SignIn.tsx | 227 ++++++++++++++++++ .../components/AccountMenu/index.tsx | 147 ++++++++---- .../javascripts/components/Checkbox.tsx | 32 +++ app/assets/javascripts/components/Icon.tsx | 24 ++ .../javascripts/components/IconButton.tsx | 7 +- .../javascripts/components/InputWithIcon.tsx | 89 +++++++ ...onsLogout.tsx => OtherSessionsSignOut.tsx} | 22 +- .../directives/views/passwordWizard.ts | 114 ++++----- .../preferences/PreferencesView.tsx | 22 +- .../preferences/panes/AccountPreferences.tsx | 6 +- .../panes/account/Authentication.tsx | 98 +++++--- .../{LogOutView.tsx => SignOutView.tsx} | 44 ++-- .../preferences/panes/account/Sync.tsx | 93 +++---- .../panes/account/changePassword/index.tsx | 34 ++- .../preferences/panes/account/index.ts | 2 +- .../ui_models/app_state/account_menu_state.ts | 86 +++++-- .../ui_models/app_state/preferences_state.ts | 9 + .../javascripts/views/footer/footer-view.pug | 11 +- .../javascripts/views/footer/footer_view.ts | 36 +-- app/assets/stylesheets/_menus.scss | 12 + app/assets/stylesheets/_sn.scss | 50 ++++ 51 files changed, 1566 insertions(+), 407 deletions(-) create mode 100644 app/assets/icons/ic-arrow-left.svg create mode 100644 app/assets/icons/ic-check-circle.svg create mode 100644 app/assets/icons/ic-chevron-down.svg create mode 100644 app/assets/icons/ic-cloud-off.svg create mode 100644 app/assets/icons/ic-email.svg create mode 100644 app/assets/icons/ic-eye-off.svg create mode 100644 app/assets/icons/ic-eye.svg create mode 100644 app/assets/icons/ic-lock.svg create mode 100644 app/assets/icons/ic-server.svg create mode 100644 app/assets/icons/ic-signin.svg create mode 100644 app/assets/icons/ic-signout.svg create mode 100644 app/assets/icons/ic-sync.svg create mode 100644 app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/CreateAccount.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/SignIn.tsx create mode 100644 app/assets/javascripts/components/Checkbox.tsx create mode 100644 app/assets/javascripts/components/InputWithIcon.tsx rename app/assets/javascripts/components/{OtherSessionsLogout.tsx => OtherSessionsSignOut.tsx} (77%) rename app/assets/javascripts/preferences/panes/account/{LogOutView.tsx => SignOutView.tsx} (69%) diff --git a/app/assets/icons/ic-accessibility.svg b/app/assets/icons/ic-accessibility.svg index de1e6249e..ffb780043 100644 --- a/app/assets/icons/ic-accessibility.svg +++ b/app/assets/icons/ic-accessibility.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-arrow-left.svg b/app/assets/icons/ic-arrow-left.svg new file mode 100644 index 000000000..f80337cff --- /dev/null +++ b/app/assets/icons/ic-arrow-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-check-circle.svg b/app/assets/icons/ic-check-circle.svg new file mode 100644 index 000000000..ddb5737f7 --- /dev/null +++ b/app/assets/icons/ic-check-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-chevron-down.svg b/app/assets/icons/ic-chevron-down.svg new file mode 100644 index 000000000..1c89552e6 --- /dev/null +++ b/app/assets/icons/ic-chevron-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-cloud-off.svg b/app/assets/icons/ic-cloud-off.svg new file mode 100644 index 000000000..49015714d --- /dev/null +++ b/app/assets/icons/ic-cloud-off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-copy.svg b/app/assets/icons/ic-copy.svg index 9ad40e8f1..694626a33 100644 --- a/app/assets/icons/ic-copy.svg +++ b/app/assets/icons/ic-copy.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-download.svg b/app/assets/icons/ic-download.svg index de2c70fc2..923b753bd 100644 --- a/app/assets/icons/ic-download.svg +++ b/app/assets/icons/ic-download.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-email.svg b/app/assets/icons/ic-email.svg new file mode 100644 index 000000000..378c18e0c --- /dev/null +++ b/app/assets/icons/ic-email.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-eye-off.svg b/app/assets/icons/ic-eye-off.svg new file mode 100644 index 000000000..76cf09013 --- /dev/null +++ b/app/assets/icons/ic-eye-off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-eye.svg b/app/assets/icons/ic-eye.svg new file mode 100644 index 000000000..9248599f6 --- /dev/null +++ b/app/assets/icons/ic-eye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-help.svg b/app/assets/icons/ic-help.svg index c312b7255..eaed4c3f7 100644 --- a/app/assets/icons/ic-help.svg +++ b/app/assets/icons/ic-help.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-info.svg b/app/assets/icons/ic-info.svg index 47ea73219..14107de40 100644 --- a/app/assets/icons/ic-info.svg +++ b/app/assets/icons/ic-info.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-keyboard.svg b/app/assets/icons/ic-keyboard.svg index 9a18af39c..8068326fd 100644 --- a/app/assets/icons/ic-keyboard.svg +++ b/app/assets/icons/ic-keyboard.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-listed.svg b/app/assets/icons/ic-listed.svg index 03e347717..3bac23a5a 100644 --- a/app/assets/icons/ic-listed.svg +++ b/app/assets/icons/ic-listed.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-lock.svg b/app/assets/icons/ic-lock.svg new file mode 100644 index 000000000..946b674fc --- /dev/null +++ b/app/assets/icons/ic-lock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-security.svg b/app/assets/icons/ic-security.svg index badc9d1ad..dfa4b37cc 100644 --- a/app/assets/icons/ic-security.svg +++ b/app/assets/icons/ic-security.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-server.svg b/app/assets/icons/ic-server.svg new file mode 100644 index 000000000..faa3ea19e --- /dev/null +++ b/app/assets/icons/ic-server.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-settings.svg b/app/assets/icons/ic-settings.svg index cc14f94a8..2191bea9a 100644 --- a/app/assets/icons/ic-settings.svg +++ b/app/assets/icons/ic-settings.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-signin.svg b/app/assets/icons/ic-signin.svg new file mode 100644 index 000000000..f211b7c45 --- /dev/null +++ b/app/assets/icons/ic-signin.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-signout.svg b/app/assets/icons/ic-signout.svg new file mode 100644 index 000000000..3b66c862a --- /dev/null +++ b/app/assets/icons/ic-signout.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-star.svg b/app/assets/icons/ic-star.svg index 638dae331..f74b0d567 100644 --- a/app/assets/icons/ic-star.svg +++ b/app/assets/icons/ic-star.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-sync.svg b/app/assets/icons/ic-sync.svg new file mode 100644 index 000000000..b93f0ba9d --- /dev/null +++ b/app/assets/icons/ic-sync.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/icons/ic-themes.svg b/app/assets/icons/ic-themes.svg index 88606ca76..33abb061a 100644 --- a/app/assets/icons/ic-themes.svg +++ b/app/assets/icons/ic-themes.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/icons/ic-user.svg b/app/assets/icons/ic-user.svg index 65ac58800..1bdfaf61f 100644 --- a/app/assets/icons/ic-user.svg +++ b/app/assets/icons/ic-user.svg @@ -1,3 +1,3 @@ - + diff --git a/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx b/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx new file mode 100644 index 000000000..82865ede9 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx @@ -0,0 +1,73 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useState } from 'preact/hooks'; +import { Checkbox } from '../Checkbox'; +import { Icon } from '../Icon'; +import { InputWithIcon } from '../InputWithIcon'; + +type Props = { + application: WebApplication; + appState: AppState; + disabled?: boolean; +}; + +export const AdvancedOptions: FunctionComponent = observer( + ({ appState, application, disabled = false, children }) => { + const { server, setServer, enableServerOption, setEnableServerOption } = + appState.accountMenu; + const [showAdvanced, setShowAdvanced] = useState(false); + + const handleServerOptionChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setEnableServerOption(e.target.checked); + } + }; + + const handleSyncServerChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setServer(e.target.value); + application.setCustomHost(e.target.value); + } + }; + + const toggleShowAdvanced = () => { + setShowAdvanced(!showAdvanced); + }; + + return ( + <> +
+ Advanced options + +
+ + {showAdvanced ? ( +
+ {children} + + +
+ ) : null} + + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/Authentication.tsx b/app/assets/javascripts/components/AccountMenu/Authentication.tsx index a3b42a0b8..cc401159a 100644 --- a/app/assets/javascripts/components/AccountMenu/Authentication.tsx +++ b/app/assets/javascripts/components/AccountMenu/Authentication.tsx @@ -3,7 +3,7 @@ import { STRING_ACCOUNT_MENU_UNCHECK_MERGE, STRING_GENERATING_LOGIN_KEYS, STRING_GENERATING_REGISTER_KEYS, - STRING_NON_MATCHING_PASSWORDS + STRING_NON_MATCHING_PASSWORDS, } from '@/strings'; import { JSXInternal } from 'preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; @@ -17,13 +17,9 @@ import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; appState: AppState; -} - -const Authentication = observer(({ - application, - appState, - }: Props) => { +}; +const Authentication = observer(({ application, appState }: Props) => { const [showAdvanced, setShowAdvanced] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false); const [email, setEmail] = useState(''); @@ -39,12 +35,12 @@ const Authentication = observer(({ const { server, notesAndTagsCount, - showLogin, + showSignIn, showRegister, - setShowLogin, + setShowSignIn, setShowRegister, setServer, - closeAccountMenu + closeAccountMenu, } = appState.accountMenu; const user = application.getUser(); @@ -58,11 +54,11 @@ const Authentication = observer(({ // Reset password and confirmation fields when hiding the form useEffect(() => { - if (!showLogin && !showRegister) { + if (!showSignIn && !showRegister) { setPassword(''); setPasswordConfirmation(''); } - }, [showLogin, showRegister]); + }, [showSignIn, showRegister]); const handleHostInputChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; @@ -75,7 +71,7 @@ const Authentication = observer(({ const passwordConfirmationInputRef = useRef(); const handleSignInClick = () => { - setShowLogin(true); + setShowSignIn(true); setIsEmailFocused(true); }; @@ -90,7 +86,7 @@ const Authentication = observer(({ passwordConfirmationInputRef.current?.blur(); }; - const login = async () => { + const signin = async () => { setStatus(STRING_GENERATING_LOGIN_KEYS); setIsAuthenticating(true); @@ -105,13 +101,13 @@ const Authentication = observer(({ if (!error) { setIsAuthenticating(false); setPassword(''); - setShowLogin(false); + setShowSignIn(false); closeAccountMenu(); return; } - setShowLogin(true); + setShowSignIn(true); setStatus(undefined); setPassword(''); @@ -150,10 +146,11 @@ const Authentication = observer(({ } }; - const handleAuthFormSubmit = (event: - TargetedEvent | - TargetedMouseEvent | - TargetedKeyboardEvent + const handleAuthFormSubmit = ( + event: + | TargetedEvent + | TargetedMouseEvent + | TargetedKeyboardEvent ) => { event.preventDefault(); @@ -163,8 +160,8 @@ const Authentication = observer(({ blurAuthFields(); - if (showLogin) { - login(); + if (showSignIn) { + signin(); } else { register(); } @@ -186,19 +183,23 @@ const Authentication = observer(({ setEmail(value); }; - const handlePasswordConfirmationChange = (event: TargetedEvent) => { + const handlePasswordConfirmationChange = ( + event: TargetedEvent + ) => { const { value } = event.target as HTMLInputElement; setPasswordConfirmation(value); }; - const handleMergeLocalData = async (event: TargetedEvent) => { + const handleMergeLocalData = async ( + event: TargetedEvent + ) => { const { checked } = event.target as HTMLInputElement; setShouldMergeLocal(checked); if (!checked) { const confirmResult = await confirmDialog({ text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, - confirmButtonStyle: 'danger' + confirmButtonStyle: 'danger', }); setShouldMergeLocal(!confirmResult); } @@ -206,10 +207,12 @@ const Authentication = observer(({ return ( <> - {!user && !showLogin && !showRegister && ( + {!user && !showSignIn && !showRegister && (
-
Sign in or register to enable sync and end-to-end encryption.
+
+ Sign in or register to enable sync and end-to-end encryption. +
- Standard Notes is free on every platform, and comes - standard with sync and encryption. + Standard Notes is free on every platform, and comes standard with + sync and encryption.
)} - {(showLogin || showRegister) && ( + {(showSignIn || showRegister) && (
- {showLogin ? 'Sign In' : 'Register'} + {showSignIn ? 'Sign In' : 'Register'}
-
+
- {showRegister && - } + {showRegister && ( + + )}
@@ -301,24 +310,28 @@ const Authentication = observer(({ required />
- {showLogin && ( + {showSignIn && ( )} @@ -327,9 +340,12 @@ const Authentication = observer(({ )} {!isAuthenticating && (
-
)} @@ -337,9 +353,9 @@ const Authentication = observer(({
No Password Reset.
- Because your notes are encrypted using your password, - Standard Notes does not have a password reset option. - You cannot forget your password. + Because your notes are encrypted using your password, Standard + Notes does not have a password reset option. You cannot forget + your password.
)} @@ -358,7 +374,7 @@ const Authentication = observer(({ setIsEphemeral(prevState => !prevState)} + onChange={() => setIsEphemeral((prevState) => !prevState)} />

Stay signed in

@@ -371,7 +387,9 @@ const Authentication = observer(({ checked={shouldMergeLocal} onChange={handleMergeLocalData} /> -

Merge local data ({notesAndTagsCount}) notes and tags

+

+ Merge local data ({notesAndTagsCount}) notes and tags +

)} @@ -379,7 +397,8 @@ const Authentication = observer(({ )}
- )} + )} + ); }); diff --git a/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx b/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx new file mode 100644 index 000000000..5511e2cae --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx @@ -0,0 +1,172 @@ +import { STRING_NON_MATCHING_PASSWORDS } from '@/strings'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { StateUpdater, useRef, useState } from 'preact/hooks'; +import { AccountMenuPane } from '.'; +import { Button } from '../Button'; +import { Checkbox } from '../Checkbox'; +import { IconButton } from '../IconButton'; +import { InputWithIcon } from '../InputWithIcon'; +import { AdvancedOptions } from './AdvancedOptions'; + +type Props = { + appState: AppState; + application: WebApplication; + setMenuPane: (pane: AccountMenuPane) => void; + email: string; + password: string; + setPassword: StateUpdater; +}; + +export const ConfirmPassword: FunctionComponent = observer( + ({ application, appState, setMenuPane, email, password, setPassword }) => { + const { notesAndTagsCount } = appState.accountMenu; + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isRegistering, setIsRegistering] = useState(false); + const [isEphemeral, setIsEphemeral] = useState(false); + const [shouldMergeLocal, setShouldMergeLocal] = useState(true); + + const passwordInputRef = useRef(); + + const handlePasswordChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setConfirmPassword(e.target.value); + } + }; + + const handleEphemeralChange = () => { + setIsEphemeral(!isEphemeral); + }; + + const handleShouldMergeChange = () => { + setShouldMergeLocal(!shouldMergeLocal); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleConfirmFormSubmit(e); + } + }; + + const handleConfirmFormSubmit = (e: Event) => { + e.preventDefault(); + + if (!password) { + passwordInputRef?.current.focus(); + return; + } + + if (password === confirmPassword) { + setIsRegistering(true); + application + .register(email, password, isEphemeral, shouldMergeLocal) + .then((res) => { + if (res.error) { + throw new Error(res.error.message); + } + appState.accountMenu.closeAccountMenu(); + appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu); + }) + .catch((err) => { + console.error(err); + application.alertService.alert(err).finally(() => { + setPassword(''); + handleGoBack(); + }); + }) + .finally(() => { + setIsRegistering(false); + }); + } else { + application.alertService + .alert(STRING_NON_MATCHING_PASSWORDS) + .finally(() => { + setConfirmPassword(''); + passwordInputRef?.current.focus(); + }); + } + }; + + const handleGoBack = () => { + setMenuPane(AccountMenuPane.Register); + }; + + return ( + <> +
+ +
Confirm password
+
+
+ Because your notes are encrypted using your password,{' '} + + Standard Notes does not have a password reset option + + . If you forget your password, you will permanently lose access to + your data. +
+
+ +
- {(showLogin || showRegister) && ( - Cancel + {(showSignIn || showRegister) && ( + + Cancel + )} - {!showLogin && !showRegister && ( + {!showSignIn && !showRegister && ( {user ? 'Sign out' : 'Clear session data'} diff --git a/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx b/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx new file mode 100644 index 000000000..81fd0f5ea --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/GeneralAccountMenu.tsx @@ -0,0 +1,166 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { Icon } from '../Icon'; +import { formatLastSyncDate } from '@/preferences/panes/account/Sync'; +import { SyncQueueStrategy } from '@standardnotes/snjs'; +import { STRING_GENERIC_SYNC_ERROR } from '@/strings'; +import { useState } from 'preact/hooks'; +import { AccountMenuPane } from '.'; +import { FunctionComponent } from 'preact'; + +type Props = { + appState: AppState; + application: WebApplication; + setMenuPane: (pane: AccountMenuPane) => void; + closeMenu: () => void; +}; + +const iconClassName = 'color-grey-1 mr-2'; + +export const GeneralAccountMenu: FunctionComponent = observer( + ({ application, appState, setMenuPane, closeMenu }) => { + const [isSyncingInProgress, setIsSyncingInProgress] = useState(false); + const [lastSyncDate, setLastSyncDate] = useState( + formatLastSyncDate(application.getLastSyncDate() as Date) + ); + + const doSynchronization = async () => { + setIsSyncingInProgress(true); + + application + .sync({ + queueStrategy: SyncQueueStrategy.ForceSpawnNew, + checkIntegrity: true, + }) + .then((res) => { + if (res && res.error) { + throw new Error(); + } else { + setLastSyncDate( + formatLastSyncDate(application.getLastSyncDate() as Date) + ); + } + }) + .catch(() => { + application.alertService.alert(STRING_GENERIC_SYNC_ERROR); + }) + .finally(() => { + setIsSyncingInProgress(false); + }); + }; + + const user = application.getUser(); + + return ( + <> +
+
Account
+
+ +
+
+ {user ? ( + <> +
+
You're signed in as:
+
{user.email}
+
+
+ {isSyncingInProgress ? ( +
+
+ Syncing... +
+ ) : ( +
+ + Last synced: {lastSyncDate} +
+ )} +
+ +
+
+ + ) : ( + <> +
+
+ You’re offline. Sign in to sync your notes and preferences + across all your devices and enable end-to-end encryption. +
+
+ + Offline +
+
+ + )} +
+ {user ? ( + + ) : ( + <> + + + + )} + + {user ? ( + <> +
+ + + ) : null} + + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/SignIn.tsx b/app/assets/javascripts/components/AccountMenu/SignIn.tsx new file mode 100644 index 000000000..3b1ab702f --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/SignIn.tsx @@ -0,0 +1,227 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { AccountMenuPane } from '.'; +import { Button } from '../Button'; +import { Checkbox } from '../Checkbox'; +import { Icon } from '../Icon'; +import { IconButton } from '../IconButton'; +import { InputWithIcon } from '../InputWithIcon'; +import { AdvancedOptions } from './AdvancedOptions'; + +type Props = { + appState: AppState; + application: WebApplication; + setMenuPane: (pane: AccountMenuPane) => void; +}; + +export const SignInPane: FunctionComponent = observer( + ({ application, appState, setMenuPane }) => { + const { notesAndTagsCount } = appState.accountMenu; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isInvalid, setIsInvalid] = useState(false); + const [isEphemeral, setIsEphemeral] = useState(false); + const [isStrictSignin, setIsStrictSignin] = useState(false); + const [isSigningIn, setIsSigningIn] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [shouldMergeLocal, setShouldMergeLocal] = useState(true); + + const emailInputRef = useRef(); + const passwordInputRef = useRef(); + + useEffect(() => { + if (emailInputRef?.current) { + emailInputRef.current.focus(); + } + }, []); + + const resetInvalid = () => { + if (isInvalid) { + setIsInvalid(false); + } + }; + + const handleEmailChange = (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setEmail(e.target.value); + } + }; + + const handlePasswordChange = (e: Event) => { + if (isInvalid) { + setIsInvalid(false); + } + if (e.target instanceof HTMLInputElement) { + setPassword(e.target.value); + } + }; + + const handleEphemeralChange = () => { + setIsEphemeral(!isEphemeral); + }; + + const handleStrictSigninChange = () => { + setIsStrictSignin(!isStrictSignin); + }; + + const handleShouldMergeChange = () => { + setShouldMergeLocal(!shouldMergeLocal); + }; + + const signIn = () => { + setIsSigningIn(true); + emailInputRef?.current.blur(); + passwordInputRef?.current.blur(); + + application + .signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal) + .then((res) => { + if (res.error) { + throw new Error(res.error.message); + } + appState.accountMenu.closeAccountMenu(); + }) + .catch((err) => { + console.error(err); + if (err.toString().includes('Invalid email or password')) { + setIsInvalid(true); + } else { + application.alertService.alert(err); + } + setPassword(''); + passwordInputRef?.current.blur(); + }) + .finally(() => { + setIsSigningIn(false); + }); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSignInFormSubmit(e); + } + }; + + const handleSignInFormSubmit = (e: Event) => { + e.preventDefault(); + + if (!email || email.length === 0) { + emailInputRef?.current.focus(); + return; + } + + if (!password || password.length === 0) { + passwordInputRef?.current.focus(); + return; + } + + signIn(); + }; + + return ( + <> +
+ setMenuPane(AccountMenuPane.GeneralMenu)} + focusable={true} + disabled={isSigningIn} + /> +
Sign in
+
+
+
+ + + {isInvalid ? ( +
+ Invalid email or password. +
+ ) : null} +
+
+
+ +
+ + + + +
+
+ + ); + } +); diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index 14bdacecf..77da5608c 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -2,72 +2,115 @@ import { observer } from 'mobx-react-lite'; import { toDirective } from '@/components/utils'; import { AppState } from '@/ui_models/app_state'; import { WebApplication } from '@/ui_models/application'; -import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; -import Authentication from '@/components/AccountMenu/Authentication'; -import Footer from '@/components/AccountMenu/Footer'; -import User from '@/components/AccountMenu/User'; -import { useEffect } from 'preact/hooks'; +import { useState } from 'preact/hooks'; +import { GeneralAccountMenu } from './GeneralAccountMenu'; +import { FunctionComponent } from 'preact'; +import { SignInPane } from './SignIn'; +import { CreateAccount } from './CreateAccount'; +import { ConfirmSignoutContainer } from '../ConfirmSignoutModal'; +import { ConfirmPassword } from './ConfirmPassword'; + +export enum AccountMenuPane { + GeneralMenu, + SignIn, + Register, + ConfirmPassword, +} type Props = { appState: AppState; application: WebApplication; }; -const AccountMenu = observer(({ application, appState }: Props) => { - const { - show: showAccountMenu, - showLogin, - showRegister, - setShowLogin, - setShowRegister, - closeAccountMenu - } = appState.accountMenu; +type PaneSelectorProps = Props & { + menuPane: AccountMenuPane; + setMenuPane: (pane: AccountMenuPane) => void; + closeMenu: () => void; +}; - const user = application.getUser(); +const MenuPaneSelector: FunctionComponent = observer( + ({ application, appState, menuPane, setMenuPane, closeMenu }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); - useEffect(() => { - // Reset "Login" and "Registration" sections state when hiding account menu, - // so the next time account menu is opened these sections are closed - if (!showAccountMenu) { - setShowLogin(false); - setShowRegister(false); - } - }, [setShowLogin, setShowRegister, showAccountMenu]); - - return ( -
-
-
-
Account
- Close -
-
- + ); + case AccountMenuPane.SignIn: + return ( + + ); + case AccountMenuPane.Register: + return ( + + ); + case AccountMenuPane.ConfirmPassword: + return ( + + ); + } + } +); + +const AccountMenu: FunctionComponent = observer( + ({ application, appState }) => { + const { + currentPane, + setCurrentPane, + shouldAnimateCloseMenu, + closeAccountMenu, + } = appState.accountMenu; + + return ( +
+
+ - {!showLogin && !showRegister && user && ( -
- -
- )}
-
-
- ); -}); - -export const AccountMenuDirective = toDirective( - AccountMenu + ); + } ); + +export const AccountMenuDirective = toDirective(AccountMenu); diff --git a/app/assets/javascripts/components/Checkbox.tsx b/app/assets/javascripts/components/Checkbox.tsx new file mode 100644 index 000000000..e453f55d6 --- /dev/null +++ b/app/assets/javascripts/components/Checkbox.tsx @@ -0,0 +1,32 @@ +import { FunctionComponent } from 'preact'; + +type CheckboxProps = { + name: string; + checked: boolean; + onChange: (e: Event) => void; + disabled?: boolean; + label: string; +}; + +export const Checkbox: FunctionComponent = ({ + name, + checked, + onChange, + disabled, + label, +}) => { + return ( + + ); +}; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 9e2ca1b1a..051ac9c19 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -36,11 +36,35 @@ import InfoIcon from '../../icons/ic-info.svg'; import CheckIcon from '../../icons/ic-check.svg'; import CheckBoldIcon from '../../icons/ic-check-bold.svg'; import AccountCircleIcon from '../../icons/ic-account-circle.svg'; +import CloudOffIcon from '../../icons/ic-cloud-off.svg'; +import SignInIcon from '../../icons/ic-signin.svg'; +import SignOutIcon from '../../icons/ic-signout.svg'; +import CheckCircleIcon from '../../icons/ic-check-circle.svg'; +import SyncIcon from '../../icons/ic-sync.svg'; +import ArrowLeftIcon from '../../icons/ic-arrow-left.svg'; +import ChevronDownIcon from '../../icons/ic-chevron-down.svg'; +import EmailIcon from '../../icons/ic-email.svg'; +import ServerIcon from '../../icons/ic-server.svg'; +import EyeIcon from '../../icons/ic-eye.svg'; +import EyeOffIcon from '../../icons/ic-eye-off.svg'; +import LockIcon from '../../icons/ic-lock.svg'; import { toDirective } from './utils'; import { FunctionalComponent } from 'preact'; const ICONS = { + lock: LockIcon, + eye: EyeIcon, + 'eye-off': EyeOffIcon, + server: ServerIcon, + email: EmailIcon, + 'chevron-down': ChevronDownIcon, + 'arrow-left': ArrowLeftIcon, + sync: SyncIcon, + 'check-circle': CheckCircleIcon, + signIn: SignInIcon, + signOut: SignOutIcon, + 'cloud-off': CloudOffIcon, 'pencil-off': PencilOffIcon, 'plain-text': PlainTextIcon, 'rich-text': RichTextIcon, diff --git a/app/assets/javascripts/components/IconButton.tsx b/app/assets/javascripts/components/IconButton.tsx index e229a23ae..45ce99942 100644 --- a/app/assets/javascripts/components/IconButton.tsx +++ b/app/assets/javascripts/components/IconButton.tsx @@ -19,6 +19,8 @@ interface Props { title: string; focusable: boolean; + + disabled?: boolean; } /** @@ -31,6 +33,8 @@ export const IconButton: FunctionComponent = ({ icon, title, focusable, + iconClassName = '', + disabled = false, }) => { const click = (e: MouseEvent) => { e.preventDefault(); @@ -42,8 +46,9 @@ export const IconButton: FunctionComponent = ({ title={title} className={`no-border cursor-pointer bg-transparent flex flex-row items-center hover:brightness-130 p-0 ${focusableClass} ${className}`} onClick={click} + disabled={disabled} > - + ); }; diff --git a/app/assets/javascripts/components/InputWithIcon.tsx b/app/assets/javascripts/components/InputWithIcon.tsx new file mode 100644 index 000000000..5277d0238 --- /dev/null +++ b/app/assets/javascripts/components/InputWithIcon.tsx @@ -0,0 +1,89 @@ +import { FunctionComponent, Ref } from 'preact'; +import { JSXInternal } from 'preact/src/jsx'; +import { forwardRef } from 'preact/compat'; +import { Icon, IconType } from './Icon'; +import { IconButton } from './IconButton'; + +type ToggleProps = { + toggleOnIcon: IconType; + toggleOffIcon: IconType; + title: string; + toggled: boolean; + onClick: (toggled: boolean) => void; +}; + +type Props = { + icon: IconType; + inputType: 'text' | 'email' | 'password'; + className?: string; + iconClassName?: string; + value: string | undefined; + onChange: JSXInternal.GenericEventHandler; + onFocus?: JSXInternal.GenericEventHandler; + onKeyDown?: JSXInternal.KeyboardEventHandler; + disabled?: boolean; + placeholder: string; + toggle?: ToggleProps; +}; + +const DISABLED_CLASSNAME = 'bg-grey-5 cursor-not-allowed'; + +export const InputWithIcon: FunctionComponent = forwardRef( + ( + { + icon, + inputType, + className, + iconClassName, + value, + onChange, + onFocus, + onKeyDown, + disabled, + toggle, + placeholder, + }, + ref: Ref + ) => { + const handleToggle = () => { + if (toggle) toggle.onClick(!toggle?.toggled); + }; + + return ( +
+
+ +
+ + {toggle ? ( +
+ +
+ ) : null} +
+ ); + } +); diff --git a/app/assets/javascripts/components/OtherSessionsLogout.tsx b/app/assets/javascripts/components/OtherSessionsSignOut.tsx similarity index 77% rename from app/assets/javascripts/components/OtherSessionsLogout.tsx rename to app/assets/javascripts/components/OtherSessionsSignOut.tsx index a6da7ef9f..b72487bd1 100644 --- a/app/assets/javascripts/components/OtherSessionsLogout.tsx +++ b/app/assets/javascripts/components/OtherSessionsSignOut.tsx @@ -14,19 +14,18 @@ type Props = { appState: AppState; }; -export const OtherSessionsLogoutContainer = observer((props: Props) => { - if (!props.appState.accountMenu.otherSessionsLogOut) { +export const OtherSessionsSignOutContainer = observer((props: Props) => { + if (!props.appState.accountMenu.otherSessionsSignOut) { return null; } - return ; + return ; }); -const ConfirmOtherSessionsLogout = observer( +const ConfirmOtherSessionsSignOut = observer( ({ application, appState }: Props) => { - const cancelRef = useRef(); function closeDialog() { - appState.accountMenu.setOtherSessionsLogout(false); + appState.accountMenu.setOtherSessionsSignOut(false); } return ( @@ -41,9 +40,10 @@ const ConfirmOtherSessionsLogout = observer(

- This action will sign out all other devices signed into your account, - and remove your data from those devices when they next regain connection - to the internet. You may sign back in on those devices at any time. + This action will sign out all other devices signed into + your account, and remove your data from those devices when + they next regain connection to the internet. You may sign + back in on those devices at any time.

@@ -60,9 +60,9 @@ const ConfirmOtherSessionsLogout = observer( application.revokeAllOtherSessions(); closeDialog(); application.alertService.alert( - "You have successfully revoked your sessions from other devices.", + 'You have successfully revoked your sessions from other devices.', undefined, - "Finish" + 'Finish' ); }} > diff --git a/app/assets/javascripts/directives/views/passwordWizard.ts b/app/assets/javascripts/directives/views/passwordWizard.ts index 4f0dfe8ee..13c2f5dc2 100644 --- a/app/assets/javascripts/directives/views/passwordWizard.ts +++ b/app/assets/javascripts/directives/views/passwordWizard.ts @@ -1,48 +1,52 @@ import { WebApplication } from '@/ui_models/application'; -import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types'; +import { + PasswordWizardScope, + PasswordWizardType, + WebDirective, +} from './../../types'; import template from '%/directives/password-wizard.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; -const DEFAULT_CONTINUE_TITLE = "Continue"; +const DEFAULT_CONTINUE_TITLE = 'Continue'; enum Steps { PasswordStep = 1, - FinishStep = 2 + FinishStep = 2, } type FormData = { - currentPassword?: string, - newPassword?: string, - newPasswordConfirmation?: string, - status?: string -} + currentPassword?: string; + newPassword?: string; + newPasswordConfirmation?: string; + status?: string; +}; type State = { - lockContinue: boolean - formData: FormData, - continueTitle: string, - step: Steps, - title: string, - showSpinner: boolean - processing: boolean -} + lockContinue: boolean; + formData: FormData; + continueTitle: string; + step: Steps; + title: string; + showSpinner: boolean; + processing: boolean; +}; type Props = { - type: PasswordWizardType, - changePassword: boolean, - securityUpdate: boolean -} + type: PasswordWizardType; + changePassword: boolean; + securityUpdate: boolean; +}; -class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { - $element: JQLite - application!: WebApplication - type!: PasswordWizardType - isContinuing = false +class PasswordWizardCtrl + extends PureViewCtrl + implements PasswordWizardScope +{ + $element: JQLite; + application!: WebApplication; + type!: PasswordWizardType; + isContinuing = false; /* @ngInject */ - constructor( - $element: JQLite, - $timeout: ng.ITimeoutService, - ) { + constructor($element: JQLite, $timeout: ng.ITimeoutService) { super($timeout); this.$element = $element; this.registerWindowUnloadStopper(); @@ -53,13 +57,13 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW this.initProps({ type: this.type, changePassword: this.type === PasswordWizardType.ChangePassword, - securityUpdate: this.type === PasswordWizardType.AccountUpgrade + securityUpdate: this.type === PasswordWizardType.AccountUpgrade, }); this.setState({ formData: {}, continueTitle: DEFAULT_CONTINUE_TITLE, step: Steps.PasswordStep, - title: this.props.changePassword ? 'Change Password' : 'Account Update' + title: this.props.changePassword ? 'Change Password' : 'Account Update', }); } @@ -78,7 +82,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW resetContinueState() { this.setState({ showSpinner: false, - continueTitle: DEFAULT_CONTINUE_TITLE + continueTitle: DEFAULT_CONTINUE_TITLE, }); this.isContinuing = false; } @@ -95,7 +99,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW this.isContinuing = true; await this.setState({ showSpinner: true, - continueTitle: "Generating Keys..." + continueTitle: 'Generating Keys...', }); const valid = await this.validateCurrentPassword(); if (!valid) { @@ -110,8 +114,8 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW this.isContinuing = false; this.setState({ showSpinner: false, - continueTitle: "Finish", - step: Steps.FinishStep + continueTitle: 'Finish', + step: Steps.FinishStep, }); } @@ -119,43 +123,43 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW return this.setState({ formData: { ...this.state.formData, - ...formData - } + ...formData, + }, }); } async validateCurrentPassword() { const currentPassword = this.state.formData.currentPassword; - const newPass = this.props.securityUpdate ? currentPassword : this.state.formData.newPassword; + const newPass = this.props.securityUpdate + ? currentPassword + : this.state.formData.newPassword; if (!currentPassword || currentPassword.length === 0) { this.application.alertService!.alert( - "Please enter your current password." + 'Please enter your current password.' ); return false; } if (this.props.changePassword) { if (!newPass || newPass.length === 0) { - this.application.alertService!.alert( - "Please enter a new password." - ); + this.application.alertService!.alert('Please enter a new password.'); return false; } if (newPass !== this.state.formData.newPasswordConfirmation) { this.application.alertService!.alert( - "Your new password does not match its confirmation." + 'Your new password does not match its confirmation.' ); this.setFormDataState({ - status: undefined + status: undefined, }); return false; } } if (!this.application.getUser()?.email) { this.application.alertService!.alert( - "We don't have your email stored. Please log out then log back in to fix this issue." + "We don't have your email stored. Please sign out then log back in to fix this issue." ); this.setFormDataState({ - status: undefined + status: undefined, }); return false; } @@ -166,7 +170,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW ); if (!success) { this.application.alertService!.alert( - "The current password you entered is not correct. Please try again." + 'The current password you entered is not correct. Please try again.' ); } return success; @@ -176,10 +180,10 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW await this.application.downloadBackup(); await this.setState({ lockContinue: true, - processing: true + processing: true, }); await this.setFormDataState({ - status: "Processing encryption keys…" + status: 'Processing encryption keys…', }); const newPassword = this.props.securityUpdate ? this.state.formData.currentPassword @@ -195,16 +199,16 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW }); if (!success) { this.setFormDataState({ - status: "Unable to process your password. Please try again." + status: 'Unable to process your password. Please try again.', }); } else { this.setState({ formData: { ...this.state.formData, status: this.props.changePassword - ? "Successfully changed password." - : "Successfully performed account update." - } + ? 'Successfully changed password.' + : 'Successfully performed account update.', + }, }); } return success; @@ -213,7 +217,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordW dismiss() { if (this.state.lockContinue) { this.application.alertService!.alert( - "Cannot close window until pending tasks are complete." + 'Cannot close window until pending tasks are complete.' ); } else { const elem = this.$element; @@ -234,7 +238,7 @@ export class PasswordWizard extends WebDirective { this.bindToController = true; this.scope = { type: '=', - application: '=' + application: '=', }; } } diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index e6b2b47bd..870348f24 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -1,14 +1,20 @@ import { RoundIconButton } from '@/components/RoundIconButton'; import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; -import { AccountPreferences, HelpAndFeedback, Listed, General, Security } from './panes'; +import { + AccountPreferences, + HelpAndFeedback, + Listed, + General, + Security, +} from './panes'; import { observer } from 'mobx-react-lite'; import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { AppState } from '@/ui_models/app_state'; -import { useEffect } from 'preact/hooks'; +import { useEffect, useMemo } from 'preact/hooks'; import { Extensions } from './panes/Extensions'; interface PreferencesProps extends MfaProps { @@ -22,7 +28,9 @@ const PaneSelector: FunctionComponent< > = observer((props) => { switch (props.menu.selectedPaneId) { case 'general': - return + return ( + + ); case 'account': return ( = observer( (props) => { + const menu = useMemo(() => new PreferencesMenu(), []); useEffect(() => { + menu.selectPane(props.appState.preferences.currentPane); const removeEscKeyObserver = props.application.io.addKeyObserver({ key: 'Escape', onKeyDown: (event) => { event.preventDefault(); props.closePreferences(); - } + }, }); return () => { removeEscKeyObserver(); }; - }, [props]); - const menu = new PreferencesMenu(); + }, [props, menu]); + return (
diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 6d27c63cf..1523e9a0f 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -2,7 +2,7 @@ import { Sync, SubscriptionWrapper, Credentials, - LogOutWrapper, + SignOutWrapper, Authentication, } from '@/preferences/panes/account'; import { PreferencesPane } from '@/preferences/components'; @@ -23,7 +23,7 @@ export const AccountPreferences = observer( return ( - + ); } @@ -33,7 +33,7 @@ export const AccountPreferences = observer( - + ); } diff --git a/app/assets/javascripts/preferences/panes/account/Authentication.tsx b/app/assets/javascripts/preferences/panes/account/Authentication.tsx index d98fc839e..72e1e27f6 100644 --- a/app/assets/javascripts/preferences/panes/account/Authentication.tsx +++ b/app/assets/javascripts/preferences/panes/account/Authentication.tsx @@ -1,41 +1,65 @@ -import { Button } from "@/components/Button"; -import { PreferencesGroup, PreferencesSegment, Subtitle, Text, Title } from "@/preferences/components"; -import { WebApplication } from "@/ui_models/application"; -import { AppState } from "@/ui_models/app_state"; -import { observer } from "mobx-react-lite"; -import { FunctionComponent } from "preact"; +import { AccountMenuPane } from '@/components/AccountMenu'; +import { Button } from '@/components/Button'; +import { + PreferencesGroup, + PreferencesSegment, + Subtitle, + Text, + Title, +} from '@/preferences/components'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; -export const Authentication: FunctionComponent<{ application: WebApplication, appState: AppState }> = - observer(({ appState }) => { +export const Authentication: FunctionComponent<{ + application: WebApplication; + appState: AppState; +}> = observer(({ appState }) => { + const clickSignIn = () => { + appState.preferences.closePreferences(); + appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn); + appState.accountMenu.setShow(true); + }; - const clickSignIn = () => { - appState.preferences.closePreferences(); - appState.accountMenu.setShowLogin(true); - appState.accountMenu.setShow(true); - }; + const clickRegister = () => { + appState.preferences.closePreferences(); + appState.accountMenu.setCurrentPane(AccountMenuPane.Register); + appState.accountMenu.setShow(true); + }; - const clickRegister = () => { - appState.preferences.closePreferences(); - appState.accountMenu.setShowRegister(true); - appState.accountMenu.setShow(true); - }; - - return ( - - -
- You're not signed in - Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption. -
-
-
-
- Standard Notes is free on every platform, and comes standard with sync and encryption. + return ( + + +
+ You're not signed in + + Sign in to sync your notes and preferences across all your devices + and enable end-to-end encryption. + +
+
+
- - - ); - }); +
+ + Standard Notes is free on every platform, and comes standard with + sync and encryption. + +
+ + + ); +}); diff --git a/app/assets/javascripts/preferences/panes/account/LogOutView.tsx b/app/assets/javascripts/preferences/panes/account/SignOutView.tsx similarity index 69% rename from app/assets/javascripts/preferences/panes/account/LogOutView.tsx rename to app/assets/javascripts/preferences/panes/account/SignOutView.tsx index 5acfeed05..725e8b7f3 100644 --- a/app/assets/javascripts/preferences/panes/account/LogOutView.tsx +++ b/app/assets/javascripts/preferences/panes/account/SignOutView.tsx @@ -1,6 +1,6 @@ import { Button } from '@/components/Button'; import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; -import { OtherSessionsLogoutContainer } from '@/components/OtherSessionsLogout'; +import { OtherSessionsSignOutContainer } from '@/components/OtherSessionsSignOut'; import { PreferencesGroup, PreferencesSegment, @@ -13,30 +13,33 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; -const LogOutView: FunctionComponent<{ +const SignOutView: FunctionComponent<{ application: WebApplication; appState: AppState; }> = observer(({ application, appState }) => { - return ( <> - Log out + Sign out
Other devices - Want to log out on all devices except this one? + Want to sign out on all devices except this one?
@@ -45,20 +48,19 @@ const LogOutView: FunctionComponent<{
-
-
-
- ); -}); + + + ); + } +); diff --git a/app/assets/javascripts/preferences/panes/account/changePassword/index.tsx b/app/assets/javascripts/preferences/panes/account/changePassword/index.tsx index 1691537b3..93fa3de3c 100644 --- a/app/assets/javascripts/preferences/panes/account/changePassword/index.tsx +++ b/app/assets/javascripts/preferences/panes/account/changePassword/index.tsx @@ -3,7 +3,7 @@ import { ModalDialog, ModalDialogButtons, ModalDialogDescription, - ModalDialogLabel + ModalDialogLabel, } from '@/components/shared/ModalDialog'; import { Button } from '@/components/Button'; import { FunctionalComponent } from 'preact'; @@ -15,29 +15,31 @@ import { useBeforeUnload } from '@/hooks/useBeforeUnload'; enum SubmitButtonTitles { Default = 'Continue', GeneratingKeys = 'Generating Keys...', - Finish = 'Finish' + Finish = 'Finish', } enum Steps { InitialStep, - FinishStep + FinishStep, } type Props = { onCloseDialog: () => void; application: WebApplication; -} +}; export const ChangePassword: FunctionalComponent = ({ onCloseDialog, - application + application, }) => { const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [newPasswordConfirmation, setNewPasswordConfirmation] = useState(''); const [isContinuing, setIsContinuing] = useState(false); const [lockContinue, setLockContinue] = useState(false); - const [submitButtonTitle, setSubmitButtonTitle] = useState(SubmitButtonTitles.Default); + const [submitButtonTitle, setSubmitButtonTitle] = useState( + SubmitButtonTitles.Default + ); const [currentStep, setCurrentStep] = useState(Steps.InitialStep); useBeforeUnload(); @@ -46,16 +48,12 @@ export const ChangePassword: FunctionalComponent = ({ const validateCurrentPassword = async () => { if (!currentPassword || currentPassword.length === 0) { - applicationAlertService.alert( - 'Please enter your current password.' - ); + applicationAlertService.alert('Please enter your current password.'); return false; } if (!newPassword || newPassword.length === 0) { - applicationAlertService.alert( - 'Please enter a new password.' - ); + applicationAlertService.alert('Please enter a new password.'); return false; } if (newPassword !== newPasswordConfirmation) { @@ -67,7 +65,7 @@ export const ChangePassword: FunctionalComponent = ({ if (!application.getUser()?.email) { applicationAlertService.alert( - 'We don\'t have your email stored. Please log out then log back in to fix this issue.' + "We don't have your email stored. Please sign out then sign back in to fix this issue." ); return false; } @@ -172,15 +170,15 @@ export const ChangePassword: FunctionalComponent = ({ {currentStep === Steps.InitialStep && (
diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx index d6d535bb3..c6f104ccc 100644 --- a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx +++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx @@ -180,13 +180,13 @@ export const ExtensionItem: FunctionComponent<{ {isEditorOrTags && ( <> {extension.active ? -
} From e0c6aab03d5b003bb35fd8d95744b7c61eea853a Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Mon, 11 Oct 2021 15:41:15 +0200 Subject: [PATCH 68/71] feat(preferences): show latest extension version (#675) --- .../preferences/panes/Extensions.tsx | 53 +++++++++++++++---- .../extensions-segments/ExtensionItem.tsx | 20 +++---- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/Extensions.tsx b/app/assets/javascripts/preferences/panes/Extensions.tsx index 1f273a8e4..20381385a 100644 --- a/app/assets/javascripts/preferences/panes/Extensions.tsx +++ b/app/assets/javascripts/preferences/panes/Extensions.tsx @@ -11,6 +11,7 @@ import { } from '../components'; import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments'; import { useEffect, useRef, useState } from 'preact/hooks'; +import { FeatureDescription } from '@standardnotes/features'; const loadExtensions = (application: WebApplication) => application.getItems([ ContentType.ActionsExtension, @@ -18,6 +19,22 @@ 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 }) => { @@ -25,6 +42,7 @@ export const Extensions: FunctionComponent<{ 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); @@ -34,6 +52,12 @@ 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)); @@ -66,17 +90,24 @@ export const Extensions: FunctionComponent<{ return ( - - { - extensions - .filter(extension => extension.package_info.identifier !== 'org.standardnotes.extensions-manager') - .sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase())) - .map((extension, i) => ( - - )) - } - + {extensions.length > 0 && + + { + extensions + .filter(extension => extension.package_info.identifier !== 'org.standardnotes.extensions-manager') + .sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase())) + .map((extension, i) => ( + + )) + } + + } {!confirmableExtension && diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx index c6f104ccc..9709c8b5b 100644 --- a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx +++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx @@ -8,14 +8,13 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { Button } from "@/components/Button"; const ExtensionVersions: FunctionComponent<{ - extension: SNComponent -}> = ({ extension }) => { + installedVersion: string, + latestVersion: string | undefined, +}> = ({ installedVersion, latestVersion }) => { return ( -
-
- Installed version {extension.package_info.version} -
-
+ <> + Installed version {installedVersion} {latestVersion && <>(latest is {latestVersion})} + ); }; @@ -98,9 +97,10 @@ export const ExtensionItem: FunctionComponent<{ application: WebApplication, extension: SNComponent, first: boolean, + latestVersion: string | undefined, uninstall: (extension: SNComponent) => void, toggleActivate: (extension: SNComponent) => void, -}> = ({ application, extension, first, uninstall, toggleActivate }) => { +}> = ({ 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); @@ -156,6 +156,8 @@ export const ExtensionItem: FunctionComponent<{ 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 ( @@ -168,7 +170,7 @@ export const ExtensionItem: FunctionComponent<{
- + {localInstallable && } {localInstallable && } From 6f18664564be8d64ba8ea1c3c96dc6b9fb796f43 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Mon, 11 Oct 2021 17:29:41 +0200 Subject: [PATCH 69/71] feat(preferences): no-subscription for non logged in users (#676) * feat(preferences): no-subscription for non logged in users * fix: check if user has account using application.hasAccount() --- .../components/AccountMenu/Authentication.tsx | 4 +--- .../components/AccountMenu/Footer.tsx | 4 +--- .../preferences/panes/AccountPreferences.tsx | 5 ++-- .../preferences/panes/account/SignOutView.tsx | 3 +-- .../account/subscription/NoSubscription.tsx | 23 ++++++++++--------- .../account/subscription/Subscription.tsx | 2 +- 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/components/AccountMenu/Authentication.tsx b/app/assets/javascripts/components/AccountMenu/Authentication.tsx index cc401159a..3687f0865 100644 --- a/app/assets/javascripts/components/AccountMenu/Authentication.tsx +++ b/app/assets/javascripts/components/AccountMenu/Authentication.tsx @@ -43,8 +43,6 @@ const Authentication = observer(({ application, appState }: Props) => { closeAccountMenu, } = appState.accountMenu; - const user = application.getUser(); - useEffect(() => { if (isEmailFocused) { emailInputRef.current.focus(); @@ -207,7 +205,7 @@ const Authentication = observer(({ application, appState }: Props) => { return ( <> - {!user && !showSignIn && !showRegister && ( + {!application.hasAccount() && !showSignIn && !showRegister && (
diff --git a/app/assets/javascripts/components/AccountMenu/Footer.tsx b/app/assets/javascripts/components/AccountMenu/Footer.tsx index c5c291342..ac4b67f9d 100644 --- a/app/assets/javascripts/components/AccountMenu/Footer.tsx +++ b/app/assets/javascripts/components/AccountMenu/Footer.tsx @@ -17,8 +17,6 @@ const Footer = observer(({ application, appState }: Props) => { setSigningOut, } = appState.accountMenu; - const user = application.getUser(); - const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } = appState; @@ -62,7 +60,7 @@ const Footer = observer(({ application, appState }: Props) => { )} {!showSignIn && !showRegister && ( - {user ? 'Sign out' : 'Clear session data'} + {application.hasAccount() ? 'Sign out' : 'Clear session data'} )}
diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 1523e9a0f..90161807a 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -17,12 +17,12 @@ type Props = { export const AccountPreferences = observer( ({ application, appState }: Props) => { - const isLoggedIn = application.getUser(); - if (!isLoggedIn) { + if (!application.hasAccount()) { return ( + ); @@ -32,7 +32,6 @@ export const AccountPreferences = observer( - ); diff --git a/app/assets/javascripts/preferences/panes/account/SignOutView.tsx b/app/assets/javascripts/preferences/panes/account/SignOutView.tsx index 725e8b7f3..3c713dc04 100644 --- a/app/assets/javascripts/preferences/panes/account/SignOutView.tsx +++ b/app/assets/javascripts/preferences/panes/account/SignOutView.tsx @@ -96,8 +96,7 @@ export const SignOutWrapper: FunctionComponent<{ application: WebApplication; appState: AppState; }> = observer(({ application, appState }) => { - const isLoggedIn = application.getUser() != undefined; - if (!isLoggedIn) + if (!application.hasAccount()) return ( ); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx index 071331823..ca5a1c953 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx @@ -1,5 +1,5 @@ import { FunctionalComponent } from "preact"; -import { Text } from '@/preferences/components'; +import { LinkButton, Text } from '@/preferences/components'; import { Button } from '@/components/Button'; import { WebApplication } from "@/ui_models/application"; import { useState } from "preact/hooks"; @@ -44,18 +44,19 @@ export const NoSubscription: FunctionalComponent<{ )}
-
); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx index 0e9ce9126..780af8c8c 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -71,7 +71,7 @@ export const Subscription: FunctionComponent = observer(({
Subscription {error ? ( - No subscription information available. + ) : loading ? ( Loading subscription information... ) : userSubscription && userSubscription.endsAt > now ? ( From bf24453472955fd1fc9fa2b813e55311622966d1 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 12 Oct 2021 15:41:22 -0300 Subject: [PATCH 70/71] chore(version-snjs): 2.14.13 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 724336ae5..45ab8d44d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@reach/listbox": "^0.16.1", "@standardnotes/features": "1.6.1", "@standardnotes/sncrypto-web": "1.5.2", - "@standardnotes/snjs": "2.14.11", + "@standardnotes/snjs": "2.14.13", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12", diff --git a/yarn.lock b/yarn.lock index 4bf64aec4..9e833e9e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2149,10 +2149,10 @@ "@standardnotes/sncrypto-common" "^1.5.2" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.14.11": - version "2.14.11" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.11.tgz#e80a23df763c44d463fe6b73ea0b12522afad9e7" - integrity sha512-L33rhU/NK2ev+o8QXWbVT7jZtVwIwvNwT46N/3Lv5RQXkY7wa1yxO08NHt4BfHqrmXI3mF5qnBH3mcYoOtBY7A== +"@standardnotes/snjs@2.14.13": + version "2.14.13" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.13.tgz#791935559538bf51484159d87651b667dc2dcebd" + integrity sha512-nLmP8BvN8zqc7i6mspr2UKhGxdHFZhW0+uUbGu3yjLiMAnfQpxx8pLP9uWdshx+5Cc8cCzCBq+ekA0OZt4UK3Q== dependencies: "@standardnotes/auth" "3.7.2" "@standardnotes/common" "1.2.0" From 72edbfafcaf5512aa5f30a0b69e69df90622ffae Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 12 Oct 2021 15:48:43 -0300 Subject: [PATCH 71/71] fix: replace setContent with unsafe_setCustomContent --- app/assets/javascripts/directives/views/revisionPreviewModal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/directives/views/revisionPreviewModal.ts b/app/assets/javascripts/directives/views/revisionPreviewModal.ts index 36768d61d..c65ec7ca7 100644 --- a/app/assets/javascripts/directives/views/revisionPreviewModal.ts +++ b/app/assets/javascripts/directives/views/revisionPreviewModal.ts @@ -102,7 +102,7 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc }); } else { this.application.changeAndSaveItem(this.uuid, (mutator) => { - mutator.setContent(this.content); + mutator.unsafe_setCustomContent(this.content); }, true, PayloadSource.RemoteActionRetrieved); } this.dismiss();