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:
parent
2e5bbb9f62
commit
a27c82da90
48 changed files with 814 additions and 476 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
378
app/components/option_item/option_item.test.tsx
Normal file
378
app/components/option_item/option_item.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
contentContainerStyle: {
|
||||
marginTop: 8,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
80
app/screens/component_library/option_item.cl.tsx
Normal file
80
app/screens/component_library/option_item.cl.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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}/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
|
|||
{
|
||||
"flex": 1,
|
||||
"justifyContent": "flex-end",
|
||||
"margin": 16,
|
||||
"marginVertical": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue