diff --git a/.eslintignore b/.eslintignore index cef372f88..4b6451c93 100644 --- a/.eslintignore +++ b/.eslintignore @@ -549,7 +549,10 @@ packages/app-mobile/components/NoteList.js packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js -packages/app-mobile/components/ScreenHeader.js +packages/app-mobile/components/ScreenHeader/WarningBanner.test.js +packages/app-mobile/components/ScreenHeader/WarningBanner.js +packages/app-mobile/components/ScreenHeader/WarningBox.js +packages/app-mobile/components/ScreenHeader/index.js packages/app-mobile/components/SelectDateTimeDialog.js packages/app-mobile/components/SideMenu.js packages/app-mobile/components/TextInput.js diff --git a/.gitignore b/.gitignore index e01e1cfd4..d796b36f1 100644 --- a/.gitignore +++ b/.gitignore @@ -529,7 +529,10 @@ packages/app-mobile/components/NoteList.js packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js -packages/app-mobile/components/ScreenHeader.js +packages/app-mobile/components/ScreenHeader/WarningBanner.test.js +packages/app-mobile/components/ScreenHeader/WarningBanner.js +packages/app-mobile/components/ScreenHeader/WarningBox.js +packages/app-mobile/components/ScreenHeader/index.js packages/app-mobile/components/SelectDateTimeDialog.js packages/app-mobile/components/SideMenu.js packages/app-mobile/components/TextInput.js diff --git a/packages/app-mobile/components/SaveIcon.png b/packages/app-mobile/components/ScreenHeader/SaveIcon.png similarity index 100% rename from packages/app-mobile/components/SaveIcon.png rename to packages/app-mobile/components/ScreenHeader/SaveIcon.png diff --git a/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx b/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx new file mode 100644 index 000000000..e1258c072 --- /dev/null +++ b/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { WarningBannerComponent } from './WarningBanner'; +import Setting from '@joplin/lib/models/Setting'; +import NavService from '@joplin/lib/services/NavService'; +import { render, screen, userEvent } from '@testing-library/react-native'; + +interface WrapperProps { + showMissingMasterKeyMessage?: boolean; + hasDisabledSyncItems?: boolean; + shouldUpgradeSyncTarget?: boolean; + showShouldUpgradeSyncTargetMessage?: boolean; + hasDisabledEncryptionItems?: boolean; + mustUpgradeAppMessage?: string; +} + +const WarningBannerWrapper: React.FC = props => { + return ; +}; + +describe('WarningBanner', () => { + let navServiceMock: jest.Mock<(route: unknown)=> void>; + beforeEach(() => { + navServiceMock = jest.fn(); + NavService.dispatch = navServiceMock; + jest.useFakeTimers(); + }); + + test('the missing master key alert should link to the encryption config screen', async () => { + render(); + expect(await screen.findAllByTestId('warning-box')).toHaveLength(1); + + expect(navServiceMock).not.toHaveBeenCalled(); + + const masterKeyWarning = screen.getByText(/decryption password/); + const user = userEvent.setup(); + await user.press(masterKeyWarning); + + expect(navServiceMock.mock.lastCall).toMatchObject([{ routeName: 'EncryptionConfig' }]); + }); +}); diff --git a/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx b/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx new file mode 100644 index 000000000..af5192501 --- /dev/null +++ b/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { AppState } from '../../utils/types'; +import WarningBox from './WarningBox'; +import { _ } from '@joplin/lib/locale'; +import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils'; +import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils'; +import Setting from '@joplin/lib/models/Setting'; + +interface Props { + themeId: number; + showMissingMasterKeyMessage: boolean; + hasDisabledSyncItems: boolean; + shouldUpgradeSyncTarget: boolean; + showShouldUpgradeSyncTargetMessage: boolean|undefined; + hasDisabledEncryptionItems: boolean; + mustUpgradeAppMessage: string; +} + + +export const WarningBannerComponent: React.FC = props => { + const warningComps = []; + + const renderWarningBox = (screen: string, message: string) => { + return ; + }; + + if (props.showMissingMasterKeyMessage) { + warningComps.push(renderWarningBox('EncryptionConfig', _('Press to set the decryption password.'))); + } + if (props.hasDisabledSyncItems) { + warningComps.push(renderWarningBox('Status', _('Some items cannot be synchronised. Press for more info.'))); + } + if (props.shouldUpgradeSyncTarget && props.showShouldUpgradeSyncTargetMessage !== false) { + warningComps.push(renderWarningBox('UpgradeSyncTarget', _('The sync target needs to be upgraded. Press this banner to proceed.'))); + } + if (props.mustUpgradeAppMessage) { + warningComps.push(renderWarningBox('UpgradeApp', props.mustUpgradeAppMessage)); + } + if (props.hasDisabledEncryptionItems) { + warningComps.push(renderWarningBox('Status', _('Some items cannot be decrypted.'))); + } + + return warningComps; +}; + +export default connect((state: AppState) => { + const syncInfo = localSyncInfoFromState(state); + + return { + themeId: state.settings.theme, + hasDisabledEncryptionItems: state.hasDisabledEncryptionItems, + noteSelectionEnabled: state.noteSelectionEnabled, + selectedFolderId: state.selectedFolderId, + notesParentType: state.notesParentType, + showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys), + hasDisabledSyncItems: state.hasDisabledSyncItems, + shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, + mustUpgradeAppMessage: state.mustUpgradeAppMessage, + }; +})(WarningBannerComponent); diff --git a/packages/app-mobile/components/ScreenHeader/WarningBox.tsx b/packages/app-mobile/components/ScreenHeader/WarningBox.tsx new file mode 100644 index 000000000..f336aec03 --- /dev/null +++ b/packages/app-mobile/components/ScreenHeader/WarningBox.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { useMemo, useCallback } from 'react'; +import { TouchableOpacity, StyleSheet, Text } from 'react-native'; +import { themeStyle } from '../global-style'; +import NavService from '@joplin/lib/services/NavService'; + +interface Props { + themeId: number; + targetScreen: string; + message: string; + testID?: string; +} + +const useStyles = (themeId: number) => { + return useMemo(() => { + const theme = themeStyle(themeId); + return StyleSheet.create({ + container: { + backgroundColor: '#ff9900', + flexDirection: 'row', + padding: theme.marginLeft, + }, + text: { + flex: 1, + color: 'black', + }, + }); + }, [themeId]); +}; + +const WarningBox: React.FC = props => { + const styles = useStyles(props.themeId); + + const onPress = useCallback(() => { + void NavService.go(props.targetScreen); + }, [props.targetScreen]); + + return ( + + {props.message} + + ); +}; + +export default WarningBox; diff --git a/packages/app-mobile/components/ScreenHeader.tsx b/packages/app-mobile/components/ScreenHeader/index.tsx similarity index 89% rename from packages/app-mobile/components/ScreenHeader.tsx rename to packages/app-mobile/components/ScreenHeader/index.tsx index e8ef038e5..8997ffe70 100644 --- a/packages/app-mobile/components/ScreenHeader.tsx +++ b/packages/app-mobile/components/ScreenHeader/index.tsx @@ -1,32 +1,29 @@ -const React = require('react'); - -import { connect } from 'react-redux'; +import * as React from 'react'; import { PureComponent, ReactElement } from 'react'; +import { connect } from 'react-redux'; import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions, ViewStyle } from 'react-native'; const Icon = require('react-native-vector-icons/Ionicons').default; -const { BackButtonService } = require('../services/back-button.js'); +const { BackButtonService } = require('../../services/back-button.js'); import NavService from '@joplin/lib/services/NavService'; import { Menu, MenuOptions, MenuOption, MenuTrigger } from 'react-native-popup-menu'; import { _, _n } from '@joplin/lib/locale'; -import Setting from '@joplin/lib/models/Setting'; import Note from '@joplin/lib/models/Note'; import Folder from '@joplin/lib/models/Folder'; -import { themeStyle } from './global-style'; -import { OnValueChangedListener } from './Dropdown'; -const { dialogs } = require('../utils/dialogs.js'); +import { themeStyle } from '../global-style'; +import { OnValueChangedListener } from '../Dropdown'; +const { dialogs } = require('../../utils/dialogs.js'); const DialogBox = require('react-native-dialogbox').default; -import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils'; -import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils'; import { FolderEntity } from '@joplin/lib/services/database/types'; import { State } from '@joplin/lib/reducer'; -import CustomButton from './CustomButton'; -import FolderPicker from './FolderPicker'; +import CustomButton from '../CustomButton'; +import FolderPicker from '../FolderPicker'; import { itemIsInTrash } from '@joplin/lib/services/trash'; import restoreItems from '@joplin/lib/services/trash/restoreItems'; import { ModelType } from '@joplin/lib/BaseModel'; import { PluginStates } from '@joplin/lib/services/plugins/reducer'; import { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; import { Dispatch } from 'redux'; +import WarningBanner from './WarningBanner'; // We need this to suppress the useless warning // https://github.com/oblador/react-native-vector-icons/issues/1465 @@ -41,10 +38,6 @@ const PADDING_V = 10; type OnSelectCallbackType=()=> void; type OnPressCallback=()=> void; -interface NavButtonPressEvent { - // Name of the screen to navigate to - screen: string; -} export interface MenuOptionType { onPress: OnPressCallback; @@ -90,12 +83,7 @@ interface ScreenHeaderProps { showSaveButton?: boolean; historyCanGoBack?: boolean; - showMissingMasterKeyMessage?: boolean; - hasDisabledSyncItems?: boolean; - hasDisabledEncryptionItems?: boolean; - shouldUpgradeSyncTarget?: boolean; showShouldUpgradeSyncTargetMessage?: boolean; - mustUpgradeAppMessage: string; themeId: number; } @@ -210,11 +198,6 @@ class ScreenHeaderComponent extends PureComponent this.warningBox_press({ screen: screen })} activeOpacity={0.8}> - {message} - - ); - } - public render() { const themeId = this.props.themeId; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -640,17 +611,6 @@ class ScreenHeaderComponent extends PureComponent - {warningComps} + { this.dialogbox = dialogbox; @@ -740,22 +702,15 @@ class ScreenHeaderComponent extends PureComponent { - const syncInfo = localSyncInfoFromState(state); - return { historyCanGoBack: state.historyCanGoBack, locale: state.settings.locale, folders: state.folders, themeId: state.settings.theme, - hasDisabledEncryptionItems: state.hasDisabledEncryptionItems, noteSelectionEnabled: state.noteSelectionEnabled, selectedNoteIds: state.selectedNoteIds, selectedFolderId: state.selectedFolderId, notesParentType: state.notesParentType, - showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys), - hasDisabledSyncItems: state.hasDisabledSyncItems, - shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, - mustUpgradeAppMessage: state.mustUpgradeAppMessage, plugins: state.pluginService.plugins, }; })(ScreenHeaderComponent);