feat: file backups (#1024)

This commit is contained in:
Mo
2022-05-12 18:26:11 -05:00
committed by GitHub
parent b671ecb2b9
commit 942226e15a
20 changed files with 499 additions and 207 deletions

View File

@@ -23,8 +23,20 @@ type Props = {
onClickPreprocessing?: () => Promise<void>
}
const isHandlingFileDrag = (event: DragEvent) =>
event.dataTransfer?.items && Array.from(event.dataTransfer.items).some((item) => item.kind === 'file')
const isHandlingFileDrag = (event: DragEvent, application: WebApplication) => {
const items = event.dataTransfer?.items
if (!items) {
return false
}
return Array.from(items).some((item) => {
const isFile = item.kind === 'file'
const fileName = item.getAsFile()?.name || ''
const isBackupMetadataFile = application.files.isFileNameFileBackupMetadataFile(fileName)
return isFile && !isBackupMetadataFile
})
}
export const AttachedFilesButton: FunctionComponent<Props> = observer(
({ application, appState, onClickPreprocessing }) => {
@@ -234,16 +246,19 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
const [isDraggingFiles, setIsDraggingFiles] = useState(false)
const dragCounter = useRef(0)
const handleDrag = (event: DragEvent) => {
if (isHandlingFileDrag(event)) {
event.preventDefault()
event.stopPropagation()
}
}
const handleDrag = useCallback(
(event: DragEvent) => {
if (isHandlingFileDrag(event, application)) {
event.preventDefault()
event.stopPropagation()
}
},
[application],
)
const handleDragIn = useCallback(
(event: DragEvent) => {
if (!isHandlingFileDrag(event)) {
if (!isHandlingFileDrag(event, application)) {
return
}
@@ -268,29 +283,32 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
}
}
},
[open, toggleAttachedFilesMenu],
[open, toggleAttachedFilesMenu, application],
)
const handleDragOut = (event: DragEvent) => {
if (!isHandlingFileDrag(event)) {
return
}
const handleDragOut = useCallback(
(event: DragEvent) => {
if (!isHandlingFileDrag(event, application)) {
return
}
event.preventDefault()
event.stopPropagation()
event.preventDefault()
event.stopPropagation()
dragCounter.current = dragCounter.current - 1
dragCounter.current = dragCounter.current - 1
if (dragCounter.current > 0) {
return
}
if (dragCounter.current > 0) {
return
}
setIsDraggingFiles(false)
}
setIsDraggingFiles(false)
},
[application],
)
const handleDrop = useCallback(
(event: DragEvent) => {
if (!isHandlingFileDrag(event)) {
if (!isHandlingFileDrag(event, application)) {
return
}
@@ -330,7 +348,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
dragCounter.current = 0
}
},
[appState.files, appState.features.hasFiles, attachFileToNote, currentTab],
[appState.files, appState.features.hasFiles, attachFileToNote, currentTab, application],
)
useEffect(() => {
@@ -345,7 +363,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
window.removeEventListener('dragover', handleDrag)
window.removeEventListener('drop', handleDrop)
}
}, [handleDragIn, handleDrop])
}, [handleDragIn, handleDrop, handleDrag, handleDragOut])
return (
<div ref={containerRef}>

View File

@@ -90,7 +90,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
const valuesToProcess: ChallengeValue[] = []
for (const inputValue of Object.values(validatedValues)) {
const rawValue = inputValue.value
const value = new ChallengeValue(inputValue.prompt, rawValue)
const value = { prompt: inputValue.prompt, value: rawValue }
valuesToProcess.push(value)
}
const processingPrompts = valuesToProcess.map((v) => v.prompt)

View File

@@ -76,7 +76,7 @@ export class Footer extends PureComponent<Props, State> {
override componentDidMount(): void {
super.componentDidMount()
this.application.getStatusManager().onStatusChange((message) => {
this.application.status.addEventObserver((_event, message) => {
this.setState({
arbitraryStatusMessage: message,
})
@@ -124,7 +124,7 @@ export class Footer extends PureComponent<Props, State> {
}
override onAppStateEvent(eventName: AppStateEvent, data: any) {
const statusService = this.application.getStatusManager()
const statusService = this.application.status
switch (eventName) {
case AppStateEvent.EditorFocused:
if (data.eventSource === EventSource.UserInteraction) {
@@ -172,7 +172,7 @@ export class Footer extends PureComponent<Props, State> {
break
case ApplicationEvent.CompletedFullSync:
if (!this.completedInitialSync) {
this.application.getStatusManager().setMessage('')
this.application.status.setMessage('')
this.completedInitialSync = true
}
if (!this.didCheckForOffline) {
@@ -202,7 +202,7 @@ export class Footer extends PureComponent<Props, State> {
break
case ApplicationEvent.WillSync:
if (!this.completedInitialSync) {
this.application.getStatusManager().setMessage('Syncing…')
this.application.status.setMessage('Syncing…')
}
break
}
@@ -213,7 +213,7 @@ export class Footer extends PureComponent<Props, State> {
}
updateSyncStatus() {
const statusManager = this.application.getStatusManager()
const statusManager = this.application.status
const syncStatus = this.application.sync.getSyncStatus()
const stats = syncStatus.getStats()
if (syncStatus.hasError()) {
@@ -243,7 +243,7 @@ export class Footer extends PureComponent<Props, State> {
}
updateLocalDataStatus() {
const statusManager = this.application.getStatusManager()
const statusManager = this.application.status
const syncStatus = this.application.sync.getSyncStatus()
const stats = syncStatus.getStats()
const encryption = this.application.isEncryptionAvailable()

View File

@@ -0,0 +1,355 @@
import { WebApplication } from '@/UIModels/Application'
import { observer } from 'mobx-react-lite'
import {
PreferencesGroup,
PreferencesSegment,
Title,
Text,
Subtitle,
} from '@/Components/Preferences/PreferencesComponents'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { Button } from '@/Components/Button/Button'
import { FileBackupMetadataFile, FileContent, FileHandleRead } from '@standardnotes/snjs'
import { Switch } from '@/Components/Switch'
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
import { EncryptionStatusItem } from '../Security/Encryption'
import { Icon } from '@/Components/Icon'
import { StreamingFileApi } from '@standardnotes/filepicker'
import { FunctionComponent } from 'preact'
type Props = {
application: WebApplication
}
export const FileBackups = observer(({ application }: Props) => {
const [backupsEnabled, setBackupsEnabled] = useState(false)
const [backupsLocation, setBackupsLocation] = useState('')
const backupsService = useMemo(() => application.fileBackups, [application.fileBackups])
if (!backupsService) {
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>File Backups</Title>
<Subtitle>Automatically save encrypted backups of files uploaded to any device to this computer.</Subtitle>
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
</PreferencesSegment>
<PreferencesSegment>
<BackupsDropZone application={application} />
</PreferencesSegment>
</PreferencesGroup>
</>
)
}
useEffect(() => {
void backupsService.isFilesBackupsEnabled().then(setBackupsEnabled)
}, [backupsService])
useEffect(() => {
if (backupsEnabled) {
void backupsService.getFilesBackupsLocation().then(setBackupsLocation)
}
}, [backupsService, backupsEnabled])
const changeBackupsLocation = useCallback(async () => {
await backupsService.changeFilesBackupsLocation()
setBackupsLocation(await backupsService.getFilesBackupsLocation())
}, [backupsService])
const openBackupsLocation = useCallback(async () => {
await backupsService.openFilesBackupsLocation()
}, [backupsService])
const toggleBackups = useCallback(async () => {
if (backupsEnabled) {
await backupsService.disableFilesBackups()
} else {
await backupsService.enableFilesBackups()
}
setBackupsEnabled(await backupsService.isFilesBackupsEnabled())
}, [backupsService, backupsEnabled])
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>File Backups</Title>
<div className="flex items-center justify-between">
<div className="flex flex-col mr-10">
<Subtitle>
Automatically save encrypted backups of files uploaded on any device to this computer.
</Subtitle>
</div>
<Switch onChange={toggleBackups} checked={backupsEnabled} />
</div>
{!backupsEnabled && (
<>
<HorizontalSeparator classes="mt-5 mb-4" />
<Text>File backups are not enabled. Enable to choose where your files are backed up.</Text>
</>
)}
</PreferencesSegment>
{backupsEnabled && (
<>
<PreferencesSegment>
<>
<Text className="mb-3">
Files backups are enabled. When you upload a new file on any device and open this application, files
will be downloaded in encrypted form to:
</Text>
<EncryptionStatusItem
status={backupsLocation}
icon={[<Icon type="attachment-file" className="min-w-5 min-h-5" />]}
checkmark={false}
/>
<div className="flex flex-row mt-5">
<Button
variant="normal"
label="Open Backups Location"
className={'mr-3 text-xs'}
onClick={openBackupsLocation}
/>
<Button
variant="normal"
label="Change Backups Location"
className={'mr-3 text-xs'}
onClick={changeBackupsLocation}
/>
</div>
</>
</PreferencesSegment>
</>
)}
<PreferencesSegment>
<BackupsDropZone application={application} />
</PreferencesSegment>
</PreferencesGroup>
</>
)
})
const isHandlingBackupDrag = (event: DragEvent, application: WebApplication) => {
const items = event.dataTransfer?.items
if (!items) {
return false
}
return Array.from(items).every((item) => {
const isFile = item.kind === 'file'
const fileName = item.getAsFile()?.name || ''
const isBackupMetadataFile = application.files.isFileNameFileBackupMetadataFile(fileName)
return isFile && isBackupMetadataFile
})
}
export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
const [droppedFile, setDroppedFile] = useState<FileBackupMetadataFile | undefined>(undefined)
const [decryptedFileContent, setDecryptedFileContent] = useState<FileContent | undefined>(undefined)
const [binaryFile, setBinaryFile] = useState<FileHandleRead | undefined>(undefined)
const [isSavingAsDecrypted, setIsSavingAsDecrypted] = useState(false)
const fileSystem = useMemo(() => new StreamingFileApi(), [])
useEffect(() => {
if (droppedFile) {
void application.files.decryptBackupMetadataFile(droppedFile).then(setDecryptedFileContent)
} else {
setDecryptedFileContent(undefined)
}
}, [droppedFile, application])
const chooseRelatedBinaryFile = useCallback(async () => {
const selection = await application.files.selectFile(fileSystem)
if (selection === 'aborted' || selection === 'failed') {
return
}
setBinaryFile(selection)
}, [application, fileSystem])
const downloadBinaryFileAsDecrypted = useCallback(async () => {
if (!decryptedFileContent || !binaryFile) {
return
}
setIsSavingAsDecrypted(true)
const result = await application.files.readBackupFileAndSaveDecrypted(binaryFile, decryptedFileContent, fileSystem)
if (result === 'success') {
void application.alertService.alert(
`<strong>${decryptedFileContent.name}</strong> has been successfully decrypted and saved to your chosen directory.`,
)
setBinaryFile(undefined)
setDecryptedFileContent(undefined)
setDroppedFile(undefined)
} else if (result === 'failed') {
void application.alertService.alert(
'Unable to save file to local directory. This may be caused by failure to decrypt, or failure to save the file locally.',
)
}
setIsSavingAsDecrypted(false)
}, [decryptedFileContent, application, binaryFile, fileSystem])
const handleDrag = useCallback(
(event: DragEvent) => {
if (isHandlingBackupDrag(event, application)) {
event.preventDefault()
event.stopPropagation()
}
},
[application],
)
const handleDragIn = useCallback(
(event: DragEvent) => {
if (!isHandlingBackupDrag(event, application)) {
return
}
event.preventDefault()
event.stopPropagation()
},
[application],
)
const handleDragOut = useCallback(
(event: DragEvent) => {
if (!isHandlingBackupDrag(event, application)) {
return
}
event.preventDefault()
event.stopPropagation()
},
[application],
)
const handleDrop = useCallback(
async (event: DragEvent) => {
if (!isHandlingBackupDrag(event, application)) {
return
}
event.preventDefault()
event.stopPropagation()
const items = event.dataTransfer?.items
if (!items || items.length === 0) {
return
}
const item = items[0]
const file = item.getAsFile()
if (!file) {
return
}
const text = await file.text()
try {
const metadata = JSON.parse(text) as FileBackupMetadataFile
setDroppedFile(metadata)
} catch (error) {
console.error(error)
}
event.dataTransfer.clearData()
},
[application],
)
useEffect(() => {
window.addEventListener('dragenter', handleDragIn)
window.addEventListener('dragleave', handleDragOut)
window.addEventListener('dragover', handleDrag)
window.addEventListener('drop', handleDrop)
return () => {
window.removeEventListener('dragenter', handleDragIn)
window.removeEventListener('dragleave', handleDragOut)
window.removeEventListener('dragover', handleDrag)
window.removeEventListener('drop', handleDrop)
}
}, [handleDragIn, handleDrop, handleDrag, handleDragOut])
if (!droppedFile) {
return (
<Text>
To decrypt a backup file, drag and drop the file's respective <i>metadata.sn.json</i> file here.
</Text>
)
}
return (
<>
<PreferencesSegment>
{!decryptedFileContent && <Text>Attempting to decrypt metadata file...</Text>}
{decryptedFileContent && (
<>
<Title>Backup Decryption</Title>
<Subtitle className="success">
Successfully decrypted metadata file for <i>{decryptedFileContent.name}.</i>
</Subtitle>
<HorizontalSeparator classes={'mt-3 mb-3'} />
<div className="flex justify-between items-center">
<div>
<Subtitle>1. Choose related data file</Subtitle>
<Text>{droppedFile.file.uuid}</Text>
</div>
<div>
<Button
variant="normal"
label="Choose"
className={'px-1 text-xs min-w-40'}
onClick={chooseRelatedBinaryFile}
disabled={!!binaryFile}
/>
</div>
</div>
<HorizontalSeparator classes={'mt-3 mb-3'} />
<div className="flex justify-between items-center">
<Subtitle>2. Decrypt and save file to your computer</Subtitle>
<div>
<Button
variant="normal"
label={isSavingAsDecrypted ? undefined : 'Save'}
className={'px-1 text-xs min-w-40'}
onClick={downloadBinaryFileAsDecrypted}
disabled={isSavingAsDecrypted || !binaryFile}
>
{isSavingAsDecrypted && (
<div className="flex justify-center w-full">
<div className="sk-spinner w-5 h-5 spinner-info"></div>
</div>
)}
</Button>
</div>
</div>
</>
)}
</PreferencesSegment>
</>
)
}

View File

@@ -5,6 +5,7 @@ import { PreferencesPane } from '@/Components/Preferences/PreferencesComponents'
import { CloudLink } from './CloudBackups'
import { DataBackups } from './DataBackups'
import { EmailBackups } from './EmailBackups'
import { FileBackups } from './FileBackups'
interface Props {
appState: AppState
@@ -15,6 +16,7 @@ export const Backups: FunctionComponent<Props> = ({ application, appState }) =>
return (
<PreferencesPane>
<DataBackups application={application} appState={appState} />
<FileBackups application={application} />
<EmailBackups application={application} />
<CloudLink application={application} />
</PreferencesPane>

View File

@@ -7,16 +7,17 @@ import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/
const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}`
const EncryptionStatusItem: FunctionComponent<{
export const EncryptionStatusItem: FunctionComponent<{
icon: ComponentChild
status: string
}> = ({ icon, status }) => (
checkmark?: boolean
}> = ({ icon, status, checkmark = true }) => (
<div className="w-full rounded py-1.5 px-3 text-input my-1 min-h-8 flex flex-row items-center bg-contrast no-border focus-within:ring-info">
{icon}
<div className="min-w-3 min-h-1" />
<div className="flex-grow color-text text-sm">{status}</div>
<div className="min-w-3 min-h-1" />
<Icon className="success min-w-4 min-h-4" type="check-bold" />
{checkmark && <Icon className="success min-w-4 min-h-4" type="check-bold" />}
</div>
)

View File

@@ -21,7 +21,7 @@ export class Database {
constructor(public databaseName: string, private alertService: AlertService) {}
public deinit() {
public deinit(): void {
;(this.alertService as any) = undefined
this.db = undefined
}
@@ -29,7 +29,7 @@ export class Database {
/**
* Relinquishes the lock and allows db operations to proceed
*/
public unlock() {
public unlock(): void {
this.locked = false
}

View File

@@ -1,11 +0,0 @@
import { DeviceInterface, Environment } from '@standardnotes/snjs'
import { WebClientRequiresDesktopMethods } from './DesktopWebCommunication'
import { WebOrDesktopDeviceInterface } from './WebOrDesktopDeviceInterface'
export function isDesktopDevice(x: DeviceInterface): x is DesktopDeviceInterface {
return x.environment === Environment.Desktop
}
export interface DesktopDeviceInterface extends WebOrDesktopDeviceInterface, WebClientRequiresDesktopMethods {
environment: Environment.Desktop
}

View File

@@ -1 +1,9 @@
export { Environment, RawKeychainValue } from '@standardnotes/snjs'
export {
Environment,
RawKeychainValue,
DesktopDeviceInterface,
WebOrDesktopDeviceInterface,
DesktopClientRequiresWebMethods,
FileBackupsMapping,
FileBackupsDevice,
} from '@standardnotes/snjs'

View File

@@ -1,42 +0,0 @@
import { DecryptedTransferPayload } from '@standardnotes/snjs'
export interface WebClientRequiresDesktopMethods {
localBackupsCount(): Promise<number>
viewlocalBackups(): void
deleteLocalBackups(): Promise<void>
syncComponents(payloads: unknown[]): void
onMajorDataChange(): void
onInitialDataLoad(): void
/**
* Destroys all sensitive storage data, such as localStorage, IndexedDB, and other log files.
*/
destroyAllData(): void
onSearch(text?: string): void
downloadBackup(): void | Promise<void>
get extensionsServerHost(): string
}
export interface DesktopClientRequiresWebMethods {
updateAvailable(): void
windowGainedFocus(): void
windowLostFocus(): void
onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise<void>
requestBackupFile(): Promise<string | undefined>
didBeginBackup(): void
didFinishBackup(success: boolean): void
}

View File

@@ -1,5 +1,5 @@
import { Environment, RawKeychainValue } from '@standardnotes/snjs'
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
import { WebOrDesktopDevice } from './WebOrDesktopDevice'
const KEYCHAIN_STORAGE_KEY = 'keychain'

View File

@@ -7,9 +7,9 @@ import {
TransferPayload,
NamespacedRootKeyInKeychain,
extendArray,
WebOrDesktopDeviceInterface,
} from '@standardnotes/snjs'
import { Database } from '../Database'
import { WebOrDesktopDeviceInterface } from './WebOrDesktopDeviceInterface'
export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface {
constructor(public appVersion: string) {}
@@ -18,7 +18,7 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface
abstract environment: Environment
setApplication(application: SNApplication) {
setApplication(application: SNApplication): void {
const database = new Database(application.identifier, application.alertService)
this.databases.push(database)

View File

@@ -1,9 +0,0 @@
import { DeviceInterface, RawKeychainValue } from '@standardnotes/snjs'
export interface WebOrDesktopDeviceInterface extends DeviceInterface {
readonly appVersion: string
getKeychainValue(): Promise<RawKeychainValue>
setKeychainValue(value: RawKeychainValue): Promise<void>
}

View File

@@ -10,10 +10,10 @@ import {
DecryptedTransferPayload,
ComponentContent,
assert,
DesktopClientRequiresWebMethods,
DesktopDeviceInterface,
} from '@standardnotes/snjs'
import { WebAppEvent, WebApplication } from '@/UIModels/Application'
import { DesktopDeviceInterface } from '../Device/DesktopDeviceInterface'
import { DesktopClientRequiresWebMethods } from '@/Device/DesktopWebCommunication'
export class DesktopManager
extends ApplicationService

View File

@@ -1,30 +0,0 @@
import { removeFromArray } from '@standardnotes/snjs'
type StatusCallback = (string: string) => void
export class StatusManager {
private _message = ''
private observers: StatusCallback[] = []
get message(): string {
return this._message
}
setMessage(message: string) {
this._message = message
this.notifyObservers()
}
onStatusChange(callback: StatusCallback) {
this.observers.push(callback)
return () => {
removeFromArray(this.observers, callback)
}
}
private notifyObservers() {
for (const observer of this.observers) {
observer(this._message)
}
}
}

View File

@@ -15,6 +15,7 @@ import {
removeFromArray,
Uuid,
PayloadEmitSource,
WebOrDesktopDeviceInterface,
} from '@standardnotes/snjs'
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'
import { ActionsMenuState } from './ActionsMenuState'
@@ -31,7 +32,6 @@ import { SearchOptionsState } from './SearchOptionsState'
import { SubscriptionState } from './SubscriptionState'
import { SyncState } from './SyncState'
import { TagsState } from './TagsState'
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
import { FilePreviewModalState } from './FilePreviewModalState'
export enum AppStateEvent {
@@ -93,7 +93,7 @@ export class AppState {
private readonly tagChangedDisposer: IReactionDisposer
constructor(application: WebApplication, private device: WebOrDesktopDevice) {
constructor(application: WebApplication, private device: WebOrDesktopDeviceInterface) {
this.application = application
this.notes = new NotesState(
application,

View File

@@ -2,10 +2,8 @@ import { WebCrypto } from '@/Crypto'
import { WebAlertService } from '@/Services/AlertService'
import { ArchiveManager } from '@/Services/ArchiveManager'
import { AutolockService } from '@/Services/AutolockService'
import { DesktopDeviceInterface, isDesktopDevice } from '@/Device/DesktopDeviceInterface'
import { DesktopManager } from '@/Services/DesktopManager'
import { IOService } from '@/Services/IOService'
import { StatusManager } from '@/Services/StatusManager'
import { ThemeManager } from '@/Services/ThemeManager'
import { AppState } from '@/UIModels/AppState'
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
@@ -17,6 +15,8 @@ import {
removeFromArray,
IconsController,
Runtime,
DesktopDeviceInterface,
isDesktopDevice,
} from '@standardnotes/snjs'
type WebServices = {
@@ -24,7 +24,6 @@ type WebServices = {
desktopService?: DesktopManager
autolockService: AutolockService
archiveService: ArchiveManager
statusManager: StatusManager
themeService: ThemeManager
io: IOService
}
@@ -137,10 +136,6 @@ export class WebApplication extends SNApplication {
return undefined
}
getStatusManager() {
return this.webServices.statusManager
}
public getThemeService() {
return this.webServices.themeService
}

View File

@@ -1,15 +1,20 @@
import { WebApplication } from './Application'
import { ApplicationDescriptor, SNApplicationGroup, Platform, Runtime, InternalEventBus } from '@standardnotes/snjs'
import {
ApplicationDescriptor,
SNApplicationGroup,
Platform,
Runtime,
InternalEventBus,
isDesktopDevice,
} from '@standardnotes/snjs'
import { AppState } from '@/UIModels/AppState'
import { getPlatform, isDesktopApplication } from '@/Utils'
import { ArchiveManager } from '@/Services/ArchiveManager'
import { DesktopManager } from '@/Services/DesktopManager'
import { IOService } from '@/Services/IOService'
import { AutolockService } from '@/Services/AutolockService'
import { StatusManager } from '@/Services/StatusManager'
import { ThemeManager } from '@/Services/ThemeManager'
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
import { isDesktopDevice } from '@/Device/DesktopDeviceInterface'
export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
constructor(
@@ -53,7 +58,6 @@ export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
const archiveService = new ArchiveManager(application)
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
const autolockService = new AutolockService(application, new InternalEventBus())
const statusManager = new StatusManager()
const themeService = new ThemeManager(application)
application.setWebServices({
@@ -62,7 +66,6 @@ export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
desktopService: isDesktopDevice(this.device) ? new DesktopManager(application, this.device) : undefined,
io,
autolockService,
statusManager,
themeService,
})