import { WebApplication } from '@/Application/Application' import { ApplicationGroup } from '@/Application/ApplicationGroup' import { AbstractComponent } from '@/Components/Abstract/PureComponent' import { destroyAllObjectProperties, preventRefreshing } from '@/Utils' import { ApplicationEvent, ApplicationDescriptor, WebAppEvent } from '@standardnotes/snjs' import { STRING_NEW_UPDATE_READY, STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT, STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE, STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, } from '@/Constants/Strings' import { alertDialog, confirmDialog } from '@standardnotes/ui-services' import Icon from '@/Components/Icon/Icon' import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu' import { Fragment } from 'react' import { AccountMenuPane } from '../AccountMenu/AccountMenuPane' import { EditorEventSource } from '@/Types/EditorEventSource' import QuickSettingsButton from './QuickSettingsButton' import AccountMenuButton from './AccountMenuButton' import StyledTooltip from '../StyledTooltip/StyledTooltip' import UpgradeNow from './UpgradeNow' import PreferencesButton from './PreferencesButton' type Props = { application: WebApplication applicationGroup: ApplicationGroup } type State = { outOfSync: boolean dataUpgradeAvailable: boolean hasPasscode: boolean descriptors: ApplicationDescriptor[] showBetaWarning: boolean showSyncResolution: boolean newUpdateAvailable: boolean showAccountMenu: boolean showQuickSettingsMenu: boolean offline: boolean hasError: boolean arbitraryStatusMessage?: string } class Footer extends AbstractComponent { public user?: unknown private didCheckForOffline = false private completedInitialSync = false private showingDownloadStatus = false private webEventListenerDestroyer: () => void private removeStatusObserver!: () => void constructor(props: Props) { super(props, props.application) this.state = { hasError: false, offline: true, outOfSync: false, dataUpgradeAvailable: false, hasPasscode: false, descriptors: props.applicationGroup.getDescriptors(), showBetaWarning: false, showSyncResolution: false, newUpdateAvailable: false, showAccountMenu: false, showQuickSettingsMenu: false, } this.webEventListenerDestroyer = props.application.addWebEventObserver((event, data) => { const statusService = this.application.status switch (event) { case WebAppEvent.NewUpdateAvailable: this.onNewUpdateAvailable() break case WebAppEvent.EditorFocused: if ((data as any).eventSource === EditorEventSource.UserInteraction) { this.closeAccountMenu() } break case WebAppEvent.BeganBackupDownload: statusService.setMessage('Saving local backup…') break case WebAppEvent.EndedBackupDownload: { const successMessage = 'Successfully saved backup.' const errorMessage = 'Unable to save local backup.' statusService.setMessage((data as any).success ? successMessage : errorMessage) const twoSeconds = 2000 setTimeout(() => { if (statusService.message === successMessage || statusService.message === errorMessage) { statusService.setMessage('') } }, twoSeconds) break } } }) } override deinit() { this.removeStatusObserver() ;(this.removeStatusObserver as unknown) = undefined this.webEventListenerDestroyer() ;(this.webEventListenerDestroyer as unknown) = undefined super.deinit() destroyAllObjectProperties(this) } override componentDidMount(): void { super.componentDidMount() this.removeStatusObserver = this.application.status.addEventObserver((_event, message) => { this.setState({ arbitraryStatusMessage: message, }) }) this.autorun(() => { const showBetaWarning = this.viewControllerManager.showBetaWarning this.setState({ showBetaWarning: showBetaWarning, showAccountMenu: this.viewControllerManager.accountMenuController.show, showQuickSettingsMenu: this.viewControllerManager.quickSettingsMenuController.open, }) }) } reloadUpgradeStatus() { this.application .checkForSecurityUpdate() .then((available) => { this.setState({ dataUpgradeAvailable: available, }) }) .catch(console.error) } override async onAppLaunch() { super.onAppLaunch().catch(console.error) this.reloadPasscodeStatus().catch(console.error) this.reloadUser() this.reloadUpgradeStatus() this.updateOfflineStatus() this.findErrors() } reloadUser() { this.user = this.application.getUser() } async reloadPasscodeStatus() { const hasPasscode = this.application.hasPasscode() this.setState({ hasPasscode: hasPasscode, }) } override async onAppKeyChange() { super.onAppKeyChange().catch(console.error) this.reloadPasscodeStatus().catch(console.error) } override onAppEvent(eventName: ApplicationEvent) { switch (eventName) { case ApplicationEvent.KeyStatusChanged: this.reloadUpgradeStatus() break case ApplicationEvent.EnteredOutOfSync: this.setState({ outOfSync: true, }) break case ApplicationEvent.ExitedOutOfSync: this.setState({ outOfSync: false, }) break case ApplicationEvent.CompletedFullSync: if (!this.completedInitialSync) { this.application.status.setMessage('') this.completedInitialSync = true } if (!this.didCheckForOffline) { this.didCheckForOffline = true if (this.state.offline && this.application.items.getNoteCount() === 0) { this.viewControllerManager.accountMenuController.setShow(true) } } this.findErrors() this.updateOfflineStatus() break case ApplicationEvent.SyncStatusChanged: this.updateSyncStatus() break case ApplicationEvent.FailedSync: this.updateSyncStatus() this.findErrors() this.updateOfflineStatus() break case ApplicationEvent.LocalDataIncrementalLoad: case ApplicationEvent.LocalDataLoaded: this.updateLocalDataStatus() break case ApplicationEvent.SignedIn: case ApplicationEvent.SignedOut: this.reloadUser() break case ApplicationEvent.WillSync: if (!this.completedInitialSync) { this.application.status.setMessage('Syncing…') } break } } updateSyncStatus() { const statusManager = this.application.status const syncStatus = this.application.sync.getSyncStatus() const stats = syncStatus.getStats() if (syncStatus.hasError()) { statusManager.setMessage('Unable to Sync') } else if (stats.downloadCount > 20) { const text = `Downloading ${stats.downloadCount} items. Keep app open.` statusManager.setMessage(text) this.showingDownloadStatus = true } else if (this.showingDownloadStatus) { this.showingDownloadStatus = false statusManager.setMessage('Download Complete.') setTimeout(() => { statusManager.setMessage('') }, 2000) } else if (stats.uploadTotalCount > 20) { const completionPercentage = stats.uploadCompletionCount === 0 ? 0 : stats.uploadCompletionCount / stats.uploadTotalCount const stringPercentage = completionPercentage.toLocaleString(undefined, { style: 'percent', }) statusManager.setMessage(`Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`) } else { statusManager.setMessage('') } } updateLocalDataStatus() { const statusManager = this.application.status const syncStatus = this.application.sync.getSyncStatus() const stats = syncStatus.getStats() const encryption = this.application.isEncryptionAvailable() if (stats.localDataDone) { statusManager.setMessage('') return } const notesString = `${stats.localDataCurrent}/${stats.localDataTotal} items...` const loadingStatus = encryption ? `Decrypting ${notesString}` : `Loading ${notesString}` statusManager.setMessage(loadingStatus) } updateOfflineStatus() { this.setState({ offline: this.application.noAccount(), }) } findErrors() { this.setState({ hasError: this.application.sync.getSyncStatus().hasError(), }) } securityUpdateClickHandler = async () => { if ( await confirmDialog({ title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE, text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT, confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, }) ) { preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => { await this.application.upgradeProtocolVersion() }).catch(console.error) } } accountMenuClickHandler = () => { this.viewControllerManager.accountMenuController.toggleShow() } quickSettingsClickHandler = () => { this.viewControllerManager.quickSettingsMenuController.toggle() } syncResolutionClickHandler = () => { this.setState({ showSyncResolution: !this.state.showSyncResolution, }) } closeAccountMenu = () => { this.viewControllerManager.accountMenuController.setShow(false) this.viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu) } lockClickHandler = () => { this.application.lock().catch(console.error) } onNewUpdateAvailable = () => { this.setState({ newUpdateAvailable: true, }) } newUpdateClickHandler = () => { this.setState({ newUpdateAvailable: false, }) this.application.alertService.alert(STRING_NEW_UPDATE_READY).catch(console.error) } betaMessageClickHandler = () => { alertDialog({ title: 'You are using a beta version of the app', text: 'If you wish to go back to a stable version, make sure to sign out ' + 'of this beta app first.', }).catch(console.error) } clickOutsideAccountMenu = () => { this.viewControllerManager.accountMenuController.closeAccountMenu() } clickOutsideQuickSettingsMenu = () => { this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu() } openPreferences = () => { this.clickOutsideQuickSettingsMenu() this.viewControllerManager.preferencesController.openPreferences() } override render() { return (
) } } export default Footer