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

14
packages/mobile/.eslintrc Normal file
View File

@@ -0,0 +1,14 @@
{
"env": {
"node": true,
"commonjs": true
},
"extends": ["@react-native-community", "plugin:react-hooks/recommended", "../../.eslintrc.json"],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-var-requires": "off",
"eqeqeq": ["off"]
},
"ignorePatterns": ["metro.config.js"]
}

View File

@@ -1,28 +0,0 @@
module.exports = {
root: true,
extends: ['@react-native-community', 'prettier', '../../node_modules/@standardnotes/config/src/.eslintrc'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'prettier'],
ignorePatterns: ['.eslintrc.js', 'metro.config.js'],
overrides: [
{
files: ['*.ts', '*.tsx'],
rules: {
'@typescript-eslint/no-shadow': ['error'],
'no-shadow': 'off',
'no-undef': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'no-invalid-this': 'warn',
'no-console': 'warn',
eqeqeq: ['warn', 'smart'],
'no-void': 'off',
},
},
],
rules: {
'prettier/prettier': 'warn',
},
}

View File

@@ -1,11 +0,0 @@
module.exports = {
arrowParens: 'avoid',
bracketSpacing: true,
jsxSingleQuote: false,
quoteProps: 'as-needed',
semi: false,
singleQuote: true,
tabWidth: 2,
printWidth: 120,
trailingComma: 'all',
}

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const faker = require('faker')
import { by, device, element, expect, waitFor } from 'detox'
@@ -13,14 +12,10 @@ export const expectToBeVisible = async (testedElement: Detox.IndexableNativeElem
const checkAfterReinstall = async () => {
if (device.getPlatform() === 'ios') {
const alertElement = element(
by.label('Delete Local Data').and(by.type('_UIAlertControllerActionView'))
)
const alertElement = element(by.label('Delete Local Data').and(by.type('_UIAlertControllerActionView')))
const alertVisible = await expectToBeVisible(alertElement)
if (alertVisible) {
await element(
by.label('Delete Local Data').and(by.type('_UIAlertControllerActionView'))
).tap()
await element(by.label('Delete Local Data').and(by.type('_UIAlertControllerActionView'))).tap()
}
}
}

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const helpers = require('../../Helpers')
import { by, device, element, expect, waitFor } from 'detox'
import { openSettingsScreen } from '../../Helpers'
@@ -45,9 +44,7 @@ fdescribe('Account section', () => {
await element(by.id('passwordField')).typeText(helpers.randomCredentials.password)
await element(by.id('otherOptionsButton')).tap()
await element(by.id('syncServerField')).clearText()
await element(by.id('syncServerField')).typeText(
helpers.randomCredentials.syncServerUrl + '\n'
)
await element(by.id('syncServerField')).typeText(helpers.randomCredentials.syncServerUrl + '\n')
// wait for buttons to be visible after closing keyboard on smaller devices
await waitFor(element(by.id('registerButton')))
.toBeVisible()
@@ -90,9 +87,7 @@ fdescribe('Account section', () => {
await element(by.id('passwordField')).typeText(helpers.randomCredentials.password)
await element(by.id('otherOptionsButton')).tap()
await element(by.id('syncServerField')).clearText()
await element(by.id('syncServerField')).typeText(
helpers.randomCredentials.syncServerUrl + '\n'
)
await element(by.id('syncServerField')).typeText(helpers.randomCredentials.syncServerUrl + '\n')
// wait for button to be visible after keyboard close
await waitFor(element(by.id('signInButton')))
.toBeVisible()

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const helpers = require('../../Helpers')
import { by, device, element, expect } from 'detox'

View File

@@ -21,6 +21,18 @@ if (__DEV__ === false) {
}
/* eslint-enable no-console */
const originalWarn = console.warn
console.warn = function filterWarnings(msg) {
const supressedWarnings = [
"[react-native-gesture-handler] Seems like you're using an old API with gesture components",
]
if (!supressedWarnings.some(entry => msg.includes(entry))) {
originalWarn.apply(console, arguments)
}
}
enableAndroidFontFix()
AppRegistry.registerComponent(appName, () => App)

View File

@@ -11,9 +11,10 @@
"ios-dev": "react-native run-ios --scheme StandardNotesDev",
"ios-prod": "react-native run-ios --scheme StandardNotes",
"clear-cache": "watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*",
"lint": "yarn lint:eslint && yarn lint:prettier",
"lint:eslint": "yarn eslint . --ext .ts,.tsx --fix --quiet",
"lint:prettier": "prettier ./src --write",
"lint": "yarn eslint . --ext .ts,.tsx",
"lint:fix": "yarn lint --fix",
"format": "prettier ./src",
"format:fix": "yarn format --write",
"build": "yarn android:bundle && yarn install:pods",
"tsc": "tsc --noEmit",
"start": "react-native start",

View File

@@ -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'
@@ -68,7 +69,7 @@ const AppComponent: React.FC<{
setThemeServiceRef(themeServiceInstance)
await application.prepareForLaunch({
receiveChallenge: async challenge => {
receiveChallenge: async (challenge) => {
application.promptForChallenge(challenge)
},
})
@@ -135,7 +136,7 @@ export const App = (props: { env: TEnvironment }) => {
const [appGroup, setAppGroup] = useState<ApplicationGroup>(() => createNewAppGroup())
useEffect(() => {
const removeAppChangeObserver = appGroup.addEventObserver(event => {
const removeAppChangeObserver = appGroup.addEventObserver((event) => {
if (event === ApplicationGroupEvent.PrimaryApplicationSet) {
const mobileApplication = appGroup.primaryApplication as MobileApplication
setApplication(mobileApplication)
@@ -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 (
<ApplicationContext.Provider value={application}>
<AppComponent env={props.env} key={application.Uuid} application={application} />
</ApplicationContext.Provider>
<ApplicationGroupContext.Provider value={appGroup}>
<ApplicationContext.Provider value={application}>
<AppComponent env={props.env} key={application.Uuid} application={application} />
</ApplicationContext.Provider>
</ApplicationGroupContext.Provider>
)
}

View File

@@ -63,7 +63,7 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
const noteDrawerRef = useRef<DrawerLayout>(null)
useEffect(() => {
const removeObserver = application?.getAppState().addStateChangeObserver(event => {
const removeObserver = application?.getAppState().addStateChangeObserver((event) => {
if (event === AppStateType.EditorClosed) {
noteDrawerRef.current?.closeDrawer()
if (!isInTabletMode && props.navigation.canGoBack()) {
@@ -76,7 +76,7 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
}, [application, props.navigation, isInTabletMode])
useEffect(() => {
const removeObserver = application?.getStatusManager().addHeaderStatusObserver(messages => {
const removeObserver = application?.getStatusManager().addHeaderStatusObserver((messages) => {
setNotesStatus(messages[SCREEN_NOTES])
setComposeStatus(messages[SCREEN_COMPOSE])
})
@@ -161,7 +161,7 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
const screenStatus = isInTabletMode ? composeStatus || notesStatus : notesStatus
const title = route.params?.title ?? (children || '')
const subtitle = [screenStatus?.status, route.params?.subTitle].filter(x => !!x).join(' • ')
const subtitle = [screenStatus?.status, route.params?.subTitle].filter((x) => !!x).join(' • ')
return <HeaderTitleView title={title} subtitle={subtitle} subtitleColor={screenStatus?.color} />
},

View File

@@ -0,0 +1,4 @@
import { ApplicationGroup } from '@Lib/ApplicationGroup'
import React from 'react'
export const ApplicationGroupContext = React.createContext<ApplicationGroup | undefined>(undefined)

View File

@@ -55,7 +55,7 @@ const ButtonLabel = styled.Text<{ primary?: boolean }>`
color: ${({ theme, primary }) => {
return primary ? theme.stylekitInfoContrastColor : theme.stylekitForegroundColor
}};
font-size: ${props => props.theme.mainTextFontSize}px;
font-size: ${(props) => props.theme.mainTextFontSize}px;
`
export const Button: React.FC<Props> = ({ onPress, label, primary, fullWidth, last }: Props) => {

View File

@@ -17,7 +17,7 @@ type Props = {
}
type ContainerProps = Pick<Props, 'maxHeight'> & TableCellProps
const Container = styled(SectionedTableCellTouchableHighlight).attrs(props => ({
const Container = styled(SectionedTableCellTouchableHighlight).attrs((props) => ({
underlayColor: props.theme.stylekitBorderColor,
}))<ContainerProps>`
padding-top: ${12}px;
@@ -32,9 +32,9 @@ const ButtonContainer = styled.View``
type ButtonLabelProps = Pick<Props, 'leftAligned' | 'bold' | 'disabled' | 'important'>
const ButtonLabel = styled.Text<ButtonLabelProps>`
text-align: ${props => (props.leftAligned ? 'left' : 'center')};
text-align: ${(props) => (props.leftAligned ? 'left' : 'center')};
text-align-vertical: center;
color: ${props => {
color: ${(props) => {
let color = Platform.OS === 'android' ? props.theme.stylekitForegroundColor : props.theme.stylekitInfoColor
if (props.disabled) {
color = 'gray'
@@ -43,7 +43,7 @@ const ButtonLabel = styled.Text<ButtonLabelProps>`
}
return color
}};
font-size: ${props => props.theme.mainTextFontSize}px;
font-size: ${(props) => props.theme.mainTextFontSize}px;
${({ bold }) =>
bold &&
css`
@@ -56,7 +56,7 @@ const ButtonLabel = styled.Text<ButtonLabelProps>`
`}
`
export const ButtonCell: React.FC<Props> = props => (
export const ButtonCell: React.FC<Props> = (props) => (
<Container
first={props.first}
last={props.last}

View File

@@ -7,10 +7,10 @@ type Props = {
}
export const Circle = styled.View<Props>`
width: ${props => props.size ?? 12}px;
height: ${props => props.size ?? 12}px;
border-radius: ${props => (props.size ?? 12) / 2}px;
background-color: ${props => props.backgroundColor};
border-color: ${props => props.borderColor};
width: ${(props) => props.size ?? 12}px;
height: ${(props) => props.size ?? 12}px;
border-radius: ${(props) => (props.size ?? 12) / 2}px;
background-color: ${(props) => props.backgroundColor};
border-color: ${(props) => props.borderColor};
border-width: 1px;
`

View File

@@ -9,14 +9,14 @@ type Props = {
}
const Container = styled.View`
/* background-color: ${props => props.theme.stylekitContrastBackgroundColor}; */
/* background-color: ${(props) => props.theme.stylekitContrastBackgroundColor}; */
flex: 1;
justify-content: center;
${Platform.OS === 'android' && 'align-items: flex-start; min-width: 100px;'}
`
const Title = styled.Text`
color: ${props => props.theme.stylekitForegroundColor};
color: ${(props) => props.theme.stylekitForegroundColor};
font-weight: bold;
font-size: 18px;
text-align: center;
@@ -25,13 +25,13 @@ const SubTitle = styled.Text.attrs(() => ({
adjustsFontSizeToFit: true,
numberOfLines: 1,
}))<{ color?: string }>`
color: ${props => props.color ?? props.theme.stylekitForegroundColor};
opacity: ${props => (props.color ? 1 : 0.6)};
color: ${(props) => props.color ?? props.theme.stylekitForegroundColor};
opacity: ${(props) => (props.color ? 1 : 0.6)};
font-size: ${Platform.OS === 'android' ? 13 : 12}px;
${Platform.OS === 'ios' && 'text-align: center'}
`
export const HeaderTitleView: React.FC<Props> = props => (
export const HeaderTitleView: React.FC<Props> = (props) => (
<Container>
<Title>{props.title}</Title>
{props.subtitle && props.subtitle.length > 0 ? (

View File

@@ -17,19 +17,19 @@ const Container = styled.View<Pick<Props, 'backgroundColor'>>`
/* flex-grow: 0; */
justify-content: space-between;
flex-direction: row;
padding-right: ${props => props.theme.paddingLeft}px;
padding-right: ${(props) => props.theme.paddingLeft}px;
padding-bottom: 10px;
padding-top: 10px;
background-color: ${props => props.backgroundColor ?? props.theme.stylekitBackgroundColor};
background-color: ${(props) => props.backgroundColor ?? props.theme.stylekitBackgroundColor};
`
const TitleContainer = styled.View``
const Title = styled.Text<Pick<Props, 'tinted'>>`
background-color: ${props => props.theme.stylekitBackgroundColor};
font-size: ${props => {
background-color: ${(props) => props.theme.stylekitBackgroundColor};
font-size: ${(props) => {
return Platform.OS === 'android' ? props.theme.mainTextFontSize - 2 : props.theme.mainTextFontSize - 4
}}px;
padding-left: ${props => props.theme.paddingLeft}px;
color: ${props => {
padding-left: ${(props) => props.theme.paddingLeft}px;
color: ${(props) => {
if (props.tinted) {
return props.theme.stylekitInfoColor
}
@@ -39,11 +39,11 @@ const Title = styled.Text<Pick<Props, 'tinted'>>`
font-weight: ${Platform.OS === 'android' ? 'bold' : 'normal'};
`
const SubTitle = styled.Text`
background-color: ${props => props.theme.stylekitBackgroundColor};
font-size: ${props => props.theme.mainTextFontSize - 5}px;
background-color: ${(props) => props.theme.stylekitBackgroundColor};
font-size: ${(props) => props.theme.mainTextFontSize - 5}px;
margin-top: 4px;
padding-left: ${props => props.theme.paddingLeft}px;
color: ${props => props.theme.stylekitNeutralColor};
padding-left: ${(props) => props.theme.paddingLeft}px;
color: ${(props) => props.theme.stylekitNeutralColor};
`
const ButtonContainer = styled.TouchableOpacity`
flex: 1;
@@ -51,10 +51,10 @@ const ButtonContainer = styled.TouchableOpacity`
justify-content: center;
`
const Button = styled.Text`
color: ${props => props.theme.stylekitInfoColor};
color: ${(props) => props.theme.stylekitInfoColor};
`
export const SectionHeader: React.FC<Props> = props => (
export const SectionHeader: React.FC<Props> = (props) => (
<Container>
<TitleContainer>
{!!props.title && (

View File

@@ -22,7 +22,7 @@ type Props = {
last?: boolean
}
const TouchableContainer = styled(SectionedTableCellTouchableHighlight).attrs(props => ({
const TouchableContainer = styled(SectionedTableCellTouchableHighlight).attrs((props) => ({
underlayColor: props.theme.stylekitBorderColor,
}))`
flex-direction: column;
@@ -33,7 +33,7 @@ const TouchableContainer = styled(SectionedTableCellTouchableHighlight).attrs(pr
`
const ContentContainer = styled.View<Pick<Props, 'leftAlignIcon'>>`
flex: 1;
justify-content: ${props => {
justify-content: ${(props) => {
return props.leftAlignIcon ? 'flex-start' : 'space-between'
}};
flex-direction: row;
@@ -46,7 +46,7 @@ const IconContainer = styled.View`
type LabelProps = Pick<Props, 'bold' | 'tinted' | 'dimmed' | 'selected' | 'color'>
const Label = styled.Text<LabelProps>`
min-width: 80%;
color: ${props => {
color: ${(props) => {
let color = props.theme.stylekitForegroundColor
if (props.tinted) {
color = props.theme.stylekitInfoColor
@@ -59,7 +59,7 @@ const Label = styled.Text<LabelProps>`
}
return color
}};
font-size: ${props => props.theme.mainTextFontSize}px;
font-size: ${(props) => props.theme.mainTextFontSize}px;
${({ bold, selected }) =>
((selected && selected() === true) || bold) &&
css`
@@ -67,7 +67,7 @@ const Label = styled.Text<LabelProps>`
`};
`
export const SectionedAccessoryTableCell: React.FC<Props> = props => {
export const SectionedAccessoryTableCell: React.FC<Props> = (props) => {
const themeContext = useContext(ThemeContext)
const onPress = () => {
if (props.disabled) {

View File

@@ -15,11 +15,11 @@ type Props = {
type ContainerProps = Omit<Props, 'title' | 'onPress' | 'options'>
export const Container = styled.View<ContainerProps>`
border-bottom-color: ${props => props.theme.stylekitBorderColor};
border-bottom-color: ${(props) => props.theme.stylekitBorderColor};
border-bottom-width: 1px;
padding-left: ${props => props.theme.paddingLeft}px;
padding-right: ${props => props.theme.paddingLeft}px;
background-color: ${props => props.theme.stylekitBackgroundColor};
padding-left: ${(props) => props.theme.paddingLeft}px;
padding-right: ${(props) => props.theme.paddingLeft}px;
background-color: ${(props) => props.theme.stylekitBackgroundColor};
${({ first, theme }) =>
first &&
css`
@@ -38,9 +38,9 @@ export const Container = styled.View<ContainerProps>`
`
const Title = styled.Text<{ leftAligned?: boolean }>`
font-size: ${props => props.theme.mainTextFontSize}px;
color: ${props => props.theme.stylekitForegroundColor};
text-align: ${props => (props.leftAligned ? 'left' : 'center')};
font-size: ${(props) => props.theme.mainTextFontSize}px;
color: ${(props) => props.theme.stylekitForegroundColor};
text-align: ${(props) => (props.leftAligned ? 'left' : 'center')};
width: 42%;
min-width: 0px;
`
@@ -50,13 +50,13 @@ const OptionsContainer = styled.View`
flex-direction: row;
align-items: center;
justify-content: center;
background-color: ${props => props.theme.stylekitBackgroundColor};
background-color: ${(props) => props.theme.stylekitBackgroundColor};
`
const ButtonTouchable = styled.TouchableHighlight.attrs(props => ({
const ButtonTouchable = styled.TouchableHighlight.attrs((props) => ({
underlayColor: props.theme.stylekitBorderColor,
}))`
border-left-color: ${props => props.theme.stylekitBorderColor};
border-left-color: ${(props) => props.theme.stylekitBorderColor};
border-left-width: 1px;
flex-grow: 1;
padding: 10px;
@@ -64,7 +64,7 @@ const ButtonTouchable = styled.TouchableHighlight.attrs(props => ({
`
const ButtonTitle = styled.Text<{ selected: boolean }>`
color: ${props => {
color: ${(props) => {
return props.selected ? props.theme.stylekitInfoColor : props.theme.stylekitNeutralColor
}};
font-size: 16px;
@@ -72,11 +72,11 @@ const ButtonTitle = styled.Text<{ selected: boolean }>`
width: 100%;
`
export const SectionedOptionsTableCell: React.FC<Props> = props => (
export const SectionedOptionsTableCell: React.FC<Props> = (props) => (
<Container first={props.first}>
<Title leftAligned={props.leftAligned}>{props.title}</Title>
<OptionsContainer>
{props.options.map(option => {
{props.options.map((option) => {
return (
<ButtonTouchable key={option.title} onPress={() => props.onPress(option)}>
<ButtonTitle selected={option.selected}>{option.title}</ButtonTitle>

View File

@@ -9,12 +9,12 @@ export type Props = {
}
export const SectionedTableCell = styled.View<Props>`
border-bottom-color: ${props => props.theme.stylekitBorderColor};
border-bottom-color: ${(props) => props.theme.stylekitBorderColor};
border-bottom-width: 1px;
padding-left: ${props => props.theme.paddingLeft}px;
padding-right: ${props => props.theme.paddingLeft}px;
padding-bottom: ${props => (props.textInputCell ? 0 : 12)}px;
background-color: ${props => props.theme.stylekitBackgroundColor};
padding-left: ${(props) => props.theme.paddingLeft}px;
padding-right: ${(props) => props.theme.paddingLeft}px;
padding-bottom: ${(props) => (props.textInputCell ? 0 : 12)}px;
background-color: ${(props) => props.theme.stylekitBackgroundColor};
${({ first, theme }) =>
first &&
css`
@@ -34,12 +34,12 @@ export const SectionedTableCell = styled.View<Props>`
`
export const SectionedTableCellTouchableHighlight = styled.TouchableHighlight<Props>`
border-bottom-color: ${props => props.theme.stylekitBorderColor};
border-bottom-color: ${(props) => props.theme.stylekitBorderColor};
border-bottom-width: 1px;
padding-left: ${props => props.theme.paddingLeft}px;
padding-right: ${props => props.theme.paddingLeft}px;
padding-bottom: ${props => (props.textInputCell ? 0 : 12)}px;
background-color: ${props => props.theme.stylekitBackgroundColor};
padding-left: ${(props) => props.theme.paddingLeft}px;
padding-right: ${(props) => props.theme.paddingLeft}px;
padding-bottom: ${(props) => (props.textInputCell ? 0 : 12)}px;
background-color: ${(props) => props.theme.stylekitBackgroundColor};
${({ first, theme }) =>
first &&
css`

View File

@@ -3,5 +3,5 @@ import styled from 'styled-components/native'
export const TableSection = styled.View`
margin-top: 10px;
margin-bottom: 10px;
background-color: ${props => props.theme.stylekitBackgroundColor};
background-color: ${(props) => props.theme.stylekitBackgroundColor};
`

View File

@@ -12,7 +12,7 @@ export const ToastWrapper: FC = () => {
const { updateProgressBar, progressBarWidth } = useProgressBar()
const toastStyles: ToastConfig = {
info: props => {
info: (props) => {
const percentComplete = props.props?.percentComplete || 0
updateProgressBar(percentComplete)
@@ -35,13 +35,13 @@ export const ToastWrapper: FC = () => {
</View>
)
},
success: props => {
success: (props) => {
const percentComplete = props.props?.percentComplete || 0
updateProgressBar(percentComplete)
return <SuccessToast {...props} style={styles(props.props).success} />
},
error: props => {
error: (props) => {
const percentComplete = props.props?.percentComplete || 0
updateProgressBar(percentComplete)

View File

@@ -560,7 +560,7 @@ export const useFiles = ({ note }: Props) => {
return
}
if (shouldAttachToNote(currentTab)) {
uploadedFiles.forEach(file => attachFileToNote(file, false))
uploadedFiles.forEach((file) => attachFileToNote(file, false))
}
},
},
@@ -587,7 +587,7 @@ export const useFiles = ({ note }: Props) => {
},
},
]
const osSpecificOptions = Platform.OS === 'android' ? options.filter(option => option.key !== 'library') : options
const osSpecificOptions = Platform.OS === 'android' ? options.filter((option) => option.key !== 'library') : options
showActionSheet({
title: 'Choose action',
options: osSpecificOptions,
@@ -783,7 +783,7 @@ export const useFiles = ({ note }: Props) => {
},
]
const osDependentActions =
Platform.OS === 'ios' ? actions.filter(action => action.text !== 'Download') : [...actions]
Platform.OS === 'ios' ? actions.filter((action) => action.text !== 'Download') : [...actions]
showActionSheet({
title: file.name,
options: osDependentActions,

View File

@@ -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
}

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)
},

View File

@@ -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<void>
}
[SCREEN_INPUT_MODAL_PASSCODE]: undefined
[SCREEN_AUTHENTICATE]: {
challenge: Challenge
@@ -275,6 +281,28 @@ export const MainStackComponent = ({ env }: { env: TEnvironment }) => {
})}
component={BlockingModal}
/>
<MainStack.Screen
name={SCREEN_INPUT_MODAL_WORKSPACE_NAME}
options={({ route }) => ({
title: 'Workspace',
gestureEnabled: false,
headerTitle: ({ children }) => {
return <HeaderTitleView title={route.params?.title ?? (children || '')} />
},
headerLeft: ({ disabled, onPress }) => (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
disabled={disabled}
title={Platform.OS === 'ios' ? 'Cancel' : ''}
iconName={Platform.OS === 'ios' ? undefined : ThemeService.nameForIcon(ICON_CLOSE)}
onPress={onPress}
/>
</HeaderButtons>
),
})}
component={WorkspaceInputModal}
/>
</MainStack.Navigator>
)
}

View File

@@ -71,7 +71,7 @@ export const Authenticate = ({
const [passcodeKeyboardType, setPasscodeKeyboardType] = useState<PasscodeKeyboardType | undefined>(
PasscodeKeyboardType.Default,
)
const [singleValidation] = useState(() => !(challenge.prompts.filter(prompt => prompt.validates).length > 0))
const [singleValidation] = useState(() => !(challenge.prompts.filter((prompt) => prompt.validates).length > 0))
const [showSwitchKeyboard, setShowSwitchKeyboard] = useState<boolean>(false)
const [{ challengeValues, challengeValueStates }, dispatch] = useReducer(
@@ -217,7 +217,7 @@ export const Authenticate = ({
onValueChange(newChallengeValue)
return validateChallengeValue(newChallengeValue)
})
.catch(error => {
.catch((error) => {
FingerprintScanner.release()
if (error.name === 'DeviceLocked') {
onValueLocked(challengeValue)
@@ -250,7 +250,7 @@ export const Authenticate = ({
onValueChange(newChallengeValue)
return validateChallengeValue(newChallengeValue)
})
.catch(error_1 => {
.catch((error_1) => {
onValueChange({ ...challengeValue, value: false })
FingerprintScanner.release()
if (error_1.name !== 'SystemCancel') {
@@ -340,7 +340,7 @@ export const Authenticate = ({
)
useEffect(() => {
const remove = application?.getAppState().addStateChangeObserver(state => {
const remove = application?.getAppState().addStateChangeObserver((state) => {
if (state === AppStateType.ResumingFromBackground) {
if (!isAuthenticating.current) {
beginAuthenticatingForNextChallengeReason()
@@ -440,7 +440,7 @@ export const Authenticate = ({
Keyboard.dismiss()
const biometricChallengeValue = Object.values(challengeValues).find(
value => value.prompt.validation === ChallengeValidation.Biometric,
(value) => value.prompt.validation === ChallengeValidation.Biometric,
)
const state = challengeValueStates[biometricChallengeValue?.prompt.id as number]
if (state === AuthenticationValueStateType.Locked || state === AuthenticationValueStateType.Success) {
@@ -506,8 +506,8 @@ export const Authenticate = ({
const readyToSubmit = useMemo(
() =>
Object.values(challengeValues)
.map(challengeValue => challengeValue.value)
.filter(value => !value).length === 0,
.map((challengeValue) => challengeValue.value)
.filter((value) => !value).length === 0,
[challengeValues],
)
@@ -556,7 +556,7 @@ export const Authenticate = ({
key={Platform.OS === 'android' ? keyboardType : undefined}
ref={Array.of(firstInputRef, secondInputRef, thirdInputRef, fourthInputRef)[index] as any}
placeholder={challengeValue.prompt.placeholder}
onChangeText={text => {
onChangeText={(text) => {
onValueChange({ ...challengeValue, value: text })
}}
value={(challengeValue.value || '') as string}
@@ -621,7 +621,7 @@ export const Authenticate = ({
}
const isPending = useMemo(
() => Object.values(challengeValueStates).findIndex(state => state === AuthenticationValueStateType.Pending) >= 0,
() => Object.values(challengeValueStates).findIndex((state) => state === AuthenticationValueStateType.Pending) >= 0,
[challengeValueStates],
)
@@ -644,7 +644,7 @@ export const Authenticate = ({
return (
<HeaderHeightContext.Consumer>
{headerHeight => (
{(headerHeight) => (
<StyledKeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={headerHeight}

View File

@@ -220,7 +220,7 @@ export const ComponentView = ({
onLoadStart()
}
const onShouldStartLoadWithRequest: OnShouldStartLoadWithRequest = request => {
const onShouldStartLoadWithRequest: OnShouldStartLoadWithRequest = (request) => {
log('Setting last iframe URL to', request.url)
/** The first request can typically be 'about:blank', which we want to ignore */
if (!didLoadRootUrl.current) {

View File

@@ -88,7 +88,7 @@ export class Compose extends React.Component<PropsWhenNavigating | PropsWhenRend
this.context = context
const noteUuid = 'noteUuid' in props ? props.noteUuid : props.route.params.noteUuid
const editor = this.context.editorGroup.itemControllers.find(c => c.item.uuid === noteUuid) as NoteViewController
const editor = this.context.editorGroup.itemControllers.find((c) => c.item.uuid === noteUuid) as NoteViewController
if (!editor) {
throw 'Unable to to find note controller'
}
@@ -149,7 +149,7 @@ export class Compose extends React.Component<PropsWhenNavigating | PropsWhenRend
void this.reloadComponentEditorState()
})
this.removeAppEventObserver = this.context?.addEventObserver(async eventName => {
this.removeAppEventObserver = this.context?.addEventObserver(async (eventName) => {
if (eventName === ApplicationEvent.CompletedFullSync) {
/** if we're still dirty, don't change status, a sync is likely upcoming. */
if (!this.note.dirty && this.state.saveError) {
@@ -169,7 +169,7 @@ export class Compose extends React.Component<PropsWhenNavigating | PropsWhenRend
}
})
this.removeStateEventObserver = this.context?.getAppState().addStateEventObserver(state => {
this.removeStateEventObserver = this.context?.getAppState().addStateEventObserver((state) => {
if (state === AppStateEventType.DrawerOpen) {
this.dismissKeyboard()
/**
@@ -352,7 +352,7 @@ export class Compose extends React.Component<PropsWhenNavigating | PropsWhenRend
await this.context.mutator.changeItem(
this.note,
mutator => {
(mutator) => {
const noteMutator = mutator as NoteMutator
if (newTitle != null) {
@@ -483,7 +483,7 @@ export class Compose extends React.Component<PropsWhenNavigating | PropsWhenRend
return (
<Container>
<ThemeContext.Consumer>
{theme => (
{(theme) => (
<>
{this.noteLocked && (
<LockedContainer>
@@ -507,7 +507,7 @@ export class Compose extends React.Component<PropsWhenNavigating | PropsWhenRend
</LockedContainer>
)}
<ThemeServiceContext.Consumer>
{themeService => (
{(themeService) => (
<>
<NoteTitleInput
testID="noteTitleField"

View File

@@ -11,7 +11,7 @@ import { Container, Input } from './InputModal.styled'
type Props = ModalStackNavigationProp<typeof SCREEN_INPUT_MODAL_FILE_NAME>
export const FileInputModal: FC<Props> = props => {
export const FileInputModal: FC<Props> = (props) => {
const { file, renameFile } = props.route.params
const themeService = useContext(ThemeServiceContext)
const application = useSafeApplicationContext()

View File

@@ -41,7 +41,7 @@ export const TagInputModal = (props: Props) => {
const onSubmit = useCallback(async () => {
if (props.route.params.tagUuid) {
const tag = application?.items.findItem(props.route.params.tagUuid) as SNTag
await application?.mutator.changeItem(tag, mutator => {
await application?.mutator.changeItem(tag, (mutator) => {
const tagMutator = mutator as TagMutator
tagMutator.title = text
if (props.route.params.noteUuid) {
@@ -54,7 +54,7 @@ export const TagInputModal = (props: Props) => {
} else {
const tag = await application!.mutator.findOrCreateTag(text)
if (props.route.params.noteUuid) {
await application?.mutator.changeItem(tag, mutator => {
await application?.mutator.changeItem(tag, (mutator) => {
const tagMutator = mutator as TagMutator
const note = application.items.findItem(props.route.params.noteUuid!)
if (note) {

View File

@@ -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<typeof SCREEN_INPUT_MODAL_WORKSPACE_NAME>
export const WorkspaceInputModal: FC<Props> = (props) => {
const { descriptor, renameWorkspace } = props.route.params
const themeService = useContext(ThemeServiceContext)
const application = useSafeApplicationContext()
const workspaceNameInputRef = useRef<TextInput>(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 (
<Container>
<TableSection>
<SectionedTableCell textInputCell first={true}>
<Input
ref={workspaceNameInputRef as any}
placeholder={'Workspace name'}
onChangeText={setWorkspaceName}
value={workspaceName}
autoCorrect={false}
autoCapitalize={'none'}
keyboardAppearance={themeService?.keyboardColorForActiveTheme()}
underlineColorAndroid={'transparent'}
onSubmitEditing={onSubmit}
/>
</SectionedTableCell>
<ButtonCell maxHeight={45} disabled={workspaceName.length === 0} title={'Save'} bold onPress={onSubmit} />
</TableSection>
</Container>
)
}

View File

@@ -64,7 +64,7 @@ const useSessions = (): [
setErrorMessage('An unknown error occurred while revoking the session.')
}
} else {
setSessions(sessions.filter(session => session.uuid !== uuid))
setSessions(sessions.filter((session) => session.uuid !== uuid))
}
}
@@ -136,7 +136,7 @@ export const ManageSessions: React.FC = () => {
return (
<FlatList<RemoteSession>
keyExtractor={item => item.uuid}
keyExtractor={(item) => item.uuid}
contentContainerStyle={{ paddingBottom: insets.bottom }}
initialNumToRender={7}
windowSize={7}

View File

@@ -11,7 +11,7 @@ type Props = {
currentSession: boolean
}
const Container = styled(SectionedTableCellTouchableHighlight).attrs(props => ({
const Container = styled(SectionedTableCellTouchableHighlight).attrs((props) => ({
underlayColor: props.theme.stylekitBorderColor,
}))<TableCellProps>`
padding-top: ${12}px;
@@ -21,7 +21,7 @@ const ButtonContainer = styled.View``
type ButtonLabelProps = Pick<Props, 'disabled'>
const ButtonLabel = styled.Text<ButtonLabelProps>`
color: ${props => {
color: ${(props) => {
let color = props.theme.stylekitForegroundColor
if (props.disabled) {
color = 'gray'
@@ -29,7 +29,7 @@ const ButtonLabel = styled.Text<ButtonLabelProps>`
return color
}};
font-weight: bold;
font-size: ${props => props.theme.mainTextFontSize}px;
font-size: ${(props) => props.theme.mainTextFontSize}px;
${({ disabled }) =>
disabled &&
css`
@@ -46,7 +46,7 @@ export const SubTitleText = styled.Text<{ current: boolean }>`
line-height: 21px;
`
export const SessionCell: React.FC<Props> = props => (
export const SessionCell: React.FC<Props> = (props) => (
<Container testID={props.testID} disabled={props.disabled} onPress={props.onPress}>
<ButtonContainer>
<ButtonLabel disabled={props.disabled}>{props.title}</ButtonLabel>

View File

@@ -61,9 +61,9 @@ export const NoteHistory = (props: Props) => {
fontStyle={{
color: theme.stylekitForegroundColor,
}}
values={routes.map(route => route.title)}
values={routes.map((route) => route.title)}
selectedIndex={tabBarProps.navigationState.index}
onChange={event => {
onChange={(event) => {
setIndex(event.nativeEvent.selectedSegmentIndex)
}}
/>

View File

@@ -12,7 +12,7 @@ type Props = {
subTitle?: string
}
const Container = styled(SectionedTableCellTouchableHighlight).attrs(props => ({
const Container = styled(SectionedTableCellTouchableHighlight).attrs((props) => ({
underlayColor: props.theme.stylekitBorderColor,
}))<TableCellProps>`
padding-top: ${12}px;
@@ -22,7 +22,7 @@ const ButtonContainer = styled.View``
type ButtonLabelProps = Pick<Props, 'disabled'>
const ButtonLabel = styled.Text<ButtonLabelProps>`
color: ${props => {
color: ${(props) => {
let color = props.theme.stylekitForegroundColor
if (props.disabled) {
color = 'gray'
@@ -30,7 +30,7 @@ const ButtonLabel = styled.Text<ButtonLabelProps>`
return color
}};
font-weight: bold;
font-size: ${props => props.theme.mainTextFontSize}px;
font-size: ${(props) => props.theme.mainTextFontSize}px;
${({ disabled }) =>
disabled &&
css`
@@ -45,7 +45,7 @@ export const SubTitleText = styled.Text`
line-height: 21px;
`
export const NoteHistoryCell: React.FC<Props> = props => (
export const NoteHistoryCell: React.FC<Props> = (props) => (
<Container
first={props.first}
last={props.last}

View File

@@ -43,7 +43,7 @@ export const NoteHistoryPreview = ({
} else {
await application?.mutator.changeAndSaveItem(
originalNote,
mutator => {
(mutator) => {
mutator.setCustomContent(revision.payload.content)
},
true,

View File

@@ -70,7 +70,7 @@ export const RemoteHistory: React.FC<Props> = ({ note, onPress }) => {
return (
<FlatList<RevisionListEntry>
keyExtractor={item => item.uuid}
keyExtractor={(item) => item.uuid}
contentContainerStyle={{ paddingBottom: insets.bottom }}
initialNumToRender={10}
windowSize={10}

View File

@@ -42,7 +42,7 @@ export const SessionHistory: React.FC<Props> = ({ note, onPress }) => {
return (
<FlatList<NoteHistoryEntry>
keyExtractor={item => item.previewTitle()}
keyExtractor={(item) => item.previewTitle()}
contentContainerStyle={{ paddingBottom: insets.bottom }}
initialNumToRender={10}
windowSize={10}

View File

@@ -8,7 +8,7 @@ export const Container = styled.View<{ selected: boolean; distance: number }>`
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
padding: ${props => props.distance}px 0 0 ${props => props.distance}px;
padding: ${(props) => props.distance}px 0 0 ${(props) => props.distance}px;
background-color: ${({ theme, selected }) => {
return selected ? theme.stylekitInfoColor : theme.stylekitBackgroundColor
}};
@@ -16,10 +16,10 @@ export const Container = styled.View<{ selected: boolean; distance: number }>`
export const NoteDataContainer = styled.View<{ distance: number }>`
border-bottom-color: ${({ theme }) => hexToRGBA(theme.stylekitBorderColor, 0.75)};
border-bottom-width: 1px;
padding-bottom: ${props => props.distance}px;
padding-bottom: ${(props) => props.distance}px;
flex-grow: 1;
flex-shrink: 1;
padding-right: ${props => props.distance}px;
padding-right: ${(props) => props.distance}px;
`
export const DeletedText = styled.Text`
color: ${({ theme }) => theme.stylekitInfoColor};
@@ -54,7 +54,7 @@ export const TagText = styled.Text<{ selected: boolean }>`
color: ${({ theme, selected }) => {
return selected ? theme.stylekitInfoContrastColor : theme.stylekitForegroundColor
}};
opacity: ${props => (props.selected ? 0.8 : 0.5)};
opacity: ${(props) => (props.selected ? 0.8 : 0.5)};
`
export const DetailsText = styled(TagText)`
margin-right: 0;

View File

@@ -62,7 +62,7 @@ export const NoteCell = ({
await application?.mutator.deleteItem(note)
},
() => {
void changeNote(mutator => {
void changeNote((mutator) => {
mutator.trashed = true
}, false)
},
@@ -105,7 +105,7 @@ export const NoteCell = ({
text: note.pinned ? 'Unpin' : 'Pin',
key: 'pin',
callback: () =>
changeNote(mutator => {
changeNote((mutator) => {
mutator.pinned = !note.pinned
}, false),
})
@@ -123,7 +123,7 @@ export const NoteCell = ({
return
}
void changeNote(mutator => {
void changeNote((mutator) => {
mutator.archived = !note.archived
}, false)
},
@@ -133,7 +133,7 @@ export const NoteCell = ({
text: note.locked ? 'Enable editing' : 'Prevent editing',
key: 'lock',
callback: () =>
changeNote(mutator => {
changeNote((mutator) => {
mutator.locked = !note.locked
}, false),
})
@@ -157,7 +157,7 @@ export const NoteCell = ({
text: 'Restore',
key: 'restore-note',
callback: () => {
void changeNote(mutator => {
void changeNote((mutator) => {
mutator.trashed = false
}, false)
},

View File

@@ -44,7 +44,7 @@ export const NoteCellFlags = ({ note, highlight }: { note: SNNote; highlight: bo
return flags.length > 0 ? (
<FlagsContainer>
{flags.map(flag => (
{flags.map((flag) => (
<FlagContainer key={flag.text.concat(flag.color)} color={flag.color} selected={highlight}>
<FlagLabel selected={highlight}>{flag.text}</FlagLabel>
</FlagContainer>

View File

@@ -15,7 +15,7 @@ export const styles = StyleSheet.create({
})
export const Container = styled.View`
background-color: ${props => props.theme.stylekitBackgroundColor};
background-color: ${(props) => props.theme.stylekitBackgroundColor};
flex: 1;
`
@@ -32,8 +32,8 @@ interface LoadingTextProps {
export const LoadingText = styled.Text<LoadingTextProps>`
position: absolute;
opacity: 0.5;
color: ${props => props.theme.stylekitForegroundColor};
text-align: ${props => props.textAlign ?? 'left'};
color: ${(props) => props.theme.stylekitForegroundColor};
text-align: ${(props) => props.textAlign ?? 'left'};
`
export const HeaderContainer = styled.View`
@@ -43,7 +43,7 @@ export const HeaderContainer = styled.View`
`
export const SearchBarContainer = styled.View`
background-color: ${props => props.theme.stylekitBackgroundColor};
background-color: ${(props) => props.theme.stylekitBackgroundColor};
z-index: 2;
`

View File

@@ -82,7 +82,7 @@ export const NoteList = (props: Props) => {
})
useEffect(() => {
const unsubscribeStateEventObserver = application?.getAppState().addStateEventObserver(state => {
const unsubscribeStateEventObserver = application?.getAppState().addStateEventObserver((state) => {
if (state === AppStateEventType.DrawerOpen) {
dismissKeyboard()
}
@@ -99,7 +99,7 @@ export const NoteList = (props: Props) => {
}, [noteListScrolled, props.notes])
useEffect(() => {
const unsubscribeTagChangedEventObserver = application?.getAppState().addStateChangeObserver(event => {
const unsubscribeTagChangedEventObserver = application?.getAppState().addStateChangeObserver((event) => {
if (event === AppStateType.TagChanged) {
scrollListToTop()
}
@@ -223,7 +223,7 @@ export const NoteList = (props: Props) => {
<FlatList
ref={noteListRef}
style={styles.list}
keyExtractor={item => item?.uuid}
keyExtractor={(item) => item?.uuid}
contentContainerStyle={[{ paddingBottom: insets.bottom }, props.notes.length > 0 ? {} : { height: '100%' }]}
initialNumToRender={6}
windowSize={6}

View File

@@ -83,7 +83,7 @@ export const Notes = React.memo(
title = selectedTag.title
if (selectedTag instanceof SNTag && selectedTag.parentId) {
const parents = application.items.getTagParentChain(selectedTag)
const hierarchy = parents.map(tag => tag.title).join(' ⫽ ')
const hierarchy = parents.map((tag) => tag.title).join(' ⫽ ')
subTitle = hierarchy.length > 0 ? `in ${hierarchy}` : undefined
}
}
@@ -163,7 +163,7 @@ export const Notes = React.memo(
useEffect(() => {
let mounted = true
const removeEditorObserver = application.editorGroup.addActiveControllerChangeObserver(activeEditor => {
const removeEditorObserver = application.editorGroup.addActiveControllerChangeObserver((activeEditor) => {
if (mounted) {
setSelectedNoteId(activeEditor?.item?.uuid)
}
@@ -305,7 +305,7 @@ export const Notes = React.memo(
toggleIncludeTrashed,
])
const getFirstSelectableNote = useCallback((newNotes: SNNote[]) => newNotes.find(note => !note.protected), [])
const getFirstSelectableNote = useCallback((newNotes: SNNote[]) => newNotes.find((note) => !note.protected), [])
const selectFirstNote = useCallback(
(newNotes: SNNote[]) => {
@@ -475,7 +475,7 @@ export const Notes = React.memo(
)
useEffect(() => {
const removeAppStateChangeHandler = application.getAppState().addStateChangeObserver(state => {
const removeAppStateChangeHandler = application.getAppState().addStateChangeObserver((state) => {
if (state === AppStateType.TagChanged) {
reloadNotesDisplayOptions()
reloadNotes(true, true)

View File

@@ -11,17 +11,17 @@ const Container = styled.View`
padding: ${PADDING}px;
border-width: 1px;
border-radius: 4px;
border-color: ${props => props.theme.stylekitBorderColor};
border-color: ${(props) => props.theme.stylekitBorderColor};
`
const CenterContainer = styled.View`
justify-content: center;
`
const UserIcon = styled(Icon)`
font-size: 24px;
color: ${props => props.theme.stylekitInfoColor};
color: ${(props) => props.theme.stylekitInfoColor};
`
const ForwardIcon = styled(UserIcon)`
color: ${props => props.theme.stylekitNeutralColor};
color: ${(props) => props.theme.stylekitNeutralColor};
`
const TextContainer = styled.View`
flex: 1;
@@ -30,12 +30,12 @@ const TextContainer = styled.View`
const BoldText = styled.Text`
font-size: 15px;
font-weight: 600;
color: ${props => props.theme.stylekitForegroundColor};
color: ${(props) => props.theme.stylekitForegroundColor};
`
const SubText = styled.Text`
margin-top: 2px;
font-size: 11px;
color: ${props => props.theme.stylekitNeutralColor};
color: ${(props) => props.theme.stylekitNeutralColor};
`
export { Touchable, Container, CenterContainer, UserIcon, ForwardIcon, TextContainer, BoldText, SubText }

View File

@@ -26,7 +26,7 @@ export const Root = () => {
const [keyboardHeight, setKeyboardHeight] = useState<number | undefined>(undefined)
useEffect(() => {
const removeStateObserver = application?.getAppState().addStateChangeObserver(state => {
const removeStateObserver = application?.getAppState().addStateChangeObserver((state) => {
if (state === AppStateType.GainingFocus) {
void application.sync.sync()
}
@@ -50,7 +50,7 @@ export const Root = () => {
}
}
})
const removeNoteObserver = application?.editorGroup.addActiveControllerChangeObserver(activeController => {
const removeNoteObserver = application?.editorGroup.addActiveControllerChangeObserver((activeController) => {
if (activeController instanceof NoteViewController) {
setActiveNoteView(activeController)
} else {
@@ -100,7 +100,7 @@ export const Root = () => {
}
const toggleNoteList = () => {
setNoteListCollapsed(value => !value)
setNoteListCollapsed((value) => !value)
}
const collapseIconBottomPosition = (keyboardHeight ?? 0) > (height ?? 0) / 2 ? (keyboardHeight ?? 0) + 40 : '50%'

View File

@@ -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}
/>

View File

@@ -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 (

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -54,7 +54,7 @@ export const Files: FC<Props> = ({ note }) => {
return (
<FilesContainer>
{attachedFiles.sort(filesService.sortByName).map(file => {
{attachedFiles.sort(filesService.sortByName).map((file) => {
const iconType = application.iconsController.getIconForFileType(file.mimeType)
return (

View File

@@ -102,7 +102,7 @@ export const Listed: FC<TProps> = ({ note }) => {
showActionSheet({
title: item.display_name,
options: item.actions.map(action => ({
options: item.actions.map((action) => ({
text: (action as Action).label,
callback: async () => {
setIsActionInProgress(true)

View File

@@ -36,7 +36,7 @@ export const MainSideMenu = React.memo(({ drawerRef }: Props) => {
const styles = useStyles(theme)
useEffect(() => {
const removeTagChangeObserver = application!.getAppState().addStateChangeObserver(state => {
const removeTagChangeObserver = application!.getAppState().addStateChangeObserver((state) => {
if (state === AppStateType.TagChanged) {
setSelectedTag(application!.getAppState().getSelectedTag())
}
@@ -160,7 +160,7 @@ export const MainSideMenu = React.memo(({ drawerRef }: Props) => {
const themeOptions = useMemo(() => {
const options: SideMenuOption[] = themeService!
.systemThemes()
.map(systemTheme => ({
.map((systemTheme) => ({
text: systemTheme?.name,
key: systemTheme?.uuid,
iconDesc: iconDescriptorForTheme(systemTheme),
@@ -172,7 +172,7 @@ export const MainSideMenu = React.memo(({ drawerRef }: Props) => {
.concat(
themes
.sort((a, b) => a.name.localeCompare(b.name))
.map(mapTheme => ({
.map((mapTheme) => ({
text: mapTheme.name,
key: mapTheme.uuid,
iconDesc: iconDescriptorForTheme(mapTheme),
@@ -207,7 +207,7 @@ export const MainSideMenu = React.memo(({ drawerRef }: Props) => {
const onTagSelect = useCallback(
async (tag: SNTag | SmartView) => {
if (tag.conflictOf) {
void application!.mutator.changeAndSaveItem(tag, mutator => {
void application!.mutator.changeAndSaveItem(tag, (mutator) => {
mutator.conflictOf = undefined
})
}
@@ -251,7 +251,7 @@ export const MainSideMenu = React.memo(({ drawerRef }: Props) => {
<SideMenuHero testID="settingsButton" onPress={openSettings} onOutOfSyncPress={outOfSyncPressed} />
<FlatList
style={styles.sections}
data={['themes-section', 'views-section', 'tags-section'].map(key => ({
data={['themes-section', 'views-section', 'tags-section'].map((key) => ({
key,
themeOptions,
onTagSelect,

View File

@@ -122,7 +122,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
}
},
() => {
void changeNote(mutator => {
void changeNote((mutator) => {
mutator.trashed = true
}, false)
props.drawerRef?.closeDrawer()
@@ -225,7 +225,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
const disassociateComponentWithCurrentNote = useCallback(
async (component: SNComponent) => {
if (note) {
return application.mutator.changeItem(component, m => {
return application.mutator.changeItem(component, (m) => {
const mutator = m as ComponentMutator
mutator.removeAssociatedItemId(note.uuid)
mutator.disassociateWithItem(note.uuid)
@@ -256,7 +256,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
if (!note?.prefersPlainEditor) {
await application.mutator.changeItem(
note,
mutator => {
(mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.prefersPlainEditor = true
},
@@ -275,7 +275,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
if (prefersPlain) {
await application.mutator.changeItem(
note,
mutator => {
(mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.prefersPlainEditor = false
},
@@ -294,7 +294,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
async (component?: SNComponent) => {
const currentDefault = application.componentManager
.componentsForArea(ComponentArea.Editor)
.filter(e => e.isMobileDefault)[0]
.filter((e) => e.isMobileDefault)[0]
let isDefault = false
if (!component) {
@@ -314,14 +314,14 @@ export const NoteSideMenu = React.memo((props: Props) => {
const setAsDefault = () => {
if (currentDefault) {
void application.mutator.changeItem(currentDefault, m => {
void application.mutator.changeItem(currentDefault, (m) => {
const mutator = m as ComponentMutator
mutator.isMobileDefault = false
})
}
if (component) {
void application.mutator.changeAndSaveItem(component, m => {
void application.mutator.changeAndSaveItem(component, (m) => {
const mutator = m as ComponentMutator
mutator.isMobileDefault = true
})
@@ -329,7 +329,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
}
const removeAsDefault = () => {
void application.mutator.changeItem(currentDefault, m => {
void application.mutator.changeItem(currentDefault, (m) => {
const mutator = m as ComponentMutator
mutator.isMobileDefault = false
})
@@ -376,7 +376,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
},
},
]
components.map(component => {
components.map((component) => {
options.push({
text: FindNativeFeature(component.identifier)?.name || component.name,
subtext: component.isMobileDefault ? 'Mobile Default' : undefined,
@@ -435,7 +435,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
const pinOption = note.pinned ? 'Unpin' : 'Pin'
const pinEvent = () =>
changeNote(mutator => {
changeNote((mutator) => {
mutator.pinned = !note.pinned
}, false)
@@ -447,7 +447,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
)
return
}
void changeNote(mutator => {
void changeNote((mutator) => {
mutator.archived = !note.archived
}, false)
leaveEditor()
@@ -455,7 +455,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
const lockOption = note.locked ? 'Enable editing' : 'Prevent editing'
const lockEvent = () =>
changeNote(mutator => {
changeNote((mutator) => {
mutator.locked = !note.locked
}, false)
@@ -506,7 +506,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
})
}
let options: SideMenuOption[] = rawOptions.map(rawOption => ({
let options: SideMenuOption[] = rawOptions.map((rawOption) => ({
text: rawOption.text,
key: rawOption.icon,
iconDesc: {
@@ -523,7 +523,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
text: 'Restore',
key: 'restore-note',
onSelect: () => {
void changeNote(mutator => {
void changeNote((mutator) => {
mutator.trashed = false
}, false)
},
@@ -574,11 +574,11 @@ export const NoteSideMenu = React.memo((props: Props) => {
const onTagSelect = useCallback(
async (tag: SNTag | SmartView, addTagHierachy: boolean) => {
const isSelected = selectedTags.findIndex(selectedTag => selectedTag.uuid === tag.uuid) > -1
const isSelected = selectedTags.findIndex((selectedTag) => selectedTag.uuid === tag.uuid) > -1
if (note) {
if (isSelected) {
await application.mutator.changeItem(tag, mutator => {
await application.mutator.changeItem(tag, (mutator) => {
mutator.removeItemAsRelationship(note)
})
} else {
@@ -607,7 +607,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
<SafeAreaContainer edges={['top', 'bottom', 'right']}>
<FlatList
style={styles.sections}
data={Object.values(MenuSections).map(key => ({
data={Object.values(MenuSections).map((key) => ({
key,
noteOptions,
editorComponents: editors,
@@ -650,7 +650,7 @@ export const NoteSideMenu = React.memo((props: Props) => {
<TagSelectionList
hasBottomPadding={Platform.OS === 'android'}
contentType={ContentType.Tag}
onTagSelect={tag => item.onTagSelect(tag, shouldAddTagHierarchy)}
onTagSelect={(tag) => item.onTagSelect(tag, shouldAddTagHierarchy)}
selectedTags={item.selectedTags}
emptyPlaceholder={'Create a new tag using the tag button in the bottom right corner.'}
/>

View File

@@ -2,7 +2,7 @@ import { Platform } from 'react-native'
import styled, { css } from 'styled-components/native'
export const Touchable = styled.TouchableOpacity<{ isSubtext: boolean }>`
min-height: ${props => (props.isSubtext ? 52 : 42)}px;
min-height: ${(props) => (props.isSubtext ? 52 : 42)}px;
`
export const CellContent = styled.View<{
iconSide: 'right' | 'left' | null
@@ -32,7 +32,7 @@ export const TextContainer = styled.View<{
isSubtext: boolean
selected?: boolean
}>`
min-height: ${props => (props.isSubtext ? 38 : 24)}px;
min-height: ${(props) => (props.isSubtext ? 38 : 24)}px;
margin-left: 6px;
flex-shrink: 1;
${({ selected, theme }) =>

View File

@@ -46,7 +46,7 @@ const renderIcon = (desc: SideMenuOption['iconDesc'], color: string) => {
return <RegularText>*</RegularText>
}
export const SideMenuCell: React.FC<SideMenuOption> = props => {
export const SideMenuCell: React.FC<SideMenuOption> = (props) => {
const theme = useContext(ThemeContext)
const colorForTextClass = (textClass: SideMenuOption['textClass']) => {
if (!textClass) {

View File

@@ -13,7 +13,7 @@ type Props = {
testID: ViewProps['testID']
}
export const SideMenuHero: React.FC<Props> = props => {
export const SideMenuHero: React.FC<Props> = (props) => {
// Context
const application = useContext(ApplicationContext)
const theme = useContext(ThemeContext)
@@ -26,7 +26,7 @@ export const SideMenuHero: React.FC<Props> = props => {
useEffect(() => {
const observedContentTypes = [ContentType.Note, ContentType.Tag]
const removeStreamItems = application?.streamItems(observedContentTypes, _items => {
const removeStreamItems = application?.streamItems(observedContentTypes, (_items) => {
const notesAndTagsCount = application?.items.getItems(observedContentTypes).length ?? 0
if (notesAndTagsCount !== itemsCount) {

View File

@@ -4,7 +4,7 @@ export const Root = styled.View`
padding-bottom: 6px;
`
export const Header = styled.TouchableOpacity<{ collapsed: boolean }>`
height: ${props => (props.collapsed ? 50 : 22)}px;
height: ${(props) => (props.collapsed ? 50 : 22)}px;
`
export const Title = styled.Text`
color: ${({ theme }) => theme.stylekitInfoColor};

View File

@@ -39,7 +39,7 @@ type Props = {
options?: SideMenuOption[]
}
export const SideMenuSection: React.FC<Props> = React.memo(props => {
export const SideMenuSection: React.FC<Props> = React.memo((props) => {
const [collapsed, setCollapsed] = useState(Boolean(props.collapsed))
const options = useMemo(() => {
return props.options || []
@@ -58,7 +58,7 @@ export const SideMenuSection: React.FC<Props> = React.memo(props => {
{!collapsed && (
<>
{options.map(option => {
{options.map((option) => {
return (
<SideMenuCell
text={option.text}

View File

@@ -100,7 +100,7 @@ export const TagSelectionList = React.memo(
let children: SNTag[] = []
if (showFolders && item instanceof SNTag) {
const rawChildren = application.items.getTagChildren(item).map(tag => tag.uuid)
const rawChildren = application.items.getTagChildren(item).map((tag) => tag.uuid)
children = (tags as SNTag[]).filter((tag: SNTag) => rawChildren.includes(tag.uuid))
}
@@ -130,7 +130,7 @@ export const TagSelectionList = React.memo(
windowSize={10}
maxToRenderPerBatch={10}
data={children}
keyExtractor={childTag => childTag.uuid}
keyExtractor={(childTag) => childTag.uuid}
renderItem={renderItem}
/>
)}
@@ -147,7 +147,7 @@ export const TagSelectionList = React.memo(
windowSize={10}
maxToRenderPerBatch={10}
data={renderedTags as SNTag[]}
keyExtractor={item => item.uuid}
keyExtractor={(item) => item.uuid}
renderItem={renderItem}
/>
{tags.length === 0 && <EmptyPlaceholder>{emptyPlaceholder}</EmptyPlaceholder>}

View File

@@ -29,7 +29,7 @@ export enum Tabs {
type Props = ModalStackNavigationProp<typeof SCREEN_UPLOADED_FILES_LIST>
export const UploadedFilesList: FC<Props> = props => {
export const UploadedFilesList: FC<Props> = (props) => {
const { AttachedFiles, AllFiles } = Tabs
const { note } = props.route.params
@@ -54,7 +54,7 @@ export const UploadedFilesList: FC<Props> = props => {
const filteredList = useMemo(() => {
return searchString
? filesList.filter(file => file.name.toLowerCase().includes(searchString.toLowerCase()))
? filesList.filter((file) => file.name.toLowerCase().includes(searchString.toLowerCase()))
: filesList
}, [filesList, searchString])
@@ -128,7 +128,7 @@ export const UploadedFilesList: FC<Props> = props => {
ref={filesListRef}
data={filteredList}
renderItem={renderItem}
keyExtractor={item => item.uuid}
keyExtractor={(item) => item.uuid}
onScroll={onScroll}
/>
) : (

View File

@@ -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

View File

@@ -3,7 +3,7 @@ const PREFIX_STANDARD_NOTES = '--sn-stylekit'
const PREFIX_STANDARD_NOTES_BURN = '--sn-'
function camelCaseToDashed(camel: string) {
return camel.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
return camel.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
}
export function objectToCss(object: any) {

View File

@@ -49,12 +49,12 @@ export const useCustomActionSheet = () => {
},
]
const tempOptions = options.concat(cancelOption)
const destructiveIndex = tempOptions.findIndex(item => item.destructive)
const destructiveIndex = tempOptions.findIndex((item) => item.destructive)
const cancelIndex = tempOptions.length - 1
showActionSheetWithOptions(
{
options: tempOptions.map(option => option.text),
options: tempOptions.map((option) => option.text),
destructiveButtonIndex: destructiveIndex,
cancelButtonIndex: cancelIndex,
title,
@@ -72,7 +72,7 @@ export const useCustomActionSheet = () => {
},
anchor: anchor ? findNodeHandle(anchor) ?? undefined : undefined,
},
buttonIndex => {
(buttonIndex) => {
const option = tempOptions[buttonIndex!]
option.callback && option.callback(option)
},

View File

@@ -78,7 +78,7 @@ export class ThemeService {
}
private registerObservers() {
this.unsubsribeAppEventObserver = this.application?.addEventObserver(async event => {
this.unsubsribeAppEventObserver = this.application?.addEventObserver(async (event) => {
/**
* If there are any migrations we need to set default theme to start UI
*/
@@ -259,7 +259,7 @@ export class ThemeService {
private async resolveInitialThemeForMode() {
try {
const savedThemeId = await this.getThemeForMode(this.getColorScheme())
const matchingThemeId = Object.keys(this.themes).find(themeId => themeId === savedThemeId)
const matchingThemeId = Object.keys(this.themes).find((themeId) => themeId === savedThemeId)
if (matchingThemeId) {
this.setActiveTheme(matchingThemeId)
void this.application?.mobileComponentManager.preloadThirdPartyThemeIndexPath()
@@ -278,7 +278,7 @@ export class ThemeService {
systemThemes() {
return Object.values(this.themes)
.filter(theme => theme.mobileContent.isSystemTheme)
.filter((theme) => theme.mobileContent.isSystemTheme)
.sort((a, b) => {
if (a.name < b.name) {
return -1
@@ -292,7 +292,7 @@ export class ThemeService {
nonSystemThemes() {
return Object.values(this.themes)
.filter(theme => !theme.mobileContent.isSystemTheme)
.filter((theme) => !theme.mobileContent.isSystemTheme)
.sort((a, b) => {
if (a.name < b.name) {
return -1
@@ -465,7 +465,7 @@ export class ThemeService {
private async loadCachedThemes() {
const rawValue = (await this.application!.getValue(CACHED_THEMES_KEY, StorageValueModes.Nonwrapped)) || []
const themes = (rawValue as DecryptedTransferPayload<ComponentContent>[]).map(rawPayload => {
const themes = (rawValue as DecryptedTransferPayload<ComponentContent>[]).map((rawPayload) => {
const payload = new DecryptedPayload<ComponentContent>(rawPayload)
return new MobileTheme(payload)
@@ -480,7 +480,7 @@ export class ThemeService {
const themes = this.nonSystemThemes()
return this.application!.setValue(
CACHED_THEMES_KEY,
themes.map(t => t.payloadRepresentation()),
themes.map((t) => t.payloadRepresentation()),
StorageValueModes.Nonwrapped,
)
}