diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 9cc5b8d46..579500b6f 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -34,7 +34,6 @@ import { } from './directives/functional'; import { - AccountMenu, ActionsMenu, ComponentModal, ComponentView, @@ -59,6 +58,7 @@ import { SessionsModalDirective } from './components/SessionsModal'; import { NoAccountWarningDirective } from './components/NoAccountWarning'; import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning'; import { SearchOptionsDirective } from './components/SearchOptions'; +import { AccountMenuDirective } from './components/AccountMenu'; import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes'; import { NotesContextMenuDirective } from './components/NotesContextMenu'; @@ -137,7 +137,6 @@ const startApplication: StartApplication = async function startApplication( // Directives - Views angular .module('app') - .directive('accountMenu', () => new AccountMenu()) .directive('accountSwitcher', () => new AccountSwitcher()) .directive('actionsMenu', () => new ActionsMenu()) .directive('challengeModal', () => new ChallengeModal()) @@ -153,6 +152,7 @@ const startApplication: StartApplication = async function startApplication( .directive('historyMenu', () => new HistoryMenu()) .directive('syncResolutionMenu', () => new SyncResolutionMenu()) .directive('sessionsModal', SessionsModalDirective) + .directive('accountMenu', AccountMenuDirective) .directive('noAccountWarning', NoAccountWarningDirective) .directive('protectedNotePanel', NoProtectionsdNoteWarningDirective) .directive('searchOptions', SearchOptionsDirective) diff --git a/app/assets/javascripts/components/AccountMenu.tsx b/app/assets/javascripts/components/AccountMenu.tsx new file mode 100644 index 000000000..1c4e30a5b --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu.tsx @@ -0,0 +1,1009 @@ +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 { useCallback, useEffect, useRef, useState } from 'preact/hooks'; + +import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils'; +import { storage, StorageKey } from '@Services/localStorage'; +import { disableErrorReporting, enableErrorReporting, errorReportingId } from '@Services/errorReporting'; +import { + STRING_ACCOUNT_MENU_UNCHECK_MERGE, + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, + STRING_E2E_ENABLED, + STRING_ENC_NOT_ENABLED, + STRING_GENERATING_LOGIN_KEYS, + STRING_GENERATING_REGISTER_KEYS, + STRING_IMPORT_SUCCESS, + STRING_INVALID_IMPORT_FILE, + STRING_LOCAL_ENC_ENABLED, + STRING_NON_MATCHING_PASSCODES, + STRING_NON_MATCHING_PASSWORDS, + STRING_UNSUPPORTED_BACKUP_FILE_VERSION, + StringImportError, + StringUtils +} from '@/strings'; +import { ApplicationEvent, BackupFile } from '@node_modules/@standardnotes/snjs'; +import { PasswordWizardType } from '@/types'; +import { JSXInternal } from '@node_modules/preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import TargetedKeyboardEvent = JSXInternal.TargetedKeyboardEvent; +import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; +import { alertDialog, confirmDialog } from '@Services/alertService'; +import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; + +type Props = { + appState: AppState; + application: WebApplication; +}; + +const AccountMenu = observer(({ application, appState }: Props) => { + const getProtectionsDisabledUntil = (): 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; + }; + + const passcodeInputRef = useRef(); + const emailInputRef = useRef(); + const passwordInputRef = useRef(); + const passwordConfirmationInputRef = useRef(); + + const [showLogin, setShowLogin] = useState(false); + const [showRegister, setShowRegister] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirmation, setPasswordConfirmation] = useState(undefined); + const [status, setStatus] = useState(undefined); + + const [isEphemeral, setIsEphemeral] = useState(false); + const [isStrictSignIn, setIsStrictSignIn] = useState(false); + + const [passcode, setPasscode] = useState(undefined); + const [passcodeConfirmation, setPasscodeConfirmation] = useState(undefined); + + const [encryptionStatusString, setEncryptionStatusString] = useState(undefined); + const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false); + const [shouldMergeLocal, setShouldMergeLocal] = useState(true); + + const [server, setServer] = useState(application.getHost()); + const [url, setUrl] = useState(application.getHost()); + const [showPasscodeForm, setShowPasscodeForm] = useState(false); + const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); + const [isImportDataLoading, setIsImportDataLoading] = useState(false); + + const [isErrorReportingEnabled] = useState(() => storage.get(StorageKey.DisableErrorReporting) === false); + const [appVersion] = useState(() => `v${((window as any).electronAppVersion || application.bridge.appVersion)}`); + const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); + const [isBackupEncrypted, setIsBackupEncrypted] = useState(isEncryptionEnabled); + const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); + const [user, setUser] = useState(application.getUser()); + const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); + const [hasProtections] = useState(application.hasProtectionSources()); + + const [isEmailFocused, setIsEmailFocused] = useState(false); + const [isPasscodeFocused, setIsPasscodeFocused] = useState(false); + + const { notesAndTagsCount } = appState.accountMenu; + + const refreshCredentialState = () => { + setUser(application.getUser()); + setCanAddPasscode(!application.isEphemeralSession()); + setHasPasscode(application.hasPasscode()); + setShowPasscodeForm(false); + }; + + const loadHost = () => { + const host = application.getHost(); + setServer(host); + setUrl(host); + }; + + const reloadAutoLockInterval = useCallback(async () => { + const interval = await application.getAutolockService().getAutoLockInterval(); + setSelectedAutoLockInterval(interval); + }, [application]); + + const refreshEncryptionStatus = () => { + const hasUser = application.hasAccount(); + const hasPasscode = application.hasPasscode(); + + setHasPasscode(hasPasscode); + + const encryptionEnabled = hasUser || hasPasscode; + + const newEncryptionStatusString = hasUser + ? STRING_E2E_ENABLED + : hasPasscode + ? STRING_LOCAL_ENC_ENABLED + : STRING_ENC_NOT_ENABLED; + + setEncryptionStatusString(newEncryptionStatusString); + setIsEncryptionEnabled(encryptionEnabled); + setIsBackupEncrypted(encryptionEnabled); + }; + + const errorReportingIdValue = errorReportingId(); + const keyStorageInfo = StringUtils.keyStorageInfo(application); + const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); + const showBetaWarning = appState.showBetaWarning; + + const closeAccountMenu = () => { + appState.accountMenu.closeAccountMenu(); + }; + + const handleSignInClick = () => { + setShowLogin(true); + setIsEmailFocused(true); + }; + + const handleRegisterClick = () => { + setShowRegister(true); + setIsEmailFocused(true); + }; + + const blurAuthFields = () => { + emailInputRef.current.blur(); + passwordInputRef.current.blur(); + passwordConfirmationInputRef.current?.blur(); + }; + + const login = async () => { + setStatus(STRING_GENERATING_LOGIN_KEYS); + setIsAuthenticating(true); + + const response = await application.signIn( + email as string, + password, + isStrictSignIn, + isEphemeral, + shouldMergeLocal + ); + const error = response.error; + if (!error) { + setIsAuthenticating(false); + setPassword(''); + + closeAccountMenu(); + return; + } + + setShowLogin(true); + setStatus(undefined); + setPassword(''); + + if (error.message) { + await application.alertService.alert(error.message); + } + + setIsAuthenticating(false); + }; + + const register = async () => { + if (passwordConfirmation !== password) { + application.alertService.alert(STRING_NON_MATCHING_PASSWORDS); + return; + } + setStatus(STRING_GENERATING_REGISTER_KEYS); + setIsAuthenticating(true); + + const response = await application.register( + email as string, + password, + isEphemeral, + shouldMergeLocal + ); + + const error = response.error; + if (error) { + setStatus(undefined); + setIsAuthenticating(false); + + application.alertService.alert(error.message); + } else { + setIsAuthenticating(false); + closeAccountMenu(); + } + }; + + const handleAuthFormSubmit = (event: + TargetedEvent | + TargetedMouseEvent | + TargetedKeyboardEvent + ) => { + event.preventDefault(); + + if (!email || !password) { + return; + } + + blurAuthFields(); + + if (showLogin) { + login(); + } else { + register(); + } + }; + + const handleHostInputChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setUrl(value); + application.setHost(value); + }; + + const handleKeyPressKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + handleAuthFormSubmit(event as TargetedKeyboardEvent); + } + }; + + const handlePasswordChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPassword(value); + }; + + const handleEmailChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setEmail(value); + }; + + const handlePasswordConfirmationChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasswordConfirmation(value); + }; + + const handleMergeLocalData = async (event: TargetedEvent) => { + const { checked } = event.target as HTMLInputElement; + + if (!checked) { + setShouldMergeLocal(checked); + + const confirmResult = await confirmDialog({ + text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, + confirmButtonStyle: 'danger' + }); + setShouldMergeLocal(!confirmResult); + } + }; + + const openPasswordWizard = () => { + closeAccountMenu(); + application.presentPasswordWizard(PasswordWizardType.ChangePassword); + }; + + const openSessionsModal = () => { + closeAccountMenu(); + appState.openSessionsModal(); + }; + + const getEncryptionStatusForNotes = () => { + const length = notesAndTagsCount; + return `${length}/${length} notes and tags encrypted`; + }; + + const enableProtections = () => { + application.clearProtectionSession(); + }; + + const handleAddPassCode = () => { + setShowPasscodeForm(true); + setIsPasscodeFocused(true); + }; + + const submitPasscodeForm = async (event: TargetedEvent | TargetedMouseEvent) => { + event.preventDefault(); + + if (passcode !== passcodeConfirmation) { + await alertDialog({ + text: STRING_NON_MATCHING_PASSCODES + }); + setIsPasscodeFocused(true); + return; + } + + await preventRefreshing( + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, + async () => { + const successful = application.hasPasscode() + ? await application.changePasscode(passcode as string) + : await application.addPasscode(passcode as string); + + if (!successful) { + setIsPasscodeFocused(true); + } + } + ); + + setPasscode(undefined); + setPasscodeConfirmation(undefined); + setShowPasscodeForm(false); + + refreshEncryptionStatus(); + }; + + const handlePasscodeChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasscode(value); + }; + + const handleConfirmPasscodeChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasscodeConfirmation(value); + }; + + const selectAutoLockInterval = async (interval: number) => { + if (!(await application.authorizeAutolockIntervalChange())) { + return; + } + await application.getAutolockService().setAutoLockInterval(interval); + reloadAutoLockInterval(); + }; + + const disableBetaWarning = () => { + appState.disableBetaWarning(); + }; + + const hidePasswordForm = () => { + setShowLogin(false); + setShowRegister(false); + setPassword(''); + setPasswordConfirmation(undefined); + }; + + const signOut = () => { + appState.accountMenu.setSigningOut(true); + }; + + const changePasscodePressed = () => { + handleAddPassCode(); + }; + + const removePasscodePressed = async () => { + await preventRefreshing( + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, + async () => { + if (await application.removePasscode()) { + await application + .getAutolockService() + .deleteAutolockPreference(); + await reloadAutoLockInterval(); + refreshEncryptionStatus(); + } + } + ); + }; + + const downloadDataArchive = () => { + application.getArchiveService().downloadBackup(isBackupEncrypted); + }; + + + const readFile = async (file: File): Promise => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target!.result as string); + resolve(data); + } catch (e) { + application.alertService.alert(STRING_INVALID_IMPORT_FILE); + } + }; + reader.readAsText(file); + }); + }; + + const performImport = async (data: BackupFile) => { + setIsImportDataLoading(true); + + const result = await application.importData(data); + + setIsImportDataLoading(false); + + if (!result) { + return; + } + + let statusText = STRING_IMPORT_SUCCESS; + if ('error' in result) { + statusText = result.error; + } else if (result.errorCount) { + statusText = StringImportError(result.errorCount); + } + void alertDialog({ + text: statusText + }); + }; + + const importFileSelected = async (event: TargetedEvent) => { + const { files } = (event.target as HTMLInputElement); + + if (!files) { + return; + } + const file = files[0]; + const data = await readFile(file); + if (!data) { + return; + } + + const version = data.version || data.keyParams?.version || data.auth_params?.version; + if (!version) { + await performImport(data); + return; + } + + if ( + application.protocolService.supportedVersions().includes(version) + ) { + await performImport(data); + } else { + setIsImportDataLoading(false); + void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION }); + } + }; + + 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. + ` + }); + }; + + // Add the required event observers + useEffect(() => { + const removeAppLaunchedObserver = application.addEventObserver( + async () => { + refreshCredentialState(); + loadHost(); + reloadAutoLockInterval(); + refreshEncryptionStatus(); + }, + ApplicationEvent.Launched + ); + + const removeKeyStatusChangedObserver = application.addEventObserver( + async () => { + refreshCredentialState(); + }, + ApplicationEvent.KeyStatusChanged + ); + + const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( + async () => { + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); + }, + ApplicationEvent.ProtectionSessionExpiryDateChanged + ); + + return () => { + removeAppLaunchedObserver(); + removeKeyStatusChangedObserver(); + removeProtectionSessionExpiryDateChangedObserver(); + }; + }, []); + + // `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial + // value of `selectedAutoLockInterval` + useEffect(() => { + reloadAutoLockInterval(); + }, [reloadAutoLockInterval]); + + useEffect(() => { + refreshEncryptionStatus(); + }, [refreshEncryptionStatus]); + + useEffect(() => { + if (isEmailFocused) { + emailInputRef.current.focus(); + setIsEmailFocused(false); + } + if (isPasscodeFocused) { + passcodeInputRef.current.focus(); + setIsPasscodeFocused(false); + } + }, [isEmailFocused, isPasscodeFocused]); + + return ( +
+
+
+
Account
+ Close +
+
+ {!user && !showLogin && !showRegister && ( +
+
+
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. +
+
+ )} + {(showLogin || showRegister) && ( +
+
+ {showLogin ? 'Sign In' : 'Register'} +
+
+
+ + + {showRegister && + } + + {showAdvanced && ( +
+
+
+ Advanced Options +
+
+ + +
+ {showLogin && ( + + )} +
+
+ )} + {!isAuthenticating && ( +
+ +
+ )} + {showRegister && ( +
+
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. +
+
+ )} + {status && ( +
+
+
+
{status}
+
+
+ )} + {!isAuthenticating && ( +
+ + {notesAndTagsCount > 0 && ( + + )} +
+ )} + +
+ )} + {!showLogin && !showRegister && ( +
+ {user && ( +
+ {appState.sync.errorMessage && ( +
+
Sync Unreachable
+
+ Hmm...we can't seem to sync your account. + The reason: {appState.sync.errorMessage} +
+ + Need help? + +
+ )} +
+
+
+ {user.email} +
+
+ {server} +
+
+
+ + )} + +
+
+ Encryption +
+ {isEncryptionEnabled && ( +
+ {getEncryptionStatusForNotes()} +
+ )} +

+ {encryptionStatusString} +

+
+ {hasProtections && ( +
+
Protections
+ {protectionsDisabledUntil && ( +
+ Protections are disabled until {protectionsDisabledUntil} +
+ )} + {!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 && ( +
+ +
+ )} +
+ )} +
+
Passcode Lock
+ {!hasPasscode && ( +
+ {canAddPasscode && ( + <> + {!showPasscodeForm && ( +
+ +
+ )} +

+ Add a passcode to lock the application and + encrypt on-device key storage. +

+ {keyStorageInfo && ( +

{keyStorageInfo}

+ )} + + )} + {!canAddPasscode && ( +

+ Adding a passcode is not supported in temporary sessions. Please sign + out, then sign back in with the "Stay signed in" option checked. +

+ )} +
+ )} + {showPasscodeForm && ( +
+
+ + + + + + )} + {hasPasscode && !showPasscodeForm && ( + <> +
Passcode lock is enabled
+
+
Options
+
+
+
+
Autolock
+ {passcodeAutoLockOptions.map(option => { + return ( + selectAutoLockInterval(option.value)}> + {option.label} + + ); + })} +
+
+
The autolock timer begins when the window or tab loses focus.
+ +
+ + )} +
+ {isImportDataLoading ? ( +
+ ) : ( +
+
Data Backups
+
Download a backup of all your data.
+ {isEncryptionEnabled && ( +
+
+ + +
+
+ )} +
+
+ + +
+ {isDesktopApplication() && ( +

+ Backups are automatically created on desktop and can be managed + via the "Backups" top-level menu. +

+ )} +
+
+ )} +
+
Error Reporting
+
+ Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'} +
+

+ Help us improve Standard Notes by automatically submitting + anonymized error reports. +

+ {errorReportingIdValue && ( + <> +

+ Your random identifier is + strong {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. +

+ + )} +
+ +
+ +
+
+ )} +
+ +
+
+
+ {appVersion} + {showBetaWarning && ( + + ( + Hide beta warning + ) + + )} +
+ {(showLogin || showRegister) && ( + Cancel + )} + {!showLogin && !showRegister && ( + + {user ? 'Sign out' : 'Clear session data'} + + )} +
+
+
+
+ ); +}); + +export const AccountMenuDirective = toDirective( + AccountMenu +); diff --git a/app/assets/javascripts/components/ConfirmSignoutModal.tsx b/app/assets/javascripts/components/ConfirmSignoutModal.tsx index 6d1b9ef8f..6a9ae8226 100644 --- a/app/assets/javascripts/components/ConfirmSignoutModal.tsx +++ b/app/assets/javascripts/components/ConfirmSignoutModal.tsx @@ -15,7 +15,7 @@ type Props = { appState: AppState; }; -const ConfirmSignoutContainer = observer((props: Props) => { +export const ConfirmSignoutContainer = observer((props: Props) => { if (!props.appState.accountMenu.signingOut) { return null; } diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts deleted file mode 100644 index abea399ab..000000000 --- a/app/assets/javascripts/directives/views/accountMenu.ts +++ /dev/null @@ -1,608 +0,0 @@ -import { WebDirective } from './../../types'; -import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils'; -import template from '%/directives/account-menu.pug'; -import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; -import { - STRING_ACCOUNT_MENU_UNCHECK_MERGE, - STRING_E2E_ENABLED, - STRING_LOCAL_ENC_ENABLED, - STRING_ENC_NOT_ENABLED, - STRING_IMPORT_SUCCESS, - STRING_NON_MATCHING_PASSCODES, - STRING_NON_MATCHING_PASSWORDS, - STRING_INVALID_IMPORT_FILE, - STRING_GENERATING_LOGIN_KEYS, - STRING_GENERATING_REGISTER_KEYS, - StringImportError, - STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, - STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, - STRING_UNSUPPORTED_BACKUP_FILE_VERSION, - StringUtils, -} from '@/strings'; -import { PasswordWizardType } from '@/types'; -import { - ApplicationEvent, - BackupFile, - ContentType, -} from '@standardnotes/snjs'; -import { confirmDialog, alertDialog } from '@/services/alertService'; -import { storage, StorageKey } from '@/services/localStorage'; -import { - disableErrorReporting, - enableErrorReporting, - errorReportingId, -} from '@/services/errorReporting'; - -const ELEMENT_NAME_AUTH_EMAIL = 'email'; -const ELEMENT_NAME_AUTH_PASSWORD = 'password'; -const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf'; - -type FormData = { - email: string; - user_password: string; - password_conf: string; - confirmPassword: boolean; - showLogin: boolean; - showRegister: boolean; - showPasscodeForm: boolean; - strictSignin?: boolean; - ephemeral: boolean; - mergeLocal?: boolean; - url: string; - authenticating: boolean; - status: string; - passcode: string; - confirmPasscode: string; - changingPasscode: boolean; -}; - -type AccountMenuState = { - formData: Partial; - appVersion: string; - passcodeAutoLockOptions: any; - user: any; - mutable: any; - importData: any; - encryptionStatusString?: string; - server?: string; - encryptionEnabled?: boolean; - selectedAutoLockInterval?: unknown; - showBetaWarning: boolean; - errorReportingEnabled: boolean; - syncInProgress: boolean; - syncError?: string; - showSessions: boolean; - errorReportingId: string | null; - keyStorageInfo: string | null; - protectionsDisabledUntil: string | null; -}; - -class AccountMenuCtrl extends PureViewCtrl { - public appVersion: string; - /** @template */ - private closeFunction?: () => void; - private removeProtectionLengthObserver?: () => void; - - public passcodeInput!: JQLite; - - /* @ngInject */ - constructor($timeout: ng.ITimeoutService, appVersion: string) { - super($timeout); - this.appVersion = appVersion; - } - - /** @override */ - getInitialState() { - return { - appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion), - passcodeAutoLockOptions: this.application - .getAutolockService() - .getAutoLockIntervalOptions(), - user: this.application.getUser(), - formData: { - mergeLocal: true, - ephemeral: false, - }, - mutable: {}, - showBetaWarning: false, - errorReportingEnabled: - storage.get(StorageKey.DisableErrorReporting) === false, - showSessions: false, - errorReportingId: errorReportingId(), - keyStorageInfo: StringUtils.keyStorageInfo(this.application), - importData: null, - syncInProgress: false, - protectionsDisabledUntil: this.getProtectionsDisabledUntil(), - }; - } - - getState() { - return this.state as AccountMenuState; - } - - async onAppKeyChange() { - super.onAppKeyChange(); - this.setState(this.refreshedCredentialState()); - } - - async onAppLaunch() { - super.onAppLaunch(); - this.setState(this.refreshedCredentialState()); - this.loadHost(); - this.reloadAutoLockInterval(); - this.refreshEncryptionStatus(); - } - - refreshedCredentialState() { - return { - user: this.application.getUser(), - canAddPasscode: !this.application.isEphemeralSession(), - hasPasscode: this.application.hasPasscode(), - showPasscodeForm: false, - }; - } - - async $onInit() { - super.$onInit(); - this.setState({ - showSessions: await this.application.userCanManageSessions(), - }); - - const sync = this.appState.sync; - this.autorun(() => { - this.setState({ - syncInProgress: sync.inProgress, - syncError: sync.errorMessage, - }); - }); - this.autorun(() => { - this.setState({ - showBetaWarning: this.appState.showBetaWarning, - }); - }); - - this.removeProtectionLengthObserver = this.application.addEventObserver( - async () => { - this.setState({ - protectionsDisabledUntil: this.getProtectionsDisabledUntil(), - }); - }, - ApplicationEvent.ProtectionSessionExpiryDateChanged - ); - } - - deinit() { - this.removeProtectionLengthObserver?.(); - super.deinit(); - } - - close() { - this.$timeout(() => { - this.closeFunction?.(); - }); - } - - hasProtections() { - return this.application.hasProtectionSources(); - } - - private getProtectionsDisabledUntil(): string | null { - const protectionExpiry = this.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; - } - - async loadHost() { - const host = await this.application.getHost(); - this.setState({ - server: host, - formData: { - ...this.getState().formData, - url: host, - }, - }); - } - - enableProtections() { - this.application.clearProtectionSession(); - } - - onHostInputChange() { - const url = this.getState().formData.url!; - this.application!.setHost(url); - } - - refreshEncryptionStatus() { - const hasUser = this.application!.hasAccount(); - const hasPasscode = this.application!.hasPasscode(); - const encryptionEnabled = hasUser || hasPasscode; - - this.setState({ - encryptionStatusString: hasUser - ? STRING_E2E_ENABLED - : hasPasscode - ? STRING_LOCAL_ENC_ENABLED - : STRING_ENC_NOT_ENABLED, - encryptionEnabled, - mutable: { - ...this.getState().mutable, - backupEncrypted: encryptionEnabled, - }, - }); - } - - submitMfaForm() { - this.login(); - } - - blurAuthFields() { - const names = [ - ELEMENT_NAME_AUTH_EMAIL, - ELEMENT_NAME_AUTH_PASSWORD, - ELEMENT_NAME_AUTH_PASSWORD_CONF, - ]; - for (const name of names) { - const element = document.getElementsByName(name)[0]; - if (element) { - element.blur(); - } - } - } - - submitAuthForm() { - if ( - !this.getState().formData.email || - !this.getState().formData.user_password - ) { - return; - } - this.blurAuthFields(); - if (this.getState().formData.showLogin) { - this.login(); - } else { - this.register(); - } - } - - async setFormDataState(formData: Partial) { - return this.setState({ - formData: { - ...this.getState().formData, - ...formData, - }, - }); - } - - async login() { - await this.setFormDataState({ - status: STRING_GENERATING_LOGIN_KEYS, - authenticating: true, - }); - const formData = this.getState().formData; - const response = await this.application!.signIn( - formData.email!, - formData.user_password!, - formData.strictSignin, - formData.ephemeral, - formData.mergeLocal - ); - const error = response.error; - if (!error) { - await this.setFormDataState({ - authenticating: false, - user_password: undefined, - }); - this.close(); - return; - } - await this.setFormDataState({ - showLogin: true, - status: undefined, - user_password: undefined, - }); - if (error.message) { - this.application!.alertService!.alert(error.message); - } - await this.setFormDataState({ - authenticating: false, - }); - } - - async register() { - const confirmation = this.getState().formData.password_conf; - if (confirmation !== this.getState().formData.user_password) { - this.application!.alertService!.alert(STRING_NON_MATCHING_PASSWORDS); - return; - } - await this.setFormDataState({ - confirmPassword: false, - status: STRING_GENERATING_REGISTER_KEYS, - authenticating: true, - }); - const response = await this.application!.register( - this.getState().formData.email!, - this.getState().formData.user_password!, - this.getState().formData.ephemeral, - this.getState().formData.mergeLocal - ); - const error = response.error; - if (error) { - await this.setFormDataState({ - status: undefined, - }); - await this.setFormDataState({ - authenticating: false, - }); - this.application!.alertService!.alert(error.message); - } else { - await this.setFormDataState({ authenticating: false }); - this.close(); - } - } - - async mergeLocalChanged() { - if (!this.getState().formData.mergeLocal) { - this.setFormDataState({ - mergeLocal: !(await confirmDialog({ - text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, - confirmButtonStyle: 'danger', - })), - }); - } - } - - openPasswordWizard() { - this.close(); - this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword); - } - - openSessionsModal() { - this.close(); - this.appState.openSessionsModal(); - } - - signOut() { - this.appState.accountMenu.setSigningOut(true); - } - - showRegister() { - this.setFormDataState({ - showRegister: true, - }); - } - - async readFile(file: File): Promise { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const data = JSON.parse(e.target!.result as string); - resolve(data); - } catch (e) { - this.application!.alertService!.alert(STRING_INVALID_IMPORT_FILE); - } - }; - reader.readAsText(file); - }); - } - - /** - * @template - */ - async importFileSelected(files: File[]) { - const file = files[0]; - const data = await this.readFile(file); - if (!data) { - return; - } - if (data.version || data.auth_params || data.keyParams) { - const version = - data.version || data.keyParams?.version || data.auth_params?.version; - if ( - this.application.protocolService.supportedVersions().includes(version) - ) { - await this.performImport(data); - } else { - await this.setState({ importData: null }); - void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION }); - } - } else { - await this.performImport(data); - } - } - - async performImport(data: BackupFile) { - await this.setState({ - importData: { - ...this.getState().importData, - loading: true, - }, - }); - const result = await this.application.importData(data); - this.setState({ - importData: null, - }); - if (!result) { - return; - } else if ('error' in result) { - void alertDialog({ - text: result.error, - }); - } else if (result.errorCount) { - void alertDialog({ - text: StringImportError(result.errorCount), - }); - } else { - void alertDialog({ - text: STRING_IMPORT_SUCCESS, - }); - } - } - - async downloadDataArchive() { - this.application - .getArchiveService() - .downloadBackup(this.getState().mutable.backupEncrypted); - } - - notesAndTagsCount() { - return this.application.getItems([ContentType.Note, ContentType.Tag]) - .length; - } - - encryptionStatusForNotes() { - const length = this.notesAndTagsCount(); - return length + '/' + length + ' notes and tags encrypted'; - } - - async reloadAutoLockInterval() { - const interval = await this.application!.getAutolockService().getAutoLockInterval(); - this.setState({ - selectedAutoLockInterval: interval, - }); - } - - async selectAutoLockInterval(interval: number) { - if (!(await this.application.authorizeAutolockIntervalChange())) { - return; - } - await this.application!.getAutolockService().setAutoLockInterval(interval); - this.reloadAutoLockInterval(); - } - - hidePasswordForm() { - this.setFormDataState({ - showLogin: false, - showRegister: false, - user_password: undefined, - password_conf: undefined, - }); - } - - hasPasscode() { - return this.application!.hasPasscode(); - } - - addPasscodeClicked() { - this.setFormDataState({ - showPasscodeForm: true, - }); - } - - async submitPasscodeForm() { - const passcode = this.getState().formData.passcode!; - if (passcode !== this.getState().formData.confirmPasscode!) { - await alertDialog({ - text: STRING_NON_MATCHING_PASSCODES, - }); - this.passcodeInput[0].focus(); - return; - } - - await preventRefreshing( - STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, - async () => { - const successful = this.application.hasPasscode() - ? await this.application.changePasscode(passcode) - : await this.application.addPasscode(passcode); - if (!successful) { - this.passcodeInput[0].focus(); - } - } - ); - this.setFormDataState({ - passcode: undefined, - confirmPasscode: undefined, - showPasscodeForm: false, - }); - this.refreshEncryptionStatus(); - } - - async changePasscodePressed() { - this.getState().formData.changingPasscode = true; - this.addPasscodeClicked(); - } - - async removePasscodePressed() { - await preventRefreshing( - STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, - async () => { - if (await this.application!.removePasscode()) { - await this.application - .getAutolockService() - .deleteAutolockPreference(); - await this.reloadAutoLockInterval(); - this.refreshEncryptionStatus(); - } - } - ); - } - - 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. - `, - }); - } - - toggleErrorReportingEnabled() { - if (this.state.errorReportingEnabled) { - disableErrorReporting(); - } else { - enableErrorReporting(); - } - if (!this.state.syncInProgress) { - window.location.reload(); - } - } - - isDesktopApplication() { - return isDesktopApplication(); - } -} - -export class AccountMenu extends WebDirective { - constructor() { - super(); - this.restrict = 'E'; - this.template = template; - this.controller = AccountMenuCtrl; - this.controllerAs = 'self'; - this.bindToController = true; - this.scope = { - closeFunction: '&', - application: '=', - }; - } -} diff --git a/app/assets/javascripts/directives/views/index.ts b/app/assets/javascripts/directives/views/index.ts index 99931e2bb..273a3e944 100644 --- a/app/assets/javascripts/directives/views/index.ts +++ b/app/assets/javascripts/directives/views/index.ts @@ -1,4 +1,3 @@ -export { AccountMenu } from './accountMenu'; export { ActionsMenu } from './actionsMenu'; export { ComponentModal } from './componentModal'; export { ComponentView } from './componentView'; diff --git a/app/assets/javascripts/ui_models/app_state/account_menu_state.ts b/app/assets/javascripts/ui_models/app_state/account_menu_state.ts index 2ef9eb719..1c0a9acae 100644 --- a/app/assets/javascripts/ui_models/app_state/account_menu_state.ts +++ b/app/assets/javascripts/ui_models/app_state/account_menu_state.ts @@ -1,29 +1,58 @@ -import { action, makeObservable, observable } from "mobx"; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { ContentType } from '@node_modules/@standardnotes/snjs'; +import { WebApplication } from '@/ui_models/application'; +import { SNItem } from '@node_modules/@standardnotes/snjs/dist/@types/models/core/item'; export class AccountMenuState { show = false; signingOut = false; + notesAndTags: SNItem[] = []; - constructor() { + constructor( + private application: WebApplication, + appEventListeners: (() => void)[] + ) { makeObservable(this, { show: observable, signingOut: observable, + notesAndTags: observable, setShow: action, toggleShow: action, setSigningOut: action, + + notesAndTagsCount: computed }); + + appEventListeners.push( + this.application.streamItems( + [ContentType.Note, ContentType.Tag], + () => { + runInAction(() => { + this.notesAndTags = this.application.getItems([ContentType.Note, ContentType.Tag]); + }); + } + ) + ); } setShow = (show: boolean): void => { this.show = show; - } + }; + + closeAccountMenu = (): void => { + this.setShow(false); + }; setSigningOut = (signingOut: boolean): void => { this.signingOut = signingOut; - } + }; toggleShow = (): void => { this.show = !this.show; + }; + + get notesAndTagsCount(): number { + return this.notesAndTags.length; } } diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index c57d88fb3..b17b82524 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -14,7 +14,6 @@ import { Editor } from '@/ui_models/editor'; import { action, makeObservable, observable } from 'mobx'; import { Bridge } from '@/services/bridge'; import { storage, StorageKey } from '@/services/localStorage'; -import { AccountMenuState } from './account_menu_state'; import { ActionsMenuState } from './actions_menu_state'; import { NoteTagsState } from './note_tags_state'; import { NoAccountWarningState } from './no_account_warning_state'; @@ -22,6 +21,7 @@ import { SyncState } from './sync_state'; import { SearchOptionsState } from './search_options_state'; import { NotesState } from './notes_state'; import { TagsState } from './tags_state'; +import { AccountMenuState } from '@/ui_models/app_state/account_menu_state'; export enum AppStateEvent { TagChanged, @@ -61,7 +61,7 @@ export class AppState { onVisibilityChange: any; selectedTag?: SNTag; showBetaWarning: boolean; - readonly accountMenu = new AccountMenuState(); + readonly accountMenu: AccountMenuState; readonly actionsMenu = new ActionsMenuState(); readonly noAccountWarning: NoAccountWarningState; readonly noteTags: NoteTagsState; @@ -104,6 +104,10 @@ export class AppState { application, this.appEventObserverRemovers ); + this.accountMenu = new AccountMenuState( + application, + this.appEventObserverRemovers + ); this.searchOptions = new SearchOptionsState( application, this.appEventObserverRemovers diff --git a/app/assets/javascripts/views/footer/footer-view.pug b/app/assets/javascripts/views/footer/footer-view.pug index 0630cbdb3..e9a43c939 100644 --- a/app/assets/javascripts/views/footer/footer-view.pug +++ b/app/assets/javascripts/views/footer/footer-view.pug @@ -13,11 +13,11 @@ .sk-app-bar-item-column .sk-label.title(ng-class='{red: ctrl.hasError}') Account account-menu( - close-function='ctrl.closeAccountMenu()', ng-click='$event.stopPropagation()', - ng-if='ctrl.showAccountMenu', + app-state='ctrl.appState' application='ctrl.application' - ) + ng-if='ctrl.showAccountMenu', + ) .sk-app-bar-item a.no-decoration.sk-label.title( href='https://standardnotes.org/help', diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug deleted file mode 100644 index 78b3e51e7..000000000 --- a/app/assets/templates/directives/account-menu.pug +++ /dev/null @@ -1,324 +0,0 @@ -.sn-component - #account-panel.sk-panel - .sk-panel-header - .sk-panel-header-title Account - a.sk-a.info.close-button(ng-click='self.close()') Close - .sk-panel-content - .sk-panel-section.sk-panel-hero( - ng-if=` - !self.state.user && - !self.state.formData.showLogin && - !self.state.formData.showRegister` - ) - .sk-panel-row - .sk-h1 Sign in or register to enable sync and end-to-end encryption. - .flex.my-1 - button( - class="sn-button info flex-grow text-base py-3 mr-1.5" - ng-click='self.state.formData.showLogin = true' - ) Sign In - button( - class="sn-button info flex-grow text-base py-3 ml-1.5" - ng-click='self.showRegister()' - ) Register - .sk-panel-row.sk-p - | Standard Notes is free on every platform, and comes - | standard with sync and encryption. - .sk-panel-section(ng-if=` - self.state.formData.showLogin || - self.state.formData.showRegister` - ) - .sk-panel-section-title - | {{self.state.formData.showLogin ? "Sign In" : "Register"}} - form.sk-panel-form(ng-submit='self.submitAuthForm()' novalidate) - .sk-panel-section - input.sk-input.contrast( - name='email', - ng-model='self.state.formData.email', - ng-model-options='{allowInvalid: true}', - placeholder='Email', - required='', - should-focus='true', - sn-autofocus='true', - spellcheck='false', - type='email' - ) - input.sk-input.contrast( - name='password', - ng-model='self.state.formData.user_password', - placeholder='Password', - required='', - sn-enter='self.submitAuthForm()', - type='password' - ) - input.sk-input.contrast( - name='password_conf', - ng-if='self.state.formData.showRegister', - ng-model='self.state.formData.password_conf', - placeholder='Confirm Password', - required='', - sn-enter='self.submitAuthForm()', - type='password' - ) - .sk-panel-row - a.sk-panel-row.sk-bold( - ng-click=` - self.state.formData.showAdvanced = !self.state.formData.showAdvanced - ` - ) - | Advanced Options - .sk-notification.unpadded.contrast.advanced-options.sk-panel-row( - ng-if='self.state.formData.showAdvanced' - ) - .sk-panel-column.stretch - .sk-notification-title.sk-panel-row.padded-row Advanced Options - .bordered-row.padded-row - label.sk-label Sync Server Domain - input.sk-input.sk-base( - name='server', - ng-model='self.state.formData.url', - ng-change='self.onHostInputChange()' - placeholder='Server URL', - required='', - type='text' - ) - label.sk-label.padded-row.sk-panel-row.justify-left( - ng-if='self.state.formData.showLogin' - ) - .sk-horizontal-group.tight - input.sk-input( - ng-model='self.state.formData.strictSignin', - type='checkbox' - ) - p.sk-p Use strict sign in - span - a.info( - href='https://standardnotes.org/help/security', - rel='noopener', - target='_blank' - ) (Learn more) - .sk-panel-section.form-submit(ng-if='!self.state.formData.authenticating') - button.sn-button.info.text-base.py-3.text-center( - type="submit" - ng-disabled='self.state.formData.authenticating' - ) {{self.state.formData.showLogin ? "Sign In" : "Register"}} - .sk-notification.neutral(ng-if='self.state.formData.showRegister') - .sk-notification-title No Password Reset. - .sk-notification-text - | Because your notes are encrypted using your password, - | Standard Notes does not have a password reset option. - | You cannot forget your password. - .sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status') - .sk-horizontal-group - .sk-spinner.small.neutral - .sk-label {{self.state.formData.status}} - .sk-panel-section.no-bottom-pad(ng-if='!self.state.formData.authenticating') - label.sk-panel-row.justify-left - .sk-horizontal-group.tight - input( - ng-false-value='true', - ng-model='self.state.formData.ephemeral', - ng-true-value='false', - type='checkbox' - ) - p.sk-p Stay signed in - label.sk-panel-row.justify-left(ng-if='self.notesAndTagsCount() > 0') - .sk-horizontal-group.tight - input( - ng-bind='true', - ng-change='self.mergeLocalChanged()', - ng-model='self.state.formData.mergeLocal', - type='checkbox' - ) - p.sk-p Merge local data ({{self.notesAndTagsCount()}} notes and tags) - div( - ng-if=` - !self.state.formData.showLogin && - !self.state.formData.showRegister` - ) - .sk-panel-section(ng-if='self.state.user') - .sk-notification.danger(ng-if='self.state.syncError') - .sk-notification-title Sync Unreachable - .sk-notification-text - | Hmm...we can't seem to sync your account. - | The reason: {{self.state.syncError}} - a.sk-a.info-contrast.sk-bold.sk-panel-row( - href='https://standardnotes.org/help', - rel='noopener', - target='_blank' - ) Need help? - .sk-panel-row - .sk-panel-column - .sk-h1.sk-bold.wrap {{self.state.user.email}} - .sk-subtitle.neutral {{self.state.server}} - .sk-panel-row - a.sk-a.info.sk-panel-row.condensed( - ng-click="self.openPasswordWizard()" - ) Change Password - a.sk-a.info.sk-panel-row.condensed( - ng-click="self.openSessionsModal()" - ) Manage Sessions - .sk-panel-section - .sk-panel-section-title Encryption - .sk-panel-section-subtitle.info(ng-if='self.state.encryptionEnabled') - | {{self.encryptionStatusForNotes()}} - p.sk-p - | {{self.state.encryptionStatusString}} - .sk-panel-section(ng-if="self.hasProtections()") - .sk-panel-section-title Protections - .sk-panel-section-subtitle.info(ng-if="self.state.protectionsDisabledUntil") - | Protections are disabled until {{self.state.protectionsDisabledUntil}} - .sk-panel-section-subtitle.info(ng-if="!self.state.protectionsDisabledUntil") - | Protections are enabled - p.sk-p - | Actions like viewing protected notes, exporting decrypted backups, - | or revoking an active session, require additional authentication - | like entering your account password or application passcode. - .sk-panel-row(ng-if="self.state.protectionsDisabledUntil") - button.sn-button.small.info(ng-click="self.enableProtections()") - | Enable protections - .sk-panel-section - .sk-panel-section-title Passcode Lock - div(ng-if='!self.state.hasPasscode') - div(ng-if='self.state.canAddPasscode') - .sk-panel-row(ng-if='!self.state.formData.showPasscodeForm') - button.sn-button.small.info( - ng-click='self.addPasscodeClicked(); $event.stopPropagation();' - ) Add Passcode - p.sk-p - | Add a passcode to lock the application and - | encrypt on-device key storage. - p(ng-if='self.state.keyStorageInfo') - | {{self.state.keyStorageInfo}} - div(ng-if='!self.state.canAddPasscode') - p.sk-p - | Adding a passcode is not supported in temporary sessions. Please sign - | out, then sign back in with the "Stay signed in" option checked. - form.sk-panel-form( - ng-if='self.state.formData.showPasscodeForm', - ng-submit='self.submitPasscodeForm()' - ) - .sk-panel-row - input.sk-input.contrast( - ng-ref='self.passcodeInput' - ng-model='self.state.formData.passcode' - placeholder='Passcode' - should-focus='true' - sn-autofocus='true' - type='password' - ) - input.sk-input.contrast( - ng-model='self.state.formData.confirmPasscode', - placeholder='Confirm Passcode', - type='password' - ) - button.sn-button.small.info.mt-2(type='submit') Set Passcode - button.sn-button.small.outlined.ml-2( - ng-click='self.state.formData.showPasscodeForm = false' - ) Cancel - div(ng-if='self.state.hasPasscode && !self.state.formData.showPasscodeForm') - .sk-panel-section-subtitle.info Passcode lock is enabled - .sk-notification.contrast - .sk-notification-title Options - .sk-notification-text - .sk-panel-row - .sk-horizontal-group - .sk-h4.sk-bold Autolock - a.sk-a.info( - ng-class=`{ - 'boxed' : option.value == self.state.selectedAutoLockInterval - }`, - ng-click='self.selectAutoLockInterval(option.value)', - ng-repeat='option in self.state.passcodeAutoLockOptions' - ) - | {{option.label}} - .sk-p The autolock timer begins when the window or tab loses focus. - .sk-panel-row - a.sk-a.info.sk-panel-row.condensed( - ng-click='self.changePasscodePressed()' - ) Change Passcode - a.sk-a.danger.sk-panel-row.condensed( - ng-click='self.removePasscodePressed()' - ) Remove Passcode - .sk-panel-section(ng-if='!self.state.importData.loading') - .sk-panel-section-title Data Backups - .sk-p - | Download a backup of all your data. - form.sk-panel-form.sk-panel-row(ng-if='self.state.encryptionEnabled') - .sk-input-group - label.sk-horizontal-group.tight - input( - ng-change='self.state.mutable.backupEncrypted = true', - ng-model='self.state.mutable.backupEncrypted', - ng-value='true', - type='radio' - ) - p.sk-p Encrypted - label.sk-horizontal-group.tight - input( - ng-change='self.state.mutable.backupEncrypted = false', - ng-model='self.state.mutable.backupEncrypted', - ng-value='false', - type='radio' - ) - p.sk-p Decrypted - .sk-panel-row - .flex - button.sn-button.small.info(ng-click='self.downloadDataArchive()') - | Download Backup - label.sn-button.small.flex.items-center.info.ml-2 - input( - file-change='->', - handler='self.importFileSelected(files)', - style='display: none;', - type='file' - ) - | Import Backup - p.mt-5(ng-if='self.isDesktopApplication()') - | Backups are automatically created on desktop and can be managed - | via the "Backups" top-level menu. - .sk-panel-row - .sk-spinner.small.info(ng-if='self.state.importData.loading') - .sk-panel-section - .sk-panel-section-title Error Reporting - .sk-panel-section-subtitle.info - | Automatic error reporting is {{ self.state.errorReportingEnabled ? 'enabled' : 'disabled' }} - p.sk-p - | Help us improve Standard Notes by automatically submitting - | anonymized error reports. - p.sk-p.selectable(ng-if="self.state.errorReportingId") - | Your random identifier is - strong {{ self.state.errorReportingId }} - p.sk-p(ng-if="self.state.errorReportingId") - | 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. - .sk-panel-row - button(ng-click="self.toggleErrorReportingEnabled()").sn-button.small.info - | {{ self.state.errorReportingEnabled ? 'Disable' : 'Enable'}} Error Reporting - .sk-panel-row - a(ng-click="self.openErrorReportingDialog()").sk-a What data is being sent? - confirm-signout( - app-state='self.appState' - application='self.application' - ) - .sk-panel-footer - .sk-panel-row - .sk-p.left.neutral - span {{self.state.appVersion}} - span(ng-if="self.state.showBetaWarning") - span ( - a.sk-a(ng-click="self.appState.disableBetaWarning()") Hide beta warning - span ) - a.sk-a.right( - ng-click='self.hidePasswordForm()', - ng-if='self.state.formData.showLogin || self.state.formData.showRegister' - ) - | Cancel - a.sk-a.right.danger.capitalize( - ng-click='self.signOut()', - ng-if=` - !self.state.formData.showLogin && - !self.state.formData.showRegister` - ) - | {{ self.state.user ? "Sign out" : "Clear session data" }} diff --git a/app/assets/templates/directives/input-modal.pug b/app/assets/templates/directives/input-modal.pug index ec3c43e66..b3e8c43b5 100644 --- a/app/assets/templates/directives/input-modal.pug +++ b/app/assets/templates/directives/input-modal.pug @@ -14,9 +14,9 @@ .sk-panel-column.stretch form(ng-submit="ctrl.submit()") input.sk-input.contrast( - ng-model="ctrl.formData.input" - should-focus="true" - sn-autofocus="true" + ng-model="ctrl.formData.input" + should-focus="true" + sn-autofocus="true" type="{{ctrl.type}}" ) .sk-panel-footer