From 257a24166eaf94aa96192c48a5ce7ba12ab02282 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sun, 8 Jan 2023 04:22:41 -0800 Subject: [PATCH] Chore: Mobile: Migrate action button to `react-native-paper` (#7477) --- .eslintignore | 3 + .gitignore | 3 + .../net/cozic/joplin/MainApplication.java | 1 + packages/app-mobile/android/settings.gradle | 2 + .../app-mobile/components/ActionButton.tsx | 73 ++++++++ .../app-mobile/components/action-button.js | 169 ------------------ .../app-mobile/components/screens/Note.tsx | 12 +- .../app-mobile/components/screens/notes.js | 45 ++++- packages/app-mobile/ios/Podfile.lock | 14 +- packages/app-mobile/package.json | 2 + packages/app-mobile/root.tsx | 24 ++- packages/lib/reducer.ts | 3 +- yarn.lock | 51 +++++- 13 files changed, 218 insertions(+), 184 deletions(-) create mode 100644 packages/app-mobile/components/ActionButton.tsx delete mode 100644 packages/app-mobile/components/action-button.js diff --git a/.eslintignore b/.eslintignore index 1ab0d68eeb..29aed09957 100644 --- a/.eslintignore +++ b/.eslintignore @@ -864,6 +864,9 @@ packages/app-desktop/utils/markupLanguageUtils.js.map packages/app-mobile/PluginAssetsLoader.d.ts packages/app-mobile/PluginAssetsLoader.js packages/app-mobile/PluginAssetsLoader.js.map +packages/app-mobile/components/ActionButton.d.ts +packages/app-mobile/components/ActionButton.js +packages/app-mobile/components/ActionButton.js.map packages/app-mobile/components/BackButtonDialogBox.d.ts packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/BackButtonDialogBox.js.map diff --git a/.gitignore b/.gitignore index b28b45df3c..0e23f6a993 100644 --- a/.gitignore +++ b/.gitignore @@ -852,6 +852,9 @@ packages/app-desktop/utils/markupLanguageUtils.js.map packages/app-mobile/PluginAssetsLoader.d.ts packages/app-mobile/PluginAssetsLoader.js packages/app-mobile/PluginAssetsLoader.js.map +packages/app-mobile/components/ActionButton.d.ts +packages/app-mobile/components/ActionButton.js +packages/app-mobile/components/ActionButton.js.map packages/app-mobile/components/BackButtonDialogBox.d.ts packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/BackButtonDialogBox.js.map diff --git a/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/MainApplication.java b/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/MainApplication.java index e5740b5868..b421afe7e6 100644 --- a/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/MainApplication.java +++ b/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/MainApplication.java @@ -9,6 +9,7 @@ import androidx.multidex.MultiDex; import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; +import com.oblador.vectoricons.VectorIconsPackage; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; diff --git a/packages/app-mobile/android/settings.gradle b/packages/app-mobile/android/settings.gradle index 060030b35b..c36bb7a80d 100644 --- a/packages/app-mobile/android/settings.gradle +++ b/packages/app-mobile/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'Joplin' +include ':react-native-vector-icons' +project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/react-native-gradle-plugin') diff --git a/packages/app-mobile/components/ActionButton.tsx b/packages/app-mobile/components/ActionButton.tsx new file mode 100644 index 0000000000..d687ca3b89 --- /dev/null +++ b/packages/app-mobile/components/ActionButton.tsx @@ -0,0 +1,73 @@ +const React = require('react'); +import { useState, useCallback, useMemo } from 'react'; + +const Icon = require('react-native-vector-icons/Ionicons').default; +import { FAB, Portal } from 'react-native-paper'; +import { _ } from '@joplin/lib/locale'; + + +type OnButtonPress = ()=> void; +interface ButtonSpec { + icon: string; + label: string; + color?: string; + onPress?: OnButtonPress; +} + +interface ActionButtonProps { + buttons?: ButtonSpec[]; + + // If not given, an "add" button will be used. + mainButton?: ButtonSpec; +} + +const defaultOnPress = () => {}; + +// Returns a render function compatible with React Native Paper. +const getIconRenderFunction = (iconName: string) => { + return (props: any) => ; +}; + +const useIcon = (iconName: string) => { + return useMemo(() => { + return getIconRenderFunction(iconName); + }, [iconName]); +}; + +const ActionButton = (props: ActionButtonProps) => { + const [open, setOpen] = useState(false); + const onMenuToggled = useCallback( + (state: { open: boolean }) => setOpen(state.open) + , [setOpen]); + + + const actions = useMemo(() => (props.buttons ?? []).map(button => { + return { + ...button, + icon: getIconRenderFunction(button.icon), + onPress: button.onPress ?? defaultOnPress, + }; + }), [props.buttons]); + + const closedIcon = useIcon(props.mainButton?.icon ?? 'md-add'); + const openIcon = useIcon('close'); + + return ( + + + + ); +}; + +export default ActionButton; diff --git a/packages/app-mobile/components/action-button.js b/packages/app-mobile/components/action-button.js deleted file mode 100644 index ca2f804cba..0000000000 --- a/packages/app-mobile/components/action-button.js +++ /dev/null @@ -1,169 +0,0 @@ -const React = require('react'); - -const { StyleSheet } = require('react-native'); -const Note = require('@joplin/lib/models/Note').default; -const Icon = require('react-native-vector-icons/Ionicons').default; -const ReactNativeActionButton = require('react-native-action-button').default; -const { connect } = require('react-redux'); -const { _ } = require('@joplin/lib/locale'); - -// We need this to suppress the useless warning -// https://github.com/oblador/react-native-vector-icons/issues/1465 -Icon.loadFont().catch((error) => { console.info(error); }); - -const styles = StyleSheet.create({ - actionButtonIcon: { - fontSize: 20, - height: 22, - color: 'white', - }, - itemText: { - // fontSize: 14, // Cannot currently set fontsize since the bow surrounding the label has a fixed size - }, -}); - -class ActionButtonComponent extends React.Component { - constructor() { - super(); - this.state = { - buttonIndex: 0, - }; - - this.renderIconMultiStates = this.renderIconMultiStates.bind(this); - this.renderIcon = this.renderIcon.bind(this); - } - - UNSAFE_componentWillReceiveProps(newProps) { - if ('buttonIndex' in newProps) { - this.setState({ buttonIndex: newProps.buttonIndex }); - } - } - - async newNoteNavigate(folderId, isTodo) { - const newNote = await Note.save({ - parent_id: folderId, - is_todo: isTodo ? 1 : 0, - }, { provisional: true }); - - this.props.dispatch({ - type: 'NAV_GO', - routeName: 'Note', - noteId: newNote.id, - }); - } - - newTodo_press() { - this.newNoteNavigate(this.props.parentFolderId, true); - } - - newNote_press() { - this.newNoteNavigate(this.props.parentFolderId, false); - } - - renderIconMultiStates() { - const button = this.props.buttons[this.state.buttonIndex]; - - return ; - } - - renderIcon() { - const mainButton = this.props.mainButton ? this.props.mainButton : {}; - const iconName = mainButton.icon ?? 'md-add'; - - // Icons don't have alt text by default. We need to add it: - const iconTitle = mainButton.title ?? _('Add new'); - - // TODO: If the button toggles a sub-menu, state whether the submenu is open - // or closed. - - return ( - - ); - } - - render() { - const buttons = this.props.buttons ? this.props.buttons : []; - - if (this.props.addFolderNoteButtons) { - if (this.props.folders.length) { - buttons.push({ - title: _('New to-do'), - onPress: () => { - this.newTodo_press(); - }, - color: '#9b59b6', - icon: 'md-checkbox-outline', - }); - - buttons.push({ - title: _('New note'), - onPress: () => { - this.newNote_press(); - }, - color: '#9b59b6', - icon: 'md-document', - }); - } - } - - const buttonComps = []; - for (let i = 0; i < buttons.length; i++) { - const button = buttons[i]; - const buttonTitle = button.title ? button.title : ''; - const key = `${buttonTitle.replace(/\s/g, '_')}_${button.icon}`; - buttonComps.push( - // TODO: By default, ReactNativeActionButton also adds a title, which is focusable - // by the screen reader. As such, each item currently is double-focusable - - - - ); - } - - if (!buttonComps.length && !this.props.mainButton) { - return null; - } - - if (this.props.multiStates) { - if (!this.props.buttons || !this.props.buttons.length) throw new Error('Multi-state button requires at least one state'); - if (this.state.buttonIndex < 0 || this.state.buttonIndex >= this.props.buttons.length) throw new Error(`Button index out of bounds: ${this.state.buttonIndex}/${this.props.buttons.length}`); - const button = this.props.buttons[this.state.buttonIndex]; - return ( - { - button.onPress(); - }} - /> - ); - } else { - return ( - - {buttonComps} - - ); - } - } -} - -const ActionButton = connect(state => { - return { - folders: state.folders, - locale: state.settings.locale, - }; -})(ActionButtonComponent); - -module.exports = { ActionButton }; diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index 2d9eb17179..8552bd5e63 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -22,7 +22,7 @@ const md5 = require('md5'); const { BackButtonService } = require('../../services/back-button.js'); import NavService from '@joplin/lib/services/NavService'; import BaseModel from '@joplin/lib/BaseModel'; -const { ActionButton } = require('../action-button.js'); +import ActionButton from '../ActionButton'; const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils'); const mimeUtils = require('@joplin/lib/mime-utils.js').mime; import ScreenHeader from '../ScreenHeader'; @@ -1145,21 +1145,19 @@ class NoteScreenComponent extends BaseScreenComponent { } const renderActionButton = () => { - const buttons = []; - - buttons.push({ - title: _('Edit'), + const editButton = { + label: _('Edit'), icon: 'md-create', onPress: () => { this.setState({ mode: 'edit' }); this.doFocusUpdate_ = true; }, - }); + }; if (this.state.mode === 'edit') return null; - return ; + return ; }; const actionButtonComp = renderActionButton(); diff --git a/packages/app-mobile/components/screens/notes.js b/packages/app-mobile/components/screens/notes.js index bc4c6d747d..5ce6926952 100644 --- a/packages/app-mobile/components/screens/notes.js +++ b/packages/app-mobile/components/screens/notes.js @@ -11,7 +11,7 @@ const Setting = require('@joplin/lib/models/Setting').default; const { themeStyle } = require('../global-style.js'); const { ScreenHeader } = require('../ScreenHeader'); const { _ } = require('@joplin/lib/locale'); -const { ActionButton } = require('../action-button.js'); +const ActionButton = require('../ActionButton').default; const { dialogs } = require('../../utils/dialogs.js'); const DialogBox = require('react-native-dialogbox').default; const { BaseScreenComponent } = require('../base-screen.js'); @@ -179,6 +179,19 @@ class NotesScreenComponent extends BaseScreenComponent { }); } + newNoteNavigate = async (folderId, isTodo) => { + const newNote = await Note.save({ + parent_id: folderId, + is_todo: isTodo ? 1 : 0, + }, { provisional: true }); + + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'Note', + noteId: newNote.id, + }); + }; + parentItem(props = null) { if (!props) props = this.props; @@ -238,7 +251,35 @@ class NotesScreenComponent extends BaseScreenComponent { const addFolderNoteButtons = !!buttonFolderId; const thisComp = this; - const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : ; + + const makeActionButtonComp = () => { + if (addFolderNoteButtons && this.props.folders.length > 0) { + const buttons = []; + buttons.push({ + label: _('New to-do'), + onPress: () => { + const isTodo = true; + this.newNoteNavigate(buttonFolderId, isTodo); + }, + color: '#9b59b6', + icon: 'md-checkbox-outline', + }); + + buttons.push({ + label: _('New note'), + onPress: () => { + const isTodo = false; + this.newNoteNavigate(buttonFolderId, isTodo); + }, + color: '#9b59b6', + icon: 'md-document', + }); + return ; + } + return null; + }; + + const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : makeActionButtonComp(); return ( diff --git a/packages/app-mobile/ios/Podfile.lock b/packages/app-mobile/ios/Podfile.lock index c75249dbaf..0d9a423a31 100644 --- a/packages/app-mobile/ios/Podfile.lock +++ b/packages/app-mobile/ios/Podfile.lock @@ -248,6 +248,12 @@ PODS: - React-Core - react-native-rsa-native (2.0.5): - React + - react-native-safe-area-context (4.4.1): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - ReactCommon/turbomodule/core - react-native-slider (4.4.0): - React-Core - react-native-sqlite-storage (6.0.1): @@ -376,6 +382,7 @@ DEPENDENCIES: - react-native-image-resizer (from `../node_modules/react-native-image-resizer`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-rsa-native (from `../node_modules/react-native-rsa-native`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`) - react-native-version-info (from `../node_modules/react-native-version-info`) @@ -469,6 +476,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/netinfo" react-native-rsa-native: :path: "../node_modules/react-native-rsa-native" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" react-native-slider: :path: "../node_modules/@react-native-community/slider" react-native-sqlite-storage: @@ -530,10 +539,10 @@ SPEC CHECKSUMS: FBLazyVector: 2b47ff52037bd9ae07cc9b051c9975797814b736 FBReactNativeSpec: 0e0d384ef17a33b385f13f0c7f97702c7cd17858 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 476ee3e89abb49e07f822b48323c51c57124b572 + glog: 5337263514dd6f09803962437687240c5dc39aa4 JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7 JoplinRNShareExtension: 485f3e6dad83b7b77f1572eabc249f869ee55c02 - RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8 + RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9 RCTRequired: 0f06b6068f530932d10e1a01a5352fad4eaacb74 RCTTypeSafety: b0ee81f10ef1b7d977605a2b266823dabd565e65 React: 3becd12bd51ea8a43bdde7e09d0f40fba7820e03 @@ -556,6 +565,7 @@ SPEC CHECKSUMS: react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a + react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-slider: d2938a12c4e439a227c70eec65d119136eb4aeb5 react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9 diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index 21ac09686c..4e7af138ad 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -51,9 +51,11 @@ "react-native-image-picker": "4.10.3", "react-native-image-resizer": "1.4.5", "react-native-modal-datetime-picker": "14.0.1", + "react-native-paper": "5.0.2", "react-native-popup-menu": "0.16.1", "react-native-quick-actions": "0.3.13", "react-native-rsa-native": "2.0.5", + "react-native-safe-area-context": "4.4.1", "react-native-securerandom": "1.0.1", "react-native-share": "8.1.0", "react-native-side-menu-updated": "1.3.2", diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 34b08872e8..fdc5423966 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -36,6 +36,7 @@ const DropdownAlert = require('react-native-dropdownalert').default; const AlarmServiceDriver = require('./services/AlarmServiceDriver').default; const SafeAreaView = require('./components/SafeAreaView'); const { connect, Provider } = require('react-redux'); +import { Provider as PaperProvider, MD2DarkTheme as PaperDarkTheme, MD2LightTheme as PaperLightTheme } from 'react-native-paper'; const { BackButtonService } = require('./services/back-button.js'); import NavService from '@joplin/lib/services/NavService'; import { createStore, applyMiddleware } from 'redux'; @@ -108,6 +109,7 @@ import SyncTargetNone from '@joplin/lib/SyncTargetNone'; import { setRSA } from '@joplin/lib/services/e2ee/ppk'; import RSA from './services/e2ee/RSA.react-native'; import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils'; +import { Theme, ThemeAppearance } from '@joplin/lib/themes/type'; import { AppState } from './utils/types'; import sensorInfo from './components/biometrics/sensorInfo'; @@ -881,7 +883,7 @@ class AppComponent extends React.Component { public render() { if (this.props.appState !== 'ready') return null; - const theme = themeStyle(this.props.themeId); + const theme: Theme = themeStyle(this.props.themeId); let sideMenuContent = null; let menuPosition = 'left'; @@ -912,7 +914,7 @@ class AppComponent extends React.Component { // const statusBarStyle = theme.appearance === 'light-content'; const statusBarStyle = 'light-content'; - return ( + const mainContent = ( ); + + + const paperTheme = theme.appearance === ThemeAppearance.Dark ? PaperDarkTheme : PaperLightTheme; + + // Wrap everything in a PaperProvider -- this allows using components from react-native-paper + return ( + + {mainContent} + + ); } } diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index 7d3214ba9a..01017cf2fe 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -7,6 +7,7 @@ import BaseModel from './BaseModel'; import { Store } from 'redux'; import { ProfileConfig } from './services/profileConfig/types'; import * as ArrayUtils from './ArrayUtils'; +import { FolderEntity } from './services/database/types'; const fastDeepEqual = require('fast-deep-equal'); const { ALL_NOTES_FILTER_ID } = require('./reserved-ids'); const { createSelectorCreator, defaultMemoize } = require('reselect'); @@ -55,7 +56,7 @@ export interface State { noteSelectionEnabled?: boolean; notesSource: string; notesParentType: string; - folders: any[]; + folders: FolderEntity[]; tags: any[]; masterKeys: any[]; notLoadedMasterKeys: string[]; diff --git a/yarn.lock b/yarn.lock index f225ef5b98..d5b820e987 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3150,6 +3150,18 @@ __metadata: languageName: node linkType: hard +"@callstack/react-theme-provider@npm:^3.0.8": + version: 3.0.8 + resolution: "@callstack/react-theme-provider@npm:3.0.8" + dependencies: + deepmerge: ^3.2.0 + hoist-non-react-statics: ^3.3.0 + peerDependencies: + react: ">=16.3.0" + checksum: 6077a4795aea4eb06a2a2ffe5cf299c3fdcba56530aa68eba5c3ac0728ce02be2f6e9e71278be303065cb7fbe252c35639538477d5d05ee14f6325d550c8c696 + languageName: node + linkType: hard + "@cloudcmd/create-element@npm:^2.0.0": version: 2.0.2 resolution: "@cloudcmd/create-element@npm:2.0.2" @@ -4732,9 +4744,11 @@ __metadata: react-native-image-picker: 4.10.3 react-native-image-resizer: 1.4.5 react-native-modal-datetime-picker: 14.0.1 + react-native-paper: 5.0.2 react-native-popup-menu: 0.16.1 react-native-quick-actions: 0.3.13 react-native-rsa-native: 2.0.5 + react-native-safe-area-context: 4.4.1 react-native-securerandom: 1.0.1 react-native-share: 8.1.0 react-native-side-menu-updated: 1.3.2 @@ -11994,7 +12008,7 @@ __metadata: languageName: node linkType: hard -"color@npm:3.2.1": +"color@npm:3.2.1, color@npm:^3.1.2": version: 3.2.1 resolution: "color@npm:3.2.1" dependencies: @@ -27687,6 +27701,22 @@ __metadata: languageName: node linkType: hard +"react-native-paper@npm:5.0.2": + version: 5.0.2 + resolution: "react-native-paper@npm:5.0.2" + dependencies: + "@callstack/react-theme-provider": ^3.0.8 + color: ^3.1.2 + use-event-callback: ^0.1.0 + peerDependencies: + react: "*" + react-native: "*" + react-native-safe-area-context: "*" + react-native-vector-icons: "*" + checksum: 46481e3db2b297f2f02d1d05710d7ab329f901acc5a663e4c81fb7db83534e5d63067e9287bb396703e50d955ddfb8c41d7546cbd1ea2825a6888fd2e0fa80de + languageName: node + linkType: hard + "react-native-popup-menu@npm:0.16.1": version: 0.16.1 resolution: "react-native-popup-menu@npm:0.16.1" @@ -27708,6 +27738,16 @@ __metadata: languageName: node linkType: hard +"react-native-safe-area-context@npm:4.4.1": + version: 4.4.1 + resolution: "react-native-safe-area-context@npm:4.4.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: ef7c41ea59a34b114c6481fb130e66ef85e8d5b88acb46279131367761ca9fbf22cd310fe613f49b6c9b56dbd83e044be640f0532eda1d3856bf708e96335a35 + languageName: node + linkType: hard + "react-native-securerandom@npm:1.0.1": version: 1.0.1 resolution: "react-native-securerandom@npm:1.0.1" @@ -33141,6 +33181,15 @@ __metadata: languageName: node linkType: hard +"use-event-callback@npm:^0.1.0": + version: 0.1.0 + resolution: "use-event-callback@npm:0.1.0" + peerDependencies: + react: ">=16.8" + checksum: 1e15fb21306c74f877e9d57686546c363165429412dcb9260254d2dd8f56692cb01ba2162f9169e6fc15b01cf3921b9cd8a9c60cf777d0143afcee92c1a7976a + languageName: node + linkType: hard + "use-isomorphic-layout-effect@npm:^1.1.2": version: 1.1.2 resolution: "use-isomorphic-layout-effect@npm:1.1.2"