From b9dc22603154286b9ddbeff1504332694f3a6056 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Sat, 21 Sep 2024 04:57:26 -0700 Subject: [PATCH] Chore: Mobile: Migrate `NoteItem` and `Checkbox` to TypeScript (#11094) --- .eslintignore | 2 + .gitignore | 2 + packages/app-mobile/components/Checkbox.tsx | 67 +++++++++++ .../components/{note-item.js => NoteItem.tsx} | 108 +++++++++--------- packages/app-mobile/components/NoteList.tsx | 2 +- packages/app-mobile/components/checkbox.js | 77 ------------- .../app-mobile/components/screens/Note.tsx | 2 +- .../app-mobile/components/screens/search.tsx | 2 +- 8 files changed, 130 insertions(+), 132 deletions(-) create mode 100644 packages/app-mobile/components/Checkbox.tsx rename packages/app-mobile/components/{note-item.js => NoteItem.tsx} (62%) delete mode 100644 packages/app-mobile/components/checkbox.js diff --git a/.eslintignore b/.eslintignore index 321a38956..1ae03ae0b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -548,6 +548,7 @@ packages/app-mobile/commands/util/showResource.js packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/BetaChip.js packages/app-mobile/components/CameraView.js +packages/app-mobile/components/Checkbox.js packages/app-mobile/components/DialogManager.js packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/Dropdown.test.js @@ -611,6 +612,7 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js packages/app-mobile/components/NoteEditor/types.js +packages/app-mobile/components/NoteItem.js packages/app-mobile/components/NoteList.js packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js diff --git a/.gitignore b/.gitignore index e424df4be..fcef98154 100644 --- a/.gitignore +++ b/.gitignore @@ -525,6 +525,7 @@ packages/app-mobile/commands/util/showResource.js packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/BetaChip.js packages/app-mobile/components/CameraView.js +packages/app-mobile/components/Checkbox.js packages/app-mobile/components/DialogManager.js packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/Dropdown.test.js @@ -588,6 +589,7 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js packages/app-mobile/components/NoteEditor/types.js +packages/app-mobile/components/NoteItem.js packages/app-mobile/components/NoteList.js packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js diff --git a/packages/app-mobile/components/Checkbox.tsx b/packages/app-mobile/components/Checkbox.tsx new file mode 100644 index 000000000..3bc56c0e4 --- /dev/null +++ b/packages/app-mobile/components/Checkbox.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { TouchableHighlight, StyleSheet, TextStyle } from 'react-native'; +const Icon = require('react-native-vector-icons/Ionicons').default; + +interface Props { + checked: boolean; + accessibilityLabel?: string; + onChange?: (checked: boolean)=> void; + style?: TextStyle; + iconStyle?: TextStyle; +} + +const useStyles = (baseStyles: TextStyle|undefined, iconStyle: TextStyle|undefined) => { + return useMemo(() => { + return StyleSheet.create({ + container: { + ...(baseStyles ?? {}), + justifyContent: 'center', + alignItems: 'center', + }, + icon: { + fontSize: 20, + height: 22, + color: baseStyles?.color, + ...iconStyle, + }, + }); + }, [baseStyles, iconStyle]); +}; + +const Checkbox: React.FC = props => { + const [checked, setChecked] = useState(props.checked); + + useEffect(() => { + setChecked(props.checked); + }, [props.checked]); + + const onPress = useCallback(() => { + setChecked(checked => { + const newChecked = !checked; + props.onChange?.(newChecked); + return newChecked; + }); + }, [props.onChange]); + + const iconName = checked ? 'checkbox-outline' : 'square-outline'; + const styles = useStyles(props.style, props.iconStyle); + + const accessibilityState = useMemo(() => ({ + checked, + }), [checked]); + + return ( + + + + ); +}; + +export default Checkbox; diff --git a/packages/app-mobile/components/note-item.js b/packages/app-mobile/components/NoteItem.tsx similarity index 62% rename from packages/app-mobile/components/note-item.js rename to packages/app-mobile/components/NoteItem.tsx index b7615c98b..3e55ce0f0 100644 --- a/packages/app-mobile/components/note-item.js +++ b/packages/app-mobile/components/NoteItem.tsx @@ -1,34 +1,40 @@ -const React = require('react'); -const Component = React.Component; -const { connect } = require('react-redux'); -const { Text, TouchableOpacity, View, StyleSheet } = require('react-native'); -const { Checkbox } = require('./checkbox.js'); -const Note = require('@joplin/lib/models/Note').default; -const time = require('@joplin/lib/time').default; -const { themeStyle } = require('./global-style'); -const { _ } = require('@joplin/lib/locale'); +import * as React from 'react'; +import { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import { Text, TouchableOpacity, View, StyleSheet, TextStyle, ViewStyle } from 'react-native'; +import Checkbox from './Checkbox'; +import Note from '@joplin/lib/models/Note'; +import time from '@joplin/lib/time'; +import { themeStyle } from './global-style'; +import { _ } from '@joplin/lib/locale'; +import { AppState } from '../utils/types'; +import { Dispatch } from 'redux'; +import { NoteEntity } from '@joplin/lib/services/database/types'; -class NoteItemComponent extends Component { - constructor() { - super(); - this.styles_ = {}; +interface Props { + dispatch: Dispatch; + themeId: number; + note: NoteEntity; + noteSelectionEnabled: boolean; + selectedNoteIds: string[]; +} + +interface State {} + +type Styles = Record; + +class NoteItemComponent extends PureComponent { + private styles_: Record = {}; + public constructor(props: Props) { + super(props); } - noteItem_press(noteId) { - this.props.dispatch({ - type: 'NAV_GO', - routeName: 'Note', - noteId: noteId, - }); - } - - styles() { + private styles() { const theme = themeStyle(this.props.themeId); - if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId]; this.styles_ = {}; - const styles = { + const styles: Record = { listItem: { flexDirection: 'row', // height: 40, @@ -49,6 +55,17 @@ class NoteItemComponent extends Component { selectionWrapper: { backgroundColor: theme.backgroundColor, }, + checkboxStyle: { + color: theme.color, + paddingRight: 10, + paddingTop: theme.itemMarginTop, + paddingBottom: theme.itemMarginBottom, + paddingLeft: theme.marginLeft, + }, + checkedOpacityStyle: { + opacity: 0.4, + }, + uncheckedOpacityStyle: { }, }; styles.listItemWithCheckbox = { ...styles.listItem }; @@ -57,7 +74,7 @@ class NoteItemComponent extends Component { delete styles.listItemWithCheckbox.paddingLeft; styles.listItemTextWithCheckbox = { ...styles.listItemText }; - styles.listItemTextWithCheckbox.marginTop = styles.listItem.paddingTop - 1; + styles.listItemTextWithCheckbox.marginTop = theme.itemMarginTop - 1; styles.listItemTextWithCheckbox.marginBottom = styles.listItem.paddingBottom; styles.selectionWrapperSelected = { ...styles.selectionWrapper }; @@ -67,7 +84,7 @@ class NoteItemComponent extends Component { return this.styles_[this.props.themeId]; } - async todoCheckbox_change(checked) { + private todoCheckbox_change = async (checked: boolean) => { if (!this.props.note) return; const newNote = { @@ -77,9 +94,9 @@ class NoteItemComponent extends Component { await Note.save(newNote); this.props.dispatch({ type: 'NOTE_SORT' }); - } + }; - onPress() { + private onPress = () => { if (!this.props.note) return; if (this.props.note.encryption_applied) return; @@ -95,38 +112,26 @@ class NoteItemComponent extends Component { noteId: this.props.note.id, }); } - } + }; - onLongPress() { + private onLongPress = () => { if (!this.props.note) return; this.props.dispatch({ type: this.props.noteSelectionEnabled ? 'NOTE_SELECTION_TOGGLE' : 'NOTE_SELECTION_START', id: this.props.note.id, }); - } + }; - render() { + public render() { const note = this.props.note ? this.props.note : {}; const isTodo = !!Number(note.is_todo); - - const theme = themeStyle(this.props.themeId); - - // IOS: display: none crashes the app - const checkboxStyle = !isTodo ? { display: 'none' } : { color: theme.color }; - - if (isTodo) { - checkboxStyle.paddingRight = 10; - checkboxStyle.paddingTop = theme.itemMarginTop; - checkboxStyle.paddingBottom = theme.itemMarginBottom; - checkboxStyle.paddingLeft = theme.marginLeft; - } - const checkboxChecked = !!Number(note.todo_completed); + const checkboxStyle = this.styles().checkboxStyle; const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem; const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText; - const opacityStyle = isTodo && checkboxChecked ? { opacity: 0.4 } : {}; + const opacityStyle = isTodo && checkboxChecked ? this.styles().checkedOpacityStyle : this.styles().uncheckedOpacityStyle; const isSelected = this.props.noteSelectionEnabled && this.props.selectedNoteIds.indexOf(note.id) >= 0; const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper; @@ -134,16 +139,16 @@ class NoteItemComponent extends Component { const noteTitle = Note.displayTitle(note); return ( - this.onPress()} onLongPress={() => this.onLongPress()} activeOpacity={0.5}> + - this.todoCheckbox_change(checked)} + onChange={this.todoCheckbox_change} accessibilityLabel={_('to-do: %s', noteTitle)} - /> + /> : null } {noteTitle} @@ -153,7 +158,7 @@ class NoteItemComponent extends Component { } } -const NoteItem = connect(state => { +export default connect((state: AppState) => { return { themeId: state.settings.theme, noteSelectionEnabled: state.noteSelectionEnabled, @@ -161,4 +166,3 @@ const NoteItem = connect(state => { }; })(NoteItemComponent); -module.exports = { NoteItem }; diff --git a/packages/app-mobile/components/NoteList.tsx b/packages/app-mobile/components/NoteList.tsx index e5a6b9dd8..bd4e5e2de 100644 --- a/packages/app-mobile/components/NoteList.tsx +++ b/packages/app-mobile/components/NoteList.tsx @@ -10,7 +10,7 @@ import getEmptyFolderMessage from '@joplin/lib/components/shared/NoteList/getEmp import Folder from '@joplin/lib/models/Folder'; const { _ } = require('@joplin/lib/locale'); -const { NoteItem } = require('./note-item.js'); +import NoteItem from './NoteItem'; import { themeStyle } from './global-style'; interface NoteListProps { diff --git a/packages/app-mobile/components/checkbox.js b/packages/app-mobile/components/checkbox.js deleted file mode 100644 index 3c71c72cf..000000000 --- a/packages/app-mobile/components/checkbox.js +++ /dev/null @@ -1,77 +0,0 @@ -const React = require('react'); -const Component = React.Component; -const { View, TouchableHighlight } = require('react-native'); -const Icon = require('react-native-vector-icons/Ionicons').default; - -const styles = { - checkboxIcon: { - fontSize: 20, - height: 22, - // marginRight: 10, - }, -}; - -class Checkbox extends Component { - constructor() { - super(); - this.state = { - checked: false, - }; - } - - UNSAFE_componentWillMount() { - this.setState({ checked: this.props.checked }); - } - - UNSAFE_componentWillReceiveProps(newProps) { - if ('checked' in newProps) { - this.setState({ checked: newProps.checked }); - } - } - - onPress() { - const newChecked = !this.state.checked; - this.setState({ checked: newChecked }); - if (this.props.onChange) this.props.onChange(newChecked); - } - - render() { - const iconName = this.state.checked ? 'checkbox-outline' : 'square-outline'; - - const style = this.props.style ? { ...this.props.style } : {}; - style.justifyContent = 'center'; - style.alignItems = 'center'; - - const checkboxIconStyle = { ...styles.checkboxIcon }; - if (style.color) checkboxIconStyle.color = style.color; - - if (style.paddingTop) checkboxIconStyle.marginTop = style.paddingTop; - if (style.paddingBottom) checkboxIconStyle.marginBottom = style.paddingBottom; - if (style.paddingLeft) checkboxIconStyle.marginLeft = style.paddingLeft; - if (style.paddingRight) checkboxIconStyle.marginRight = style.paddingRight; - - const thStyle = { - justifyContent: 'center', - alignItems: 'center', - }; - - if (style && style.display === 'none') return ; - - // if (style.display) thStyle.display = style.display; - - return ( - this.onPress()} - style={thStyle} - accessibilityRole="checkbox" - accessibilityState={{ - checked: this.state.checked, - }} - accessibilityLabel={this.props.accessibilityLabel ?? ''}> - - - ); - } -} - -module.exports = { Checkbox }; diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index cdf337162..dd97b919c 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -26,7 +26,7 @@ import * as mimeUtils from '@joplin/lib/mime-utils'; import ScreenHeader, { MenuOptionType } from '../ScreenHeader'; import NoteTagsDialog from './NoteTagsDialog'; import time from '@joplin/lib/time'; -const { Checkbox } = require('../checkbox.js'); +import Checkbox from '../Checkbox'; import { _, currentLocale } from '@joplin/lib/locale'; import { reg } from '@joplin/lib/registry'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; diff --git a/packages/app-mobile/components/screens/search.tsx b/packages/app-mobile/components/screens/search.tsx index 7a12255c3..92bb444ab 100644 --- a/packages/app-mobile/components/screens/search.tsx +++ b/packages/app-mobile/components/screens/search.tsx @@ -6,7 +6,7 @@ import ScreenHeader from '../ScreenHeader'; const Icon = require('react-native-vector-icons/Ionicons').default; import { _ } from '@joplin/lib/locale'; import Note from '@joplin/lib/models/Note'; -const { NoteItem } = require('../note-item.js'); +import NoteItem from '../NoteItem'; const { BaseScreenComponent } = require('../base-screen'); import { themeStyle } from '../global-style'; const DialogBox = require('react-native-dialogbox').default;