feat: mobile workspaces (#1093)

This commit is contained in:
Vardan Hakobyan
2022-06-21 15:42:43 +04:00
committed by GitHub
parent 1f903f17d1
commit 7d60dfee73
71 changed files with 599 additions and 317 deletions

View File

@@ -10,7 +10,7 @@ export class MobileAlertService extends AlertService {
return goBack
}
alert(text: string, title: string, closeButtonText?: string) {
return new Promise<void>(resolve => {
return new Promise<void>((resolve) => {
// On iOS, confirm should go first. On Android, cancel should go first.
const buttons = [
{

View File

@@ -155,7 +155,7 @@ export class ApplicationState extends ApplicationService {
const savedTag =
(this.application.items.findItem(savedTagUuid) as SNTag) ||
this.application.items.getSmartViews().find(tag => tag.uuid === savedTagUuid)
this.application.items.getSmartViews().find((tag) => tag.uuid === savedTagUuid)
if (savedTag) {
this.setSelectedTag(savedTag, false)
this.selectedTagRestored = true
@@ -288,7 +288,7 @@ export class ApplicationState extends ApplicationService {
if (note && note.conflictOf) {
void InteractionManager.runAfterInteractions(() => {
void this.application?.mutator.changeAndSaveItem(note, mutator => {
void this.application?.mutator.changeAndSaveItem(note, (mutator) => {
mutator.conflictOf = undefined
})
})
@@ -328,7 +328,7 @@ export class ApplicationState extends ApplicationService {
}
}
private keyboardDidShow: KeyboardEventListener = e => {
private keyboardDidShow: KeyboardEventListener = (e) => {
this.keyboardHeight = e.endCoordinates.height
this.notifyEventObservers(AppStateEventType.KeyboardChangeEvent)
}
@@ -353,7 +353,7 @@ export class ApplicationState extends ApplicationService {
[ContentType.Note, ContentType.Tag],
async ({ changed, inserted, removed, source }) => {
if (source === PayloadEmitSource.PreSyncSave || source === PayloadEmitSource.RemoteRetrieved) {
const removedNotes = removed.filter(i => i.content_type === ContentType.Note)
const removedNotes = removed.filter((i) => i.content_type === ContentType.Note)
for (const removedNote of removedNotes) {
const editor = this.editorForNote(removedNote.uuid)
if (editor) {
@@ -361,7 +361,7 @@ export class ApplicationState extends ApplicationService {
}
}
const notes = [...changed, ...inserted].filter(candidate => candidate.content_type === ContentType.Note)
const notes = [...changed, ...inserted].filter((candidate) => candidate.content_type === ContentType.Note)
const isBrowswingTrashedNotes =
this.selectedTag instanceof SmartView && this.selectedTag.uuid === SystemViewId.TrashedNotes
@@ -384,7 +384,7 @@ export class ApplicationState extends ApplicationService {
}
if (this.selectedTag) {
const matchingTag = [...changed, ...inserted].find(candidate => candidate.uuid === this.selectedTag.uuid)
const matchingTag = [...changed, ...inserted].find((candidate) => candidate.uuid === this.selectedTag.uuid)
if (matchingTag) {
this.selectedTag = matchingTag as SNTag
}
@@ -397,7 +397,7 @@ export class ApplicationState extends ApplicationService {
* Registers for MobileApplication events
*/
private handleApplicationEvents() {
this.removeAppEventObserver = this.application.addEventObserver(async eventName => {
this.removeAppEventObserver = this.application.addEventObserver(async (eventName) => {
switch (eventName) {
case ApplicationEvent.LocalDataIncrementalLoad:
case ApplicationEvent.LocalDataLoaded: {
@@ -441,7 +441,7 @@ export class ApplicationState extends ApplicationService {
* @returns tags that are referencing note
*/
public getNoteTags(note: SNNote) {
return this.application.items.itemsReferencingItem(note).filter(ref => {
return this.application.items.itemsReferencingItem(note).filter((ref) => {
return ref.content_type === ContentType.Tag
}) as SNTag[]
}
@@ -453,7 +453,7 @@ export class ApplicationState extends ApplicationService {
if (tag instanceof SmartView) {
return this.application.items.notesMatchingSmartView(tag)
} else {
return this.application.items.referencesForItem(tag).filter(ref => {
return this.application.items.referencesForItem(tag).filter((ref) => {
return ref.content_type === ContentType.Note
}) as SNNote[]
}

View File

@@ -61,13 +61,13 @@ export class BackupsService extends ApplicationService {
}
private async exportIOS(filename: string, data: string) {
return new Promise<boolean>(resolve => {
return new Promise<boolean>((resolve) => {
void (this.application! as MobileApplication).getAppState().performActionWithoutStateChangeImpact(async () => {
Share.share({
title: filename,
message: data,
})
.then(result => {
.then((result) => {
resolve(result.action !== Share.dismissedAction)
})
.catch(() => {
@@ -98,7 +98,7 @@ export class BackupsService extends ApplicationService {
// success
return true
})
.catch(error => {
.catch((error) => {
console.error('Error opening file', error)
return false
})
@@ -119,7 +119,7 @@ export class BackupsService extends ApplicationService {
}
private async exportViaEmailAndroid(data: string, filename: string) {
return new Promise<boolean>(resolve => {
return new Promise<boolean>((resolve) => {
const fileType = '.json' // Android creates a tmp file and expects dot with extension
let resolved = false

View File

@@ -18,7 +18,7 @@ import { Base64 } from 'js-base64'
import RNFS, { DocumentDirectoryPath } from 'react-native-fs'
import StaticServer from 'react-native-static-server'
import { unzip } from 'react-native-zip-archive'
import { componentsCdn, version, name } from '../../package.json'
import { componentsCdn, name, version } from '../../package.json'
import { MobileThemeContent } from '../Style/MobileTheme'
import { IsDev } from './Utils'
@@ -356,7 +356,7 @@ export class ComponentManager extends SNComponentManager {
}
export async function associateComponentWithNote(application: SNApplication, component: SNComponent, note: SNNote) {
return application.mutator.changeItem<ComponentMutator>(component, mutator => {
return application.mutator.changeItem<ComponentMutator>(component, (mutator) => {
mutator.removeDisassociatedItemId(note.uuid)
mutator.associateWithItem(note.uuid)
})

View File

@@ -1,5 +1,5 @@
import SNReactNative from '@standardnotes/react-native-utils'
import { ApplicationService, ButtonType, isNullOrUndefined, StorageValueModes } from '@standardnotes/snjs'
import { ApplicationService, ButtonType, StorageValueModes } from '@standardnotes/snjs'
import { MobileDeviceInterface } from './Interface'
const FIRST_RUN_KEY = 'first_run'
@@ -14,7 +14,7 @@ export class InstallationService extends ApplicationService {
}
async markApplicationAsRan() {
return this.application?.setValue(FIRST_RUN_KEY, false, StorageValueModes.Nonwrapped)
return this.application.deviceInterface.setRawStorageValue(FIRST_RUN_KEY, 'false')
}
/**
@@ -22,24 +22,20 @@ export class InstallationService extends ApplicationService {
* AsyncStorage failures, we want to confirm with the user before deleting anything.
*/
async needsWipe() {
const hasNormalKeys = this.application?.hasAccount() || this.application?.hasPasscode()
const deviceInterface = this.application?.deviceInterface as MobileDeviceInterface
const keychainKey = await deviceInterface?.getRawKeychainValue()
const hasKeychainValue = !(
isNullOrUndefined(keychainKey) ||
(typeof keychainKey === 'object' && Object.keys(keychainKey).length === 0)
)
const hasAccountOrPasscode = this.application.hasAccount() || this.application?.hasPasscode()
const deviceInterface = this.application.deviceInterface as MobileDeviceInterface
const keychainKey = await deviceInterface.getNamespacedKeychainValue(this.application.identifier)
const firstRunKey = await this.application?.getValue(FIRST_RUN_KEY, StorageValueModes.Nonwrapped)
let firstRunKeyMissing = isNullOrUndefined(firstRunKey)
/*
* Because of migration failure first run key might not be in non wrapped storage
*/
const hasKeychainValue = keychainKey != undefined
const firstRunKey = await this.application.deviceInterface.getRawStorageValue(FIRST_RUN_KEY)
let firstRunKeyMissing = firstRunKey == undefined
if (firstRunKeyMissing) {
const fallbackFirstRunValue = await this.application?.deviceInterface?.getRawStorageValue(FIRST_RUN_KEY)
firstRunKeyMissing = isNullOrUndefined(fallbackFirstRunValue)
const fallbackFirstRunValue = await this.application.getValue(FIRST_RUN_KEY, StorageValueModes.Nonwrapped)
firstRunKeyMissing = fallbackFirstRunValue == undefined
}
return !hasNormalKeys && hasKeychainValue && firstRunKeyMissing
return !hasAccountOrPasscode && hasKeychainValue && firstRunKeyMissing
}
/**

View File

@@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-community/async-storage'
import SNReactNative from '@standardnotes/react-native-utils'
import {
ApplicationIdentifier,
DeviceInterface,
@@ -32,7 +33,7 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
let text =
'The following items could not be loaded. This may happen if you are in low-memory conditions, or if the note is very large in size. We recommend breaking up large notes into smaller chunks using the desktop or web app.\n\nItems:\n'
let index = 0
text += failedItemIds.map(id => {
text += failedItemIds.map((id) => {
let result = id
if (index !== failedItemIds.length - 1) {
result += '\n'
@@ -79,8 +80,8 @@ 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))
const filtered = keys.filter((key) => {
return key.startsWith(this.getDatabaseKeyPrefix(identifier))
})
return filtered
}
@@ -205,7 +206,7 @@ export class MobileDeviceInterface implements DeviceInterface {
return
}
await Promise.all(
payloads.map(item => {
payloads.map((item) => {
return AsyncStorage.setItem(this.keyForPayloadId(item.uuid, identifier), JSON.stringify(item))
}),
)
@@ -294,7 +295,7 @@ export class MobileDeviceInterface implements DeviceInterface {
}
Linking.canOpenURL(url)
.then(supported => {
.then((supported) => {
if (!supported) {
showAlert()
return
@@ -306,15 +307,16 @@ export class MobileDeviceInterface implements DeviceInterface {
}
async clearAllDataFromDevice(_workspaceIdentifiers: string[]): Promise<{ killsApplication: boolean }> {
await this.clearRawKeychainValue()
await this.removeAllRawStorageValues()
await this.clearRawKeychainValue()
return { killsApplication: false }
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
performSoftReset() {}
performSoftReset() {
SNReactNative.exitApp()
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
performHardReset() {}

View File

@@ -30,7 +30,7 @@ export const useSignedIn = (signedInCallback?: () => void, signedOutCallback?: (
}
}
void getSignedIn()
const removeSignedInObserver = application.addEventObserver(async event => {
const removeSignedInObserver = application.addEventObserver(async (event) => {
if (event === ApplicationEvent.Launched) {
void getSignedIn()
}
@@ -74,7 +74,7 @@ export const useOutOfSync = () => {
}, [application])
React.useEffect(() => {
const removeSignedInObserver = application.addEventObserver(async event => {
const removeSignedInObserver = application.addEventObserver(async (event) => {
if (event === ApplicationEvent.EnteredOutOfSync) {
setOutOfSync(true)
} else if (event === ApplicationEvent.ExitedOutOfSync) {
@@ -103,7 +103,7 @@ export const useIsLocked = () => {
useEffect(() => {
let isMounted = true
const removeSignedInObserver = application?.getAppState().addLockStateChangeObserver(event => {
const removeSignedInObserver = application?.getAppState().addLockStateChangeObserver((event) => {
if (isMounted) {
if (event === LockStateType.Locked) {
setIsLocked(true)
@@ -131,7 +131,7 @@ export const useHasEditor = () => {
const [hasEditor, setHasEditor] = React.useState<boolean>(false)
useEffect(() => {
const removeEditorObserver = application?.editorGroup.addActiveControllerChangeObserver(newEditor => {
const removeEditorObserver = application?.editorGroup.addActiveControllerChangeObserver((newEditor) => {
setHasEditor(Boolean(newEditor))
})
return removeEditorObserver
@@ -207,7 +207,7 @@ export const useSyncStatus = () => {
}, [application, completedInitialSync, setStatus])
useEffect(() => {
const unsubscribeAppEvents = application?.addEventObserver(async eventName => {
const unsubscribeAppEvents = application?.addEventObserver(async (eventName) => {
if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
updateLocalDataStatus()
} else if (eventName === ApplicationEvent.SyncStatusChanged || eventName === ApplicationEvent.FailedSync) {
@@ -340,7 +340,7 @@ export const useProtectionSessionExpiry = () => {
const [protectionsDisabledUntil, setProtectionsDisabledUntil] = React.useState(getProtectionsDisabledUntil())
useEffect(() => {
const removeProtectionLengthSubscriber = application?.addEventObserver(async event => {
const removeProtectionLengthSubscriber = application?.addEventObserver(async (event) => {
if ([ApplicationEvent.UnprotectedSessionBegan, ApplicationEvent.UnprotectedSessionExpired].includes(event)) {
setProtectionsDisabledUntil(getProtectionsDisabledUntil())
}
@@ -389,7 +389,7 @@ export const useChangeNote = (note: SNNote | undefined, editor: NoteViewControll
if (await canChangeNote()) {
await application?.mutator.changeAndSaveItem(
note!,
mutator => {
(mutator) => {
const noteMutator = mutator as NoteMutator
mutate(noteMutator)
},