Merge pull request #573 from vardan-arm/account-menu-react

refactor: migrate account-menu to react
This commit is contained in:
Antonella Sgarlatta
2021-06-09 12:33:26 -03:00
committed by GitHub
10 changed files with 1057 additions and 948 deletions

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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<FormData>;
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<unknown, AccountMenuState> {
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<FormData>) {
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<any> {
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 <a target="_blank" rel="noreferrer" href="https://www.bugsnag.com/">Bugsnag</a>
to automatically report errors that occur while the app is running. See
<a target="_blank" rel="noreferrer" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
this article, paragraph 'Browser' under 'Sending diagnostic data',
</a>
to see what data is included in error reports.
<br><br>
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: '=',
};
}
}

View File

@@ -1,4 +1,3 @@
export { AccountMenu } from './accountMenu';
export { ActionsMenu } from './actionsMenu';
export { ComponentModal } from './componentModal';
export { ComponentView } from './componentView';

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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',

View File

@@ -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" }}

View File

@@ -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