Refactor and cleanup of option item (#9067)

* Refactor and cleanup of option item

* Fix tests and minor cleanup

* Fix bug and address feedback
This commit is contained in:
Daniel Espino García 2025-08-19 14:01:56 +02:00 committed by GitHub
parent 2e5bbb9f62
commit a27c82da90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 814 additions and 476 deletions

View file

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

View file

@ -36,7 +36,7 @@ const BookmarkType = ({channelId, type, ownerId}: Props) => {
},
};
showModal(Screens.CHANNEL_BOOKMARK, title, {channelId, closeButtonId, type, ownerId}, options);
}, [channelId, theme, type, ownerId]);
}, [formatMessage, theme.sidebarHeaderTextColor, channelId, type, ownerId]);
let icon;
let label;

View file

@ -99,7 +99,7 @@ const File_filter = ({initialFilter, setFilter, title}: FilterProps) => {
setFilter(fileType);
}
dismissBottomSheet();
}, [initialFilter]);
}, [initialFilter, setFilter]);
const separator = useCallback(() => <View style={style.divider}/>, [style]);
@ -112,7 +112,7 @@ const File_filter = ({initialFilter, setFilter, title}: FilterProps) => {
selected={initialFilter === item.filterType}
/>
);
}, [handleOnPress, initialFilter, theme]);
}, [handleOnPress, initialFilter, intl]);
return (
<BottomSheetContent

View file

@ -34,14 +34,14 @@ const OptionMenus = ({
await dismissBottomSheet();
}
setAction('downloading');
}, [setAction]);
}, [isTablet, setAction]);
const handleCopyLink = useCallback(async () => {
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 (
<>

View file

@ -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<React.SetStateAction<string | boolean>>)|((value: string | boolean) => void);
arrowStyle?: StyleProp<Intersection<TextStyle, ViewStyle>>;
containerStyle?: StyleProp<ViewStyle>;
description?: string;
destructive?: boolean;
icon?: string;
iconColor?: string;
info?: string;
info?: string | UserChipData;
inline?: boolean;
label: string;
labelContainerStyle?: StyleProp<ViewStyle>;
onRemove?: () => void;
optionDescriptionTextStyle?: StyleProp<TextStyle>;
optionLabelTextStyle?: StyleProp<TextStyle>;
radioItemProps?: Partial<RadioItemProps>;
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 = (
<RadioItem
selected={Boolean(selected)}
{...radioItemProps}
testID={radioComponentTestId}
checkedBody={isRadioCheckmark}
/>
);
} 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 = ({
<TouchableWithFeedback
hitSlop={hitSlop}
onPress={onRemove}
style={[styles.iconContainer]}
style={styles.iconContainer}
type='opacity'
testID={`${testID}.remove.button`}
>
@ -248,64 +256,74 @@ const OptionItem = ({
action?.(value || '');
}, [value, action]);
let infoComponent;
if (typeof info === 'object') {
infoComponent = (
<View style={actionComponent ? undefined : styles.iconContainer}>
<UserChip
user={info.user}
onPress={info.onPress}
teammateNameDisplay={info.teammateNameDisplay}
/>
</View>
);
} else if (info) {
infoComponent = (
<Text
style={[styles.info, !actionComponent && styles.iconContainer, destructive && {color: theme.dndIndicator}]}
testID={`${testID}.info`}
numberOfLines={1}
>
{info}
</Text>
);
}
const component = (
<View
testID={testID}
style={[styles.container, containerStyle]}
style={styles.container}
onLayout={onLayout}
>
<View style={styles.row}>
<View style={[styles.labelContainer, labelContainerStyle]}>
{Boolean(icon) && (
<View style={styles.iconContainer}>
<OptionIcon
icon={icon!}
iconColor={iconColor}
destructive={destructive}
/>
</View>
)}
{type === OptionTypeConst.RADIO && radioComponent}
<View style={labelStyle}>
<Text
style={[labelTextStyle, optionLabelTextStyle]}
testID={`${testID}.label`}
numberOfLines={1}
>
{label}
</Text>
{Boolean(description) &&
<Text
style={[descriptionTextStyle, optionDescriptionTextStyle]}
testID={`${testID}.description`}
numberOfLines={descriptionNumberOfLines}
>
{description}
</Text>
}
<View style={labelContainerStyle}>
{Boolean(icon) && (
<View style={styles.iconContainer}>
<OptionIcon
icon={icon!}
iconColor={iconColor}
destructive={destructive}
/>
</View>
</View>
</View>
{Boolean(actionComponent || info) &&
<View style={styles.actionContainer}>
{
Boolean(info) &&
)}
{type === OptionTypeConst.RADIO && radioComponent}
<View style={labelStyle}>
<Text
style={[styles.info, !actionComponent && styles.iconContainer, destructive && {color: theme.dndIndicator}]}
testID={`${testID}.info`}
style={labelTextStyle}
testID={`${testID}.label`}
numberOfLines={1}
>
{info}
{label}
</Text>
}
<View style={styles.actionSubContainer}>
{actionComponent}
{Boolean(description) &&
<Text
style={descriptionTextStyle}
testID={`${testID}.description`}
numberOfLines={descriptionNumberOfLines}
>
{description}
</Text>
}
</View>
</View>
{Boolean(actionComponent || infoComponent) &&
<View style={actionContainerStyle}>
{infoComponent}
{actionComponent}
</View>
}
</View>
);
if (Object.values(TouchableOptionTypes).includes(type)) {
if ((Object.values(TouchableOptionTypes) as string[]).includes(type)) {
return (
<TouchableOpacity onPress={onPress}>
{component}

View file

@ -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<typeof OptionItem> {
return {
label: 'Test Option',
type: 'default',
testID: 'option-item',
};
}
it('renders correctly with default props', () => {
const props = getBaseProps();
const {getByTestId, getByText, queryByTestId} = renderWithIntlAndTheme(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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 {...props}/>);
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(<OptionItem {...props}/>);
let label = getByText('Test Option');
expect(label).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator});
props.destructive = false;
rerender(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
// No inline style yet
expect(getByText('Test Option')).toHaveStyle({fontWeight: '400'});
props.description = 'Test description';
rerender(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
let info = getByText('Destructive info');
expect(info).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator});
props.destructive = false;
rerender(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
let selectedIcon = getByTestId('option-item.selected');
expect(selectedIcon).toBeTruthy();
props.selected = false;
rerender(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
let toggle = getByTestId('option-item.toggled.true.button');
expect(toggle).toBeTruthy();
props.selected = false;
rerender(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
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(<OptionItem {...props}/>);
const optionItem = getByTestId('option-item');
expect(optionItem).toBeTruthy();
act(() => {
fireEvent.press(optionItem);
});
expect(props.action).not.toHaveBeenCalled();
});
});

View file

@ -58,7 +58,7 @@ const RadioItem = ({selected, checkedBody, testID}: RadioItemProps) => {
}
return (<View style={styles.center}/>);
}, [checkedBody]);
}, [checkedBody, styles.center, styles.checkedBodyContainer, theme.buttonColor]);
return (
<View

View file

@ -40,13 +40,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
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),
},

View file

@ -18,6 +18,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
paddingHorizontal: 20,
},
contentContainerStyle: {
marginTop: 8,

View file

@ -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<OptionItemProps>;
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 = ({
<>
<OptionItem
action={onPress}
arrowStyle={styles.chevronStyle}
containerStyle={{marginLeft: 16}}
icon={config.icon}
info={info}
label={label}
optionLabelTextStyle={[styles.menuLabel, props.optionLabelTextStyle]}
type={Platform.select({ios: 'arrow', default: 'default'})}
{...props}
/>

View file

@ -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 (
<OptionItem
optionDescriptionTextStyle={styles.optionDescriptionTextStyle}
optionLabelTextStyle={styles.optionLabelTextStyle}
containerStyle={[styles.container, Boolean(props.description) && {marginVertical: 12}]}
{...props}
type={useRadioButton ? 'radio' : props.type}
/>

View file

@ -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<ViewStyle>;
isGroupSeparator?: boolean;
}
const SettingSeparator = ({lineStyles, isGroupSeparator = false}: SettingSeparatorProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (<View style={[styles.separator, isGroupSeparator && styles.groupSeparator, lineStyles]}/>);
const SettingSeparator = ({isGroupSeparator = false}: SettingSeparatorProps) => {
if (Platform.OS === 'android') {
return null;
}
return (
<MenuDivider
marginBottom={isGroupSeparator ? 16 : 0}
marginTop={0}
/>
);
};
export default SettingSeparator;

View file

@ -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) => (
<BindingOptionItem
@ -59,9 +59,9 @@ const ChannelInfoAppBindings = ({channelId, teamId, dismissChannelInfo, serverUr
};
const BindingOptionItem = ({binding, onPress}: {binding: AppBinding; onPress: (binding: AppBinding) => void}) => {
const handlePress = useCallback(preventDoubleTap(() => {
const handlePress = usePreventDoubleTap(useCallback(() => {
onPress(binding);
}), [binding, onPress]);
}, [binding, onPress]));
return (
<OptionItem

View file

@ -27,7 +27,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
borderRadius: 4,
marginHorizontal: 20,
marginVertical: 12,
paddingHorizontal: 16,
height: MUTED_BANNER_HEIGHT,

View file

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

View file

@ -37,6 +37,39 @@ export const useStringProp = (
return [preparedProp, selector];
};
export const useNumberProp = (
propName: string,
defaultValue: number,
): HookResult<number | undefined> => {
const [value, setValue] = useState<number | undefined>(defaultValue);
const stringToValue = useCallback((v: string) => {
const numberValue = parseInt(v);
if (isNaN(numberValue)) {
setValue(undefined);
return;
}
setValue(numberValue);
}, []);
const selector = useMemo(() => (
<TextSetting
label={propName}
disabled={false}
multiline={false}
secureTextEntry={false}
keyboardType='numeric'
onChange={stringToValue}
optional={false}
testID={`${propName}.input`}
value={value?.toString()}
location={Screens.COMPONENT_LIBRARY}
/>
), [propName, stringToValue, value]);
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
return [preparedProp, selector];
};
export const useBooleanProp = (
propName: string,
defaultValue: boolean,

View file

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

View file

@ -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}
<View>{components}</View>
</>
);
};
export default OptionItemComponentLibrary;

View file

@ -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 (
<OptionItem
action={goToTeamSelectorList}
containerStyle={styles.teamSelector}
label={label}
type={Platform.select({ios: 'arrow', default: 'default'})}
info={selectedTeam ? selectedTeam.display_name : placeholder}
labelContainerStyle={styles.labelContainerStyle}
/>
<View>
<MenuDivider
marginTop={0}
marginBottom={0}
/>
<OptionItem
action={goToTeamSelectorList}
label={label}
type={Platform.select({ios: 'arrow', default: 'default'})}
info={selectedTeam ? selectedTeam.display_name : placeholder}
longInfo={true}
/>
<MenuDivider
marginTop={0}
marginBottom={0}
/>
</View>
);
};

View file

@ -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({
<TouchableWithoutFeedback
onPress={blur}
>
<View>
<View style={styles.mainView}>
{showSelector && (
<OptionItem
testID='channel_info_form.make_private'
@ -300,7 +297,6 @@ export default function ChannelInfoForm({
type={'toggle'}
selected={isPrivate}
icon={'lock-outline'}
containerStyle={styles.makePrivateContainer}
onLayout={onLayoutMakePrivate}
/>
)}
@ -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}
/>
<View
style={styles.fieldContainer}
onLayout={onLayoutPurpose}
>
<FloatingTextInput
@ -358,9 +352,7 @@ export default function ChannelInfoForm({
</View>
</>
)}
<View
style={styles.fieldContainer}
>
<View>
<FloatingTextInput
autoCorrect={false}
autoCapitalize={'none'}

View file

@ -64,10 +64,7 @@ const AccountOptions = ({user, enableCustomUserStatuses, isTablet, theme}: Accou
</View>
<View style={styles.divider}/>
<View style={styles.group}>
<YourProfile
isTablet={isTablet}
theme={theme}
/>
<YourProfile isTablet={isTablet}/>
<Settings/>
</View>
<View style={styles.divider}/>

View file

@ -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 (
<OptionItem
@ -43,9 +29,9 @@ const LogOut = () => {
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}
/>
);
};

View file

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

View file

@ -8,17 +8,16 @@ import {DeviceEventEmitter} from 'react-native';
import OptionItem from '@components/option_item';
import {Events, Screens} from '@constants';
import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
import {usePreventDoubleTap} from '@hooks/utils';
import {showModal} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
type Props = {
isTablet: boolean;
theme: Theme;
}
const YourProfile = ({isTablet, theme}: Props) => {
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 (
<OptionItem

View file

@ -28,7 +28,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
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 () => {

View file

@ -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 (
<OptionItem
@ -66,7 +62,6 @@ const Modifier = ({item, searchRef, searchValue, setSearchValue}: Props) => {
testID={item.testID}
description={' ' + item.description}
type='default'
containerStyle={styles.container}
/>
);
};

View file

@ -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 (
<OptionItem
action={onPress}
testID={'mobile.search.show_more'}
type='default'
label={intl.formatMessage({id, defaultMessage})}
optionLabelTextStyle={styles.showMore}
label={intl.formatMessage(showMore ? messages.showLess : messages.showMore)}
/>
);
};

View file

@ -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 = (
<>
<View style={styles.divider}/>
<MenuDivider/>
<Text
style={styles.title}
numberOfLines={2}

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {StyleSheet} from 'react-native';
import {removeSearchFromTeamSearchHistory} from '@actions/local/team';
import OptionItem from '@components/option_item';
@ -10,12 +9,6 @@ import {useServerUrl} from '@context/server';
import type TeamSearchHistoryModel from '@typings/database/models/servers/team_search_history';
const styles = StyleSheet.create({
container: {
marginLeft: 20,
},
});
type Props = {
setRecentValue: (value: string) => 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}
/>
);
};

View file

@ -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(() => {

View file

@ -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<OptionItemProps, 'type'> & {
type?: OptionType;
}
@ -21,7 +14,6 @@ const PickerOption = ({type, ...rest}: Props) => {
return (
<OptionItem
labelContainerStyle={style.labelContainer}
testID={testID}
type={type || 'select'}
{...rest}

View file

@ -10,25 +10,14 @@ import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {usePreventDoubleTap} from '@hooks/utils';
import {goToScreen, popTopScreen} from '@screens/navigation';
import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {FileInfo} from 'expo-file-system';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
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<number | undefined>(0);
const [files, setFiles] = useState<FileInfo[]>(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}
>
<SettingOption
containerStyle={styles.itemStyle}
destructive={true}
icon='trash-can-outline'
info={getFormattedFileSize(dataSize || 0)}
@ -124,7 +110,6 @@ const AdvancedSettings = ({
onPress={onPressComponentLibrary}
>
<SettingOption
containerStyle={styles.itemStyle}
label={intl.formatMessage({id: 'settings.advanced.component_library', defaultMessage: 'Component library'})}
testID='advanced_settings.component_library.option'
type='none'

View file

@ -36,8 +36,6 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ
setIsMilitaryTimeFormat(clockType === CLOCK_TYPE.MILITARY);
}, []);
const close = () => 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);

View file

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

View file

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

View file

@ -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]);

View file

@ -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 (
<SettingContainer testID='timezone_display_settings'>

View file

@ -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)
/>
)}
</View>
<SettingSeparator
lineStyles={styles.lineStyles}
/>
<SettingSeparator/>
</TouchableOpacity>
);
};

View file

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

View file

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

View file

@ -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
<SettingContainer testID='email_notification_settings'>
<SettingBlock
disableFooter={!sendEmailNotifications}
footerText={emailFooterText}
headerText={emailHeaderText}
footerText={messages.emailFooterText}
headerText={messages.emailHeaderText}
>
{sendEmailNotifications &&
<>
@ -179,8 +188,8 @@ const NotificationEmail = ({componentId, currentUser, emailInterval, enableEmail
</SettingBlock>
{isCRTEnabled && notifyInterval !== Preferences.INTERVAL_NEVER.toString() && (
<SettingBlock
footerText={emailFooterCRTText}
headerText={emailHeaderCRTText}
footerText={messages.emailFooterCRTText}
headerText={messages.emailHeaderCRTText}
>
<SettingOption
action={setEmailThreads}

View file

@ -132,8 +132,6 @@ const MentionSettings = ({componentId, currentUser, isCRTEnabled}: Props) => {
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(() => {

View file

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

View file

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

View file

@ -6,7 +6,7 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
{
"flex": 1,
"justifyContent": "flex-end",
"margin": 16,
"marginVertical": 16,
}
}
>

View file

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

View file

@ -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 (
<SettingItem
optionLabelTextStyle={{color: theme.linkColor}}
onPress={onPress}
optionName='download_logs'
separator={false}
testID='settings.download_logs.option'
type='default'
type='link'
/>
);
}
@ -66,12 +63,11 @@ const ReportProblem = ({
return (
<SettingItem
optionLabelTextStyle={{color: theme.linkColor}}
onPress={onPress}
optionName='report_problem'
separator={false}
testID='settings.report_problem.option'
type='default'
type='link'
/>
);
};

View file

@ -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' && <View style={styles.helpGroup}/>}
{Platform.OS === 'android' && <MenuDivider/>}
{showHelp &&
<SettingItem
optionLabelTextStyle={{color: theme.linkColor}}
onPress={openHelp}
optionName='help'
separator={false}
testID='settings.help.option'
type='default'
type='link'
/>
}
<ReportProblem/>