From a27c82da90cdcc32ff71c595b28a2509917b01f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 19 Aug 2025 14:01:56 +0200 Subject: [PATCH] Refactor and cleanup of option item (#9067) * Refactor and cleanup of option item * Fix tests and minor cleanup * Fix bug and address feedback --- .../convert_to_channel_label.tsx | 6 +- .../channel_bookmarks/bookmark_type.tsx | 2 +- app/components/files/file_filter.tsx | 4 +- .../option_menus/option_menus.tsx | 6 +- app/components/option_item/index.tsx | 184 +++++---- .../option_item/option_item.test.tsx | 378 ++++++++++++++++++ app/components/option_item/radio_item.tsx | 2 +- app/components/settings/block.tsx | 3 - app/components/settings/container.tsx | 1 + app/components/settings/item.tsx | 21 - app/components/settings/option.tsx | 26 -- app/components/settings/separator.tsx | 45 +-- .../channel_info/app_bindings/index.tsx | 10 +- .../muted_banner.tsx | 1 - .../notify_about.tsx | 5 +- app/screens/component_library/hooks.tsx | 33 ++ app/screens/component_library/index.tsx | 2 + .../component_library/option_item.cl.tsx | 80 ++++ .../convert_gm_to_channel/team_selector.tsx | 53 ++- .../channel_info_form.tsx | 28 +- .../home/account/components/options/index.tsx | 5 +- .../components/options/logout/index.tsx | 22 +- .../components/options/settings/index.tsx | 6 +- .../components/options/your_profile/index.tsx | 9 +- .../home/search/initial/modifiers/index.tsx | 5 +- .../search/initial/modifiers/modifier.tsx | 29 +- .../search/initial/modifiers/show_more.tsx | 35 +- .../search/initial/recent_searches/index.tsx | 6 +- .../initial/recent_searches/recent_item.tsx | 8 - app/screens/home/search/search.tsx | 14 +- .../components/picker_option.tsx | 8 - app/screens/settings/advanced/advanced.tsx | 25 +- .../settings/display_clock/display_clock.tsx | 6 +- .../settings/display_crt/display_crt.tsx | 6 +- .../settings/display_theme/custom_theme.tsx | 4 +- .../settings/display_theme/theme_tiles.tsx | 11 +- .../display_timezone/display_timezone.tsx | 35 +- .../display_timezone_select/timezone_row.tsx | 7 +- .../notification_auto_responder.tsx | 6 +- .../notification_call/notification_call.tsx | 11 +- .../notification_email/notification_email.tsx | 65 +-- .../notification_mention/mention_settings.tsx | 8 +- .../notification_push/push_status.tsx | 9 +- .../notification_push/push_thread.tsx | 9 +- ...end_test_notification_notice.test.tsx.snap | 2 +- .../send_test_notification_notice.tsx | 14 +- .../report_problem/report_problem.tsx | 8 +- app/screens/settings/settings.tsx | 27 +- 48 files changed, 814 insertions(+), 476 deletions(-) create mode 100644 app/components/option_item/option_item.test.tsx create mode 100644 app/screens/component_library/option_item.cl.tsx diff --git a/app/components/channel_actions/convert_to_channel/convert_to_channel_label.tsx b/app/components/channel_actions/convert_to_channel/convert_to_channel_label.tsx index 5758f5908..ff49b819b 100644 --- a/app/components/channel_actions/convert_to_channel/convert_to_channel_label.tsx +++ b/app/components/channel_actions/convert_to_channel/convert_to_channel_label.tsx @@ -6,8 +6,8 @@ import {useIntl} from 'react-intl'; import OptionItem from '@components/option_item'; import {Screens} from '@constants'; +import {usePreventDoubleTap} from '@hooks/utils'; import {dismissBottomSheet, goToScreen} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; type Props = { channelId: string; @@ -16,11 +16,11 @@ type Props = { const ConvertToChannelLabel = ({channelId}: Props) => { const {formatMessage} = useIntl(); - const goToConvertToPrivateChannel = useCallback(preventDoubleTap(async () => { + const goToConvertToPrivateChannel = usePreventDoubleTap(useCallback(async () => { await dismissBottomSheet(); const title = formatMessage({id: 'channel_info.convert_gm_to_channel.screen_title', defaultMessage: 'Convert to Private Channel'}); goToScreen(Screens.CONVERT_GM_TO_CHANNEL, title, {channelId}); - }), [channelId]); + }, [channelId, formatMessage])); return ( { }, }; showModal(Screens.CHANNEL_BOOKMARK, title, {channelId, closeButtonId, type, ownerId}, options); - }, [channelId, theme, type, ownerId]); + }, [formatMessage, theme.sidebarHeaderTextColor, channelId, type, ownerId]); let icon; let label; diff --git a/app/components/files/file_filter.tsx b/app/components/files/file_filter.tsx index ef5b8d326..55ba51fa7 100644 --- a/app/components/files/file_filter.tsx +++ b/app/components/files/file_filter.tsx @@ -99,7 +99,7 @@ const File_filter = ({initialFilter, setFilter, title}: FilterProps) => { setFilter(fileType); } dismissBottomSheet(); - }, [initialFilter]); + }, [initialFilter, setFilter]); const separator = useCallback(() => , [style]); @@ -112,7 +112,7 @@ const File_filter = ({initialFilter, setFilter, title}: FilterProps) => { selected={initialFilter === item.filterType} /> ); - }, [handleOnPress, initialFilter, theme]); + }, [handleOnPress, initialFilter, intl]); return ( { if (!isTablet) { await dismissBottomSheet(); } setAction('copying'); - }, [setAction]); + }, [isTablet, setAction]); const handlePermalink = useCallback(async () => { if (fileInfo.post_id) { @@ -51,7 +51,7 @@ const OptionMenus = ({ showPermalink(serverUrl, '', fileInfo.post_id); setAction('opening'); } - }, [intl, serverUrl, fileInfo.post_id, setAction]); + }, [fileInfo.post_id, isTablet, serverUrl, setAction]); return ( <> diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx index ff66535c2..1f0514a0b 100644 --- a/app/components/option_item/index.tsx +++ b/app/components/option_item/index.tsx @@ -2,8 +2,9 @@ // See LICENSE.txt for license information. import React, {useCallback, useMemo} from 'react'; -import {type LayoutChangeEvent, Platform, type StyleProp, Switch, Text, type TextStyle, TouchableOpacity, View, type ViewStyle} from 'react-native'; +import {type LayoutChangeEvent, Platform, Switch, Text, TouchableOpacity, View} from 'react-native'; +import UserChip from '@components/chips/user_chip'; import CompassIcon from '@components/compass_icon'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {useTheme} from '@context/theme'; @@ -11,7 +12,9 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import OptionIcon from './option_icon'; -import RadioItem, {type RadioItemProps} from './radio_item'; +import RadioItem from './radio_item'; + +import type UserModel from '@typings/database/models/servers/user'; const TouchableOptionTypes = { ARROW: 'arrow', @@ -19,7 +22,8 @@ const TouchableOptionTypes = { RADIO: 'radio', REMOVE: 'remove', SELECT: 'select', -}; + LINK: 'link', +} as const; const OptionTypeConst = { NONE: 'none', @@ -43,18 +47,15 @@ const hitSlop = {top: 11, bottom: 11, left: 11, right: 11}; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { actionContainer: { - flex: 1, flexDirection: 'row', alignItems: 'center', - marginLeft: 16, - }, - actionSubContainer: { - marginLeft: 'auto', }, container: { flexDirection: 'row', alignItems: 'center', minHeight: ITEM_HEIGHT, + gap: 12, + justifyContent: 'space-between', }, destructive: { color: theme.dndIndicator, @@ -66,7 +67,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }, iconContainer: {marginRight: 16}, info: { - flex: 1, textAlign: 'right', color: changeOpacity(theme.centerChannelColor, 0.56), ...typography('Body', 100), @@ -89,6 +89,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { justifyContent: 'center', }, labelContainer: { + flex: 1, flexDirection: 'row', alignItems: 'center', }, @@ -96,48 +97,38 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { color: theme.centerChannelColor, ...typography('Body', 200), }, - removeContainer: { - flex: 1, - alignItems: 'flex-end', - color: theme.centerChannelColor, - marginRight: 20, - ...typography('Body', 200), - }, - row: { - flex: 3, - flexDirection: 'row', - }, }; }); +type UserChipData = { + user: UserModel; + onPress: (id: string) => void; + teammateNameDisplay: string; +} + export type OptionItemProps = { action?: (React.Dispatch>)|((value: string | boolean) => void); - arrowStyle?: StyleProp>; - containerStyle?: StyleProp; description?: string; destructive?: boolean; icon?: string; iconColor?: string; - info?: string; + info?: string | UserChipData; inline?: boolean; label: string; - labelContainerStyle?: StyleProp; onRemove?: () => void; - optionDescriptionTextStyle?: StyleProp; - optionLabelTextStyle?: StyleProp; - radioItemProps?: Partial; selected?: boolean; testID?: string; type: OptionType; value?: string; onLayout?: (event: LayoutChangeEvent) => void; descriptionNumberOfLines?: number; + longInfo?: boolean; + nonDestructiveDescription?: boolean; + isRadioCheckmark?: boolean; } const OptionItem = ({ action, - arrowStyle, - containerStyle, description, destructive, icon, @@ -145,40 +136,57 @@ const OptionItem = ({ info, inline = false, label, - labelContainerStyle, onRemove, - optionDescriptionTextStyle, - optionLabelTextStyle, - radioItemProps, selected, testID = 'optionItem', type, value, onLayout, descriptionNumberOfLines, + longInfo, + nonDestructiveDescription = false, + isRadioCheckmark = false, }: OptionItemProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); const isInLine = inline && Boolean(description); + const shouldDescriptionShowDestructive = destructive && !nonDestructiveDescription; + + const labelContainerStyle = useMemo(() => { + const extraStyle = longInfo ? {flex: undefined} : {}; + return [styles.labelContainer, extraStyle]; + }, [longInfo, styles.labelContainer]); const labelStyle = useMemo(() => { return isInLine ? styles.inlineLabel : styles.label; - }, [inline, styles, isInLine]); + }, [styles, isInLine]); const labelTextStyle = useMemo(() => { return [ isInLine ? styles.inlineLabelText : styles.labelText, destructive && styles.destructive, + type === 'link' && {color: theme.linkColor}, ]; - }, [destructive, styles, isInLine]); + }, [destructive, styles, isInLine, type, theme.linkColor]); const descriptionTextStyle = useMemo(() => { return [ isInLine ? styles.inlineDescription : styles.description, - destructive && styles.destructive, + shouldDescriptionShowDestructive && styles.destructive, ]; - }, [destructive, styles, isInLine]); + }, [ + isInLine, + styles.inlineDescription, + styles.description, + styles.destructive, + shouldDescriptionShowDestructive, + ]); + + const actionContainerStyle = useMemo(() => { + const extraStyle = longInfo ? {maxWidth: 300} : {}; + return [styles.actionContainer, extraStyle]; + }, [longInfo, styles.actionContainer]); let actionComponent; let radioComponent; @@ -196,8 +204,8 @@ const OptionItem = ({ radioComponent = ( ); } else if (type === OptionTypeConst.TOGGLE) { @@ -223,7 +231,7 @@ const OptionItem = ({ color={changeOpacity(theme.centerChannelColor, 0.32)} name='chevron-right' size={24} - style={arrowStyle} + testID={`${testID}.arrow.icon`} /> ); } else if (type === OptionTypeConst.REMOVE) { @@ -231,7 +239,7 @@ const OptionItem = ({ @@ -248,64 +256,74 @@ const OptionItem = ({ action?.(value || ''); }, [value, action]); + let infoComponent; + if (typeof info === 'object') { + infoComponent = ( + + + + ); + } else if (info) { + infoComponent = ( + + {info} + + ); + } + const component = ( - - - {Boolean(icon) && ( - - - - )} - {type === OptionTypeConst.RADIO && radioComponent} - - - {label} - - {Boolean(description) && - - {description} - - } + + {Boolean(icon) && ( + + - - - {Boolean(actionComponent || info) && - - { - Boolean(info) && + )} + {type === OptionTypeConst.RADIO && radioComponent} + - {info} + {label} - } - - {actionComponent} + {Boolean(description) && + + {description} + + } + {Boolean(actionComponent || infoComponent) && + + {infoComponent} + {actionComponent} + } ); - if (Object.values(TouchableOptionTypes).includes(type)) { + if ((Object.values(TouchableOptionTypes) as string[]).includes(type)) { return ( {component} diff --git a/app/components/option_item/option_item.test.tsx b/app/components/option_item/option_item.test.tsx new file mode 100644 index 000000000..4cf8ebb32 --- /dev/null +++ b/app/components/option_item/option_item.test.tsx @@ -0,0 +1,378 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {type ComponentProps} from 'react'; + +import UserChip from '@components/chips/user_chip'; +import Preferences from '@constants/preferences'; +import {act, fireEvent, renderWithIntlAndTheme} from '@test/intl-test-helper'; +import TestHelper from '@test/test_helper'; + +import OptionIcon from './option_icon'; +import RadioItem from './radio_item'; + +import OptionItem from './index'; + +jest.mock('./option_icon'); +jest.mocked(OptionIcon).mockImplementation((props) => React.createElement('OptionIcon', {...props, testID: 'option-item.icon.mock'})); + +jest.mock('@components/chips/user_chip'); +jest.mocked(UserChip).mockImplementation((props) => React.createElement('UserChip', {...props, testID: 'option-item.user_chip.mock'})); + +jest.mock('./radio_item'); +jest.mocked(RadioItem).mockImplementation((props) => React.createElement('RadioItem', props)); + +describe('OptionItem', () => { + function getBaseProps(): ComponentProps { + return { + label: 'Test Option', + type: 'default', + testID: 'option-item', + }; + } + + it('renders correctly with default props', () => { + const props = getBaseProps(); + const {getByTestId, getByText, queryByTestId} = renderWithIntlAndTheme(); + + const optionItem = getByTestId('option-item'); + expect(optionItem).toBeTruthy(); + const label = getByText('Test Option'); + expect(label).toBeTruthy(); + expect(label).toHaveStyle({fontWeight: '400'}); + + // No description + expect(queryByTestId('option-item.description')).toBeNull(); + + // No radio component + expect(queryByTestId('option-item.selected')).toBeNull(); + expect(queryByTestId('option-item.not_selected')).toBeNull(); + + // No icon + expect(queryByTestId('option-item.icon.mock')).toBeNull(); + + // No user chip + expect(queryByTestId('option-item.user_chip.mock')).toBeNull(); + + // No info + expect(queryByTestId('option-item.info')).toBeNull(); + + // No action whatsoever + expect(queryByTestId('option-item.selected')).toBeNull(); + expect(queryByTestId('option-item.toggled.false.button')).toBeNull(); + expect(queryByTestId('option-item.toggled.true.button')).toBeNull(); + expect(queryByTestId('option-item.remove.button')).toBeNull(); + expect(queryByTestId('option-item.arrow.icon')).toBeNull(); + }); + + it('renders with description when provided', () => { + const props = getBaseProps(); + props.description = 'Test description'; + + const {getByText, rerender} = renderWithIntlAndTheme(); + + let description = getByText('Test description'); + expect(description).toBeTruthy(); + expect(description.props.numberOfLines).toBeUndefined(); + expect(description).not.toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + + props.descriptionNumberOfLines = 2; + props.destructive = true; + rerender(); + + description = getByText('Test description'); + expect(description).toBeTruthy(); + expect(description.props.numberOfLines).toBe(2); + expect(description).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + }); + + it('renders with icon when provided', () => { + const props = getBaseProps(); + props.icon = 'test-icon'; + props.iconColor = '#ff0000'; + + const {getByTestId, rerender} = renderWithIntlAndTheme(); + + let optionItem = getByTestId('option-item.icon.mock'); + expect(optionItem.props.icon).toBe('test-icon'); + expect(optionItem.props.iconColor).toBe('#ff0000'); + expect(optionItem.props.destructive).toBeFalsy(); + + props.destructive = true; + rerender(); + + optionItem = getByTestId('option-item.icon.mock'); + expect(optionItem.props.destructive).toBeTruthy(); + }); + + it('renders with destructive styling when destructive is true', () => { + const props = getBaseProps(); + props.destructive = true; + + const {getByText, rerender} = renderWithIntlAndTheme(); + + let label = getByText('Test Option'); + expect(label).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + + props.destructive = false; + rerender(); + + label = getByText('Test Option'); + expect(label).not.toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + }); + + it('renders with inline layout when inline is true and description exists', () => { + const props = getBaseProps(); + props.inline = true; + + const {getByText, rerender} = renderWithIntlAndTheme(); + + // No inline style yet + expect(getByText('Test Option')).toHaveStyle({fontWeight: '400'}); + + props.description = 'Test description'; + rerender(); + + expect(getByText('Test Option')).toHaveStyle({fontWeight: '600'}); + expect(getByText('Test description')).toHaveStyle({color: Preferences.THEMES.denim.centerChannelColor}); + }); + + it('renders description as destructive only if nonDestructiveDescription is false', () => { + const props = getBaseProps(); + props.description = 'Test description'; + props.destructive = true; + props.nonDestructiveDescription = false; + + const {rerender, getByText} = renderWithIntlAndTheme(); + + let label = getByText('Test Option'); + let description = getByText('Test description'); + expect(label).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + expect(description).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + + props.nonDestructiveDescription = true; + rerender(); + + label = getByText('Test Option'); + description = getByText('Test description'); + expect(label).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + expect(description).not.toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + }); + + it('renders with info text when provided', () => { + const props = getBaseProps(); + props.info = 'Additional info'; + + const {getByText} = renderWithIntlAndTheme(); + + const info = getByText('Additional info'); + expect(info).toBeTruthy(); + }); + + it('renders with destructive info styling when destructive is true', () => { + const props = getBaseProps(); + props.info = 'Destructive info'; + props.destructive = true; + + const {getByText, rerender} = renderWithIntlAndTheme(); + + let info = getByText('Destructive info'); + expect(info).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + + props.destructive = false; + rerender(); + + info = getByText('Destructive info'); + expect(info).not.toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); + }); + + it('renders with user chip when info is UserChipData', () => { + const props = getBaseProps(); + props.info = { + user: TestHelper.fakeUserModel(), + onPress: jest.fn(), + teammateNameDisplay: 'nickname_full_name', + }; + + const {getByTestId} = renderWithIntlAndTheme(); + + const userChip = getByTestId('option-item.user_chip.mock'); + expect(userChip).toBeTruthy(); + expect(userChip.props.user).toBe(props.info.user); + expect(userChip.props.onPress).toBe(props.info.onPress); + expect(userChip.props.teammateNameDisplay).toBe(props.info.teammateNameDisplay); + }); + + it('shows link type correctly', () => { + const props = getBaseProps(); + props.type = 'link'; + props.label = 'Test Link'; + + const {getByText} = renderWithIntlAndTheme(); + + const link = getByText('Test Link'); + expect(link).toBeTruthy(); + expect(link).toHaveStyle({color: Preferences.THEMES.denim.linkColor}); + }); + + it('shows select type correctly', () => { + const props = getBaseProps(); + props.type = 'select'; + props.selected = true; + + const {getByTestId, rerender, queryByTestId} = renderWithIntlAndTheme(); + + let selectedIcon = getByTestId('option-item.selected'); + expect(selectedIcon).toBeTruthy(); + + props.selected = false; + rerender(); + + selectedIcon = queryByTestId('option-item.selected'); + expect(selectedIcon).toBeNull(); + }); + + it('shows radio type correctly', () => { + const props = getBaseProps(); + props.type = 'radio'; + props.selected = true; + props.isRadioCheckmark = true; + + const {getByTestId, rerender, queryByTestId} = renderWithIntlAndTheme(); + + let radioItem = getByTestId('option-item.selected'); + expect(radioItem).toBeTruthy(); + expect(radioItem.props.checkedBody).toBe(true); + expect(radioItem.props.selected).toBe(true); + + props.selected = false; + rerender(); + + radioItem = queryByTestId('option-item.not_selected'); + expect(radioItem).toBeTruthy(); + expect(radioItem.props.checkedBody).toBe(true); + expect(radioItem.props.selected).toBe(false); + + props.isRadioCheckmark = false; + rerender(); + + radioItem = queryByTestId('option-item.not_selected'); + expect(radioItem).toBeTruthy(); + expect(radioItem.props.checkedBody).toBe(false); + expect(radioItem.props.selected).toBe(false); + }); + + it('shows toggle type correctly', () => { + const props = getBaseProps(); + props.type = 'toggle'; + props.selected = true; + props.action = jest.fn(); + + const {getByTestId, rerender} = renderWithIntlAndTheme(); + + let toggle = getByTestId('option-item.toggled.true.button'); + expect(toggle).toBeTruthy(); + + props.selected = false; + rerender(); + + toggle = getByTestId('option-item.toggled.false.button'); + expect(toggle).toBeTruthy(); + }); + + it('calls action for toggles', () => { + const props = getBaseProps(); + props.type = 'toggle'; + props.selected = true; + props.action = jest.fn(); + + const {getByTestId} = renderWithIntlAndTheme(); + + const toggle = getByTestId('option-item.toggled.true.button'); + act(() => { + fireEvent(toggle, 'valueChange', false); + }); + expect(props.action).toHaveBeenCalledWith(false); + + act(() => { + fireEvent(toggle, 'valueChange', true); + }); + expect(props.action).toHaveBeenCalledWith(true); + }); + + it('doesnt call action for toggles when text is pressed', () => { + const props = getBaseProps(); + props.type = 'toggle'; + props.selected = true; + props.action = jest.fn(); + + const {getByTestId} = renderWithIntlAndTheme(); + + const optionItem = getByTestId('option-item'); + expect(optionItem).toBeTruthy(); + + act(() => { + fireEvent.press(optionItem); + }); + expect(props.action).not.toHaveBeenCalled(); + + const toggle = getByTestId('option-item.toggled.true.button'); + act(() => { + fireEvent(toggle, 'valueChange', false); + }); + expect(props.action).toHaveBeenCalled(); + }); + + it('shows arrow type correctly', () => { + const props = getBaseProps(); + props.type = 'arrow'; + + const {getByTestId} = renderWithIntlAndTheme(); + + const arrowIcon = getByTestId('option-item.arrow.icon'); + expect(arrowIcon).toBeTruthy(); + }); + + it('shows remove type correctly', () => { + const props = getBaseProps(); + props.type = 'remove'; + props.action = jest.fn(); + props.onRemove = jest.fn(); + + const {getByTestId} = renderWithIntlAndTheme(); + + const removeButton = getByTestId('option-item.remove.button'); + expect(removeButton).toBeTruthy(); + + act(() => { + fireEvent.press(removeButton); + }); + expect(props.onRemove).toHaveBeenCalled(); + expect(props.action).not.toHaveBeenCalled(); + + jest.mocked(props.onRemove).mockClear(); + jest.mocked(props.action).mockClear(); + + act(() => { + fireEvent.press(getByTestId('option-item')); + }); + expect(props.action).toHaveBeenCalled(); + expect(props.onRemove).not.toHaveBeenCalled(); + }); + + it('doesnt call action for none type', () => { + const props = getBaseProps(); + props.type = 'none'; + props.action = jest.fn(); + + const {getByTestId} = renderWithIntlAndTheme(); + + const optionItem = getByTestId('option-item'); + expect(optionItem).toBeTruthy(); + + act(() => { + fireEvent.press(optionItem); + }); + expect(props.action).not.toHaveBeenCalled(); + }); +}); diff --git a/app/components/option_item/radio_item.tsx b/app/components/option_item/radio_item.tsx index 4978d4fd6..6722fdcd9 100644 --- a/app/components/option_item/radio_item.tsx +++ b/app/components/option_item/radio_item.tsx @@ -58,7 +58,7 @@ const RadioItem = ({selected, checkedBody, testID}: RadioItemProps) => { } return (); - }, [checkedBody]); + }, [checkedBody, styles.center, styles.checkedBodyContainer, theme.buttonColor]); return ( { color: theme.centerChannelColor, ...typography('Heading', 300, 'SemiBold'), marginBottom: 8, - marginLeft: 20, marginTop: 12, - marginRight: 15, }, footer: { marginTop: 10, - marginHorizontal: 15, fontSize: 12, color: changeOpacity(theme.centerChannelColor, 0.5), }, diff --git a/app/components/settings/container.tsx b/app/components/settings/container.tsx index 6314a902b..c54fceda6 100644 --- a/app/components/settings/container.tsx +++ b/app/components/settings/container.tsx @@ -18,6 +18,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { container: { flex: 1, backgroundColor: theme.centerChannelBg, + paddingHorizontal: 20, }, contentContainerStyle: { marginTop: 8, diff --git a/app/components/settings/item.tsx b/app/components/settings/item.tsx index 98bbd5ceb..6710ec803 100644 --- a/app/components/settings/item.tsx +++ b/app/components/settings/item.tsx @@ -6,9 +6,6 @@ import {useIntl} from 'react-intl'; import {Platform} from 'react-native'; import OptionItem, {type OptionItemProps} from '@components/option_item'; -import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; import Options, {DisplayOptionConfig, NotificationsOptionConfig, SettingOptionConfig} from '../../screens/settings/config'; @@ -21,19 +18,6 @@ type SettingOptionProps = { separator?: boolean; } & Partial; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { - return { - menuLabel: { - color: theme.centerChannelColor, - ...typography('Body', 200, 'Regular'), - }, - chevronStyle: { - marginRight: 14, - color: changeOpacity(theme.centerChannelColor, 0.32), - }, - }; -}); - const SettingItem = ({ info, onPress, @@ -41,9 +25,7 @@ const SettingItem = ({ separator = true, ...props }: SettingOptionProps) => { - const theme = useTheme(); const intl = useIntl(); - const styles = getStyleSheet(theme); const config = Options[optionName]; const label = props.label || intl.formatMessage({id: config.i18nId, defaultMessage: config.defaultMessage}); @@ -52,12 +34,9 @@ const SettingItem = ({ <> diff --git a/app/components/settings/option.tsx b/app/components/settings/option.tsx index 5fd044c82..e54bc4820 100644 --- a/app/components/settings/option.tsx +++ b/app/components/settings/option.tsx @@ -5,38 +5,12 @@ import React from 'react'; import {Platform} from 'react-native'; import OptionItem, {type OptionItemProps} from '@components/option_item'; -import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - paddingHorizontal: 20, - }, - optionLabelTextStyle: { - color: theme.centerChannelColor, - ...typography('Body', 200, 'Regular'), - marginBottom: 4, - }, - optionDescriptionTextStyle: { - color: changeOpacity(theme.centerChannelColor, 0.64), - ...typography('Body', 75, 'Regular'), - }, - }; -}); const SettingOption = ({...props}: OptionItemProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); - const useRadioButton = props.type === 'select' && Platform.OS === 'android'; return ( diff --git a/app/components/settings/separator.tsx b/app/components/settings/separator.tsx index 04ba9ec6f..c8f6ff29d 100644 --- a/app/components/settings/separator.tsx +++ b/app/components/settings/separator.tsx @@ -2,45 +2,24 @@ // See LICENSE.txt for license information. import React from 'react'; -import {Platform, type StyleProp, View, type ViewStyle} from 'react-native'; +import {Platform} from 'react-native'; -import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import MenuDivider from '@components/menu_divider'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - const groupSeparator: ViewStyle = { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.12), - width: '91%', - alignSelf: 'center', - height: 1, - }; - return { - separator: { - ...Platform.select({ - ios: { - ...groupSeparator, - }, - default: { - display: 'none', - }, - }), - }, - groupSeparator: { - ...groupSeparator, - marginBottom: 16, - }, - }; -}); type SettingSeparatorProps = { - lineStyles?: StyleProp; isGroupSeparator?: boolean; } -const SettingSeparator = ({lineStyles, isGroupSeparator = false}: SettingSeparatorProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); - - return (); +const SettingSeparator = ({isGroupSeparator = false}: SettingSeparatorProps) => { + if (Platform.OS === 'android') { + return null; + } + return ( + + ); }; export default SettingSeparator; diff --git a/app/screens/channel_info/app_bindings/index.tsx b/app/screens/channel_info/app_bindings/index.tsx index f64c5dcb9..da48540c1 100644 --- a/app/screens/channel_info/app_bindings/index.tsx +++ b/app/screens/channel_info/app_bindings/index.tsx @@ -8,9 +8,9 @@ import {postEphemeralCallResponseForChannel} from '@actions/remote/apps'; import OptionItem from '@components/option_item'; import {AppBindingLocations} from '@constants/apps'; import {useAppBinding} from '@hooks/apps'; +import {usePreventDoubleTap} from '@hooks/utils'; import AppsManager from '@managers/apps_manager'; import {observeCurrentTeamId} from '@queries/servers/system'; -import {preventDoubleTap} from '@utils/tap'; import type {WithDatabaseArgs} from '@typings/database/database'; @@ -39,13 +39,13 @@ const ChannelInfoAppBindings = ({channelId, teamId, dismissChannelInfo, serverUr const handleBindingSubmit = useAppBinding(context, config); - const onPress = useCallback(preventDoubleTap(async (binding: AppBinding) => { + const onPress = usePreventDoubleTap(useCallback(async (binding: AppBinding) => { const submitPromise = handleBindingSubmit(binding); await dismissChannelInfo(); const finish = await submitPromise; await finish(); - }), [handleBindingSubmit]); + }, [dismissChannelInfo, handleBindingSubmit])); const options = bindings.map((binding) => ( void}) => { - const handlePress = useCallback(preventDoubleTap(() => { + const handlePress = usePreventDoubleTap(useCallback(() => { onPress(binding); - }), [binding, onPress]); + }, [binding, onPress])); return ( ({ container: { backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16), borderRadius: 4, - marginHorizontal: 20, marginVertical: 12, paddingHorizontal: 16, height: MUTED_BANNER_HEIGHT, diff --git a/app/screens/channel_notification_preferences/notify_about.tsx b/app/screens/channel_notification_preferences/notify_about.tsx index a53d85439..27fa82e2d 100644 --- a/app/screens/channel_notification_preferences/notify_about.tsx +++ b/app/screens/channel_notification_preferences/notify_about.tsx @@ -65,7 +65,10 @@ const NotifyAbout = ({ const {y} = e.nativeEvent.layout; notifyTitleTop.value = y > 0 ? y + 10 : BLOCK_TITLE_HEIGHT; - }, []); + + // NotifyTitleTop is a shared value, so its reference should not change between renders. + // we add it to the dependencies to satisfy the linter. + }, [notifyTitleTop]); let notifyLevelToUse = notifyLevel; if (notifyLevel === NotificationLevel.DEFAULT) { diff --git a/app/screens/component_library/hooks.tsx b/app/screens/component_library/hooks.tsx index 564ea5700..855636d8c 100644 --- a/app/screens/component_library/hooks.tsx +++ b/app/screens/component_library/hooks.tsx @@ -37,6 +37,39 @@ export const useStringProp = ( return [preparedProp, selector]; }; +export const useNumberProp = ( + propName: string, + defaultValue: number, +): HookResult => { + const [value, setValue] = useState(defaultValue); + const stringToValue = useCallback((v: string) => { + const numberValue = parseInt(v); + if (isNaN(numberValue)) { + setValue(undefined); + return; + } + setValue(numberValue); + }, []); + + const selector = useMemo(() => ( + + ), [propName, stringToValue, value]); + const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]); + + return [preparedProp, selector]; +}; + export const useBooleanProp = ( propName: string, defaultValue: boolean, diff --git a/app/screens/component_library/index.tsx b/app/screens/component_library/index.tsx index e3c373327..ad358278a 100644 --- a/app/screens/component_library/index.tsx +++ b/app/screens/component_library/index.tsx @@ -13,6 +13,7 @@ import {popTopScreen} from '@screens/navigation'; import ButtonComponentLibrary from './button.cl'; import ChipComponentLibrary from './chip.cl'; +import OptionItemComponentLibrary from './option_item.cl'; import SectionNoticeComponentLibrary from './section_notice.cl'; import TagComponentLibrary from './tag.cl'; @@ -21,6 +22,7 @@ import type {AvailableScreens} from '@typings/screens/navigation'; const componentMap = { Button: ButtonComponentLibrary, Chip: ChipComponentLibrary, + OptionItem: OptionItemComponentLibrary, Tag: TagComponentLibrary, SectionNotice: SectionNoticeComponentLibrary, }; diff --git a/app/screens/component_library/option_item.cl.tsx b/app/screens/component_library/option_item.cl.tsx new file mode 100644 index 000000000..5b963f805 --- /dev/null +++ b/app/screens/component_library/option_item.cl.tsx @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {Alert, View} from 'react-native'; + +import OptionItem from '@components/option_item'; + +import {useBooleanProp, useDropdownProp, useNumberProp, useStringProp} from './hooks'; +import {buildComponent} from './utils'; + +const propPossibilities = {}; + +const optionTypeValues = ['none', 'toggle', 'arrow', 'default', 'radio', 'remove', 'select', 'link']; + +const onAction = (value: string | boolean) => Alert.alert('Action triggered!', `Value: ${value}`); +const onRemove = () => Alert.alert('Remove triggered!'); + +const OptionItemComponentLibrary = () => { + const [label, labelSelector] = useStringProp('label', 'Option Item Label', false); + const [description, descriptionSelector] = useStringProp('description', 'This is a description for the option item', false); + const [icon, iconSelector] = useStringProp('icon', '', false); + const [iconColor, iconColorSelector] = useStringProp('iconColor', '', false); + const [info, infoSelector] = useStringProp('info', 'Info text', false); + const [type, typePosibilities, typeSelector] = useDropdownProp('type', 'default', optionTypeValues, true); + const [selected, selectedSelector] = useBooleanProp('selected', false); + const [destructive, destructiveSelector] = useBooleanProp('destructive', false); + const [inline, inlineSelector] = useBooleanProp('inline', false); + const [descriptionNumberOfLines, descriptionLinesSelector] = useNumberProp('descriptionNumberOfLines', 1); + const [longInfo, longInfoSelector] = useBooleanProp('longInfo', false); + const [nonDestructiveDescription, nonDestructiveDescriptionSelector] = useBooleanProp('nonDestructiveDescription', false); + const [isRadioCheckmark, isRadioCheckmarkSelector] = useBooleanProp('isRadioCheckmark', false); + + const components = useMemo( + () => buildComponent(OptionItem, propPossibilities, [ + typePosibilities, + ], [ + label, + description, + icon, + iconColor, + info, + type, + selected, + destructive, + inline, + descriptionNumberOfLines, + longInfo, + nonDestructiveDescription, + isRadioCheckmark, + { + action: onAction, + onRemove, + testID: 'optionItem.cl', + }, + ]), + [description, descriptionNumberOfLines, destructive, icon, iconColor, info, inline, isRadioCheckmark, label, longInfo, nonDestructiveDescription, selected, type, typePosibilities], + ); + + return ( + <> + {labelSelector} + {descriptionSelector} + {iconSelector} + {iconColorSelector} + {infoSelector} + {typeSelector} + {selectedSelector} + {destructiveSelector} + {inlineSelector} + {descriptionLinesSelector} + {longInfoSelector} + {nonDestructiveDescriptionSelector} + {isRadioCheckmarkSelector} + {components} + + ); +}; + +export default OptionItemComponentLibrary; diff --git a/app/screens/convert_gm_to_channel/team_selector.tsx b/app/screens/convert_gm_to_channel/team_selector.tsx index a66f1b443..2bf137d3f 100644 --- a/app/screens/convert_gm_to_channel/team_selector.tsx +++ b/app/screens/convert_gm_to_channel/team_selector.tsx @@ -3,27 +3,13 @@ import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {Platform} from 'react-native'; +import {Platform, View} from 'react-native'; +import MenuDivider from '@components/menu_divider'; import OptionItem from '@components/option_item'; import {Screens} from '@constants'; -import {useTheme} from '@context/theme'; +import {usePreventDoubleTap} from '@hooks/utils'; import {dismissBottomSheet, goToScreen} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; - -const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { - return { - teamSelector: { - borderTopWidth: 1, - borderBottomWidth: 1, - borderColor: changeOpacity(theme.centerChannelColor, 0.08), - }, - labelContainerStyle: { - flexShrink: 0, - }, - }; -}); type Props = { commonTeams: Team[]; @@ -33,8 +19,6 @@ type Props = { export const TeamSelector = ({commonTeams, onSelectTeam, selectedTeamId}: Props) => { const {formatMessage} = useIntl(); - const theme = useTheme(); - const styles = getStyleFromTheme(theme); const label = formatMessage({id: 'channel_into.convert_gm_to_channel.team_selector.label', defaultMessage: 'Team'}); const placeholder = formatMessage({id: 'channel_into.convert_gm_to_channel.team_selector.placeholder', defaultMessage: 'Select a Team'}); @@ -46,22 +30,31 @@ export const TeamSelector = ({commonTeams, onSelectTeam, selectedTeamId}: Props) if (team) { onSelectTeam(team); } - }, []); + }, [commonTeams, onSelectTeam]); - const goToTeamSelectorList = useCallback(preventDoubleTap(async () => { + const goToTeamSelectorList = usePreventDoubleTap(useCallback(async () => { await dismissBottomSheet(); const title = formatMessage({id: 'channel_info.convert_gm_to_channel.team_selector_list.title', defaultMessage: 'Select Team'}); goToScreen(Screens.TEAM_SELECTOR_LIST, title, {teams: commonTeams, selectTeam, selectedTeamId}); - }), [commonTeams, selectTeam, selectedTeamId]); + }, [commonTeams, formatMessage, selectTeam, selectedTeamId])); return ( - + + + + + ); }; diff --git a/app/screens/create_or_edit_channel/channel_info_form.tsx b/app/screens/create_or_edit_channel/channel_info_form.tsx index d9089525a..d3ff04585 100644 --- a/app/screens/create_or_edit_channel/channel_info_form.tsx +++ b/app/screens/create_or_edit_channel/channel_info_form.tsx @@ -61,17 +61,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ alignItems: 'center', justifyContent: 'center', }, - makePrivateContainer: { - marginBottom: MAKE_PRIVATE_MARGIN_BOTTOM, - }, - fieldContainer: { - marginBottom: FIELD_MARGIN_BOTTOM, - }, helpText: { ...typography('Body', 75, 'Regular'), color: changeOpacity(theme.centerChannelColor, 0.5), marginTop: 8, }, + mainView: { + gap: 24, + }, })); type Props = { @@ -194,14 +191,14 @@ export default function ChannelInfoForm({ const onHeaderAutocompleteChange = useCallback((value: string) => { onHeaderChange(value); propagateValue(value); - }, [onHeaderChange]); + }, [onHeaderChange, propagateValue]); const onHeaderInputChange = useCallback((value: string) => { if (!shouldProcessEvent(value)) { return; } onHeaderChange(value); - }, [onHeaderChange]); + }, [onHeaderChange, shouldProcessEvent]); const onLayoutError = useCallback((e: LayoutChangeEvent) => { setErrorHeight(e.nativeEvent.layout.height); @@ -231,9 +228,9 @@ export default function ChannelInfoForm({ const spaceOnTop = otherElementsSize - scrollPosition - AUTOCOMPLETE_ADJUST; const spaceOnBottom = (workingSpace + scrollPosition) - (otherElementsSize + headerFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION); - const autocompletePosition = spaceOnBottom > spaceOnTop ? - (otherElementsSize + headerFieldHeight) - scrollPosition : - (workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardOverlap) - otherElementsSize; + const bottomPosition = (otherElementsSize + headerFieldHeight) - scrollPosition; + const topPosition = (workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardOverlap) - otherElementsSize; + const autocompletePosition = spaceOnBottom > spaceOnTop ? bottomPosition : topPosition; const autocompleteAvailableSpace = spaceOnBottom > spaceOnTop ? spaceOnBottom : spaceOnTop; const growDown = spaceOnBottom > spaceOnTop; @@ -290,7 +287,7 @@ export default function ChannelInfoForm({ - + {showSelector && ( )} @@ -323,12 +319,10 @@ export default function ChannelInfoForm({ testID='channel_info_form.display_name.input' value={displayName} ref={nameInput} - containerStyle={styles.fieldContainer} theme={theme} onLayout={onLayoutDisplayName} /> )} - + - + diff --git a/app/screens/home/account/components/options/logout/index.tsx b/app/screens/home/account/components/options/logout/index.tsx index ee9cd8096..3c1d2d458 100644 --- a/app/screens/home/account/components/options/logout/index.tsx +++ b/app/screens/home/account/components/options/logout/index.tsx @@ -9,32 +9,18 @@ import {logout} from '@actions/remote/session'; import OptionItem from '@components/option_item'; import {Screens} from '@constants'; import {useServerDisplayName, useServerUrl} from '@context/server'; -import {useTheme} from '@context/theme'; +import {usePreventDoubleTap} from '@hooks/utils'; import {alertServerLogout} from '@utils/server'; -import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - desc: { - color: changeOpacity(theme.centerChannelColor, 0.64), - ...typography('Body', 75), - }, - }; -}); const LogOut = () => { - const theme = useTheme(); - const styles = getStyleSheet(theme); const intl = useIntl(); const serverUrl = useServerUrl(); const serverDisplayName = useServerDisplayName(); - const onLogout = useCallback(preventDoubleTap(() => { + const onLogout = usePreventDoubleTap(useCallback(() => { Navigation.updateProps(Screens.HOME, {extra: undefined}); alertServerLogout(serverDisplayName, () => logout(serverUrl, intl), intl); - }), [serverDisplayName, serverUrl, intl]); + }, [serverDisplayName, serverUrl, intl])); return ( { destructive={true} icon='exit-to-app' label={intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})} - optionDescriptionTextStyle={styles.desc} testID='account.logout.option' type='default' + nonDestructiveDescription={true} /> ); }; diff --git a/app/screens/home/account/components/options/settings/index.tsx b/app/screens/home/account/components/options/settings/index.tsx index 0c7eb15dd..699c19c49 100644 --- a/app/screens/home/account/components/options/settings/index.tsx +++ b/app/screens/home/account/components/options/settings/index.tsx @@ -6,18 +6,18 @@ import {useIntl} from 'react-intl'; import OptionItem from '@components/option_item'; import Screens from '@constants/screens'; +import {usePreventDoubleTap} from '@hooks/utils'; import {showModal} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; const Settings = () => { const intl = useIntl(); - const openSettings = useCallback(preventDoubleTap(() => { + const openSettings = usePreventDoubleTap(useCallback(() => { showModal( Screens.SETTINGS, intl.formatMessage({id: 'mobile.screen.settings', defaultMessage: 'Settings'}), ); - }), []); + }, [intl])); return ( { +const YourProfile = ({isTablet}: Props) => { const intl = useIntl(); - const openProfile = useCallback(preventDoubleTap(() => { + const openProfile = usePreventDoubleTap(useCallback(() => { if (isTablet) { DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, Screens.EDIT_PROFILE); } else { @@ -27,7 +26,7 @@ const YourProfile = ({isTablet, theme}: Props) => { intl.formatMessage({id: 'mobile.screen.your_profile', defaultMessage: 'Your Profile'}), ); } - }), [isTablet, theme]); + }, [intl, isTablet])); return ( { alignItems: 'center', flexDirection: 'row', marginTop: 20, - marginHorizontal: 18, }, titleContainer: { flex: 1, @@ -110,10 +109,10 @@ const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTe if (timeoutRef.current) { clearTimeout(timeoutRef.current); } - setTimeout(() => { + timeoutRef.current = setTimeout(() => { scrollEnabled.value = true; }, 350); - }, [showMore]); + }, [data.length, height, scrollEnabled, showMore]); useEffect(() => { return () => { diff --git a/app/screens/home/search/initial/modifiers/modifier.tsx b/app/screens/home/search/initial/modifiers/modifier.tsx index c79a88624..adc9e50f5 100644 --- a/app/screens/home/search/initial/modifiers/modifier.tsx +++ b/app/screens/home/search/initial/modifiers/modifier.tsx @@ -2,19 +2,12 @@ // See LICENSE.txt for license information. import React, {type Dispatch, type RefObject, type SetStateAction, useCallback} from 'react'; -import {StyleSheet} from 'react-native'; import OptionItem from '@components/option_item'; -import {preventDoubleTap} from '@utils/tap'; +import {usePreventDoubleTap} from '@hooks/utils'; import type {SearchRef} from '@components/search'; -const styles = StyleSheet.create({ - container: { - marginLeft: 20, - }, -}); - export type ModifierItem = { cursorPosition?: number; description: string; @@ -30,17 +23,16 @@ type Props = { } const Modifier = ({item, searchRef, searchValue, setSearchValue}: Props) => { - const handlePress = useCallback(() => { - addModifierTerm(item.term); - }, [item.term, searchValue]); - - const setNativeCursorPositionProp = (position: number) => { + const setNativeCursorPositionProp = useCallback((position: number) => { setTimeout(() => { searchRef.current?.setCaretPosition({start: position, end: position}); }, 50); - }; - const addModifierTerm = preventDoubleTap((modifierTerm) => { + // searchRef is a ref object, so its reference should not change between renders. + // We add it to the dependencies to satisfy the linter. + }, [searchRef]); + + const addModifierTerm = usePreventDoubleTap(useCallback((modifierTerm: string) => { let newValue = ''; if (!searchValue) { newValue = modifierTerm; @@ -55,7 +47,11 @@ const Modifier = ({item, searchRef, searchValue, setSearchValue}: Props) => { const position = newValue.length + item.cursorPosition; setNativeCursorPositionProp(position); } - }); + }, [item.cursorPosition, searchValue, setNativeCursorPositionProp, setSearchValue])); + + const handlePress = useCallback(() => { + addModifierTerm(item.term); + }, [addModifierTerm, item.term]); return ( { testID={item.testID} description={' ' + item.description} type='default' - containerStyle={styles.container} /> ); }; diff --git a/app/screens/home/search/initial/modifiers/show_more.tsx b/app/screens/home/search/initial/modifiers/show_more.tsx index 73bd533b5..b826dde6e 100644 --- a/app/screens/home/search/initial/modifiers/show_more.tsx +++ b/app/screens/home/search/initial/modifiers/show_more.tsx @@ -1,48 +1,35 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessages, useIntl} from 'react-intl'; import OptionItem from '@components/option_item'; -import {useTheme} from '@context/theme'; -import {t} from '@i18n'; -import {makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; type ShowMoreButtonProps = { onPress: () => void; showMore: boolean; } -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { - return { - showMore: { - color: theme.buttonBg, - paddingLeft: 20, - ...typography('Body', 200, 'SemiBold'), - }, - }; +const messages = defineMessages({ + showMore: { + id: 'mobile.search.show_more', + defaultMessage: 'Show more', + }, + showLess: { + id: 'mobile.search.show_less', + defaultMessage: 'Show less', + }, }); const ShowMoreButton = ({onPress, showMore}: ShowMoreButtonProps) => { - const theme = useTheme(); const intl = useIntl(); - const styles = getStyleSheet(theme); - - let id = t('mobile.search.show_more'); - let defaultMessage = 'Show more'; - if (showMore) { - id = t('mobile.search.show_less'); - defaultMessage = 'Show less'; - } return ( ); }; diff --git a/app/screens/home/search/initial/recent_searches/index.tsx b/app/screens/home/search/initial/recent_searches/index.tsx index d21e8dd23..ee13446c1 100644 --- a/app/screens/home/search/initial/recent_searches/index.tsx +++ b/app/screens/home/search/initial/recent_searches/index.tsx @@ -3,9 +3,10 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; -import {FlatList, type ListRenderItemInfo, Text, View} from 'react-native'; +import {FlatList, type ListRenderItemInfo, Text} from 'react-native'; import Animated from 'react-native-reanimated'; +import MenuDivider from '@components/menu_divider'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -25,7 +26,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { marginHorizontal: 20, }, title: { - paddingHorizontal: 20, paddingVertical: 12, color: theme.centerChannelColor, ...typography('Heading', 300, 'SemiBold'), @@ -62,7 +62,7 @@ const RecentSearches = ({setRecentValue, recentSearches, teamName}: Props) => { const header = ( <> - + void; item: TeamSearchHistoryModel; @@ -41,7 +34,6 @@ const RecentItem = ({item, setRecentValue}: Props) => { onRemove={handleRemove} testID={`search.recent_item.${item.term}`} type='remove' - containerStyle={styles.container} /> ); }; diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index 445160778..b5e9b97d6 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -278,6 +278,7 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => { paddingTop: scrollPaddingTop, flexGrow: 1, justifyContent: (resultsLoading || loading) ? 'center' : 'flex-start', + paddingHorizontal: 18, }; }, [loading, resultsLoading, scrollPaddingTop]); @@ -301,9 +302,16 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => { /> ); }, [ - handleModifierTextChange, handleRecentSearch, - loading, scrollEnabled, scrollPaddingTop, searchTeamId, - searchValue, styles.loading, teams, theme.buttonBg, + handleModifierTextChange, + handleRecentSearch, + loading, + scrollEnabled, + scrollPaddingTop, + searchTeamId, + searchValue, + teams, + theme.buttonBg, + updateSearchTeamId, ]); const animated = useAnimatedStyle(() => { diff --git a/app/screens/post_priority_picker/components/picker_option.tsx b/app/screens/post_priority_picker/components/picker_option.tsx index a4fc65e17..d6b00ffcf 100644 --- a/app/screens/post_priority_picker/components/picker_option.tsx +++ b/app/screens/post_priority_picker/components/picker_option.tsx @@ -2,16 +2,9 @@ // See LICENSE.txt for license information. import React from 'react'; -import {StyleSheet} from 'react-native'; import OptionItem, {type OptionItemProps, type OptionType} from '@components/option_item'; -const style = StyleSheet.create({ - labelContainer: { - alignItems: 'flex-start', - }, -}); - type Props = Omit & { type?: OptionType; } @@ -21,7 +14,6 @@ const PickerOption = ({type, ...rest}: Props) => { return ( { - return { - itemStyle: { - backgroundColor: theme.centerChannelBg, - paddingHorizontal: 20, - }, - }; -}); - const EMPTY_FILES: FileInfo[] = []; type AdvancedSettingsProps = { @@ -39,20 +28,18 @@ const AdvancedSettings = ({ componentId, isDevMode, }: AdvancedSettingsProps) => { - const theme = useTheme(); const intl = useIntl(); const serverUrl = useServerUrl(); const [dataSize, setDataSize] = useState(0); const [files, setFiles] = useState(EMPTY_FILES); - const styles = getStyleSheet(theme); - const getAllCachedFiles = async () => { + const getAllCachedFiles = useCallback(async () => { const {totalSize = 0, files: cachedFiles} = await getAllFilesInCachesDirectory(serverUrl); setDataSize(totalSize); setFiles(cachedFiles || EMPTY_FILES); - }; + }, [serverUrl]); - const onPressDeleteData = preventDoubleTap(async () => { + const onPressDeleteData = usePreventDoubleTap(useCallback(async () => { try { if (files.length > 0) { const {formatMessage} = intl; @@ -80,7 +67,7 @@ const AdvancedSettings = ({ } catch (e) { //do nothing } - }); + }, [files.length, getAllCachedFiles, intl, serverUrl])); const onPressComponentLibrary = () => { const screen = Screens.COMPONENT_LIBRARY; @@ -109,7 +96,6 @@ const AdvancedSettings = ({ activeOpacity={hasData ? 1 : 0} > popTopScreen(componentId); - const saveClockDisplayPreference = useCallback(() => { if (hasMilitaryTimeFormat !== isMilitaryTimeFormat) { const timePreference: PreferenceType = { @@ -50,8 +48,8 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ savePreference(serverUrl, [timePreference]); } - close(); - }, [hasMilitaryTimeFormat, isMilitaryTimeFormat, serverUrl]); + popTopScreen(componentId); + }, [componentId, currentUserId, hasMilitaryTimeFormat, isMilitaryTimeFormat, serverUrl]); useBackNavigation(saveClockDisplayPreference); diff --git a/app/screens/settings/display_crt/display_crt.tsx b/app/screens/settings/display_crt/display_crt.tsx index 309d445e1..8810ff399 100644 --- a/app/screens/settings/display_crt/display_crt.tsx +++ b/app/screens/settings/display_crt/display_crt.tsx @@ -35,10 +35,8 @@ const DisplayCRT = ({componentId, currentUserId, isCRTEnabled}: Props) => { const serverUrl = useServerUrl(); const intl = useIntl(); - const close = () => popTopScreen(componentId); - const saveCRTPreference = useCallback(async () => { - close(); + popTopScreen(componentId); if (isCRTEnabled !== isEnabled) { const crtPreference: PreferenceType = { category: Preferences.CATEGORIES.DISPLAY_SETTINGS, @@ -53,7 +51,7 @@ const DisplayCRT = ({componentId, currentUserId, isCRTEnabled}: Props) => { handleCRTToggled(serverUrl); } } - }, [isEnabled, isCRTEnabled, serverUrl]); + }, [componentId, isCRTEnabled, isEnabled, currentUserId, serverUrl]); useBackNavigation(saveCRTPreference); useAndroidHardwareBackHandler(componentId, saveCRTPreference); diff --git a/app/screens/settings/display_theme/custom_theme.tsx b/app/screens/settings/display_theme/custom_theme.tsx index 9a65ba7ff..bf40a2dcf 100644 --- a/app/screens/settings/display_theme/custom_theme.tsx +++ b/app/screens/settings/display_theme/custom_theme.tsx @@ -8,8 +8,6 @@ import SettingOption from '@components/settings/option'; import SettingSeparator from '@components/settings/separator'; import {useTheme} from '@context/theme'; -const radioItemProps = {checkedBody: true}; - type CustomThemeProps = { setTheme: (themeKey: string) => void; displayTheme: string | undefined; @@ -27,8 +25,8 @@ const CustomTheme = ({setTheme, displayTheme}: CustomThemeProps) => { value={'custom'} label={intl.formatMessage({id: 'settings_display.custom_theme', defaultMessage: 'Custom Theme'})} selected={theme.type?.toLowerCase() === displayTheme?.toLowerCase()} - radioItemProps={radioItemProps} testID='theme_display_settings.custom.option' + isRadioCheckmark={true} /> ); diff --git a/app/screens/settings/display_theme/theme_tiles.tsx b/app/screens/settings/display_theme/theme_tiles.tsx index 47f0a539c..24335917b 100644 --- a/app/screens/settings/display_theme/theme_tiles.tsx +++ b/app/screens/settings/display_theme/theme_tiles.tsx @@ -13,18 +13,17 @@ import {typography} from '@utils/typography'; import ThemeThumbnail from './theme_thumbnail'; -const TILE_PADDING = 8; +const TILE_PADDING = 16; const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { container: { flexDirection: 'column', - padding: TILE_PADDING, + gap: 8, }, imageWrapper: { position: 'relative', alignItems: 'flex-start', - marginBottom: 8, }, thumbnail: { resizeMode: 'stretch', @@ -42,10 +41,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }, tilesContainer: { marginBottom: 30, - paddingLeft: 8, flexDirection: 'row', flexWrap: 'wrap', backgroundColor: theme.centerChannelBg, + gap: TILE_PADDING, }, }; }); @@ -74,14 +73,14 @@ export const ThemeTile = ({ const layoutStyle = useMemo(() => { const tilesPerLine = isTablet ? 4 : 2; - const fullWidth = isTablet ? deviceWidth - 40 : deviceWidth; + const fullWidth = deviceWidth - 40; return { container: { width: (fullWidth / tilesPerLine) - TILE_PADDING, }, thumbnail: { - width: (fullWidth / tilesPerLine) - (TILE_PADDING + 16), + width: (fullWidth / tilesPerLine) - (TILE_PADDING), }, }; }, [isTablet, deviceWidth]); diff --git a/app/screens/settings/display_timezone/display_timezone.tsx b/app/screens/settings/display_timezone/display_timezone.tsx index 39edf2917..7add3b852 100644 --- a/app/screens/settings/display_timezone/display_timezone.tsx +++ b/app/screens/settings/display_timezone/display_timezone.tsx @@ -12,8 +12,8 @@ import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useBackNavigation from '@hooks/navigate_back'; +import {usePreventDoubleTap} from '@hooks/utils'; import {goToScreen, popTopScreen} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; import {getDeviceTimezone} from '@utils/timezone'; import {getTimezoneRegion, getUserTimezoneProps} from '@utils/user'; @@ -39,15 +39,15 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { })); }; - const updateManualTimezone = (mtz: string) => { - setUserTimezone({ - useAutomaticTimezone: false, - automaticTimezone: '', - manualTimezone: mtz, - }); - }; + const goToSelectTimezone = usePreventDoubleTap(useCallback(() => { + const updateManualTimezone = (mtz: string) => { + setUserTimezone({ + useAutomaticTimezone: false, + automaticTimezone: '', + manualTimezone: mtz, + }); + }; - const goToSelectTimezone = preventDoubleTap(() => { const screen = Screens.SETTINGS_DISPLAY_TIMEZONE_SELECT; const title = intl.formatMessage({id: 'settings_display.timezone.select', defaultMessage: 'Select Timezone'}); @@ -57,9 +57,7 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { }; goToScreen(screen, title, passProps); - }); - - const close = () => popTopScreen(componentId); + }, [initialTimezone.manualTimezone, initialTimezone.automaticTimezone, intl, userTimezone.manualTimezone])); const saveTimezone = useCallback(() => { const canSave = @@ -77,8 +75,15 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { updateMe(serverUrl, {timezone: timeZone}); } - close(); - }, [userTimezone, serverUrl]); + popTopScreen(componentId); + }, [ + componentId, + initialTimezone, + userTimezone.useAutomaticTimezone, + userTimezone.automaticTimezone, + userTimezone.manualTimezone, + serverUrl, + ]); useBackNavigation(saveTimezone); @@ -89,7 +94,7 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { return getTimezoneRegion(userTimezone.automaticTimezone); } return intl.formatMessage({id: 'settings_display.timezone.off', defaultMessage: 'Off'}); - }, [userTimezone.useAutomaticTimezone]); + }, [intl, userTimezone.automaticTimezone, userTimezone.useAutomaticTimezone]); return ( diff --git a/app/screens/settings/display_timezone_select/timezone_row.tsx b/app/screens/settings/display_timezone_select/timezone_row.tsx index 549ac1c7f..fabad2cb6 100644 --- a/app/screens/settings/display_timezone_select/timezone_row.tsx +++ b/app/screens/settings/display_timezone_select/timezone_row.tsx @@ -32,9 +32,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { body: { flexDirection: 'row', }, - lineStyles: { - width: '100%', - }, }; }); type TimezoneRowProps = { @@ -75,9 +72,7 @@ const TimezoneRow = ({onPressTimezone, isSelected, timezone}: TimezoneRowProps) /> )} - + ); }; diff --git a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx index 92fcf4038..b75edbea1 100644 --- a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx +++ b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx @@ -74,8 +74,6 @@ const NotificationAutoResponder = ({currentUser, componentId}: NotificationAutoR const styles = getStyleSheet(theme); - const close = () => popTopScreen(componentId); - const saveAutoResponder = useCallback(() => { const canSaveSetting = initialAutoResponderActive !== autoResponderActive || initialOOOMsg !== autoResponderMessage; @@ -91,8 +89,8 @@ const NotificationAutoResponder = ({currentUser, componentId}: NotificationAutoR fetchStatusInBatch(serverUrl, currentUser.id); } } - close(); - }, [serverUrl, autoResponderActive, autoResponderMessage, notifyProps, currentUser?.id]); + popTopScreen(componentId); + }, [componentId, initialAutoResponderActive, autoResponderActive, initialOOOMsg, autoResponderMessage, serverUrl, notifyProps, currentUser]); useBackNavigation(saveAutoResponder); diff --git a/app/screens/settings/notification_call/notification_call.tsx b/app/screens/settings/notification_call/notification_call.tsx index 339749d40..0c6b1f28b 100644 --- a/app/screens/settings/notification_call/notification_call.tsx +++ b/app/screens/settings/notification_call/notification_call.tsx @@ -42,17 +42,14 @@ const NotificationCall = ({componentId, currentUser}: Props) => { const notifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser?.notifyProps]); - const initialCallsMobileSound = useMemo(() => Boolean(notifyProps?.calls_mobile_sound ? notifyProps.calls_mobile_sound === 'true' : notifyProps?.calls_desktop_sound === 'true'), - [/* dependency array should remain empty */]); - const [callsMobileSound, setCallsMobileSound] = useState(initialCallsMobileSound); - const initialCallsMobileNotificationSound = useMemo(() => { + const [callsMobileSound, setCallsMobileSound] = useState(() => Boolean(notifyProps?.calls_mobile_sound ? notifyProps.calls_mobile_sound === 'true' : notifyProps?.calls_desktop_sound === 'true')); + const [callsMobileNotificationSound, setCallsMobileNotificationSound] = useState(() => { let initialSound = notifyProps?.calls_mobile_notification_sound ? notifyProps.calls_mobile_notification_sound : notifyProps?.calls_notification_sound; if (!initialSound) { initialSound = Calls.RINGTONE_DEFAULT; } return initialSound; - }, [/* dependency array should remain empty */]); - const [callsMobileNotificationSound, setCallsMobileNotificationSound] = useState(initialCallsMobileNotificationSound); + }); const [playingRingtone, setPlayingRingtone] = useState(false); const close = useCallback(() => { @@ -107,7 +104,7 @@ const NotificationCall = ({componentId, currentUser}: Props) => { updateMe(serverUrl, {notify_props}); } close(); - }, [serverUrl, canSaveSettings, close, notifyProps, callsMobileSound, callsMobileNotificationSound, playingRingtone]); + }, [serverUrl, canSaveSettings, close, notifyProps, callsMobileSound, callsMobileNotificationSound]); useBackNavigation(saveNotificationSettings); diff --git a/app/screens/settings/notification_email/notification_email.tsx b/app/screens/settings/notification_email/notification_email.tsx index cc15ad9d5..a47a9380a 100644 --- a/app/screens/settings/notification_email/notification_email.tsx +++ b/app/screens/settings/notification_email/notification_email.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback, useMemo, useState} from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessages, useIntl} from 'react-intl'; import {Text} from 'react-native'; import {savePreference} from '@actions/remote/preference'; @@ -16,7 +16,6 @@ import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useBackNavigation from '@hooks/navigate_back'; -import {t} from '@i18n'; import {popTopScreen} from '@screens/navigation'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -35,23 +34,24 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -const emailHeaderText = { - id: t('notification_settings.email.send'), - defaultMessage: 'Send email notifications', -}; -const emailFooterText = { - id: t('notification_settings.email.emailInfo'), - defaultMessage: 'Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.', -}; - -const emailHeaderCRTText = { - id: t('notification_settings.email.crt.send'), - defaultMessage: 'Thread reply notifications', -}; -const emailFooterCRTText = { - id: t('notification_settings.email.crt.emailInfo'), - defaultMessage: "When enabled, any reply to a thread you're following will send an email notification", -}; +const messages = defineMessages({ + emailHeaderText: { + id: 'notification_settings.email.send', + defaultMessage: 'Send email notifications', + }, + emailFooterText: { + id: 'notification_settings.email.emailInfo', + defaultMessage: 'Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.', + }, + emailHeaderCRTText: { + id: 'notification_settings.email.crt.send', + defaultMessage: 'Thread reply notifications', + }, + emailFooterCRTText: { + id: 'notification_settings.email.crt.emailInfo', + defaultMessage: "When enabled, any reply to a thread you're following will send an email notification", + }, +}); type NotificationEmailProps = { componentId: AvailableScreens; @@ -78,10 +78,8 @@ const NotificationEmail = ({componentId, currentUser, emailInterval, enableEmail const theme = useTheme(); const styles = getStyleSheet(theme); - const close = () => popTopScreen(componentId); - const saveEmail = useCallback(() => { - if (!currentUser) { + if (!currentUser?.id) { return; } @@ -109,8 +107,19 @@ const NotificationEmail = ({componentId, currentUser, emailInterval, enableEmail } Promise.all(promises); } - close(); - }, [notifyProps, notifyInterval, emailThreads, serverUrl, currentUser?.id, sendEmailNotifications]); + popTopScreen(componentId); + }, [ + componentId, + currentUser?.id, + notifyInterval, + initialInterval, + emailThreads, + initialEmailThreads, + serverUrl, + notifyProps, + sendEmailNotifications, + isCRTEnabled, + ]); useAndroidHardwareBackHandler(componentId, saveEmail); @@ -120,8 +129,8 @@ const NotificationEmail = ({componentId, currentUser, emailInterval, enableEmail {sendEmailNotifications && <> @@ -179,8 +188,8 @@ const NotificationEmail = ({componentId, currentUser, emailInterval, enableEmail {isCRTEnabled && notifyInterval !== Preferences.INTERVAL_NEVER.toString() && ( { const styles = getStyleSheet(theme); const intl = useIntl(); - const close = () => popTopScreen(componentId); - const saveMention = useCallback(() => { if (!currentUser) { return; @@ -166,10 +164,10 @@ const MentionSettings = ({componentId, currentUser, isCRTEnabled}: Props) => { updateMe(serverUrl, {notify_props}); } - close(); + popTopScreen(componentId); }, [ - currentUser, channelMentionOn, replyNotificationType, firstNameMentionOn, - usernameMentionOn, mentionKeywords, mentionProps, close, notifyProps, serverUrl, + componentId, currentUser, channelMentionOn, replyNotificationType, firstNameMentionOn, + usernameMentionOn, mentionKeywords, mentionProps, notifyProps, serverUrl, ]); const handleFirstNameToggle = useCallback(() => { diff --git a/app/screens/settings/notification_push/push_status.tsx b/app/screens/settings/notification_push/push_status.tsx index b35fe8274..a63d0fbe1 100644 --- a/app/screens/settings/notification_push/push_status.tsx +++ b/app/screens/settings/notification_push/push_status.tsx @@ -2,17 +2,16 @@ // See LICENSE.txt for license information. import React from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessage, useIntl} from 'react-intl'; import SettingBlock from '@components/settings/block'; import SettingOption from '@components/settings/option'; import SettingSeparator from '@components/settings/separator'; -import {t} from '@i18n'; -const headerText = { - id: t('notification_settings.mobile.trigger_push'), +const headerText = defineMessage({ + id: 'notification_settings.mobile.trigger_push', defaultMessage: 'Trigger push notifications when...', -}; +}); type MobilePushStatusProps = { pushStatus: UserNotifyPropsPushStatus; diff --git a/app/screens/settings/notification_push/push_thread.tsx b/app/screens/settings/notification_push/push_thread.tsx index 8173a0d4e..246ee842e 100644 --- a/app/screens/settings/notification_push/push_thread.tsx +++ b/app/screens/settings/notification_push/push_thread.tsx @@ -2,17 +2,16 @@ // See LICENSE.txt for license information. import React from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessage, useIntl} from 'react-intl'; import SettingBlock from '@components/settings/block'; import SettingOption from '@components/settings/option'; import SettingSeparator from '@components/settings/separator'; -import {t} from '@i18n'; -const headerText = { - id: t('notification_settings.push_threads.replies'), +const headerText = defineMessage({ + id: 'notification_settings.push_threads.replies', defaultMessage: 'Thread replies', -}; +}); type MobilePushThreadProps = { onMobilePushThreadChanged: (status: string) => void; diff --git a/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap b/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap index daaab225f..a33dd9818 100644 --- a/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap +++ b/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap @@ -6,7 +6,7 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = ` { "flex": 1, "justifyContent": "flex-end", - "margin": 16, + "marginVertical": 16, } } > diff --git a/app/screens/settings/notifications/send_test_notification_notice/send_test_notification_notice.tsx b/app/screens/settings/notifications/send_test_notification_notice/send_test_notification_notice.tsx index 2b102e157..1c880c39f 100644 --- a/app/screens/settings/notifications/send_test_notification_notice/send_test_notification_notice.tsx +++ b/app/screens/settings/notifications/send_test_notification_notice/send_test_notification_notice.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import {sendTestNotification} from '@actions/remote/notifications'; import SectionNotice from '@components/section_notice'; @@ -19,19 +19,19 @@ const TIME_TO_IDLE = 3000; type Props = { serverVersion: string; userId: string; -isCloud: boolean; -telemetryId: string; + isCloud: boolean; + telemetryId: string; } type ButtonState = 'idle'|'sending'|'sent'|'error'; -const styles = { +const styles = StyleSheet.create({ wrapper: { flex: 1, - justifyContent: 'flex-end' as const, - margin: 16, + justifyContent: 'flex-end', + marginVertical: 16, }, -}; +}); const SendTestNotificationNotice = ({ serverVersion, diff --git a/app/screens/settings/report_problem/report_problem.tsx b/app/screens/settings/report_problem/report_problem.tsx index 31b98471e..07fc474c4 100644 --- a/app/screens/settings/report_problem/report_problem.tsx +++ b/app/screens/settings/report_problem/report_problem.tsx @@ -6,7 +6,6 @@ import {defineMessages, useIntl} from 'react-intl'; import SettingItem from '@components/settings/item'; import {Screens} from '@constants'; -import {useTheme} from '@context/theme'; import {goToScreen} from '@screens/navigation'; import {emailLogs} from '@utils/share_logs'; @@ -32,7 +31,6 @@ const ReportProblem = ({ siteName, metadata, }: ReportProblemProps) => { - const theme = useTheme(); const intl = useIntl(); const onlyAllowLogs = allowDownloadLogs && reportAProblemType === 'hidden'; const skipReportAProblemScreen = reportAProblemType === 'email' && !allowDownloadLogs; @@ -50,12 +48,11 @@ const ReportProblem = ({ if (onlyAllowLogs) { return ( ); } @@ -66,12 +63,11 @@ const ReportProblem = ({ return ( ); }; diff --git a/app/screens/settings/settings.tsx b/app/screens/settings/settings.tsx index a11e1a307..3085a962a 100644 --- a/app/screens/settings/settings.tsx +++ b/app/screens/settings/settings.tsx @@ -3,10 +3,11 @@ import React, {useCallback, useEffect, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {Platform, View} from 'react-native'; +import {Platform} from 'react-native'; import {handleGotoLocation} from '@actions/remote/command'; import CompassIcon from '@components/compass_icon'; +import MenuDivider from '@components/menu_divider'; import SettingContainer from '@components/settings/container'; import SettingItem from '@components/settings/item'; import {Screens} from '@constants'; @@ -16,7 +17,6 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; import {dismissModal, goToScreen, setButtons} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import ReportProblem from './report_problem'; @@ -24,23 +24,6 @@ import type {AvailableScreens} from '@typings/screens/navigation'; const CLOSE_BUTTON_ID = 'close-settings'; -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { - return { - containerStyle: { - paddingLeft: 8, - marginTop: 12, - }, - helpGroup: { - width: '91%', - backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), - height: 1, - alignSelf: 'center', - - // marginTop: 20, - }, - }; -}); - type SettingsProps = { componentId: AvailableScreens; helpLink: string; @@ -57,7 +40,6 @@ const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) => const serverDisplayName = useServerDisplayName(); const serverName = siteName || serverDisplayName; - const styles = getStyleSheet(theme); const closeButton = useMemo(() => { return { @@ -138,15 +120,14 @@ const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) => optionName='about' testID='settings.about.option' /> - {Platform.OS === 'android' && } + {Platform.OS === 'android' && } {showHelp && }