diff --git a/.eslintrc b/.eslintrc index 21f27363513..b572d609cdf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,7 +11,7 @@ "parserOptions": { "project": "./app/assets/javascripts/tsconfig.json" }, - "ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js", "jest.config.js"], + "ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__"], "rules": { "standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals "no-throw-literal": 0, diff --git a/.gitignore b/.gitignore index baec9d89b11..6db1cb54065 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ yarn-error.log package-lock.json codeqldb + +coverage diff --git a/app/assets/javascripts/__mocks__/@standardnotes/snjs.js b/app/assets/javascripts/__mocks__/@standardnotes/snjs.js new file mode 100644 index 00000000000..a89d294165d --- /dev/null +++ b/app/assets/javascripts/__mocks__/@standardnotes/snjs.js @@ -0,0 +1,11 @@ +const { + ApplicationEvent, + ProtectionSessionDurations, + ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, +} = require('@standardnotes/snjs'); + +module.exports = { + ApplicationEvent: ApplicationEvent, + ProtectionSessionDurations: ProtectionSessionDurations, + ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, +}; diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 5a8b9b4803f..d9f5f81059b 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -65,7 +65,7 @@ import { StartApplication } from './startApplication'; import { Bridge } from './services/bridge'; import { SessionsModalDirective } from './components/SessionsModal'; import { NoAccountWarningDirective } from './components/NoAccountWarning'; -import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning'; +import { ProtectedNoteOverlayDirective } from './components/ProtectedNoteOverlay'; import { SearchOptionsDirective } from './components/SearchOptions'; import { AccountMenuDirective } from './components/AccountMenu'; import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; @@ -174,7 +174,7 @@ const startApplication: StartApplication = async function startApplication( .directive('accountMenu', AccountMenuDirective) .directive('quickSettingsMenu', QuickSettingsMenuDirective) .directive('noAccountWarning', NoAccountWarningDirective) - .directive('protectedNotePanel', NoProtectionsdNoteWarningDirective) + .directive('protectedNotePanel', ProtectedNoteOverlayDirective) .directive('searchOptions', SearchOptionsDirective) .directive('confirmSignout', ConfirmSignoutDirective) .directive('multipleSelectedNotesPanel', MultipleSelectedNotesDirective) diff --git a/app/assets/javascripts/components/AccountMenu/DataBackup.tsx b/app/assets/javascripts/components/AccountMenu/DataBackup.tsx deleted file mode 100644 index 808a27616de..00000000000 --- a/app/assets/javascripts/components/AccountMenu/DataBackup.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { isDesktopApplication } from '@/utils'; -import { alertDialog } from '@Services/alertService'; -import { - STRING_IMPORT_SUCCESS, - STRING_INVALID_IMPORT_FILE, - STRING_UNSUPPORTED_BACKUP_FILE_VERSION, - StringImportError -} from '@/strings'; -import { BackupFile } from '@standardnotes/snjs'; -import { useRef, useState } from 'preact/hooks'; -import { WebApplication } from '@/ui_models/application'; -import { JSXInternal } from 'preact/src/jsx'; -import TargetedEvent = JSXInternal.TargetedEvent; -import { AppState } from '@/ui_models/app_state'; -import { observer } from 'mobx-react-lite'; - -type Props = { - application: WebApplication; - appState: AppState; -} - -const DataBackup = observer(({ - application, - appState - }: Props) => { - - const fileInputRef = useRef(null); - const [isImportDataLoading, setIsImportDataLoading] = useState(false); - - const { isBackupEncrypted, isEncryptionEnabled, setIsBackupEncrypted } = appState.accountMenu; - - 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 }); - } - }; - - // Whenever "Import Backup" is either clicked or key-pressed, proceed the import - const handleImportFile = (event: TargetedEvent | KeyboardEvent) => { - if (event instanceof KeyboardEvent) { - const { code } = event; - - // Process only when "Enter" or "Space" keys are pressed - if (code !== 'Enter' && code !== 'Space') { - return; - } - // Don't proceed the event's default action - // (like scrolling in case the "space" key is pressed) - event.preventDefault(); - } - - (fileInputRef.current as HTMLInputElement).click(); - }; - - return ( - <> - {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. -

- )} -
-
- )} - - ); -}); - -export default DataBackup; diff --git a/app/assets/javascripts/components/AccountMenu/Encryption.tsx b/app/assets/javascripts/components/AccountMenu/Encryption.tsx deleted file mode 100644 index 1a98f240433..00000000000 --- a/app/assets/javascripts/components/AccountMenu/Encryption.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { AppState } from '@/ui_models/app_state'; -import { observer } from 'mobx-react-lite'; - -type Props = { - appState: AppState; -} - -const Encryption = observer(({ appState }: Props) => { - const { isEncryptionEnabled, encryptionStatusString, notesAndTagsCount } = appState.accountMenu; - - const getEncryptionStatusForNotes = () => { - const length = notesAndTagsCount; - return `${length}/${length} notes and tags encrypted`; - }; - - return ( -
-
- Encryption -
- {isEncryptionEnabled && ( -
- {getEncryptionStatusForNotes()} -
- )} -

- {encryptionStatusString} -

-
- ); -}); - -export default Encryption; diff --git a/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx b/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx deleted file mode 100644 index 92784557ef6..00000000000 --- a/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useState } from 'preact/hooks'; -import { storage, StorageKey } from '@Services/localStorage'; -import { disableErrorReporting, enableErrorReporting, errorReportingId } from '@Services/errorReporting'; -import { alertDialog } from '@Services/alertService'; -import { observer } from 'mobx-react-lite'; -import { AppState } from '@/ui_models/app_state'; - -type Props = { - appState: AppState; -} - -const ErrorReporting = observer(({ appState }: Props) => { - const [isErrorReportingEnabled] = useState(() => storage.get(StorageKey.DisableErrorReporting) === false); - const [errorReportingIdValue] = useState(() => errorReportingId()); - - const toggleErrorReportingEnabled = () => { - if (isErrorReportingEnabled) { - disableErrorReporting(); - } else { - enableErrorReporting(); - } - if (!appState.sync.inProgress) { - window.location.reload(); - } - }; - - const openErrorReportingDialog = () => { - alertDialog({ - title: 'Data sent during automatic error reporting', - text: ` - We use Bugsnag - to automatically report errors that occur while the app is running. See - - this article, paragraph 'Browser' under 'Sending diagnostic data', - - to see what data is included in error reports. -

- Error reports never include IP addresses and are fully - anonymized. We use error reports to be alerted when something in our - code is causing unexpected errors and crashes in your application - experience. - ` - }); - }; - - return ( -
-
Error Reporting
-
- Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'} -
-

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

- {errorReportingIdValue && ( - <> -

- Your random identifier is {errorReportingIdValue} -

-

- Disabling error reporting will remove that identifier from your - local storage, and a new identifier will be created should you - decide to enable error reporting again in the future. -

- - )} -
- -
- -
- ); -}); - -export default ErrorReporting; diff --git a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx deleted file mode 100644 index cdfbbc046d0..00000000000 --- a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx +++ /dev/null @@ -1,272 +0,0 @@ -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, - Strings -} from '@/strings'; -import { WebApplication } from '@/ui_models/application'; -import { preventRefreshing } from '@/utils'; -import { JSXInternal } from 'preact/src/jsx'; -import TargetedEvent = JSXInternal.TargetedEvent; -import { alertDialog } from '@Services/alertService'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; -import { ApplicationEvent } from '@standardnotes/snjs'; -import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; -import { observer } from 'mobx-react-lite'; -import { AppState } from '@/ui_models/app_state'; - -type Props = { - application: WebApplication; - appState: AppState; -}; - -const PasscodeLock = observer(({ - application, - appState, - }: Props) => { - const keyStorageInfo = StringUtils.keyStorageInfo(application); - const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); - - const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } = appState.accountMenu; - - const passcodeInputRef = useRef(null); - - const [passcode, setPasscode] = useState(undefined); - const [passcodeConfirmation, setPasscodeConfirmation] = useState(undefined); - const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); - const [isPasscodeFocused, setIsPasscodeFocused] = useState(false); - const [showPasscodeForm, setShowPasscodeForm] = useState(false); - const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); - const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); - - const handleAddPassCode = () => { - setShowPasscodeForm(true); - setIsPasscodeFocused(true); - }; - - const changePasscodePressed = () => { - handleAddPassCode(); - }; - - const reloadAutoLockInterval = useCallback(async () => { - const interval = await application.getAutolockService().getAutoLockInterval(); - setSelectedAutoLockInterval(interval); - }, [application]); - - const refreshEncryptionStatus = useCallback(() => { - const hasUser = application.hasAccount(); - const hasPasscode = application.hasPasscode(); - - setHasPasscode(hasPasscode); - - const encryptionEnabled = hasUser || hasPasscode; - - const encryptionStatusString = hasUser - ? STRING_E2E_ENABLED - : hasPasscode - ? STRING_LOCAL_ENC_ENABLED - : STRING_ENC_NOT_ENABLED; - - setEncryptionStatusString(encryptionStatusString); - setIsEncryptionEnabled(encryptionEnabled); - setIsBackupEncrypted(encryptionEnabled); - }, [application, setEncryptionStatusString, setIsBackupEncrypted, setIsEncryptionEnabled]); - - const selectAutoLockInterval = async (interval: number) => { - if (!(await application.authorizeAutolockIntervalChange())) { - return; - } - await application.getAutolockService().setAutoLockInterval(interval); - reloadAutoLockInterval(); - }; - - 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 handlePasscodeChange = (event: TargetedEvent) => { - const { value } = event.target as HTMLInputElement; - setPasscode(value); - }; - - const handleConfirmPasscodeChange = (event: TargetedEvent) => { - const { value } = event.target as HTMLInputElement; - setPasscodeConfirmation(value); - }; - - const submitPasscodeForm = async (event: TargetedEvent | TargetedMouseEvent) => { - event.preventDefault(); - - if (!passcode || passcode.length === 0) { - await alertDialog({ - text: Strings.enterPasscode, - }); - } - - 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(); - }; - - useEffect(() => { - refreshEncryptionStatus(); - }, [refreshEncryptionStatus]); - - // `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial - // value of `selectedAutoLockInterval` - useEffect(() => { - reloadAutoLockInterval(); - }, [reloadAutoLockInterval]); - - useEffect(() => { - if (isPasscodeFocused) { - passcodeInputRef.current!.focus(); - setIsPasscodeFocused(false); - } - }, [isPasscodeFocused]); - - // Add the required event observers - useEffect(() => { - const removeKeyStatusChangedObserver = application.addEventObserver( - async () => { - setCanAddPasscode(!application.isEphemeralSession()); - setHasPasscode(application.hasPasscode()); - setShowPasscodeForm(false); - }, - ApplicationEvent.KeyStatusChanged - ); - - return () => { - removeKeyStatusChangedObserver(); - }; - }, [application]); - - return ( -
-
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.
- -
- - )} -
- ); -}); - -export default PasscodeLock; diff --git a/app/assets/javascripts/components/AccountMenu/Protections.tsx b/app/assets/javascripts/components/AccountMenu/Protections.tsx deleted file mode 100644 index 8e7b1f2292b..00000000000 --- a/app/assets/javascripts/components/AccountMenu/Protections.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { WebApplication } from '@/ui_models/application'; -import { FunctionalComponent } from 'preact'; -import { useCallback, useState } from 'preact/hooks'; -import { useEffect } from 'preact/hooks'; -import { ApplicationEvent } from '@standardnotes/snjs'; -import { isSameDay } from '@/utils'; - -type Props = { - application: WebApplication; -}; - -const Protections: FunctionalComponent = ({ application }) => { - const enableProtections = () => { - application.clearProtectionSession(); - }; - - const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources()); - - const getProtectionsDisabledUntil = useCallback((): string | null => { - const protectionExpiry = application.getProtectionSessionExpiryDate(); - const now = new Date(); - if (protectionExpiry > now) { - let f: Intl.DateTimeFormat; - if (isSameDay(protectionExpiry, now)) { - f = new Intl.DateTimeFormat(undefined, { - hour: 'numeric', - minute: 'numeric' - }); - } else { - f = new Intl.DateTimeFormat(undefined, { - weekday: 'long', - day: 'numeric', - month: 'short', - hour: 'numeric', - minute: 'numeric' - }); - } - - return f.format(protectionExpiry); - } - return null; - }, [application]); - - const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); - - useEffect(() => { - const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( - async () => { - setProtectionsDisabledUntil(getProtectionsDisabledUntil()); - }, - ApplicationEvent.ProtectionSessionExpiryDateChanged - ); - - const removeKeyStatusChangedObserver = application.addEventObserver( - async () => { - setHasProtections(application.hasProtectionSources()); - }, - ApplicationEvent.KeyStatusChanged - ); - - return () => { - removeProtectionSessionExpiryDateChangedObserver(); - removeKeyStatusChangedObserver(); - }; - }, [application, getProtectionsDisabledUntil]); - - if (!hasProtections) { - return null; - } - - return ( -
-
Protections
- {protectionsDisabledUntil && ( -
- Protections are disabled until {protectionsDisabledUntil} -
- )} - {!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 && ( -
- -
- )} -
- ); -}; - -export default Protections; diff --git a/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx b/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx deleted file mode 100644 index e3e9ec29135..00000000000 --- a/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { AppState } from '@/ui_models/app_state'; -import { toDirective } from './utils'; - -type Props = { appState: AppState; onViewNote: () => void }; - -function NoProtectionsNoteWarning({ appState, onViewNote }: Props) { - return ( -
-

This note is protected

-

- Add a passcode or create an account to require authentication to view - this note. -

-
- - -
-
- ); -} - -export const NoProtectionsdNoteWarningDirective = toDirective( - NoProtectionsNoteWarning, - { - onViewNote: '&', - } -); diff --git a/app/assets/javascripts/components/ProtectedNoteOverlay.tsx b/app/assets/javascripts/components/ProtectedNoteOverlay.tsx new file mode 100644 index 00000000000..a5b6be47ac7 --- /dev/null +++ b/app/assets/javascripts/components/ProtectedNoteOverlay.tsx @@ -0,0 +1,51 @@ +import { AppState } from '@/ui_models/app_state'; +import { toDirective } from './utils'; + +type Props = { + appState: AppState; + onViewNote: () => void; + hasProtectionSources: boolean; +}; + +function ProtectedNoteOverlay({ + appState, + onViewNote, + hasProtectionSources, +}: Props) { + const instructionText = hasProtectionSources + ? 'Authenticate to view this note.' + : 'Add a passcode or create an account to require authentication to view this note.'; + + return ( +
+

This note is protected

+

{instructionText}

+
+ {!hasProtectionSources && ( + + )} + +
+
+ ); +} + +export const ProtectedNoteOverlayDirective = toDirective( + ProtectedNoteOverlay, + { + onViewNote: '&', + hasProtectionSources: '=', + } +); diff --git a/app/assets/javascripts/components/SearchOptions.tsx b/app/assets/javascripts/components/SearchOptions.tsx index b62989b7de9..a67dfcc6019 100644 --- a/app/assets/javascripts/components/SearchOptions.tsx +++ b/app/assets/javascripts/components/SearchOptions.tsx @@ -1,7 +1,7 @@ import { AppState } from '@/ui_models/app_state'; import { Icon } from './Icon'; import { toDirective, useCloseOnBlur } from './utils'; -import { useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { WebApplication } from '@/ui_models/application'; import VisuallyHidden from '@reach/visually-hidden'; import { @@ -11,7 +11,6 @@ import { } from '@reach/disclosure'; import { Switch } from './Switch'; import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; type Props = { appState: AppState; diff --git a/app/assets/javascripts/components/utils.ts b/app/assets/javascripts/components/utils.ts index 652ee79e757..3e4f9b6316d 100644 --- a/app/assets/javascripts/components/utils.ts +++ b/app/assets/javascripts/components/utils.ts @@ -1,7 +1,6 @@ import { FunctionComponent, h, render } from 'preact'; import { unmountComponentAtNode } from 'preact/compat'; -import { StateUpdater, useCallback, useState } from 'preact/hooks'; -import { useEffect } from 'react'; +import { StateUpdater, useCallback, useState, useEffect } from 'preact/hooks'; /** * @returns a callback that will close a dropdown if none of its children has diff --git a/app/assets/javascripts/jest.config.js b/app/assets/javascripts/jest.config.js index e985f0c5918..b0733b20462 100644 --- a/app/assets/javascripts/jest.config.js +++ b/app/assets/javascripts/jest.config.js @@ -1,10 +1,13 @@ -const pathsToModuleNameMapper = require('ts-jest/utils').pathsToModuleNameMapper; +const pathsToModuleNameMapper = + require('ts-jest/utils').pathsToModuleNameMapper; const tsConfig = require('./tsconfig.json'); const pathsFromTsconfig = tsConfig.compilerOptions.paths; module.exports = { + restoreMocks: true, clearMocks: true, + resetMocks: true, moduleNameMapper: { ...pathsToModuleNameMapper(pathsFromTsconfig, { prefix: '', @@ -14,7 +17,6 @@ module.exports = { '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, globals: { - window: {}, __VERSION__: '1.0.0', __DESKTOP__: false, __WEB__: true, diff --git a/app/assets/javascripts/preferences/panes/account/Sync.tsx b/app/assets/javascripts/preferences/panes/account/Sync.tsx index e8b465b39ea..b7d6147bd4b 100644 --- a/app/assets/javascripts/preferences/panes/account/Sync.tsx +++ b/app/assets/javascripts/preferences/panes/account/Sync.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/Button'; import { SyncQueueStrategy, dateToLocalizedString } from '@standardnotes/snjs'; import { STRING_GENERIC_SYNC_ERROR } from '@/strings'; import { useState } from '@node_modules/preact/hooks'; -import { observer } from '@node_modules/mobx-react-lite'; +import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; import { FunctionComponent } from 'preact'; diff --git a/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx b/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx index 2cf2caabb1b..f0e5642745d 100644 --- a/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx +++ b/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx @@ -4,7 +4,12 @@ import { useCallback, useState } from 'preact/hooks'; import { useEffect } from 'preact/hooks'; import { ApplicationEvent } from '@standardnotes/snjs'; import { isSameDay } from '@/utils'; -import { PreferencesGroup, PreferencesSegment, Title, Text } from '@/preferences/components'; +import { + PreferencesGroup, + PreferencesSegment, + Title, + Text, +} from '@/preferences/components'; import { Button } from '@/components/Button'; type Props = { @@ -16,7 +21,9 @@ export const Protections: FunctionalComponent = ({ application }) => { application.clearProtectionSession(); }; - const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources()); + const [hasProtections, setHasProtections] = useState(() => + application.hasProtectionSources() + ); const getProtectionsDisabledUntil = useCallback((): string | null => { const protectionExpiry = application.getProtectionSessionExpiryDate(); @@ -26,7 +33,7 @@ export const Protections: FunctionalComponent = ({ application }) => { if (isSameDay(protectionExpiry, now)) { f = new Intl.DateTimeFormat(undefined, { hour: 'numeric', - minute: 'numeric' + minute: 'numeric', }); } else { f = new Intl.DateTimeFormat(undefined, { @@ -34,7 +41,7 @@ export const Protections: FunctionalComponent = ({ application }) => { day: 'numeric', month: 'short', hour: 'numeric', - minute: 'numeric' + minute: 'numeric', }); } @@ -43,14 +50,23 @@ export const Protections: FunctionalComponent = ({ application }) => { return null; }, [application]); - const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); + const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState( + getProtectionsDisabledUntil() + ); useEffect(() => { - const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( + const removeUnprotectedSessionBeginObserver = application.addEventObserver( + async () => { + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); + }, + ApplicationEvent.UnprotectedSessionBegan + ); + + const removeUnprotectedSessionEndObserver = application.addEventObserver( async () => { setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }, - ApplicationEvent.ProtectionSessionExpiryDateChanged + ApplicationEvent.UnprotectedSessionExpired ); const removeKeyStatusChangedObserver = application.addEventObserver( @@ -61,7 +77,8 @@ export const Protections: FunctionalComponent = ({ application }) => { ); return () => { - removeProtectionSessionExpiryDateChangedObserver(); + removeUnprotectedSessionBeginObserver(); + removeUnprotectedSessionEndObserver(); removeKeyStatusChangedObserver(); }; }, [application, getProtectionsDisabledUntil]); @@ -74,19 +91,28 @@ export const Protections: FunctionalComponent = ({ application }) => { Protections - {protectionsDisabledUntil - ? Protections are disabled until {protectionsDisabledUntil}. - : Protections are enabled. - } + {protectionsDisabledUntil ? ( + + Unprotected access expires at {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. + Actions like viewing or searching protected notes, exporting decrypted + backups, or revoking an active session require additional + authentication such as entering your account password or application + passcode. - {protectionsDisabledUntil && -