feat: mobile workspaces (#1093)
This commit is contained in:
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user