-
+export const PreferencesGroup: FunctionalComponent = ({ children }) => (
+
+ {!Array.isArray(children)
+ ? children
+ : children.map((c, i, arr) => (
+ <>
+ {c}
+
+ >
+ ))}
+
+);
+
+export const PreferencesPane: FunctionalComponent = ({ children }) => (
+
+);
diff --git a/app/assets/javascripts/components/preferences/mock-state.ts b/app/assets/javascripts/components/preferences/preferences.ts
similarity index 78%
rename from app/assets/javascripts/components/preferences/mock-state.ts
rename to app/assets/javascripts/components/preferences/preferences.ts
index dd018f243..3a7407d4a 100644
--- a/app/assets/javascripts/components/preferences/mock-state.ts
+++ b/app/assets/javascripts/components/preferences/preferences.ts
@@ -11,7 +11,7 @@ interface PreferenceListItem extends PreferenceItem {
}
const predefinedItems: PreferenceItem[] = [
- { label: 'General', icon: 'settings-filled' },
+ { label: 'General', icon: 'settings' },
{ label: 'Account', icon: 'user' },
{ label: 'Appearance', icon: 'themes' },
{ label: 'Security', icon: 'security' },
@@ -22,22 +22,23 @@ const predefinedItems: PreferenceItem[] = [
{ label: 'Help & feedback', icon: 'help' },
];
-export class MockState {
+export class Preferences {
private readonly _items: PreferenceListItem[];
private _selectedId = 0;
constructor(items: PreferenceItem[] = predefinedItems) {
- makeObservable
(this, {
+ makeObservable(this, {
_selectedId: observable,
+ selectedItem: computed,
items: computed,
- select: action,
+ selectItem: action,
});
this._items = items.map((p, idx) => ({ ...p, id: idx }));
this._selectedId = this._items[0].id;
}
- select(id: number) {
+ selectItem(id: number) {
this._selectedId = id;
}
@@ -47,4 +48,8 @@ export class MockState {
selected: p.id === this._selectedId,
}));
}
+
+ get selectedItem(): PreferenceListItem {
+ return this._items.find((item) => item.id === this._selectedId)!;
+ }
}
diff --git a/app/assets/javascripts/components/preferences/view.tsx b/app/assets/javascripts/components/preferences/view.tsx
index 01ceb8773..9b2f8b854 100644
--- a/app/assets/javascripts/components/preferences/view.tsx
+++ b/app/assets/javascripts/components/preferences/view.tsx
@@ -1,28 +1,45 @@
import { IconButton } from '@/components/IconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
-import { PreferencesPane } from './pane';
+import { Preferences } from './preferences';
+import { PreferencesMenu } from './menu';
+import { HelpAndFeedback } from './help-feedback';
+import { observer } from 'mobx-react-lite';
interface PreferencesViewProps {
close: () => void;
}
-export const PreferencesView: FunctionComponent = ({
- close,
-}) => (
-
-
- {/* div is added so flex justify-between can center the title */}
-
- Your preferences for Standard Notes
- {
- close();
- }}
- type="normal"
- iconType="close"
- />
-
-
+export const PreferencesCanvas: FunctionComponent<{
+ preferences: Preferences;
+}> = observer(({ preferences: prefs }) => (
+
+
+ {/* Temporary selector until a full solution is implemented */}
+ {prefs.selectedItem.label === 'Help & feedback' ? (
+
+ ) : null}
-);
+));
+
+export const PreferencesView: FunctionComponent
=
+ observer(({ close }) => {
+ const prefs = new Preferences();
+ return (
+
+
+ {/* div is added so flex justify-between can center the title */}
+
+ Your preferences for Standard Notes
+ {
+ close();
+ }}
+ type="normal"
+ iconType="close"
+ />
+
+
+
+ );
+ });
diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts
deleted file mode 100644
index cd4ae8984..000000000
--- a/app/assets/javascripts/directives/views/accountMenu.ts
+++ /dev/null
@@ -1,618 +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,
- Strings,
-} 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!.setCustomHost(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 || passcode.length === 0) {
- await alertDialog({
- text: Strings.enterPasscode,
- });
- this.passcodeInput[0].focus();
- return;
- }
-
- 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..13f243dd1 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,112 @@
-import { action, makeObservable, observable } from "mobx";
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { ApplicationEvent, ContentType } from '@standardnotes/snjs';
+import { WebApplication } from '@/ui_models/application';
+import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
export class AccountMenuState {
show = false;
signingOut = false;
+ server: string | undefined = undefined;
+ notesAndTags: SNItem[] = [];
+ isEncryptionEnabled = false;
+ encryptionStatusString = '';
+ isBackupEncrypted = false;
+ showLogin = false;
+ showRegister = false;
- constructor() {
+ constructor(
+ private application: WebApplication,
+ private appEventListeners: (() => void)[]
+ ) {
makeObservable(this, {
show: observable,
signingOut: observable,
+ server: observable,
+ notesAndTags: observable,
+ isEncryptionEnabled: observable,
+ encryptionStatusString: observable,
+ isBackupEncrypted: observable,
+ showLogin: observable,
+ showRegister: observable,
setShow: action,
toggleShow: action,
setSigningOut: action,
+ setIsEncryptionEnabled: action,
+ setEncryptionStatusString: action,
+ setIsBackupEncrypted: action,
+
+ notesAndTagsCount: computed
});
+
+ this.addAppLaunchedEventObserver();
+ this.streamNotesAndTags();
}
+ addAppLaunchedEventObserver = (): void => {
+ this.appEventListeners.push(
+ this.application.addEventObserver(async () => {
+ runInAction(() => {
+ this.setServer(this.application.getHost());
+ });
+ }, ApplicationEvent.Launched)
+ );
+ };
+
+ streamNotesAndTags = (): void => {
+ this.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;
- }
+ };
+
+ setServer = (server: string | undefined): void => {
+ this.server = server;
+ };
+
+ setIsEncryptionEnabled = (isEncryptionEnabled: boolean): void => {
+ this.isEncryptionEnabled = isEncryptionEnabled;
+ };
+
+ setEncryptionStatusString = (encryptionStatusString: string): void => {
+ this.encryptionStatusString = encryptionStatusString;
+ };
+
+ setIsBackupEncrypted = (isBackupEncrypted: boolean): void => {
+ this.isBackupEncrypted = isBackupEncrypted;
+ };
+
+ setShowLogin = (showLogin: boolean): void => {
+ this.showLogin = showLogin;
+ };
+
+ setShowRegister = (showRegister: boolean): void => {
+ this.showRegister = showRegister;
+ };
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 396968cf4..04fa7ff4b 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';
import { PreferencesState } from './preferences_state';
export enum AppStateEvent {
@@ -62,7 +62,7 @@ export class AppState {
onVisibilityChange: any;
selectedTag?: SNTag;
showBetaWarning: boolean;
- readonly accountMenu = new AccountMenuState();
+ readonly accountMenu: AccountMenuState;
readonly actionsMenu = new ActionsMenuState();
readonly preferences = new PreferencesState();
readonly noAccountWarning: NoAccountWarningState;
@@ -103,6 +103,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 8abd9c47a..6970a442d 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(
ng-click='ctrl.clickPreferences()'
ng-if='ctrl.appState.enableUnfinishedFeatures'
diff --git a/app/assets/stylesheets/_preferences.scss b/app/assets/stylesheets/_preferences.scss
index d57b438e4..3d490e54c 100644
--- a/app/assets/stylesheets/_preferences.scss
+++ b/app/assets/stylesheets/_preferences.scss
@@ -30,6 +30,7 @@
@extend .border-gray-300;
@extend .border-solid;
@extend .border-1;
+ @extend .bg-default;
}
}
diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss
index 167200535..32baa1cb4 100644
--- a/app/assets/stylesheets/_sn.scss
+++ b/app/assets/stylesheets/_sn.scss
@@ -4,6 +4,14 @@
height: 90vh;
}
+.hidden {
+ display: none;
+}
+
+.hover\:underline:hover {
+ text-decoration: underline;
+}
+
.sn-icon {
@extend .h-5;
@extend .w-5;
diff --git a/app/assets/stylesheets/_ui.scss b/app/assets/stylesheets/_ui.scss
index 3822e130e..e1f7a1dee 100644
--- a/app/assets/stylesheets/_ui.scss
+++ b/app/assets/stylesheets/_ui.scss
@@ -193,6 +193,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
.cursor-pointer {
cursor: pointer;
+
+ input[type="checkbox"] {
+ cursor: pointer;
+ }
}
.fill-current {
diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug
deleted file mode 100644
index 16f6e00db..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.com/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.com/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
diff --git a/app/views/application/app.html.erb b/app/views/application/app.html.erb
index d0018ed90..24bda6878 100644
--- a/app/views/application/app.html.erb
+++ b/app/views/application/app.html.erb
@@ -34,7 +34,7 @@
window._extensions_manager_location = "<%= ENV['EXTENSIONS_MANAGER_LOCATION'] %>";
window._batch_manager_location = "<%= ENV['BATCH_MANAGER_LOCATION'] %>";
window._bugsnag_api_key = "<%= ENV['BUGSNAG_API_KEY'] %>";
- window._enable_unfinished_features = "<%= ENV['ENABLE_UNFINISHED_FEATURES'] %>"
+ window._enable_unfinished_features = "<%= ENV['ENABLE_UNFINISHED_FEATURES'] %>" === 'true';
<% if Rails.env.development? %>
diff --git a/package.json b/package.json
index 99259db29..423295257 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "standard-notes-web",
- "version": "3.8.15",
+ "version": "3.8.16",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
@@ -55,7 +55,7 @@
"pug-loader": "^2.4.0",
"sass-loader": "^8.0.2",
"serve-static": "^1.14.1",
- "sn-stylekit": "5.2.3",
+ "sn-stylekit": "5.2.5",
"ts-loader": "^8.0.17",
"typescript": "4.2.3",
"typescript-eslint": "0.0.1-alpha.0",
@@ -71,7 +71,7 @@
"@reach/checkbox": "^0.13.2",
"@reach/dialog": "^0.13.0",
"@standardnotes/sncrypto-web": "1.2.10",
- "@standardnotes/snjs": "2.7.15",
+ "@standardnotes/snjs": "2.7.17",
"mobx": "^6.1.6",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.12"
diff --git a/yarn.lock b/yarn.lock
index 2c5dc2918..9752a1df2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2029,10 +2029,10 @@
"@standardnotes/sncrypto-common" "^1.2.7"
libsodium-wrappers "^0.7.8"
-"@standardnotes/snjs@2.7.15":
- version "2.7.15"
- resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.15.tgz#abb03ff9c43a075ac919d0505f48a60a9202410d"
- integrity sha512-cJFNp/rsEKjig6HFwZhbBe5gd3TLor/kBFHeaExCXHTltNs2WvIAergXN0TLqU9XMK0qpiQrUXhUrs0l5xauAw==
+"@standardnotes/snjs@2.7.17":
+ version "2.7.17"
+ resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.17.tgz#e83986e637dbfdb9ac0d94270c6a3d7391de88ce"
+ integrity sha512-M030ex34TTMXWTTWu2ViSYEHKTScf1xbC03IIysxC9BRYpwAh996V5RQQiUW3jiW0XMHXxFYN/XhKGYHn5bixw==
dependencies:
"@standardnotes/auth" "^2.0.0"
"@standardnotes/sncrypto-common" "^1.2.9"
@@ -7908,10 +7908,10 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
-sn-stylekit@5.2.3:
- version "5.2.3"
- resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.3.tgz#24246471c03cde5129bda51a08fabef4d3c4880c"
- integrity sha512-hzziH89IY2UjmGh8OYgapb+/QVD6P6NNjnoyzSyveOh671MM9Z4IaPLZTJckgxJVjV0q7G495Pxfta5r4CSRDQ==
+sn-stylekit@5.2.5:
+ version "5.2.5"
+ resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.5.tgz#85a28da395fedbaae9f7a91c48648042cdcc8052"
+ integrity sha512-8J+8UtRvukyJOBp79RcD4IZrvJJbjYY6EdN4N125K0xW84nDjgURuPuCjwm4lnp6vcXODU6r5d3JMDJoXYq8wA==
dependencies:
"@reach/listbox" "^0.15.0"
"@reach/menu-button" "^0.15.1"