import { ApplicationContext } from '@Root/ApplicationContext' import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext' import { SCREEN_NOTES } from '@Root/Screens/screens' import { ApplicationEvent, ButtonType, isSameDay, NoteMutator, NoteViewController, SNNote, StorageEncryptionPolicy, } from '@standardnotes/snjs' import React, { useCallback, useEffect } from 'react' import { LockStateType } from './ApplicationState' export const useSignedIn = (signedInCallback?: () => void, signedOutCallback?: () => void) => { // Context const application = useSafeApplicationContext() const [isLocked] = useIsLocked() // State const [signedIn, setSignedIn] = React.useState(false) React.useEffect(() => { let mounted = true const getSignedIn = async () => { if (mounted && !isLocked) { setSignedIn(!application.noAccount()) } } void getSignedIn() const removeSignedInObserver = application.addEventObserver(async event => { if (event === ApplicationEvent.Launched) { void getSignedIn() } if (event === ApplicationEvent.SignedIn) { setSignedIn(true) signedInCallback && signedInCallback() } else if (event === ApplicationEvent.SignedOut) { setSignedIn(false) signedOutCallback && signedOutCallback() } }) return () => { mounted = false removeSignedInObserver && removeSignedInObserver() } }, [application, signedInCallback, signedOutCallback, isLocked]) return [signedIn] } export const useOutOfSync = () => { // Context const application = useSafeApplicationContext() // State const [outOfSync, setOutOfSync] = React.useState(false) React.useEffect(() => { let isMounted = true const getOutOfSync = async () => { const outOfSyncInitial = await application.sync.isOutOfSync() if (isMounted) { setOutOfSync(Boolean(outOfSyncInitial)) } } void getOutOfSync() return () => { isMounted = false } }, [application]) React.useEffect(() => { const removeSignedInObserver = application.addEventObserver(async event => { if (event === ApplicationEvent.EnteredOutOfSync) { setOutOfSync(true) } else if (event === ApplicationEvent.ExitedOutOfSync) { setOutOfSync(false) } }) return removeSignedInObserver }, [application]) return [outOfSync] } export const useIsLocked = () => { // Context const application = React.useContext(ApplicationContext) // State const [isLocked, setIsLocked] = React.useState(() => { if (!application || !application.getAppState()) { return true } return Boolean(application?.getAppState().locked) }) useEffect(() => { let isMounted = true const removeSignedInObserver = application?.getAppState().addLockStateChangeObserver(event => { if (isMounted) { if (event === LockStateType.Locked) { setIsLocked(true) } if (event === LockStateType.Unlocked) { setIsLocked(false) } } }) return () => { isMounted = false removeSignedInObserver && removeSignedInObserver() } }, [application]) return [isLocked] } export const useHasEditor = () => { // Context const application = React.useContext(ApplicationContext) // State const [hasEditor, setHasEditor] = React.useState(false) useEffect(() => { const removeEditorObserver = application?.editorGroup.addActiveControllerChangeObserver(newEditor => { setHasEditor(Boolean(newEditor)) }) return removeEditorObserver }, [application]) return [hasEditor] } export const useSyncStatus = () => { // Context const application = React.useContext(ApplicationContext) // State const [completedInitialSync, setCompletedInitialSync] = React.useState(false) const [loading, setLoading] = React.useState(false) const [decrypting, setDecrypting] = React.useState(false) const [refreshing, setRefreshing] = React.useState(false) const setStatus = useCallback( (status = '') => { application?.getStatusManager().setMessage(SCREEN_NOTES, status) }, [application], ) const updateLocalDataStatus = useCallback(() => { const syncStatus = application!.sync.getSyncStatus() const stats = syncStatus.getStats() const encryption = application!.isEncryptionAvailable() && application!.getStorageEncryptionPolicy() === StorageEncryptionPolicy.Default if (stats.localDataCurrent === 0 || stats.localDataTotal === 0 || stats.localDataDone) { setStatus() return } const notesString = `${stats.localDataCurrent}/${stats.localDataTotal} items…` const loadingStatus = encryption ? `Decrypting ${notesString}` : `Loading ${notesString}` setStatus(loadingStatus) }, [application, setStatus]) useEffect(() => { let mounted = true const isEncryptionAvailable = application!.isEncryptionAvailable() && application!.getStorageEncryptionPolicy() === StorageEncryptionPolicy.Default if (mounted) { setDecrypting(!completedInitialSync && isEncryptionAvailable) updateLocalDataStatus() setLoading(!completedInitialSync && !isEncryptionAvailable) } return () => { mounted = false } }, [application, completedInitialSync, updateLocalDataStatus]) const updateSyncStatus = useCallback(() => { const syncStatus = application!.sync.getSyncStatus() const stats = syncStatus.getStats() if (syncStatus.hasError()) { setRefreshing(false) setStatus('Unable to Sync') } else if (stats.downloadCount > 20) { const text = `Downloading ${stats.downloadCount} items. Keep app open.` setStatus(text) } else if (stats.uploadTotalCount > 20) { setStatus(`Syncing ${stats.uploadCompletionCount}/${stats.uploadTotalCount} items...`) } else if (syncStatus.syncInProgress && !completedInitialSync) { setStatus('Syncing…') } else { setStatus() } }, [application, completedInitialSync, setStatus]) useEffect(() => { const unsubscribeAppEvents = application?.addEventObserver(async eventName => { if (eventName === ApplicationEvent.LocalDataIncrementalLoad) { updateLocalDataStatus() } else if (eventName === ApplicationEvent.SyncStatusChanged || eventName === ApplicationEvent.FailedSync) { updateSyncStatus() } else if (eventName === ApplicationEvent.LocalDataLoaded) { setDecrypting(false) setLoading(false) updateLocalDataStatus() } else if (eventName === ApplicationEvent.CompletedFullSync) { if (completedInitialSync) { setRefreshing(false) } else { setCompletedInitialSync(true) } setLoading(false) updateSyncStatus() } else if (eventName === ApplicationEvent.LocalDatabaseReadError) { void application!.alertService!.alert('Unable to load local storage. Please restart the app and try again.') } else if (eventName === ApplicationEvent.LocalDatabaseWriteError) { void application!.alertService!.alert('Unable to write to local storage. Please restart the app and try again.') } else if (eventName === ApplicationEvent.SignedIn) { setLoading(true) } }) return unsubscribeAppEvents }, [application, completedInitialSync, setStatus, updateLocalDataStatus, updateSyncStatus]) const startRefreshing = () => { setRefreshing(true) } return [loading, decrypting, refreshing, startRefreshing] as [boolean, boolean, boolean, () => void] } export const useDeleteNoteWithPrivileges = ( note: SNNote, onDeleteCallback: () => void, onTrashCallback: () => void, editor?: NoteViewController, ) => { // Context const application = React.useContext(ApplicationContext) const trashNote = useCallback(async () => { const title = 'Move to Trash' const message = 'Are you sure you want to move this note to the trash?' const confirmed = await application?.alertService?.confirm(message, title, 'Confirm', ButtonType.Danger) if (confirmed) { onTrashCallback() } }, [application?.alertService, onTrashCallback]) const deleteNotePermanently = useCallback(async () => { const title = `Delete ${note!.title}` const message = 'Are you sure you want to permanently delete this note?' if (editor?.isTemplateNote) { void application?.alertService!.alert( 'This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.', ) return } const confirmed = await application?.alertService?.confirm(message, title, 'Delete', ButtonType.Danger, 'Cancel') if (confirmed) { onDeleteCallback() } }, [application?.alertService, editor?.isTemplateNote, note, onDeleteCallback]) const deleteNote = useCallback( async (permanently: boolean) => { if (note?.locked) { void application?.alertService.alert( "This note has editing disabled. If you'd like to delete it, enable editing on it, and try again.", ) return } if (permanently) { void deleteNotePermanently() } else { void trashNote() } }, [application, deleteNotePermanently, note?.locked, trashNote], ) return [deleteNote] } export const useProtectionSessionExpiry = () => { // Context const application = useSafeApplicationContext() const getProtectionsDisabledUntil = React.useCallback(() => { const protectionExpiry = application?.getProtectionSessionExpiryDate() const now = new Date() if (protectionExpiry && protectionExpiry > now) { if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { 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) } else { if (isSameDay(protectionExpiry, now)) { return protectionExpiry.toLocaleTimeString() } else { return `${protectionExpiry.toDateString()} ${protectionExpiry.toLocaleTimeString()}` } } } return null }, [application]) // State const [protectionsDisabledUntil, setProtectionsDisabledUntil] = React.useState(getProtectionsDisabledUntil()) useEffect(() => { const removeProtectionLengthSubscriber = application?.addEventObserver(async event => { if ([ApplicationEvent.UnprotectedSessionBegan, ApplicationEvent.UnprotectedSessionExpired].includes(event)) { setProtectionsDisabledUntil(getProtectionsDisabledUntil()) } }) return () => { removeProtectionLengthSubscriber && removeProtectionLengthSubscriber() } }, [application, getProtectionsDisabledUntil]) return [protectionsDisabledUntil] } export const useChangeNoteChecks = (note: SNNote | undefined, editor: NoteViewController | undefined = undefined) => { // Context const application = useSafeApplicationContext() const canChangeNote = useCallback(async () => { if (!note) { return false } if (editor && editor.isTemplateNote) { await editor.insertTemplatedNote() } if (!application.items.findItem(note.uuid)) { void application.alertService!.alert( "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.", ) return false } return true }, [application, editor, note]) return [canChangeNote] } export const useChangeNote = (note: SNNote | undefined, editor: NoteViewController | undefined = undefined) => { const application = React.useContext(ApplicationContext) const [canChangeNote] = useChangeNoteChecks(note, editor) const changeNote = useCallback( async (mutate: (mutator: NoteMutator) => void, updateTimestamps: boolean) => { if (await canChangeNote()) { await application?.mutator.changeAndSaveItem( note!, mutator => { const noteMutator = mutator as NoteMutator mutate(noteMutator) }, updateTimestamps, ) } }, [application, note, canChangeNote], ) return [changeNote] } export const useProtectOrUnprotectNote = ( note: SNNote | undefined, editor: NoteViewController | undefined = undefined, ) => { // Context const application = React.useContext(ApplicationContext) const [canChangeNote] = useChangeNoteChecks(note, editor) const protectOrUnprotectNote = useCallback(async () => { if (await canChangeNote()) { if (note!.protected) { await application?.mutator.unprotectNote(note!) } else { await application?.mutator.protectNote(note!) } } }, [application, note, canChangeNote]) return [protectOrUnprotectNote] }