From d9a9108603187c473f0fe9c3cb478b8dba050c3b Mon Sep 17 00:00:00 2001 From: vardanhakobyan Date: Thu, 2 Jun 2022 17:49:45 +0400 Subject: [PATCH 1/4] feat(wip): workspaces on mobile --- package.json | 2 +- src/App.tsx | 13 +- src/ApplicationGroupContext.tsx | 4 + src/Hooks/useSafeApplicationGroupContext.ts | 8 + src/Screens/Notes/Notes.tsx | 2 +- .../Settings/Sections/OptionsSection.tsx | 249 ++++++++++++++++-- yarn.lock | 8 +- 7 files changed, 247 insertions(+), 39 deletions(-) create mode 100644 src/ApplicationGroupContext.tsx create mode 100644 src/Hooks/useSafeApplicationGroupContext.ts diff --git a/package.json b/package.json index 922fe78c..c4d04dca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@standardnotes/react-native-textview": "1.0.2", "@standardnotes/react-native-utils": "1.0.1", "@standardnotes/sncrypto-common": "1.9.0", - "@standardnotes/snjs": "2.114.6", + "@standardnotes/snjs": "2.114.7", "@standardnotes/stylekit": "5.29.3", "@types/styled-components-react-native": "5.1.3", "js-base64": "^3.7.2", diff --git a/src/App.tsx b/src/App.tsx index 7df53060..90273ddc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { MobileApplication } from '@Lib/Application' import { ApplicationGroup } from '@Lib/ApplicationGroup' import { navigationRef } from '@Lib/NavigationService' import { DefaultTheme, NavigationContainer } from '@react-navigation/native' +import { ApplicationGroupContext } from '@Root/ApplicationGroupContext' import { MobileThemeVariables } from '@Root/Style/Themes/styled-components' import { ApplicationGroupEvent, DeinitMode, DeinitSource } from '@standardnotes/snjs' import { ThemeService, ThemeServiceContext } from '@Style/ThemeService' @@ -58,7 +59,7 @@ const AppComponent: React.FC<{ void application.launch() } }, - [application] + [application], ) useEffect(() => { @@ -145,15 +146,17 @@ export const App = (props: { env: TEnvironment }) => { } }) return removeAppChangeObserver - }, [appGroup, appGroup.primaryApplication, setAppGroup, createNewAppGroup]) + }, [appGroup, appGroup.primaryApplication, createNewAppGroup]) if (!application) { return null } return ( - - - + + + + + ) } diff --git a/src/ApplicationGroupContext.tsx b/src/ApplicationGroupContext.tsx new file mode 100644 index 00000000..cc15f8db --- /dev/null +++ b/src/ApplicationGroupContext.tsx @@ -0,0 +1,4 @@ +import { ApplicationGroup } from '@Lib/ApplicationGroup' +import React from 'react' + +export const ApplicationGroupContext = React.createContext(undefined) diff --git a/src/Hooks/useSafeApplicationGroupContext.ts b/src/Hooks/useSafeApplicationGroupContext.ts new file mode 100644 index 00000000..7919c55a --- /dev/null +++ b/src/Hooks/useSafeApplicationGroupContext.ts @@ -0,0 +1,8 @@ +import { ApplicationGroup } from '@Lib/ApplicationGroup' +import { ApplicationGroupContext } from '@Root/ApplicationGroupContext' +import { useContext } from 'react' + +export const useSafeApplicationGroupContext = () => { + const applicationGroupContext = useContext(ApplicationGroupContext) as ApplicationGroup + return applicationGroupContext +} diff --git a/src/Screens/Notes/Notes.tsx b/src/Screens/Notes/Notes.tsx index db45bf28..693b528d 100644 --- a/src/Screens/Notes/Notes.tsx +++ b/src/Screens/Notes/Notes.tsx @@ -344,7 +344,7 @@ export const Notes = React.memo( reloadNotesDisplayOptions() } - const newNotes = application.items.getDisplayableNotes() + const newNotes = application.items.getDisplayableNotes() // TODO: returns notes from all workspaces when in Main workspace const renderedNotes: SNNote[] = newNotes setNotes(renderedNotes) diff --git a/src/Screens/Settings/Sections/OptionsSection.tsx b/src/Screens/Settings/Sections/OptionsSection.tsx index d544842a..e11f0e0b 100644 --- a/src/Screens/Settings/Sections/OptionsSection.tsx +++ b/src/Screens/Settings/Sections/OptionsSection.tsx @@ -1,16 +1,19 @@ 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 { useSafeApplicationGroupContext } from '@Root/Hooks/useSafeApplicationGroupContext' import { ModalStackNavigationProp } from '@Root/ModalStack' import { SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS } from '@Root/Screens/screens' -import { ButtonType, PrefKey } from '@standardnotes/snjs' +import SNReactNative from '@standardnotes/react-native-utils' +import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType, PrefKey } from '@standardnotes/snjs' +import { CustomActionSheetOption, useCustomActionSheet } from '@Style/CustomActionSheet' import moment from 'moment' -import React, { useCallback, useContext, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { Platform } from 'react-native' import DocumentPicker from 'react-native-document-picker' import RNFS from 'react-native-fs' @@ -22,7 +25,9 @@ type Props = { export const OptionsSection = ({ title, encryptionAvailable }: Props) => { // Context - const application = useContext(ApplicationContext) + const application = useSafeApplicationContext() + const appGroup = useSafeApplicationGroupContext() + const [signedIn] = useSignedIn() const navigation = useNavigation['navigation']>() @@ -30,9 +35,68 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { const [importing, setImporting] = useState(false) const [exporting, setExporting] = useState(false) const [lastExportDate, setLastExportDate] = useState(() => - application?.getLocalPreferences().getValue(PrefKey.MobileLastExportDate, undefined) + application.getLocalPreferences().getValue(PrefKey.MobileLastExportDate, undefined), ) + const { showActionSheet } = useCustomActionSheet() + + const [applicationDescriptors, setApplicationDescriptors] = useState([]) + + useEffect(() => { + let descriptors = appGroup.getDescriptors() + setApplicationDescriptors(descriptors) + + const removeAppGroupObserver = appGroup.addEventObserver(event => { + if (event === ApplicationGroupEvent.DescriptorsDataChanged) { + descriptors = appGroup.getDescriptors() + setApplicationDescriptors(descriptors) + } + }) + + return () => { + removeAppGroupObserver() + } + }, [appGroup]) + /*useEffect(() => { + let descriptors = appGroup.getDescriptors() + setApplicationDescriptors(descriptors) + + // const removeAppGroupObserver = appGroup.addEventObserver(event => { + const removeAppGroupObserver = appGroup.addEventObserver(async event => { + if (event === ApplicationGroupEvent.DescriptorsDataChanged) { + // const applicationDescriptors = appGroup.getDescriptors() + descriptors = appGroup.getDescriptors() + setApplicationDescriptors(descriptors) + console.log('setting xxx to', JSON.stringify(descriptors)) + // application.setValue('xxx', JSON.stringify(descriptors)) + // await application.setValue('xxx', JSON.stringify(descriptors), StorageValueModes.Nonwrapped) + // await application.setValue('xxx', JSON.stringify(descriptors), StorageValueModes.Default) + console.log('here, application.isEphemeralSession()', application.isEphemeralSession()) + application.setValue('xxx', JSON.stringify(descriptors)) + // const aaa = await application.getAppState().setXxx(JSON.stringify(descriptors)) + console.log(1111); + await application.getAppState().setXxx(JSON.stringify(descriptors)) + console.log('0000'); + console.log('1.5-1.5', await application.getValue('xxx')); + console.log(222); + const storedValue = await application.getAppState().getXxx() + console.log(333); + console.log('right after setting, storedValue is ', storedValue) + + /!*setTimeout(() => { + console.log('going to read the value'); + console.log('and the set value is', application.getValue('xxx'), 100); + console.log('after reading'); + })*!/ + } + }) + + return () => { + removeAppGroupObserver() + } + // }, [appGroup]) + }, [appGroup, application])*/ + const lastExportData = useMemo(() => { if (lastExportDate) { const formattedDate = moment(lastExportDate).format('lll') @@ -57,7 +121,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,52 +140,52 @@ 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 + 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) }, - [application] + [application], ) const readImportFile = async (fileUri: string): Promise => { return RNFS.readFile(fileUri) .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.' + '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,12 +203,12 @@ 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.' + 'and cannot be imported here. Please update your application and try again.', ) } } else { @@ -159,40 +223,169 @@ 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' + 'OK', ) return } void exportData(encrypted) }, - [application?.alertService, encryptionAvailable, exportData] + [application.alertService, encryptionAvailable, exportData], ) const openManageSessions = useCallback(() => { navigation.push(SCREEN_MANAGE_SESSIONS) }, [navigation]) + const getSingleWorkspaceItemOptions = useCallback( + (descriptor: ApplicationDescriptor) => { + const worskspaceItemOptions: CustomActionSheetOption[] = [ + { + text: 'Open', + callback: async () => { + await appGroup.unloadCurrentAndActivateDescriptor(descriptor) + // TODO: find a way to check if there are memory leaks *without* the below call. + SNReactNative.exitApp() + }, + }, + { + text: 'Rename', + callback: async () => { + console.log('rename') + // await appGroup.unloadCurrentAndCreateNewDescriptor() + }, + }, + { + text: 'Remove', + destructive: true, + callback: async () => { + console.log('remove') + // await appGroup.unloadCurrentAndCreateNewDescriptor() + }, + }, + ] + return worskspaceItemOptions + }, + [appGroup], + ) + + const getActiveWorkspaceItems = useCallback(() => { + /*const descriptorItemOptions: [CustomActionSheetOption] = [{ + text: 'Add another workspace', + callback: async () => { + console.log('add another worskpace') + await appGroup.unloadCurrentAndCreateNewDescriptor() + }, + },]*/ + const descriptorItemOptions: CustomActionSheetOption[] = [] + + applicationDescriptors.forEach(descriptor => { + descriptorItemOptions.push({ + text: descriptor.label, + callback: async () => { + const singleItemOptions = getSingleWorkspaceItemOptions(descriptor) + + // console.log(`${descriptor.label} workspace click`) + showActionSheet({ + title: '', + options: singleItemOptions, + /*styles: { + titleTextStyle: { + fontWeight: descriptor.primary ? 'bold' : 'normal', + color: 'yellow !important', + }, + textStyle: { + fontWeight: descriptor.primary ? 'bold' : 'normal', + color: 'green !important', + }, + },*/ + }) + }, + }) + }) + + return descriptorItemOptions + }, [applicationDescriptors, getSingleWorkspaceItemOptions, showActionSheet]) + + const handleSwitchWorkspaceClick = useCallback(() => { + /*const activeDescriptors = applicationDescriptors.map(descriptor => { + return { + text: descriptor.label, + callback: async () => { + console.log(`${descriptor.label} workspace click`) + }, + } + })*/ + const activeDescriptors = getActiveWorkspaceItems() + const options: CustomActionSheetOption[] = [ + // TODO: show currently active descriptor as bold (or otherwise distinguishable) + ...activeDescriptors, + /*{ + text: 'Main Workspace', + callback: async () => { + console.log('main workspace click') + }, + },*/ + { + text: 'Add another workspace', + callback: async () => { + console.log('add another worskpace') + await appGroup.unloadCurrentAndCreateNewDescriptor() + SNReactNative.exitApp() + }, + }, + { + text: 'Sign out all workspaces', + callback: async () => { + try { + const confirmed = await application.alertService.confirm( + 'Are you sure you want to sign out of all workspaces on this device?', + undefined, + 'Sign out all', + ButtonType.Danger, + ) + if (!confirmed) { + return + } + await appGroup.signOutAllWorkspaces() + // TODO: do we need this here as well? + SNReactNative.exitApp() + } catch (error) { + console.error(error) + } + }, + }, + ] + showActionSheet({ title: '', options }) + }, [appGroup, application.alertService, getActiveWorkspaceItems, showActionSheet]) + 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' + 'OK', ) - }, [application?.alertService]) + }, [application.alertService]) return ( + {signedIn && ( <> diff --git a/yarn.lock b/yarn.lock index 9287c224..34d921b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1777,10 +1777,10 @@ resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.7.3.tgz#1e62a14800393be44cdb376d1d72fbc064334fc1" integrity sha512-twwYeBL+COkNF9IUM5bWrLZ4gXjg41tRxBMR3r3JcbrkyYjcNqVHf9L+YdBcndjSV3/9xwWl2pYWK1RB3UR2Xg== -"@standardnotes/snjs@2.114.6": - version "2.114.6" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.114.6.tgz#be514c306b8c3fda44c83ca4e73c4b7e79867f10" - integrity sha512-Y/ucIkixJaXeLGAk9NtqGYX9pBGE0E9ZPoj1Qoghv/PKjiEesEBDiDRZ+QGbzOOZuvpZXgQmdQpnWQwOKEwqqg== +"@standardnotes/snjs@2.114.7": + version "2.114.7" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.114.7.tgz#ed5a96f609e103d111ea150d3ff76609f515cd6d" + integrity sha512-Oa/w5ZOjDMScbgeWOtRJaaGLb7do6aidOYzkTxmXqOs+gBL1SqxE3h53joAk+wpuQq/nuZM+rnbh9XUOYKZYvg== dependencies: "@standardnotes/auth" "^3.19.2" "@standardnotes/common" "^1.22.0" From 7ce63cd2d9a7da6a6092b1bdf82e46046e6dbf23 Mon Sep 17 00:00:00 2001 From: vardanhakobyan Date: Wed, 8 Jun 2022 15:15:48 +0400 Subject: [PATCH 2/4] feat: remove current workspace, better UX for adding/switching workspace --- .../Settings/Sections/OptionsSection.tsx | 230 ++++++++++-------- 1 file changed, 125 insertions(+), 105 deletions(-) diff --git a/src/Screens/Settings/Sections/OptionsSection.tsx b/src/Screens/Settings/Sections/OptionsSection.tsx index e11f0e0b..86ea8147 100644 --- a/src/Screens/Settings/Sections/OptionsSection.tsx +++ b/src/Screens/Settings/Sections/OptionsSection.tsx @@ -57,45 +57,6 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { removeAppGroupObserver() } }, [appGroup]) - /*useEffect(() => { - let descriptors = appGroup.getDescriptors() - setApplicationDescriptors(descriptors) - - // const removeAppGroupObserver = appGroup.addEventObserver(event => { - const removeAppGroupObserver = appGroup.addEventObserver(async event => { - if (event === ApplicationGroupEvent.DescriptorsDataChanged) { - // const applicationDescriptors = appGroup.getDescriptors() - descriptors = appGroup.getDescriptors() - setApplicationDescriptors(descriptors) - console.log('setting xxx to', JSON.stringify(descriptors)) - // application.setValue('xxx', JSON.stringify(descriptors)) - // await application.setValue('xxx', JSON.stringify(descriptors), StorageValueModes.Nonwrapped) - // await application.setValue('xxx', JSON.stringify(descriptors), StorageValueModes.Default) - console.log('here, application.isEphemeralSession()', application.isEphemeralSession()) - application.setValue('xxx', JSON.stringify(descriptors)) - // const aaa = await application.getAppState().setXxx(JSON.stringify(descriptors)) - console.log(1111); - await application.getAppState().setXxx(JSON.stringify(descriptors)) - console.log('0000'); - console.log('1.5-1.5', await application.getValue('xxx')); - console.log(222); - const storedValue = await application.getAppState().getXxx() - console.log(333); - console.log('right after setting, storedValue is ', storedValue) - - /!*setTimeout(() => { - console.log('going to read the value'); - console.log('and the set value is', application.getValue('xxx'), 100); - console.log('after reading'); - })*!/ - } - }) - - return () => { - removeAppGroupObserver() - } - // }, [appGroup]) - }, [appGroup, application])*/ const lastExportData = useMemo(() => { if (lastExportDate) { @@ -239,68 +200,126 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { navigation.push(SCREEN_MANAGE_SESSIONS) }, [navigation]) + enum WorkspaceAction { + AddAnother = 'Add another workspace', + Open = 'Open', + Rename = 'Rename', + Remove = 'Remove', + SignOutAll = 'Sign out all workspaces', + } + + const { AddAnother, Open, Rename, Remove, SignOutAll } = WorkspaceAction + + const getWorkspaceActionConfirmation = useCallback( + async (action: WorkspaceAction): Promise => { + const { Info, Danger } = ButtonType + let message = '' + let buttonText = '' + let buttonType = Info + + switch (action) { + case Open: + 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?' + 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) + }, + [AddAnother, Open, Remove, SignOutAll, application.alertService], + ) + const getSingleWorkspaceItemOptions = useCallback( (descriptor: ApplicationDescriptor) => { - const worskspaceItemOptions: CustomActionSheetOption[] = [ - { - text: 'Open', + const worskspaceItemOptions: CustomActionSheetOption[] = [] + if (descriptor.primary) { + worskspaceItemOptions.push( + { + // text: 'Rename', + text: Rename, + callback: async () => { + console.log('implement rename...') + // await appGroup.unloadCurrentAndCreateNewDescriptor() + }, + }, + { + // text: 'Remove', + text: Remove, + destructive: true, + callback: async () => { + /*const confirmed = await application.alertService.confirm( + 'This action will remove this workspace and its related data from this device. Your synced data will not be affected.', + undefined, + 'Quit App', + ButtonType.Danger, + )*/ + const confirmed = await getWorkspaceActionConfirmation(Remove) + + if (!confirmed) { + return + } + + console.log( + 'implement remove and destroy the workspace (and probably call `SNReactNative.exitApp()` as well)...', + ) + // application.user.signOut().catch(console.error) + try { + await application.user.signOut() // TODO: do I need to call `SNReactNative.exitApp()` here as well? + } catch (error) { + console.error(error) + } + + // await appGroup.unloadCurrentAndCreateNewDescriptor() + }, + }, + ) + } else { + worskspaceItemOptions.push({ + text: Open, callback: async () => { + const confirmed = await getWorkspaceActionConfirmation(Open) + if (!confirmed) { + return + } + + // await application.getInstallationService().customWipeData() await appGroup.unloadCurrentAndActivateDescriptor(descriptor) // TODO: find a way to check if there are memory leaks *without* the below call. SNReactNative.exitApp() }, - }, - { - text: 'Rename', - callback: async () => { - console.log('rename') - // await appGroup.unloadCurrentAndCreateNewDescriptor() - }, - }, - { - text: 'Remove', - destructive: true, - callback: async () => { - console.log('remove') - // await appGroup.unloadCurrentAndCreateNewDescriptor() - }, - }, - ] + }) + } + return worskspaceItemOptions }, - [appGroup], + [Open, Remove, Rename, appGroup, application, getWorkspaceActionConfirmation], ) const getActiveWorkspaceItems = useCallback(() => { - /*const descriptorItemOptions: [CustomActionSheetOption] = [{ - text: 'Add another workspace', - callback: async () => { - console.log('add another worskpace') - await appGroup.unloadCurrentAndCreateNewDescriptor() - }, - },]*/ const descriptorItemOptions: CustomActionSheetOption[] = [] applicationDescriptors.forEach(descriptor => { descriptorItemOptions.push({ - text: descriptor.label, + text: `${descriptor.label}${descriptor.primary ? ' *' : ''}`, + callback: async () => { const singleItemOptions = getSingleWorkspaceItemOptions(descriptor) - // console.log(`${descriptor.label} workspace click`) showActionSheet({ title: '', options: singleItemOptions, - /*styles: { - titleTextStyle: { - fontWeight: descriptor.primary ? 'bold' : 'normal', - color: 'yellow !important', - }, - textStyle: { - fontWeight: descriptor.primary ? 'bold' : 'normal', - color: 'green !important', - }, - },*/ }) }, }) @@ -309,48 +328,49 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { return descriptorItemOptions }, [applicationDescriptors, getSingleWorkspaceItemOptions, showActionSheet]) - const handleSwitchWorkspaceClick = useCallback(() => { - /*const activeDescriptors = applicationDescriptors.map(descriptor => { - return { - text: descriptor.label, - callback: async () => { - console.log(`${descriptor.label} workspace click`) - }, + /*const customSignOut = useCallback(async () => { + try { + const confirmed = await getWorkspaceActionConfirmation(SignOutAll) + if (!confirmed) { + return } - })*/ + await appGroup.signOutAllWorkspaces() + + // TODO: do we need `exitApp` here as well? + SNReactNative.exitApp() + } catch (error) { + console.error(error) + } + }, [SignOutAll, appGroup, getWorkspaceActionConfirmation])*/ + + const handleSwitchWorkspaceClick = useCallback(() => { const activeDescriptors = getActiveWorkspaceItems() const options: CustomActionSheetOption[] = [ - // TODO: show currently active descriptor as bold (or otherwise distinguishable) ...activeDescriptors, - /*{ - text: 'Main Workspace', - callback: async () => { - console.log('main workspace click') - }, - },*/ { - text: 'Add another workspace', + text: AddAnother, callback: async () => { - console.log('add another worskpace') + const confirmed = await getWorkspaceActionConfirmation(AddAnother) + if (!confirmed) { + return + } + // await application.getInstallationService().wipeData() + // await application.getInstallationService().customWipeData() await appGroup.unloadCurrentAndCreateNewDescriptor() SNReactNative.exitApp() }, }, { - text: 'Sign out all workspaces', + text: SignOutAll, callback: async () => { try { - const confirmed = await application.alertService.confirm( - 'Are you sure you want to sign out of all workspaces on this device?', - undefined, - 'Sign out all', - ButtonType.Danger, - ) + const confirmed = await getWorkspaceActionConfirmation(SignOutAll) if (!confirmed) { return } await appGroup.signOutAllWorkspaces() - // TODO: do we need this here as well? + + // TODO: do we need `exitApp` here as well? SNReactNative.exitApp() } catch (error) { console.error(error) @@ -359,7 +379,7 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { }, ] showActionSheet({ title: '', options }) - }, [appGroup, application.alertService, getActiveWorkspaceItems, showActionSheet]) + }, [AddAnother, SignOutAll, appGroup, getActiveWorkspaceItems, getWorkspaceActionConfirmation, showActionSheet]) const showDataBackupAlert = useCallback(() => { void application.alertService.alert( From e8859bdff10bd9ba3206fd02ee4bcd7d9a7ecfd6 Mon Sep 17 00:00:00 2001 From: vardanhakobyan Date: Wed, 8 Jun 2022 16:31:27 +0400 Subject: [PATCH 3/4] feat: rename workspace --- src/ModalStack.tsx | 30 +++- .../InputModal/WorkspaceInputModal.tsx | 61 ++++++++ .../Settings/Sections/OptionsSection.tsx | 131 ++++++++---------- src/Screens/screens.ts | 1 + 4 files changed, 152 insertions(+), 71 deletions(-) create mode 100644 src/Screens/InputModal/WorkspaceInputModal.tsx diff --git a/src/ModalStack.tsx b/src/ModalStack.tsx index da8557c1..b9e2e2bd 100644 --- a/src/ModalStack.tsx +++ b/src/ModalStack.tsx @@ -14,13 +14,15 @@ import { SCREEN_INPUT_MODAL_FILE_NAME, SCREEN_INPUT_MODAL_PASSCODE, SCREEN_INPUT_MODAL_TAG, + SCREEN_INPUT_MODAL_WORKSPACE_NAME, SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS, SCREEN_UPLOADED_FILES_LIST, } from '@Root/Screens/screens' import { Settings } from '@Root/Screens/Settings/Settings' import { UploadedFilesList } from '@Root/Screens/UploadedFilesList/UploadedFilesList' -import { Challenge, DeinitMode, DeinitSource, FileItem, SNNote } from '@standardnotes/snjs' +import { WorkspaceInputModal } from '@Screens/InputModal/WorkspaceInputModal' +import { ApplicationDescriptor, Challenge, DeinitMode, DeinitSource, FileItem, SNNote } from '@standardnotes/snjs' import { ICON_CHECKMARK, ICON_CLOSE } from '@Style/Icons' import { ThemeService } from '@Style/ThemeService' import React, { memo, useContext } from 'react' @@ -48,6 +50,10 @@ export type ModalStackNavigatorParamList = { [SCREEN_UPLOADED_FILES_LIST]: HeaderTitleParams & { note: SNNote } + [SCREEN_INPUT_MODAL_WORKSPACE_NAME]: HeaderTitleParams & { + descriptor: ApplicationDescriptor + renameWorkspace: (descriptor: ApplicationDescriptor, workspaceName: string) => Promise + } [SCREEN_INPUT_MODAL_PASSCODE]: undefined [SCREEN_AUTHENTICATE]: { challenge: Challenge @@ -275,6 +281,28 @@ export const MainStackComponent = ({ env }: { env: TEnvironment }) => { })} component={BlockingModal} /> + ({ + title: 'Workspace', + gestureEnabled: false, + headerTitle: ({ children }) => { + return + }, + headerLeft: ({ disabled, onPress }) => ( + + + + ), + })} + component={WorkspaceInputModal} + /> ) } diff --git a/src/Screens/InputModal/WorkspaceInputModal.tsx b/src/Screens/InputModal/WorkspaceInputModal.tsx new file mode 100644 index 00000000..8c4ce466 --- /dev/null +++ b/src/Screens/InputModal/WorkspaceInputModal.tsx @@ -0,0 +1,61 @@ +import { ButtonCell } from '@Root/Components/ButtonCell' +import { SectionedTableCell } from '@Root/Components/SectionedTableCell' +import { TableSection } from '@Root/Components/TableSection' +import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext' +import { ModalStackNavigationProp } from '@Root/ModalStack' +import { SCREEN_INPUT_MODAL_WORKSPACE_NAME } from '@Root/Screens/screens' +import { ThemeServiceContext } from '@Style/ThemeService' +import React, { FC, useContext, useEffect, useRef, useState } from 'react' +import { TextInput } from 'react-native' +import { Container, Input } from './InputModal.styled' + +type Props = ModalStackNavigationProp + +export const WorkspaceInputModal: FC = props => { + const { descriptor, renameWorkspace } = props.route.params + const themeService = useContext(ThemeServiceContext) + const application = useSafeApplicationContext() + + const workspaceNameInputRef = useRef(null) + + const [workspaceName, setWorkspaceName] = useState(descriptor.label) + + const onSubmit = async () => { + const trimmedWorkspaceName = workspaceName.trim() + if (trimmedWorkspaceName === '') { + setWorkspaceName(descriptor.label) + await application?.alertService.alert('Workspace name cannot be empty') + workspaceNameInputRef.current?.focus() + return + } + await renameWorkspace(descriptor, trimmedWorkspaceName) + void application.sync.sync() + props.navigation.goBack() + } + + useEffect(() => { + workspaceNameInputRef.current?.focus() + }, []) + + return ( + + + + + + + + + + ) +} diff --git a/src/Screens/Settings/Sections/OptionsSection.tsx b/src/Screens/Settings/Sections/OptionsSection.tsx index 86ea8147..4250ea26 100644 --- a/src/Screens/Settings/Sections/OptionsSection.tsx +++ b/src/Screens/Settings/Sections/OptionsSection.tsx @@ -8,7 +8,7 @@ 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_MANAGE_SESSIONS, SCREEN_SETTINGS } from '@Root/Screens/screens' +import { SCREEN_INPUT_MODAL_WORKSPACE_NAME, SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS } from '@Root/Screens/screens' import SNReactNative from '@standardnotes/react-native-utils' import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType, PrefKey } from '@standardnotes/snjs' import { CustomActionSheetOption, useCustomActionSheet } from '@Style/CustomActionSheet' @@ -241,70 +241,72 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { [AddAnother, Open, Remove, SignOutAll, application.alertService], ) + const renameWorkspace = useCallback( + async (descriptor: ApplicationDescriptor, newName: string) => { + appGroup.renameDescriptor(descriptor, newName) + }, + [appGroup], + ) + + const signOutWorkspace = useCallback(async () => { + const confirmed = await getWorkspaceActionConfirmation(Remove) + + if (!confirmed) { + return + } + + try { + await application.user.signOut() // TODO: do I need to call `SNReactNative.exitApp()` here as well? + } catch (error) { + console.error(error) + } + }, [Remove, application.user, getWorkspaceActionConfirmation]) + + const openWorkspace = useCallback( + async (descriptor: ApplicationDescriptor) => { + const confirmed = await getWorkspaceActionConfirmation(Open) + if (!confirmed) { + return + } + + // await application.getInstallationService().customWipeData() + await appGroup.unloadCurrentAndActivateDescriptor(descriptor) + // TODO: find a way to check if there are memory leaks *without* the below call. + SNReactNative.exitApp() + }, + [Open, appGroup, getWorkspaceActionConfirmation], + ) + const getSingleWorkspaceItemOptions = useCallback( (descriptor: ApplicationDescriptor) => { const worskspaceItemOptions: CustomActionSheetOption[] = [] if (descriptor.primary) { worskspaceItemOptions.push( { - // text: 'Rename', text: Rename, - callback: async () => { - console.log('implement rename...') - // await appGroup.unloadCurrentAndCreateNewDescriptor() + callback: () => { + navigation.navigate(SCREEN_INPUT_MODAL_WORKSPACE_NAME, { + descriptor, + renameWorkspace, + }) }, }, { - // text: 'Remove', text: Remove, destructive: true, - callback: async () => { - /*const confirmed = await application.alertService.confirm( - 'This action will remove this workspace and its related data from this device. Your synced data will not be affected.', - undefined, - 'Quit App', - ButtonType.Danger, - )*/ - const confirmed = await getWorkspaceActionConfirmation(Remove) - - if (!confirmed) { - return - } - - console.log( - 'implement remove and destroy the workspace (and probably call `SNReactNative.exitApp()` as well)...', - ) - // application.user.signOut().catch(console.error) - try { - await application.user.signOut() // TODO: do I need to call `SNReactNative.exitApp()` here as well? - } catch (error) { - console.error(error) - } - - // await appGroup.unloadCurrentAndCreateNewDescriptor() - }, + callback: signOutWorkspace, }, ) } else { worskspaceItemOptions.push({ text: Open, - callback: async () => { - const confirmed = await getWorkspaceActionConfirmation(Open) - if (!confirmed) { - return - } - - // await application.getInstallationService().customWipeData() - await appGroup.unloadCurrentAndActivateDescriptor(descriptor) - // TODO: find a way to check if there are memory leaks *without* the below call. - SNReactNative.exitApp() - }, + callback: () => openWorkspace(descriptor), }) } return worskspaceItemOptions }, - [Open, Remove, Rename, appGroup, application, getWorkspaceActionConfirmation], + [Open, Remove, Rename, navigation, openWorkspace, renameWorkspace, signOutWorkspace], ) const getActiveWorkspaceItems = useCallback(() => { @@ -328,7 +330,18 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { return descriptorItemOptions }, [applicationDescriptors, getSingleWorkspaceItemOptions, showActionSheet]) - /*const customSignOut = useCallback(async () => { + const addAnotherWorkspace = useCallback(async () => { + const confirmed = await getWorkspaceActionConfirmation(AddAnother) + if (!confirmed) { + return + } + // await application.getInstallationService().wipeData() + // await application.getInstallationService().customWipeData() + await appGroup.unloadCurrentAndCreateNewDescriptor() + SNReactNative.exitApp() + }, [AddAnother, appGroup, getWorkspaceActionConfirmation]) + + const signOutAllWorkspaces = useCallback(async () => { try { const confirmed = await getWorkspaceActionConfirmation(SignOutAll) if (!confirmed) { @@ -341,7 +354,7 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { } catch (error) { console.error(error) } - }, [SignOutAll, appGroup, getWorkspaceActionConfirmation])*/ + }, [SignOutAll, appGroup, getWorkspaceActionConfirmation]) const handleSwitchWorkspaceClick = useCallback(() => { const activeDescriptors = getActiveWorkspaceItems() @@ -349,37 +362,15 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => { ...activeDescriptors, { text: AddAnother, - callback: async () => { - const confirmed = await getWorkspaceActionConfirmation(AddAnother) - if (!confirmed) { - return - } - // await application.getInstallationService().wipeData() - // await application.getInstallationService().customWipeData() - await appGroup.unloadCurrentAndCreateNewDescriptor() - SNReactNative.exitApp() - }, + callback: addAnotherWorkspace, }, { text: SignOutAll, - callback: async () => { - try { - const confirmed = await getWorkspaceActionConfirmation(SignOutAll) - if (!confirmed) { - return - } - await appGroup.signOutAllWorkspaces() - - // TODO: do we need `exitApp` here as well? - SNReactNative.exitApp() - } catch (error) { - console.error(error) - } - }, + callback: signOutAllWorkspaces, }, ] showActionSheet({ title: '', options }) - }, [AddAnother, SignOutAll, appGroup, getActiveWorkspaceItems, getWorkspaceActionConfirmation, showActionSheet]) + }, [AddAnother, SignOutAll, addAnotherWorkspace, getActiveWorkspaceItems, showActionSheet, signOutAllWorkspaces]) const showDataBackupAlert = useCallback(() => { void application.alertService.alert( diff --git a/src/Screens/screens.ts b/src/Screens/screens.ts index 3f74c8ea..27320e52 100644 --- a/src/Screens/screens.ts +++ b/src/Screens/screens.ts @@ -8,6 +8,7 @@ export const SCREEN_INPUT_MODAL_FILE_NAME = 'InputModalFileName' export const SCREEN_NOTE_HISTORY = 'NoteSessionHistory' as const export const SCREEN_NOTE_HISTORY_PREVIEW = 'NoteSessionHistoryPreview' as const export const SCREEN_UPLOADED_FILES_LIST = 'UploadedFilesList' as const +export const SCREEN_INPUT_MODAL_WORKSPACE_NAME = 'InputModalWorkspaceName' as const export const SCREEN_SETTINGS = 'Settings' export const SCREEN_MANAGE_SESSIONS = 'ManageSessions' as const From 17f5c2b3e789d5a7fc5a4bb76b210eff18d4dfc5 Mon Sep 17 00:00:00 2001 From: vardanhakobyan Date: Wed, 8 Jun 2022 19:07:17 +0400 Subject: [PATCH 4/4] fix: filter workspace items correctly --- src/Lib/Interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lib/Interface.ts b/src/Lib/Interface.ts index 165fd8c0..ea434dc7 100644 --- a/src/Lib/Interface.ts +++ b/src/Lib/Interface.ts @@ -80,7 +80,7 @@ export class MobileDeviceInterface implements DeviceInterface { private async getAllDatabaseKeys(identifier: ApplicationIdentifier) { const keys = await AsyncStorage.getAllKeys() const filtered = keys.filter(key => { - return key.includes(this.getDatabaseKeyPrefix(identifier)) + return key.startsWith(this.getDatabaseKeyPrefix(identifier)) }) return filtered }