feat: mobile workspaces (#1093)
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { useSignedIn } from '@Lib/SnjsHelperHooks'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
import { SectionedAccessoryTableCell } from '@Root/Components/SectionedAccessoryTableCell'
|
||||
import { SectionedOptionsTableCell } from '@Root/Components/SectionedOptionsTableCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { ButtonType, PrefKey } from '@standardnotes/snjs'
|
||||
import moment from 'moment'
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import RNFS from 'react-native-fs'
|
||||
@@ -22,7 +22,8 @@ type Props = {
|
||||
|
||||
export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
// Context
|
||||
const application = useContext(ApplicationContext)
|
||||
const application = useSafeApplicationContext()
|
||||
|
||||
const [signedIn] = useSignedIn()
|
||||
const navigation = useNavigation<ModalStackNavigationProp<typeof SCREEN_SETTINGS>['navigation']>()
|
||||
|
||||
@@ -57,7 +58,7 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
|
||||
const email = useMemo(() => {
|
||||
if (signedIn) {
|
||||
const user = application?.getUser()
|
||||
const user = application.getUser()
|
||||
return user?.email
|
||||
}
|
||||
return
|
||||
@@ -76,25 +77,25 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
|
||||
const destroyLocalData = async () => {
|
||||
if (
|
||||
await application?.alertService?.confirm(
|
||||
await application.alertService.confirm(
|
||||
'Signing out will remove all data from this device, including notes and tags. Make sure your data is synced before proceeding.',
|
||||
'Sign Out?',
|
||||
'Sign Out',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
) {
|
||||
await application!.user.signOut()
|
||||
await application.user.signOut()
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = useCallback(
|
||||
async (encrypted: boolean) => {
|
||||
setExporting(true)
|
||||
const result = await application?.getBackupsService().export(encrypted)
|
||||
const result = await application.getBackupsService().export(encrypted)
|
||||
if (result) {
|
||||
const exportDate = new Date()
|
||||
setLastExportDate(exportDate)
|
||||
void application?.getLocalPreferences().setUserPrefValue(PrefKey.MobileLastExportDate, exportDate)
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileLastExportDate, exportDate)
|
||||
}
|
||||
setExporting(false)
|
||||
},
|
||||
@@ -103,25 +104,25 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
|
||||
const readImportFile = async (fileUri: string): Promise<any> => {
|
||||
return RNFS.readFile(fileUri)
|
||||
.then(result => JSON.parse(result))
|
||||
.then((result) => JSON.parse(result))
|
||||
.catch(() => {
|
||||
void application!.alertService!.alert('Unable to open file. Ensure it is a proper JSON file and try again.')
|
||||
void application.alertService.alert('Unable to open file. Ensure it is a proper JSON file and try again.')
|
||||
})
|
||||
}
|
||||
|
||||
const performImport = async (data: any) => {
|
||||
const result = await application!.mutator.importData(data)
|
||||
const result = await application.mutator.importData(data)
|
||||
if (!result) {
|
||||
return
|
||||
} else if ('error' in result) {
|
||||
void application!.alertService!.alert(result.error.text)
|
||||
void application.alertService.alert(result.error.text)
|
||||
} else if (result.errorCount) {
|
||||
void application!.alertService!.alert(
|
||||
void application.alertService.alert(
|
||||
`Import complete. ${result.errorCount} items were not imported because ` +
|
||||
'there was an error decrypting them. Make sure the password is correct and try again.',
|
||||
)
|
||||
} else {
|
||||
void application!.alertService!.alert('Your data has been successfully imported.')
|
||||
void application.alertService.alert('Your data has been successfully imported.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +140,10 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
setImporting(true)
|
||||
if (data.version || data.auth_params || data.keyParams) {
|
||||
const version = data.version || data.keyParams?.version || data.auth_params?.version
|
||||
if (application!.protocolService.supportedVersions().includes(version)) {
|
||||
if (application.protocolService.supportedVersions().includes(version)) {
|
||||
await performImport(data)
|
||||
} else {
|
||||
void application!.alertService.alert(
|
||||
void application.alertService.alert(
|
||||
'This backup file was created using an unsupported version of the application ' +
|
||||
'and cannot be imported here. Please update your application and try again.',
|
||||
)
|
||||
@@ -159,7 +160,7 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
async (option: { key: string }) => {
|
||||
const encrypted = option.key === 'encrypted'
|
||||
if (encrypted && !encryptionAvailable) {
|
||||
void application?.alertService!.alert(
|
||||
void application.alertService.alert(
|
||||
'You must be signed in, or have a local passcode set, to generate an encrypted export file.',
|
||||
'Not Available',
|
||||
'OK',
|
||||
@@ -176,12 +177,12 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
}, [navigation])
|
||||
|
||||
const showDataBackupAlert = useCallback(() => {
|
||||
void application?.alertService.alert(
|
||||
void application.alertService.alert(
|
||||
'Because you are using the app offline without a sync account, it is your responsibility to keep your data safe and backed up. It is recommended you export a backup of your data at least once a week, or, to sign up for a sync account so that your data is backed up automatically.',
|
||||
'No Backups Created',
|
||||
'OK',
|
||||
)
|
||||
}, [application?.alertService])
|
||||
}, [application.alertService])
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
@@ -192,7 +193,7 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
<ButtonCell
|
||||
testID="manageSessionsButton"
|
||||
leftAligned={true}
|
||||
first={true}
|
||||
first={false}
|
||||
title={'Manage Sessions'}
|
||||
onPress={openManageSessions}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const PreferencesSection = () => {
|
||||
|
||||
const toggleReverseSort = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileSortNotesReverse, !sortReverse)
|
||||
setSortReverse(value => !value)
|
||||
setSortReverse((value) => !value)
|
||||
}
|
||||
|
||||
const changeSortOption = (key: CollectionSortProperty) => {
|
||||
@@ -45,15 +45,15 @@ export const PreferencesSection = () => {
|
||||
}
|
||||
const toggleNotesPreviewHidden = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileNotesHideNotePreview, !hidePreviews)
|
||||
setHidePreviews(value => !value)
|
||||
setHidePreviews((value) => !value)
|
||||
}
|
||||
const toggleNotesDateHidden = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileNotesHideDate, !hideDates)
|
||||
setHideDates(value => !value)
|
||||
setHideDates((value) => !value)
|
||||
}
|
||||
const toggleNotesEditorIconHidden = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileNotesHideEditorIcon, !hideEditorIcon)
|
||||
setHideEditorIcon(value => !value)
|
||||
setHideEditorIcon((value) => !value)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { SectionedAccessoryTableCell } from '@Components/SectionedAccessoryTableCell'
|
||||
import { SectionHeader } from '@Components/SectionHeader'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { useSafeApplicationGroupContext } from '@Root/Hooks/useSafeApplicationGroupContext'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_INPUT_MODAL_WORKSPACE_NAME, SCREEN_SETTINGS } from '@Screens/screens'
|
||||
import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType } from '@standardnotes/snjs'
|
||||
import { CustomActionSheetOption, useCustomActionSheet } from '@Style/CustomActionSheet'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export const WorkspacesSection = () => {
|
||||
const application = useSafeApplicationContext()
|
||||
const appGroup = useSafeApplicationGroupContext()
|
||||
const navigation = useNavigation<ModalStackNavigationProp<typeof SCREEN_SETTINGS>['navigation']>()
|
||||
|
||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
|
||||
|
||||
enum WorkspaceAction {
|
||||
AddAnother = 'Add another workspace',
|
||||
Activate = 'Activate',
|
||||
Rename = 'Rename',
|
||||
Remove = 'Remove',
|
||||
SignOutAll = 'Sign out all workspaces',
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let descriptors = appGroup.getDescriptors()
|
||||
setApplicationDescriptors(descriptors)
|
||||
|
||||
const removeAppGroupObserver = appGroup.addEventObserver((event) => {
|
||||
if (event === ApplicationGroupEvent.DescriptorsDataChanged) {
|
||||
descriptors = appGroup.getDescriptors()
|
||||
setApplicationDescriptors(descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeAppGroupObserver()
|
||||
}
|
||||
}, [appGroup])
|
||||
|
||||
const { showActionSheet } = useCustomActionSheet()
|
||||
|
||||
const getWorkspaceActionConfirmation = useCallback(
|
||||
async (action: WorkspaceAction): Promise<boolean> => {
|
||||
const { Info, Danger } = ButtonType
|
||||
const { AddAnother, Activate, Remove, SignOutAll } = WorkspaceAction
|
||||
let message = ''
|
||||
let buttonText = ''
|
||||
let buttonType = Info
|
||||
|
||||
switch (action) {
|
||||
case Activate:
|
||||
message = 'Your workspace will be ready for you when you come back.'
|
||||
buttonText = 'Quit App'
|
||||
break
|
||||
case AddAnother:
|
||||
message = 'Your new workspace will be ready for you when you come back.'
|
||||
buttonText = 'Quit App'
|
||||
break
|
||||
case SignOutAll:
|
||||
message =
|
||||
'Are you sure you want to sign out of all workspaces on this device? This action will restart the application.'
|
||||
buttonText = 'Sign Out All'
|
||||
break
|
||||
case Remove:
|
||||
message =
|
||||
'This action will remove this workspace and its related data from this device. Your synced data will not be affected.'
|
||||
buttonText = 'Delete Workspace'
|
||||
buttonType = Danger
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return application.alertService.confirm(message, undefined, buttonText, buttonType)
|
||||
},
|
||||
[WorkspaceAction, application.alertService],
|
||||
)
|
||||
|
||||
const renameWorkspace = useCallback(
|
||||
async (descriptor: ApplicationDescriptor, newName: string) => {
|
||||
appGroup.renameDescriptor(descriptor, newName)
|
||||
},
|
||||
[appGroup],
|
||||
)
|
||||
|
||||
const signOutWorkspace = useCallback(async () => {
|
||||
const confirmed = await getWorkspaceActionConfirmation(WorkspaceAction.Remove)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await application.user.signOut()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [WorkspaceAction.Remove, application.user, getWorkspaceActionConfirmation])
|
||||
|
||||
const openWorkspace = useCallback(
|
||||
async (descriptor: ApplicationDescriptor) => {
|
||||
const confirmed = await getWorkspaceActionConfirmation(WorkspaceAction.Activate)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
await appGroup.unloadCurrentAndActivateDescriptor(descriptor)
|
||||
},
|
||||
[WorkspaceAction.Activate, appGroup, getWorkspaceActionConfirmation],
|
||||
)
|
||||
|
||||
const getSingleWorkspaceItemOptions = useCallback(
|
||||
(descriptor: ApplicationDescriptor) => {
|
||||
const { Activate, Rename, Remove } = WorkspaceAction
|
||||
const worskspaceItemOptions: CustomActionSheetOption[] = []
|
||||
|
||||
if (descriptor.primary) {
|
||||
worskspaceItemOptions.push(
|
||||
{
|
||||
text: Rename,
|
||||
callback: () => {
|
||||
navigation.navigate(SCREEN_INPUT_MODAL_WORKSPACE_NAME, {
|
||||
descriptor,
|
||||
renameWorkspace,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
text: Remove,
|
||||
destructive: true,
|
||||
callback: signOutWorkspace,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
worskspaceItemOptions.push({
|
||||
text: Activate,
|
||||
callback: () => openWorkspace(descriptor),
|
||||
})
|
||||
}
|
||||
|
||||
return worskspaceItemOptions
|
||||
},
|
||||
[WorkspaceAction, navigation, openWorkspace, renameWorkspace, signOutWorkspace],
|
||||
)
|
||||
|
||||
const addAnotherWorkspace = useCallback(async () => {
|
||||
const confirmed = await getWorkspaceActionConfirmation(WorkspaceAction.AddAnother)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
await appGroup.unloadCurrentAndCreateNewDescriptor()
|
||||
}, [WorkspaceAction.AddAnother, appGroup, applicationDescriptors, getWorkspaceActionConfirmation])
|
||||
|
||||
const signOutAllWorkspaces = useCallback(async () => {
|
||||
try {
|
||||
const confirmed = await getWorkspaceActionConfirmation(WorkspaceAction.SignOutAll)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
await appGroup.signOutAllWorkspaces()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [WorkspaceAction.SignOutAll, appGroup, getWorkspaceActionConfirmation])
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={'Workspaces'} />
|
||||
{applicationDescriptors.map((descriptor, index) => {
|
||||
return (
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => {
|
||||
const singleItemOptions = getSingleWorkspaceItemOptions(descriptor)
|
||||
|
||||
showActionSheet({
|
||||
title: '',
|
||||
options: singleItemOptions,
|
||||
})
|
||||
}}
|
||||
key={descriptor.identifier}
|
||||
text={descriptor.label}
|
||||
first={index === 0}
|
||||
selected={() => descriptor.primary}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<SectionedAccessoryTableCell onPress={addAnotherWorkspace} text={WorkspaceAction.AddAnother} key={'add-new'} />
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={signOutAllWorkspaces}
|
||||
text={WorkspaceAction.SignOutAll}
|
||||
key={'sign-out-all'}
|
||||
/>
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { FilesSection } from '@Screens/Settings/Sections/FilesSection'
|
||||
import { WorkspacesSection } from '@Screens/Settings/Sections/WorkspacesSection'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ApplicationEvent, FeatureStatus } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
@@ -30,7 +31,7 @@ export const Settings = (props: Props) => {
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
const removeApplicationEventSubscriber = application.addEventObserver(async event => {
|
||||
const removeApplicationEventSubscriber = application.addEventObserver(async (event) => {
|
||||
if (event === ApplicationEvent.KeyStatusChanged) {
|
||||
setHasPasscode(Boolean(application.hasPasscode()))
|
||||
updateProtectionsAvailable()
|
||||
@@ -54,6 +55,7 @@ export const Settings = (props: Props) => {
|
||||
<Container keyboardShouldPersistTaps={'always'} keyboardDismissMode={'interactive'}>
|
||||
<AuthSection title="Account" signedIn={signedIn} />
|
||||
<OptionsSection encryptionAvailable={!!encryptionAvailable} title="Options" />
|
||||
<WorkspacesSection />
|
||||
<PreferencesSection />
|
||||
{application.hasAccount() && isEntitledToFiles && <FilesSection />}
|
||||
<SecuritySection
|
||||
|
||||
Reference in New Issue
Block a user