diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index f5e230f82..b0b4eaf50 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -1,7 +1,5 @@ 'use strict'; -// import { AccountMenu2 } from '@/components/AccountMenu'; - declare const __VERSION__: string; declare const __WEB__: boolean; @@ -61,7 +59,7 @@ import { SessionsModalDirective } from './components/SessionsModal'; import { NoAccountWarningDirective } from './components/NoAccountWarning'; import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning'; import { SearchOptionsDirective } from './components/SearchOptions'; -import { AccountMenu2 } from './components/AccountMenu2'; +import { AccountMenuReact } from './components/AccountMenuReact'; import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes'; import { NotesContextMenuDirective } from './components/NotesContextMenu'; @@ -153,7 +151,7 @@ const startApplication: StartApplication = async function startApplication( .directive('historyMenu', () => new HistoryMenu()) .directive('syncResolutionMenu', () => new SyncResolutionMenu()) .directive('sessionsModal', SessionsModalDirective) - .directive('accountMenu2', AccountMenu2) + .directive('accountMenuReact', AccountMenuReact) .directive('noAccountWarning', NoAccountWarningDirective) .directive('protectedNotePanel', NoProtectionsdNoteWarningDirective) .directive('searchOptions', SearchOptionsDirective) diff --git a/app/assets/javascripts/components/AccountMenu2.tsx b/app/assets/javascripts/components/AccountMenuReact.tsx similarity index 85% rename from app/assets/javascripts/components/AccountMenu2.tsx rename to app/assets/javascripts/components/AccountMenuReact.tsx index 6f6dd0299..c139cd972 100644 --- a/app/assets/javascripts/components/AccountMenu2.tsx +++ b/app/assets/javascripts/components/AccountMenuReact.tsx @@ -4,22 +4,32 @@ import { AppState } from '@/ui_models/app_state'; import { WebApplication } from '@/ui_models/application'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; -import { isDesktopApplication, isSameDay } from '@/utils'; +import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils'; import { storage, StorageKey } from '@Services/localStorage'; import { disableErrorReporting, enableErrorReporting, errorReportingId } from '@Services/errorReporting'; -import { STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED, StringUtils } from '@/strings'; +import { + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, + STRING_E2E_ENABLED, + STRING_ENC_NOT_ENABLED, + STRING_LOCAL_ENC_ENABLED, + STRING_NON_MATCHING_PASSCODES, + StringUtils +} from '@/strings'; import { ContentType } from '@node_modules/@standardnotes/snjs'; +import { PasswordWizardType } from '@/types'; +import { JSXInternal } from '@node_modules/preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import { alertDialog } from '@Services/alertService'; +import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -// interface Props {} // TODO: Vardan: implement props and remove `eslint-disable` type Props = { appState: AppState; application: WebApplication; + closeAccountMenu: () => void; }; -// const HistoryMenu = observer((props: Props) => { -// const AccountMenu = observer((props) => { -const AccountMenu = observer(({ appState, application }: Props) => { +const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props) => { const getProtectionsDisabledUntil = (): string | null => { const protectionExpiry = application.getProtectionSessionExpiryDate(); const now = new Date(); @@ -57,26 +67,24 @@ const AccountMenu = observer(({ appState, application }: Props) => { const [status, setStatus] = useState(''); const [syncError, setSyncError] = useState(undefined); + const [passcode, setPasscode] = useState(undefined); + const [passcodeConfirmation, setPasscodeConfirmation] = useState(undefined); + + const [encryptionStatusString, setEncryptionStatusString] = useState(undefined); + const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false); + const [server, setServer] = useState(undefined); const [showPasscodeForm, setShowPasscodeForm] = useState(false); const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isErrorReportingEnabled, setIsErrorReportingEnabled] = useState(false); const [appVersion, setAppVersion] = useState(''); // TODO: Vardan: figure out how to get `appVersion` similar to original code - - const user = application.getUser(); - const hasUser = application.hasAccount(); - const hasPasscode = application.hasPasscode(); - - const isEncryptionEnabled = hasUser || hasPasscode; - const encryptionStatusString = hasUser - ? STRING_E2E_ENABLED : hasPasscode - ? STRING_LOCAL_ENC_ENABLED : STRING_ENC_NOT_ENABLED; - - // TODO: Vardan: in original code initial value of `backupEncrypted` is `hasUser || hasPasscode` - - // once I have those values here, set them as initial value + const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); const [isBackupEncrypted, setIsBackupEncrypted] = useState(isEncryptionEnabled); const [isSyncInProgress, setIsSyncInProgress] = useState(false); + const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); + + const user = application.getUser(); const reloadAutoLockInterval = useCallback(async () => { const interval = await application.getAutolockService().getAutoLockInterval(); @@ -85,7 +93,6 @@ const AccountMenu = observer(({ appState, application }: Props) => { const errorReportingIdValue = errorReportingId(); - const protectionsDisabledUntil = getProtectionsDisabledUntil(); const canAddPasscode = !application.isEphemeralSession(); const keyStorageInfo = StringUtils.keyStorageInfo(application); const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); @@ -133,11 +140,13 @@ const AccountMenu = observer(({ appState, application }: Props) => { }; const openPasswordWizard = () => { - console.log('openPasswordWizard'); + closeAccountMenu(); + application.presentPasswordWizard(PasswordWizardType.ChangePassword); }; const openSessionsModal = () => { - console.log('openSessionsModal'); + closeAccountMenu(); + appState.openSessionsModal(); }; const getEncryptionStatusForNotes = () => { @@ -146,27 +155,85 @@ const AccountMenu = observer(({ appState, application }: Props) => { }; const enableProtections = () => { - console.log('enableProtections'); + application.clearProtectionSession(); + + // Get the latest the protection status + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); + }; + + 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 handleAddPassCode = () => { - console.log('handleAddPassCode'); + setShowPasscodeForm(true); }; - const submitPasscodeForm = () => { - console.log('submitPasscodeForm'); + const submitPasscodeForm = async (event: TargetedEvent | TargetedMouseEvent) => { + event.preventDefault(); + + if (passcode !== passcodeConfirmation) { + await alertDialog({ + text: STRING_NON_MATCHING_PASSCODES, + }); + passcodeInput.current.focus(); + 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) { + passcodeInput.current.focus(); + } + } + ); + + setPasscode(undefined); + setPasscodeConfirmation(undefined); + setShowPasscodeForm(false); + + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); + + refreshEncryptionStatus(); }; - const handlePasscodeChange = () => { - console.log('handlePasscodeChange'); + // TODO: Vardan: check whether this (and `handleConfirmPasscodeChange`) method is required in the end + const handlePasscodeChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasscode(value); }; - const handleConfirmPasscodeChange = () => { - console.log('handleConfirmPasscodeChange'); + const handleConfirmPasscodeChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasscodeConfirmation(value); }; - const selectAutoLockInterval = (interval: number) => { - console.log('selectAutoLockInterval', interval); + const selectAutoLockInterval = async (interval: number) => { + if (!(await application.authorizeAutolockIntervalChange())) { + return; + } + await application.getAutolockService().setAutoLockInterval(interval); + reloadAutoLockInterval(); }; const disableBetaWarning = () => { @@ -187,13 +254,26 @@ const AccountMenu = observer(({ appState, application }: Props) => { // TODO: Vardan: the name `changePasscodePressed` comes from original code; it is very similar to my `handlePasscodeChange`. // Check if `handlePasscodeChange` is not required, remove it and rename `changePasscodePressed` to `handlePasscodeChange` const changePasscodePressed = () => { - console.log('changePasscodePressed'); + handleAddPassCode(); }; // TODO: Vardan: the name `removePasscodePressed` comes from original code; // Check if I rename`changePasscodePressed` to `handlePasscodeChange`, also rename `removePasscodePressed` to `handleRemovePasscode` - const removePasscodePressed = () => { - console.log('removePasscodePressed'); + const removePasscodePressed = async () => { + await preventRefreshing( + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, + async () => { + if (await application.removePasscode()) { + await application + .getAutolockService() + .deleteAutolockPreference(); + await reloadAutoLockInterval(); + refreshEncryptionStatus(); + } + } + ); + + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }; const downloadDataArchive = () => { @@ -219,10 +299,6 @@ const AccountMenu = observer(({ appState, application }: Props) => { console.log('openErrorReportingDialog'); }; - const handleClose = () => { - console.log('close this'); - }; - // TODO: check whether this works fine (e.g. remove all tags and notes and then add one and check whether UI behaves appropriately) const notesAndTagsCount = application.getItems([ContentType.Note, ContentType.Tag]).length; const hasProtections = application.hasProtectionSources(); @@ -262,16 +338,9 @@ const AccountMenu = observer(({ appState, application }: Props) => { setServer(host); }, [application]); - - /* - const { searchOptions } = appState; - - const { - includeProtectedContents, - includeArchived, - includeTrashed, - } = searchOptions; - */ + useEffect(() => { + refreshEncryptionStatus(); + }, [refreshEncryptionStatus]); return (
{ position: 'absolute' }}>
-
+
Account
- Close + Close
{!user && !showLogin && !showRegister && ( @@ -514,7 +583,7 @@ const AccountMenu = observer(({ appState, application }: Props) => {

{protectionsDisabledUntil && (
-
@@ -554,17 +623,19 @@ const AccountMenu = observer(({ appState, application }: Props) => { {showPasscodeForm && (
- {/* TODO: Vardan: there are `should-focus` and `sn-autofocus`, implement them */} @@ -588,7 +659,7 @@ const AccountMenu = observer(({ appState, application }: Props) => { {passcodeAutoLockOptions.map(option => { return ( selectAutoLockInterval(option.value)}> {option.label} @@ -720,6 +791,7 @@ const AccountMenu = observer(({ appState, application }: Props) => { ); }); -export const AccountMenu2 = toDirective( - AccountMenu +export const AccountMenuReact = toDirective( + AccountMenu, + { closeAccountMenu: '&'} ); diff --git a/app/assets/javascripts/components/ConfirmSignoutModal.tsx b/app/assets/javascripts/components/ConfirmSignoutModal.tsx index a40c9bdc9..896d07903 100644 --- a/app/assets/javascripts/components/ConfirmSignoutModal.tsx +++ b/app/assets/javascripts/components/ConfirmSignoutModal.tsx @@ -19,7 +19,7 @@ const ConfirmSignoutContainer = observer((props: Props) => { if (!props.appState.accountMenu.signingOut) { return null; } - if (!props.appState.accountMenu2.signingOut) { + if (!props.appState.accountMenuReact.signingOut) { return null; } return ; @@ -33,15 +33,14 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { const cancelRef = useRef(); function close() { appState.accountMenu.setSigningOut(false); - appState.accountMenu2.setSigningOut(false); + appState.accountMenuReact.setSigningOut(false); } const [localBackupsCount, setLocalBackupsCount] = useState(0); useEffect(() => { application.bridge.localBackupsCount().then(setLocalBackupsCount); - // }, [appState.accountMenu.signingOut, application.bridge]); - }, [appState.accountMenu.signingOut, appState.accountMenu2.signingOut, application.bridge]); + }, [appState.accountMenu.signingOut, appState.accountMenuReact.signingOut, application.bridge]); return ( diff --git a/app/assets/javascripts/components/NoAccountWarning.tsx b/app/assets/javascripts/components/NoAccountWarning.tsx index cd2cfdb48..b098389e8 100644 --- a/app/assets/javascripts/components/NoAccountWarning.tsx +++ b/app/assets/javascripts/components/NoAccountWarning.tsx @@ -21,7 +21,7 @@ const NoAccountWarning = observer(({ appState }: Props) => { onClick={(event) => { event.stopPropagation(); appState.accountMenu.setShow(true); - appState.accountMenu2.setShow(true); + appState.accountMenuReact.setShow(true); }} > Open Account menu diff --git a/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx b/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx index 4d9ace02e..cd242b969 100644 --- a/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx +++ b/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx @@ -16,7 +16,7 @@ function NoProtectionsNoteWarning({ appState, onViewNote }: Props) { className="sn-button small info" onClick={() => { appState.accountMenu.setShow(true); - appState.accountMenu2.setShow(true); + appState.accountMenuReact.setShow(true); }} > Open account menu diff --git a/app/assets/javascripts/ui_models/app_state/account_menu_2_state.ts b/app/assets/javascripts/ui_models/app_state/account_menu_react_state.ts similarity index 94% rename from app/assets/javascripts/ui_models/app_state/account_menu_2_state.ts rename to app/assets/javascripts/ui_models/app_state/account_menu_react_state.ts index fe42cf0bd..c726fb613 100644 --- a/app/assets/javascripts/ui_models/app_state/account_menu_2_state.ts +++ b/app/assets/javascripts/ui_models/app_state/account_menu_react_state.ts @@ -1,7 +1,7 @@ import { action, makeObservable, observable } from "mobx"; import { WebApplication } from '@/ui_models/application'; -export class AccountMenuState2 { +export class AccountMenuStateReact { show = false; signingOut = false; 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 873ecb69d..e10510141 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -21,7 +21,7 @@ import { SyncState } from './sync_state'; import { SearchOptionsState } from './search_options_state'; import { NotesState } from './notes_state'; import { TagsState } from './tags_state'; -import { AccountMenuState2 } from '@/ui_models/app_state/account_menu_2_state'; +import { AccountMenuStateReact } from '@/ui_models/app_state/account_menu_react_state'; export enum AppStateEvent { TagChanged, @@ -62,8 +62,7 @@ export class AppState { selectedTag?: SNTag; showBetaWarning: boolean; readonly accountMenu = new AccountMenuState(); - // readonly accountMenu2 = new AccountMenu_2_State(); - readonly accountMenu2: AccountMenuState2; + readonly accountMenuReact: AccountMenuStateReact; readonly actionsMenu = new ActionsMenuState(); readonly noAccountWarning: NoAccountWarningState; readonly sync = new SyncState(); @@ -99,10 +98,7 @@ export class AppState { application, this.appEventObserverRemovers ); - this.accountMenu2 = new AccountMenuState2( - // application, - // this.appEventObserverRemovers - ); + this.accountMenuReact = new AccountMenuStateReact(); 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 6b9bca39b..5e4a32941 100644 --- a/app/assets/javascripts/views/footer/footer-view.pug +++ b/app/assets/javascripts/views/footer/footer-view.pug @@ -18,10 +18,12 @@ ng-if='ctrl.showAccountMenu', application='ctrl.application' ) - account-menu2( + account-menu-react( ng-click='$event.stopPropagation()', app-state='ctrl.appState' application='ctrl.application' + ng-if='ctrl.showAccountMenu', + close-account-menu='ctrl.closeAccountMenu()', ) .sk-app-bar-item a.no-decoration.sk-label.title( diff --git a/app/assets/javascripts/views/footer/footer_view.ts b/app/assets/javascripts/views/footer/footer_view.ts index b4f6c01a5..9162d7e7c 100644 --- a/app/assets/javascripts/views/footer/footer_view.ts +++ b/app/assets/javascripts/views/footer/footer_view.ts @@ -62,7 +62,7 @@ class FooterViewCtrl extends PureViewCtrl { const showBetaWarning = this.appState.showBetaWarning; this.showAccountMenu = this.appState.accountMenu.show; - this.showAccountMenu2 = this.appState.accountMenu2.show; + this.showAccountMenuReact = this.appState.accountMenuReact.show; this.setState({ showBetaWarning: showBetaWarning, showDataUpgrade: !showBetaWarning @@ -256,7 +256,7 @@ class FooterViewCtrl extends PureViewCtrl