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

@@ -17,12 +17,12 @@ import {
WebApplicationInterface,
MobileDeviceInterface,
MobileUnlockTiming,
InternalEventBus,
DecryptedItem,
EditorIdentifier,
FeatureIdentifier,
Environment,
ApplicationOptionsDefaults,
BackupServiceInterface,
} from '@standardnotes/snjs'
import { makeObservable, observable } from 'mobx'
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
@@ -93,27 +93,26 @@ export class WebApplication extends SNApplication implements WebApplicationInter
})
deviceInterface.setApplication(this)
const internalEventBus = new InternalEventBus()
this.itemControllerGroup = new ItemGroupController(this)
this.routeService = new RouteService(this, internalEventBus)
this.routeService = new RouteService(this, this.internalEventBus)
this.webServices = {} as WebServices
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
this.webServices.archiveService = new ArchiveManager(this)
this.webServices.themeService = new ThemeManager(this, internalEventBus)
this.webServices.themeService = new ThemeManager(this, this.internalEventBus)
this.webServices.autolockService = this.isNativeMobileWeb()
? undefined
: new AutolockService(this, internalEventBus)
: new AutolockService(this, this.internalEventBus)
this.webServices.desktopService = isDesktopDevice(deviceInterface)
? new DesktopManager(this, deviceInterface)
? new DesktopManager(this, deviceInterface, this.fileBackups as BackupServiceInterface)
: undefined
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
this.webServices.changelogService = new ChangelogService(this.environment, this.storage)
this.webServices.momentsService = new MomentsService(
this,
this.webServices.viewControllerManager.filesController,
internalEventBus,
this.internalEventBus,
)
if (this.isNativeMobileWeb()) {
@@ -181,6 +180,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
for (const observer of this.webEventObservers) {
observer(event, data)
}
this.internalEventBus.publish({ type: event, payload: data })
}
publishPanelDidResizeEvent(name: string, width: number, collapsed: boolean) {
@@ -268,16 +269,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.protocolUpgradeAvailable()
}
downloadBackup(): void | Promise<void> {
if (isDesktopDevice(this.deviceInterface)) {
return this.deviceInterface.downloadBackup()
}
}
async signOutAndDeleteLocalBackups(): Promise<void> {
isDesktopDevice(this.deviceInterface) && (await this.deviceInterface.deleteLocalBackups())
return this.user.signOut()
performDesktopTextBackup(): void | Promise<void> {
return this.getDesktopService()?.saveDesktopBackup()
}
isGlobalSpellcheckEnabled(): boolean {

View File

@@ -14,6 +14,8 @@ import {
DesktopDeviceInterface,
WebApplicationInterface,
WebAppEvent,
BackupServiceInterface,
DesktopWatchedDirectoriesChanges,
} from '@standardnotes/snjs'
export class DesktopManager
@@ -27,10 +29,34 @@ export class DesktopManager
dataLoaded = false
lastSearchedText?: string
constructor(application: WebApplicationInterface, private device: DesktopDeviceInterface) {
private textBackupsInterval: ReturnType<typeof setInterval> | undefined
private needsInitialTextBackup = false
constructor(
application: WebApplicationInterface,
private device: DesktopDeviceInterface,
private backups: BackupServiceInterface,
) {
super(application, new InternalEventBus())
}
async handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {
void this.backups.importWatchedDirectoryChanges(changes)
}
beginTextBackupsTimer() {
if (this.textBackupsInterval) {
clearInterval(this.textBackupsInterval)
}
this.needsInitialTextBackup = true
const hoursInterval = 12
const seconds = hoursInterval * 60 * 60
const milliseconds = seconds * 1000
this.textBackupsInterval = setInterval(this.saveDesktopBackup, milliseconds)
}
get webApplication() {
return this.application as WebApplicationInterface
}
@@ -44,14 +70,35 @@ export class DesktopManager
super.onAppEvent(eventName).catch(console.error)
if (eventName === ApplicationEvent.LocalDataLoaded) {
this.dataLoaded = true
this.device.onInitialDataLoad()
if (this.backups.isTextBackupsEnabled()) {
this.beginTextBackupsTimer()
}
} else if (eventName === ApplicationEvent.MajorDataChange) {
this.device.onMajorDataChange()
void this.saveDesktopBackup()
}
}
saveBackup() {
this.device.onMajorDataChange()
async saveDesktopBackup() {
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
const data = await this.getBackupFile()
if (data) {
await this.webApplication.fileBackups?.saveTextBackupData(data)
this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success: true })
}
}
private async getBackupFile(): Promise<string | undefined> {
const encrypted = this.application.hasProtectionSources()
const data = encrypted
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
: await this.application.createDecryptedBackupFile()
if (data) {
return JSON.stringify(data, null, 2)
}
return undefined
}
getExtServerHost(): string {
@@ -111,6 +158,11 @@ export class DesktopManager
windowLostFocus(): void {
this.webApplication.notifyWebEvent(WebAppEvent.WindowDidBlur)
if (this.needsInitialTextBackup) {
this.needsInitialTextBackup = false
void this.saveDesktopBackup()
}
}
async onComponentInstallationComplete(componentData: DecryptedTransferPayload<ComponentContent>) {
@@ -136,25 +188,4 @@ export class DesktopManager
observer.callback(updatedComponent as SNComponent)
}
}
async requestBackupFile(): Promise<string | undefined> {
const encrypted = this.application.hasProtectionSources()
const data = encrypted
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
: await this.application.createDecryptedBackupFile()
if (data) {
return JSON.stringify(data, null, 2)
}
return undefined
}
didBeginBackup() {
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
}
didFinishBackup(success: boolean) {
this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success })
}
}

View File

@@ -10,4 +10,7 @@ export {
FileBackupReadToken,
FileBackupReadChunkResponse,
FileDownloadProgress,
PlaintextBackupsMapping,
DesktopWatchedDirectoriesChanges,
DesktopWatchedDirectoriesChange,
} from '@standardnotes/snjs'

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)

View File

@@ -1,5 +1,5 @@
import { CrossControllerEvent } from '../CrossControllerEvent'
import { InternalEventBus, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
import { InternalEventBusInterface, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
import { WebApplication } from '../../Application/Application'
import { Disposer } from '@/Types/Disposer'
@@ -10,7 +10,7 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
protected disposers: Disposer[] = []
private eventObservers: ControllerEventObserver<Event, EventData>[] = []
constructor(public application: WebApplication, protected eventBus: InternalEventBus) {}
constructor(public application: WebApplication, protected eventBus: InternalEventBusInterface) {}
protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise<void> {
await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE)

View File

@@ -1,5 +1,5 @@
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
import { ApplicationEvent, InternalEventBus, StorageKey } from '@standardnotes/services'
import { ApplicationEvent, InternalEventBusInterface, StorageKey } from '@standardnotes/services'
import { isDev } from '@/Utils'
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
import { FilesController } from '../FilesController'
@@ -19,7 +19,11 @@ export class MomentsService extends AbstractViewController {
isEnabled = false
private intervalReference: ReturnType<typeof setInterval> | undefined
constructor(application: WebApplication, private filesController: FilesController, eventBus: InternalEventBus) {
constructor(
application: WebApplication,
private filesController: FilesController,
eventBus: InternalEventBusInterface,
) {
super(application, eventBus)
this.disposers.push(

View File

@@ -4,10 +4,10 @@ import { PreferenceId, RootQueryParam } from '@standardnotes/ui-services'
import { AbstractViewController } from './Abstract/AbstractViewController'
import { WebApplication } from '@/Application/Application'
const DEFAULT_PANE: PreferenceId = 'account'
const DEFAULT_PANE: PreferenceId = 'backups'
export class PreferencesController extends AbstractViewController {
private _open = false
private _open = true
currentPane: PreferenceId = DEFAULT_PANE
constructor(application: WebApplication, eventBus: InternalEventBus) {