feat: Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. (#2322)

This commit is contained in:
Mo
2023-05-02 11:05:10 -05:00
committed by GitHub
parent 3df23cdb5c
commit 7e3db49322
76 changed files with 1526 additions and 1013 deletions

View File

@@ -1,4 +1,4 @@
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useRef } from 'react'
import { STRING_SIGN_OUT_CONFIRMATION } from '@/Constants/Strings'
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
@@ -8,6 +8,7 @@ import { isDesktopApplication } from '@/Utils'
import Button from '@/Components/Button/Button'
import Icon from '../Icon/Icon'
import AlertDialog from '../AlertDialog/AlertDialog'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
type Props = {
application: WebApplication
@@ -16,30 +17,24 @@ type Props = {
}
const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewControllerManager, applicationGroup }) => {
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false)
const hasAnyBackupsEnabled =
application.fileBackups?.isFilesBackupsEnabled() ||
application.fileBackups?.isPlaintextBackupsEnabled() ||
application.fileBackups?.isTextBackupsEnabled()
const cancelRef = useRef<HTMLButtonElement>(null)
const closeDialog = useCallback(() => {
viewControllerManager.accountMenuController.setSigningOut(false)
}, [viewControllerManager.accountMenuController])
const [localBackupsCount, setLocalBackupsCount] = useState(0)
useEffect(() => {
application.desktopDevice?.localBackupsCount().then(setLocalBackupsCount).catch(console.error)
}, [viewControllerManager.accountMenuController.signingOut, application.desktopDevice])
const workspaces = applicationGroup.getDescriptors()
const showWorkspaceWarning = workspaces.length > 1 && isDesktopApplication()
const confirm = useCallback(() => {
if (deleteLocalBackups) {
application.signOutAndDeleteLocalBackups().catch(console.error)
} else {
application.user.signOut().catch(console.error)
}
application.user.signOut().catch(console.error)
closeDialog()
}, [application, closeDialog, deleteLocalBackups])
}, [application, closeDialog])
return (
<AlertDialog closeDialog={closeDialog}>
@@ -66,31 +61,26 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewContro
</div>
</div>
{localBackupsCount > 0 && (
<div className="flex">
<div className="sk-panel-row"></div>
<label className="flex items-center">
<input
type="checkbox"
checked={deleteLocalBackups}
onChange={(event) => {
setDeleteLocalBackups((event.target as HTMLInputElement).checked)
}}
/>
<span className="ml-2">
Delete {localBackupsCount} local backup file
{localBackupsCount > 1 ? 's' : ''}
</span>
</label>
<button
className="sk-a ml-1.5 cursor-pointer rounded p-0 capitalize"
onClick={() => {
application.desktopDevice?.viewlocalBackups()
}}
>
View backup files
</button>
</div>
{hasAnyBackupsEnabled && (
<>
<HorizontalSeparator classes="my-2" />
<div className="flex">
<div className="sk-panel-row"></div>
<div>
<p className="text-base text-foreground lg:text-sm">
Local backups are enabled for this workspace. Review your backup files manually to decide what to keep.
</p>
<button
className="sk-a mt-2 cursor-pointer rounded p-0 capitalize lg:text-sm"
onClick={() => {
void application.fileBackups?.openAllDirectoriesContainingBackupFiles()
}}
>
View backup files
</button>
</div>
</div>
</>
)}
<div className="mt-4 flex justify-end gap-2">

View File

@@ -35,7 +35,7 @@ export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }>
>
<div className="ml-2">
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
<div className="text-xs text-neutral">{backupInfo.absolutePath}</div>
<div className="text-xs text-neutral">{application.fileBackups?.getFileBackupAbsolutePath(backupInfo)}</div>
</div>
</MenuItem>
)}

View File

@@ -153,7 +153,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
}
async processPasswordChange() {
await this.application.downloadBackup()
await this.application.performDesktopTextBackup()
this.setState({
lockContinue: true,

View File

@@ -58,7 +58,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
}
const processEmailChange = useCallback(async () => {
await application.downloadBackup()
await application.performDesktopTextBackup()
setLockContinue(true)

View File

@@ -6,6 +6,8 @@ import DataBackups from './DataBackups'
import EmailBackups from './EmailBackups'
import FileBackupsCrossPlatform from './Files/FileBackupsCrossPlatform'
import { observer } from 'mobx-react-lite'
import TextBackupsCrossPlatform from './TextBackups/TextBackupsCrossPlatform'
import PlaintextBackupsCrossPlatform from './PlaintextBackups/PlaintextBackupsCrossPlatform'
type Props = {
viewControllerManager: ViewControllerManager
@@ -16,6 +18,8 @@ const Backups: FunctionComponent<Props> = ({ application, viewControllerManager
return (
<PreferencesPane>
<DataBackups application={application} viewControllerManager={viewControllerManager} />
<TextBackupsCrossPlatform application={application} />
<PlaintextBackupsCrossPlatform />
<FileBackupsCrossPlatform application={application} />
<EmailBackups application={application} />
</PreferencesPane>

View File

@@ -1,4 +1,3 @@
import { isDesktopApplication } from '@/Utils'
import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services'
import {
STRING_IMPORT_SUCCESS,
@@ -15,7 +14,7 @@ import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef,
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import { Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import Button from '@/Components/Button/Button'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
@@ -177,14 +176,7 @@ const DataBackups = ({ application, viewControllerManager }: Props) => {
<PreferencesGroup>
<PreferencesSegment>
<Title>Data Backups</Title>
{isDesktopApplication() && (
<Text className="mb-3">
Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
</Text>
)}
<Subtitle>Download a backup of all your data</Subtitle>
<Subtitle>Download a backup of all your text-based data</Subtitle>
{isEncryptionEnabled && (
<form className="sk-panel-form sk-panel-row">

View File

@@ -118,7 +118,7 @@ const EmailBackups = ({ application }: Props) => {
)}
<div className={`${!hasAccount ? 'pointer-events-none cursor-default opacity-50' : ''}`}>
<Subtitle>Email frequency</Subtitle>
<Subtitle>Frequency</Subtitle>
<Text>How often to receive backups.</Text>
<div className="mt-2">
{isLoading ? (

View File

@@ -15,13 +15,13 @@ const FileBackupsCrossPlatform = ({ application }: Props) => {
const fileBackupsService = useMemo(() => application.fileBackups, [application])
return fileBackupsService ? (
<FileBackupsDesktop application={application} backupsService={fileBackupsService} />
<FileBackupsDesktop backupsService={fileBackupsService} />
) : (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>File Backups</Title>
<Subtitle>Automatically save encrypted backups of files uploaded on any device to this computer.</Subtitle>
<Title>Automatic File Backups</Title>
<Subtitle>Automatically save encrypted backups of your files.</Subtitle>
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
</PreferencesSegment>
<HorizontalSeparator classes="my-4" />

View File

@@ -1,7 +1,6 @@
import { WebApplication } from '@/Application/Application'
import { observer } from 'mobx-react-lite'
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useState } from 'react'
import Button from '@/Components/Button/Button'
import Switch from '@/Components/Switch/Switch'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
@@ -10,30 +9,21 @@ import BackupsDropZone from './BackupsDropZone'
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { BackupServiceInterface } from '@standardnotes/snjs'
import { useApplication } from '@/Components/ApplicationProvider'
type Props = {
application: WebApplication
backupsService: NonNullable<WebApplication['fileBackups']>
backupsService: BackupServiceInterface
}
const FileBackupsDesktop = ({ application, backupsService }: Props) => {
const [backupsEnabled, setBackupsEnabled] = useState(false)
const [backupsLocation, setBackupsLocation] = useState('')
useEffect(() => {
void backupsService.isFilesBackupsEnabled().then(setBackupsEnabled)
}, [backupsService])
useEffect(() => {
if (backupsEnabled) {
void backupsService.getFilesBackupsLocation().then(setBackupsLocation)
}
}, [backupsService, backupsEnabled])
const FileBackupsDesktop = ({ backupsService }: Props) => {
const application = useApplication()
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isFilesBackupsEnabled())
const [backupsLocation, setBackupsLocation] = useState(backupsService.getFilesBackupsLocation())
const changeBackupsLocation = useCallback(async () => {
await backupsService.changeFilesBackupsLocation()
setBackupsLocation(await backupsService.getFilesBackupsLocation())
const newLocation = await backupsService.changeFilesBackupsLocation()
setBackupsLocation(newLocation)
}, [backupsService])
const openBackupsLocation = useCallback(async () => {
@@ -42,25 +32,24 @@ const FileBackupsDesktop = ({ application, backupsService }: Props) => {
const toggleBackups = useCallback(async () => {
if (backupsEnabled) {
await backupsService.disableFilesBackups()
backupsService.disableFilesBackups()
} else {
await backupsService.enableFilesBackups()
}
setBackupsEnabled(await backupsService.isFilesBackupsEnabled())
setBackupsEnabled(backupsService.isFilesBackupsEnabled())
setBackupsLocation(backupsService.getFilesBackupsLocation())
}, [backupsService, backupsEnabled])
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>File Backups</Title>
<Title>Automatic File Backups</Title>
<div className="flex items-center justify-between">
<div className="mr-10 flex flex-col">
<Subtitle>
Automatically save encrypted backups of files uploaded on any device to this computer.
</Subtitle>
<Subtitle>Automatically save encrypted backups of your uploaded files to this computer.</Subtitle>
</div>
<Switch onChange={toggleBackups} checked={backupsEnabled} />
</div>
@@ -85,14 +74,14 @@ const FileBackupsDesktop = ({ application, backupsService }: Props) => {
</Text>
<EncryptionStatusItem
status={backupsLocation}
status={backupsLocation || 'Not Set'}
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
checkmark={false}
/>
<div className="mt-2.5 flex flex-row">
<Button label="Open Backups Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
<Button label="Change Backups Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
</div>
</>
</PreferencesSegment>

View File

@@ -0,0 +1,28 @@
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { useMemo } from 'react'
import PlaintextBackupsDesktop from './PlaintextBackupsDesktop'
import { useApplication } from '@/Components/ApplicationProvider'
const PlaintextBackupsCrossPlatform = () => {
const application = useApplication()
const fileBackupsService = useMemo(() => application.fileBackups, [application])
return fileBackupsService ? (
<PlaintextBackupsDesktop backupsService={fileBackupsService} />
) : (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Automatic Plaintext Backups</Title>
<Subtitle>Automatically save backups of all your notes into plaintext, non-encrypted folders.</Subtitle>
<Text className="mt-3">To enable plaintext backups, use the Standard Notes desktop application.</Text>
</PreferencesSegment>
</PreferencesGroup>
</>
)
}
export default PlaintextBackupsCrossPlatform

View File

@@ -0,0 +1,89 @@
import { observer } from 'mobx-react-lite'
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import { useCallback, useState } from 'react'
import Button from '@/Components/Button/Button'
import Switch from '@/Components/Switch/Switch'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Icon from '@/Components/Icon/Icon'
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { BackupServiceInterface } from '@standardnotes/snjs'
type Props = {
backupsService: BackupServiceInterface
}
const PlaintextBackupsDesktop = ({ backupsService }: Props) => {
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isPlaintextBackupsEnabled())
const [backupsLocation, setBackupsLocation] = useState(backupsService.getPlaintextBackupsLocation())
const changeBackupsLocation = useCallback(async () => {
const newLocation = await backupsService.changePlaintextBackupsLocation()
setBackupsLocation(newLocation)
}, [backupsService])
const openBackupsLocation = useCallback(async () => {
await backupsService.openPlaintextBackupsLocation()
}, [backupsService])
const toggleBackups = useCallback(async () => {
if (backupsEnabled) {
backupsService.disablePlaintextBackups()
} else {
await backupsService.enablePlaintextBackups()
}
setBackupsEnabled(backupsService.isPlaintextBackupsEnabled())
setBackupsLocation(backupsService.getPlaintextBackupsLocation())
}, [backupsEnabled, backupsService])
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Automatic Plaintext Backups</Title>
<div className="flex items-center justify-between">
<div className="mr-10 flex flex-col">
<Subtitle>
Automatically save backups of all your notes to this computer into plaintext, non-encrypted folders.
</Subtitle>
</div>
<Switch onChange={toggleBackups} checked={backupsEnabled} />
</div>
{!backupsEnabled && (
<>
<HorizontalSeparator classes="mt-2.5 mb-4" />
<Text>Plaintext backups are not enabled. Enable to choose where your data is backed up.</Text>
</>
)}
</PreferencesSegment>
{backupsEnabled && (
<>
<HorizontalSeparator classes="my-4" />
<PreferencesSegment>
<>
<Text className="mb-3">Plaintext backups are enabled and saved to:</Text>
<EncryptionStatusItem
status={backupsLocation || 'Not Set'}
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
checkmark={false}
/>
<div className="mt-2.5 flex flex-row">
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
</div>
</>
</PreferencesSegment>
</>
)}
</PreferencesGroup>
</>
)
}
export default observer(PlaintextBackupsDesktop)

View File

@@ -0,0 +1,30 @@
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { WebApplication } from '@/Application/Application'
import { useMemo } from 'react'
import TextBackupsDesktop from './TextBackupsDesktop'
type Props = {
application: WebApplication
}
const TextBackupsCrossPlatform = ({ application }: Props) => {
const fileBackupsService = useMemo(() => application.fileBackups, [application])
return fileBackupsService ? (
<TextBackupsDesktop backupsService={fileBackupsService} />
) : (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Automatic Text Backups</Title>
<Subtitle>Automatically save encrypted and decrypted backups of your note and tag data.</Subtitle>
<Text className="mt-3">To enable text backups, use the Standard Notes desktop application.</Text>
</PreferencesSegment>
</PreferencesGroup>
</>
)
}
export default TextBackupsCrossPlatform

View File

@@ -0,0 +1,106 @@
import { observer } from 'mobx-react-lite'
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import { useCallback, useState } from 'react'
import Button from '@/Components/Button/Button'
import Switch from '@/Components/Switch/Switch'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Icon from '@/Components/Icon/Icon'
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { BackupServiceInterface } from '@standardnotes/snjs'
import { useApplication } from '@/Components/ApplicationProvider'
type Props = {
backupsService: BackupServiceInterface
}
const TextBackupsDesktop = ({ backupsService }: Props) => {
const application = useApplication()
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isTextBackupsEnabled())
const [backupsLocation, setBackupsLocation] = useState(backupsService.getTextBackupsLocation())
const changeBackupsLocation = useCallback(async () => {
const newLocation = await backupsService.changeTextBackupsLocation()
setBackupsLocation(newLocation)
}, [backupsService])
const openBackupsLocation = useCallback(async () => {
await backupsService.openTextBackupsLocation()
}, [backupsService])
const toggleBackups = useCallback(async () => {
if (backupsEnabled) {
backupsService.disableTextBackups()
} else {
await backupsService.enableTextBackups()
}
setBackupsEnabled(backupsService.isTextBackupsEnabled())
setBackupsLocation(backupsService.getTextBackupsLocation())
}, [backupsEnabled, backupsService])
const performBackup = useCallback(async () => {
void application.getDesktopService()?.saveDesktopBackup()
}, [application])
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Automatic Encrypted Text Backups</Title>
<div className="flex items-center justify-between">
<div className="mr-10 flex flex-col">
<Subtitle>
Automatically save encrypted text backups of all your note and tag data to this computer.
</Subtitle>
</div>
<Switch onChange={toggleBackups} checked={backupsEnabled} />
</div>
{!backupsEnabled && (
<>
<HorizontalSeparator classes="mt-2.5 mb-4" />
<Text>Text backups are not enabled. Enable to choose where your data is backed up.</Text>
</>
)}
</PreferencesSegment>
{backupsEnabled && (
<>
<HorizontalSeparator classes="my-4" />
<PreferencesSegment>
<>
<Text className="mb-3">Text backups are enabled and saved to:</Text>
<EncryptionStatusItem
status={backupsLocation || 'Not Set'}
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
checkmark={false}
/>
<div className="mt-2.5 flex flex-row">
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
</div>
</>
<HorizontalSeparator classes="my-4" />
<Text className="mb-3">
Backups are saved automatically throughout the day. You can perform a one-time backup now below.
</Text>
<div className="flex flex-row">
<Button label="Perform Backup" className={'mr-3 text-xs'} onClick={performBackup} />
</div>
</PreferencesSegment>
</>
)}
</PreferencesGroup>
</>
)
}
export default observer(TextBackupsDesktop)

View File

@@ -12,7 +12,7 @@ const TwoFactorTitle: FunctionComponent<Props> = ({ auth }) => {
return <Title>Two-factor authentication not available</Title>
}
return <Title>Two-factor authentication</Title>
return <Title>Two-Factor Authentication</Title>
}
export default observer(TwoFactorTitle)