Standardize tags and chips through the app (#8856)

* Standardize tags and chips through the app

* Add missing texts

* Fix tests and snapshots

* Address feedback

* Fix snapshots

* Address UX feedback

* Apply text styles to outer text component

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Daniel Espino García 2025-06-11 10:11:05 +02:00 committed by GitHub
parent 9b1d8cccee
commit cdd91dbbb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 818 additions and 331 deletions

View file

@ -108,4 +108,15 @@ describe('BaseChip', () => {
expect(getByTestId('base_chip').props.entering).toBeUndefined();
expect(getByTestId('base_chip').props.exiting).toBeUndefined();
});
it('should not have a touchable if onPress is not provided', () => {
const {queryByTestId} = renderWithIntlAndTheme(
<BaseChip
label='Test Label'
testID='base_chip'
/>,
);
expect(queryByTestId('base_chip.chip_button')).toBeNull();
});
});

View file

@ -1,25 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, Text, TouchableOpacity, useWindowDimensions} from 'react-native';
import React, {useMemo} from 'react';
import {Platform, Text, TouchableOpacity} from 'react-native';
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {useWindowDimensions} from '@hooks/device';
import {nonBreakingString} from '@utils/strings';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {CHIP_BOTTOM_MARGIN, CHIP_HEIGHT} from './constants';
import {CHIP_HEIGHT} from './constants';
type SelectedChipProps = {
onPress: () => void;
onPress?: () => void;
testID?: string;
showRemoveOption?: boolean;
showAnimation?: boolean;
label: string;
prefix?: JSX.Element;
maxWidth?: number;
}
const FADE_DURATION = 100;
@ -33,18 +35,20 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
borderRadius: 16,
height: CHIP_HEIGHT,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
marginBottom: CHIP_BOTTOM_MARGIN,
marginRight: 8,
paddingHorizontal: 7,
padding: 2,
},
text: {
marginLeft: 8,
color: theme.centerChannelColor,
...typography('Body', 100, 'SemiBold'),
...typography('Body', 100),
},
remove: {
justifyContent: 'center',
marginLeft: 7,
marginLeft: 5,
marginRight: 4,
},
chipContent: {
flexDirection: 'row',
alignItems: 'center',
},
};
});
@ -56,16 +60,25 @@ export default function BaseChip({
showAnimation,
label,
prefix,
maxWidth,
}: SelectedChipProps) {
const theme = useTheme();
const style = getStyleFromTheme(theme);
const dimensions = useWindowDimensions();
const textStyle = useMemo(() => {
// We set the max width to 70% of the screen width to make sure
// text like names get ellipsized correctly.
const textMaxWidth = maxWidth || dimensions.width * 0.70;
const marginRight = showRemoveOption ? undefined : 7;
const marginLeft = prefix ? 5 : 7;
return [style.text, {maxWidth: textMaxWidth, marginRight, marginLeft}];
}, [maxWidth, dimensions.width, showRemoveOption, style.text, prefix]);
const chipContent = (
<>
{prefix}
<Text
style={[style.text, {maxWidth: dimensions.width * 0.70}]}
style={textStyle}
numberOfLines={1}
testID={`${testID}.display_name`}
>
@ -74,7 +87,7 @@ export default function BaseChip({
</>
);
let content;
let content = chipContent;
if (showRemoveOption) {
content = (
<>
@ -86,16 +99,16 @@ export default function BaseChip({
>
<CompassIcon
name='close-circle'
size={18}
size={16}
color={changeOpacity(theme.centerChannelColor, 0.32)}
/>
</TouchableOpacity>
</>
);
} else {
} else if (onPress) {
content = (
<TouchableOpacity
style={style.remove}
style={style.chipContent}
onPress={onPress}
testID={`${testID}.chip_button`}
>

View file

@ -1,5 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const CHIP_HEIGHT = 32;
export const CHIP_BOTTOM_MARGIN = 8;
export const CHIP_HEIGHT = 24;

View file

@ -41,6 +41,7 @@ export default function UserChip({
size={20}
iconSize={20}
testID={`${testID}.profile_picture`}
showStatus={false}
/>
), [testID, user]);

View file

@ -224,6 +224,7 @@ const FloatingTextChipsInput = forwardRef<Ref, Props>(({
res.push({
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
minHeight: (CHIP_HEIGHT * 2.5) + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
gap: 8,
});
if (focused) {

View file

@ -34,7 +34,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
displayName: {
color: theme.centerChannelColor,
flexGrow: 1,
marginRight: 5,
...typography('Body', 200, 'SemiBold'),
},
displayNameCustomEmojiWidth: {

View file

@ -59,24 +59,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
wrapper: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: 5,
},
time: {
color: theme.centerChannelColor,
marginTop: 5,
opacity: 0.5,
...typography('Body', 75, 'Regular'),
},
visibleToYou: {
color: theme.centerChannelColor,
marginTop: 5,
marginLeft: 5,
opacity: 0.5,
...typography('Body', 75, 'Regular'),
},
postPriority: {
alignSelf: 'center',
marginLeft: 6,
},
};
});
@ -142,11 +137,9 @@ const Header = (props: HeaderProps) => {
/>
)}
{showPostPriority && post.metadata?.priority?.priority && (
<View style={style.postPriority}>
<PostPriorityLabel
label={post.metadata.priority.priority}
/>
</View>
<PostPriorityLabel
label={post.metadata.priority.priority}
/>
)}
{!isCRTEnabled && showReply && commentCount > 0 &&
<HeaderReply

View file

@ -8,7 +8,7 @@ import {fetchAndSwitchToThread} from '@actions/remote/thread';
import CompassIcon from '@components/compass_icon';
import {SEARCH} from '@constants/screens';
import {useServerUrl} from '@context/server';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {makeStyleSheetFromTheme} from '@utils/theme';
import type PostModel from '@typings/database/models/servers/post';
@ -24,13 +24,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
replyWrapper: {
flex: 1,
justifyContent: 'flex-end',
},
replyIconContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
alignItems: 'center',
justifyContent: 'flex-end',
minWidth: 40,
paddingTop: 2,
flex: 1,
},
@ -47,10 +45,10 @@ const HeaderReply = ({commentCount, location, post, theme}: HeaderReplyProps) =>
const style = getStyleSheet(theme);
const serverUrl = useServerUrl();
const onPress = useCallback(preventDoubleTap(() => {
const onPress = usePreventDoubleTap(useCallback(() => {
const rootId = post.rootId || post.id;
fetchAndSwitchToThread(serverUrl, rootId);
}), [serverUrl]);
}, [post.id, post.rootId, serverUrl]));
return (
<View

View file

@ -2,10 +2,9 @@
// See LICENSE.txt for license information.
import React from 'react';
import {StyleSheet} from 'react-native';
import {defineMessage} from 'react-intl';
import Tag, {BotTag, GuestTag} from '@components/tag';
import {t} from '@i18n';
type HeaderTagProps = {
isAutomation?: boolean;
@ -13,38 +12,25 @@ type HeaderTagProps = {
showGuestTag?: boolean;
}
const style = StyleSheet.create({
tag: {
marginLeft: 0,
marginRight: 5,
marginBottom: 5,
},
});
const autoResponderMessage = defineMessage({id: 'post_info.auto_responder', defaultMessage: 'Automatic Reply'});
const HeaderTag = ({
isAutomation, isAutoResponder, showGuestTag,
}: HeaderTagProps) => {
if (isAutomation) {
return (
<BotTag
style={style.tag}
testID='post_header.bot.tag'
/>
<BotTag testID='post_header.bot.tag'/>
);
} else if (showGuestTag) {
return (
<GuestTag
style={style.tag}
testID='post_header.guest.tag'
/>
<GuestTag testID='post_header.guest.tag'/>
);
} else if (isAutoResponder) {
return (
<Tag
id={t('post_info.auto_responder')}
defaultMessage={'Automatic Reply'}
style={style.tag}
message={autoResponderMessage}
testID='post_header.auto_responder.tag'
uppercase={true}
/>
);
}

View file

@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Tag from '@components/tag';
import {PostPriorityType} from '@constants/post';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import PostPriorityLabel from './post_priority_label';
jest.mock('@components/tag', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(Tag).mockImplementation((props) => React.createElement('Tag', {...props}));
describe('PostPriorityLabel', () => {
it('should return null for standard priority', () => {
const {toJSON} = renderWithIntlAndTheme(
<PostPriorityLabel label={PostPriorityType.STANDARD}/>);
expect(toJSON()).toBeNull();
});
it('should render urgent priority label correctly', () => {
const {getByTestId} = renderWithIntlAndTheme(
<PostPriorityLabel label={PostPriorityType.URGENT}/>);
const tag = getByTestId('urgent_post_priority_label');
expect(tag.props.message.id).toBe('post_priority.label.urgent');
expect(tag.props.icon).toBe('alert-outline');
expect(tag.props.type).toBe('danger');
expect(tag.props.size).toBe('xs');
expect(tag.props.uppercase).toBe(true);
});
it('should render important priority label correctly', () => {
const {getByTestId} = renderWithIntlAndTheme(
<PostPriorityLabel label={PostPriorityType.IMPORTANT}/>);
const tag = getByTestId('important_post_priority_label');
expect(tag.props.message.id).toBe('post_priority.label.important');
expect(tag.props.icon).toBe('alert-circle-outline');
expect(tag.props.type).toBe('info');
expect(tag.props.size).toBe('xs');
expect(tag.props.uppercase).toBe(true);
});
});

View file

@ -2,68 +2,42 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {type StyleProp, StyleSheet, Text, View, type ViewStyle} from 'react-native';
import {defineMessages} from 'react-intl';
import CompassIcon from '@components/compass_icon';
import {PostPriorityColors, PostPriorityType} from '@constants/post';
import {typography} from '@utils/typography';
const style = StyleSheet.create({
container: {
alignSelf: 'flex-start',
flexDirection: 'row',
borderRadius: 4,
alignItems: 'center',
paddingHorizontal: 4,
},
urgent: {
backgroundColor: PostPriorityColors.URGENT,
},
important: {
backgroundColor: PostPriorityColors.IMPORTANT,
},
label: {
color: '#fff',
...typography('Body', 25, 'SemiBold'),
},
icon: {
color: '#fff',
fontSize: 12,
marginRight: 4,
},
});
import Tag from '@components/tag';
import {PostPriorityType} from '@constants/post';
type Props = {
label: PostPriority['priority'];
};
const PostPriorityLabel = ({label}: Props) => {
const intl = useIntl();
const messages = defineMessages({
urgent: {
id: 'post_priority.label.urgent',
defaultMessage: 'URGENT',
},
important: {
id: 'post_priority.label.important',
defaultMessage: 'IMPORTANT',
},
});
const containerStyle: StyleProp<ViewStyle> = [style.container];
let iconName = '';
let labelText = '';
if (label === PostPriorityType.URGENT) {
containerStyle.push(style.urgent);
iconName = 'alert-outline';
labelText = intl.formatMessage({id: 'post_priority.label.urgent', defaultMessage: 'URGENT'});
} else if (label === PostPriorityType.IMPORTANT) {
containerStyle.push(style.important);
iconName = 'alert-circle-outline';
labelText = intl.formatMessage({id: 'post_priority.label.important', defaultMessage: 'IMPORTANT'});
const PostPriorityLabel = ({label}: Props) => {
if (label === PostPriorityType.STANDARD) {
return null;
}
const isUrgent = label === PostPriorityType.URGENT; // else it is important
return (
<View
<Tag
message={isUrgent ? messages.urgent : messages.important}
icon={isUrgent ? 'alert-outline' : 'alert-circle-outline'}
type={isUrgent ? 'danger' : 'info'}
size='xs'
testID={`${label}_post_priority_label`}
style={containerStyle}
>
<CompassIcon
name={iconName}
style={style.icon}
/>
<Text style={style.label}>{labelText}</Text>
</View>
uppercase={true}
/>
);
};

View file

@ -5,7 +5,6 @@ exports[`Section notice match snapshot 1`] = `
style={
[
{
"borderRadius": 4,
"borderStyle": "solid",
"borderWidth": 1,
},
@ -13,7 +12,9 @@ exports[`Section notice match snapshot 1`] = `
"backgroundColor": "rgba(93,137,234,0.08)",
"borderColor": "rgba(93,137,234,0.16)",
},
undefined,
{
"borderRadius": 4,
},
]
}
testID="sectionNoticeContainer"

View file

@ -2,10 +2,11 @@
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {Pressable, Text, View, type StyleProp, type ViewStyle} from 'react-native';
import {Pressable, Text, View} from 'react-native';
import Tag from '@components/tag';
import {useTheme} from '@context/theme';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -27,10 +28,8 @@ type Props = {
onDismissClick?: () => void;
location: AvailableScreens;
tags?: string[];
tagsVariant?: 'default' | 'subtle';
testID?: string;
containerStyle?: StyleProp<ViewStyle>;
iconSize?: number;
squareCorners?: boolean;
}
const iconByType = {
@ -47,6 +46,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
container: {
borderWidth: 1,
borderStyle: 'solid',
},
roundCorners: {
borderRadius: 4,
},
content: {
@ -150,6 +151,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 4,
},
};
@ -166,10 +168,8 @@ const SectionNotice = ({
type = 'info',
location,
tags,
tagsVariant = 'default',
squareCorners,
testID,
containerStyle,
iconSize = 20,
}: Props) => {
const theme = useTheme();
const styles = getStyleFromTheme(theme);
@ -182,9 +182,9 @@ const SectionNotice = ({
const combinedContainerStyle = useMemo(() => [
styles.container,
styles[`${type}Container`],
containerStyle,
], [type, containerStyle]);
const iconStyle = useMemo(() => styles[`${type}Icon`], [type]);
!squareCorners && styles.roundCorners,
], [styles, type, squareCorners]);
const iconStyle = useMemo(() => styles[`${type}Icon`], [styles, type]);
return (
<View
style={combinedContainerStyle}
@ -195,7 +195,7 @@ const SectionNotice = ({
<CompassIcon
name={icon}
style={iconStyle}
size={iconSize}
size={20}
testID='sectionNoticeHeaderIcon'
/>
)}
@ -206,6 +206,8 @@ const SectionNotice = ({
theme={theme}
location={location}
baseTextStyle={styles.baseText}
textStyles={getMarkdownTextStyles(theme)}
blockStyles={getMarkdownBlockStyles(theme)}
value={text}
/>
)}
@ -214,9 +216,8 @@ const SectionNotice = ({
{tags.map((tag) => (
<Tag
key={tag}
id={`tag.${tag}`}
defaultMessage={tag}
variant={tagsVariant}
testID={`tag.${tag}`}
message={tag}
/>
))}
</View>

View file

@ -7,7 +7,7 @@ import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming}
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import Button from '@components/button';
import {CHIP_BOTTOM_MARGIN, CHIP_HEIGHT} from '@components/chips/constants';
import {CHIP_HEIGHT} from '@components/chips/constants';
import SelectedUserChip from '@components/chips/selected_user_chip';
import Toast from '@components/toast';
import {useTheme} from '@context/theme';
@ -83,7 +83,8 @@ type Props = {
}
const BUTTON_HEIGHT = 48;
const CHIP_HEIGHT_WITH_MARGIN = CHIP_HEIGHT + CHIP_BOTTOM_MARGIN;
const CHIP_GAP = 8;
const CHIP_HEIGHT_WITH_MARGIN = CHIP_HEIGHT + CHIP_GAP;
const EXPOSED_CHIP_HEIGHT = 0.33 * CHIP_HEIGHT;
const MAX_CHIP_ROWS = 2;
const SCROLL_MARGIN_TOP = 20;
@ -125,6 +126,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
flexGrow: 1,
flexWrap: 'wrap',
gap: CHIP_GAP,
},
message: {
color: theme.centerChannelBg,

View file

@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import Tag from './base_tag';
describe('Tag', () => {
const defaultProps = {
message: 'Test Tag',
};
it('should render correctly', () => {
const {getByText} = renderWithIntlAndTheme(<Tag {...defaultProps}/>);
expect(getByText('Test Tag')).toBeDefined();
});
it('should render icon correctly', () => {
const {getByTestId, rerender, queryByTestId} = renderWithIntlAndTheme(
<Tag
{...defaultProps}
icon='check'
testID='test-tag'
/>,
);
expect(getByTestId('test-tag.icon')).toBeDefined();
rerender(
<Tag
{...defaultProps}
testID='test-tag'
/>,
);
expect(queryByTestId('test-tag.icon')).toBeNull();
});
it('should render uppercase correctly', () => {
const {getByText} = renderWithIntlAndTheme(
<Tag
{...defaultProps}
uppercase={true}
/>,
);
expect(getByText('Test Tag')).toBeDefined();
expect(getByText('Test Tag')).toHaveStyle({textTransform: 'uppercase'});
});
});

View file

@ -0,0 +1,154 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {Text, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography, type FontSizes} from '@utils/typography';
import type {MessageDescriptor} from 'react-intl';
type TagProps = {
message: MessageDescriptor | string;
icon?: string;
type?: 'general' | 'info' | 'danger' | 'success' | 'warning' | 'infoDim';
size?: 'xs' | 's' | 'm';
uppercase?: boolean;
testID?: string;
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
borderRadius: 4,
paddingVertical: 2,
paddingHorizontal: 4,
alignItems: 'center',
justifyContent: 'center',
},
generalContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
generalText: {
color: theme.centerChannelColor,
},
infoContainer: {
backgroundColor: theme.buttonBg,
},
infoText: {
color: theme.buttonColor,
},
dangerContainer: {
backgroundColor: theme.dndIndicator,
},
dangerText: {
color: theme.buttonColor,
},
successContainer: {
backgroundColor: theme.onlineIndicator,
},
successText: {
color: theme.buttonColor,
},
warningContainer: {
backgroundColor: theme.awayIndicator,
},
warningText: {
color: theme.buttonColor,
},
infoDimContainer: {
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
},
infoDimText: {
color: theme.buttonBg,
},
};
});
const textTypographyPerSize: Record<Required<TagProps>['size'], FontSizes> = {
xs: 25,
s: 75,
m: 100,
};
const iconSizePerSize: Record<Required<TagProps>['size'], number> = {
xs: 10,
s: 12,
m: 14,
};
const Tag = ({
message,
icon,
type = 'general',
size = 's',
uppercase = false,
testID,
}: TagProps) => {
const theme = useTheme();
const styles = getStyleFromTheme(theme);
const textStyle = useMemo(() => {
const sizeRelated = typography('Heading', textTypographyPerSize[size]);
const colorRelated = styles[`${type}Text`];
const uppercaseRelated = uppercase ? {textTransform: 'uppercase' as const} : {};
return [sizeRelated, colorRelated, uppercaseRelated];
}, [size, styles, type, uppercase]);
const containerStyle = useMemo(() => {
const colorRelated = styles[`${type}Container`];
return [styles.container, colorRelated];
}, [styles, type]);
let iconComponent;
if (icon) {
iconComponent = (
<CompassIcon
size={iconSizePerSize[size]}
name={icon}
color={styles[`${type}Text`].color}
testID={testID ? `${testID}.icon` : undefined}
/>
);
}
let textComponent;
if (typeof message === 'string') {
textComponent = (
<Text
style={textStyle}
testID={testID}
>
{message}
</Text>
);
} else {
textComponent = (
<FormattedText
{...message}
style={textStyle}
testID={testID}
/>
);
}
return (
<View style={containerStyle}>
{/* We wrap the icon and text in a Text component to avoid
the ellipsis to go out of the box on iOS */}
<Text
numberOfLines={1}
style={textStyle}
>
{iconComponent}
{' '}
{textComponent}
</Text>
</View>
);
};
export default Tag;

View file

@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import Tag from './base_tag';
import BotTag from './bot_tag';
jest.mock('./base_tag', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(Tag).mockImplementation((props) => React.createElement('Tag', {...props}));
describe('BotTag', () => {
it('should render with the correct props', () => {
const {getByTestId} = renderWithIntlAndTheme(
<BotTag
testID='bot-tag'
size='m'
/>,
);
const tag = getByTestId('bot-tag');
expect(tag.props.message.id).toBe('post_info.bot');
expect(tag.props.message.defaultMessage).toBe('Bot');
expect(tag.props.uppercase).toBe(true);
expect(tag.props.size).toBe('m');
});
it('should render with default props', () => {
const {getByTestId} = renderWithIntlAndTheme(
<BotTag
testID='bot-tag'
/>,
);
const tag = getByTestId('bot-tag');
expect(tag.props.message.id).toBe('post_info.bot');
expect(tag.props.message.defaultMessage).toBe('Bot');
expect(tag.props.uppercase).toBe(true);
expect(tag.props.size).toBeUndefined();
});
});

View file

@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type ComponentProps} from 'react';
import {defineMessage} from 'react-intl';
import Tag from './base_tag';
const botMessage = defineMessage({
id: 'post_info.bot',
defaultMessage: 'Bot',
});
type BotTagProps = Omit<ComponentProps<typeof Tag>, 'message' | 'icon' | 'type'>;
const BotTag = ({
size,
testID,
}: BotTagProps) => {
return (
<Tag
message={botMessage}
testID={testID}
uppercase={true}
size={size}
/>
);
};
export default BotTag;

View file

@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import Tag from './base_tag';
import GuestTag from './guest_tag';
jest.mock('./base_tag', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mocked(Tag).mockImplementation((props) => React.createElement('Tag', {...props}));
describe('GuestTag', () => {
it('should render with the correct props', () => {
const {getByTestId} = renderWithIntlAndTheme(
<GuestTag
testID='guest-tag'
size='m'
/>,
);
const tag = getByTestId('guest-tag');
expect(tag.props.message.id).toBe('post_info.guest');
expect(tag.props.message.defaultMessage).toBe('Guest');
expect(tag.props.uppercase).toBe(true);
expect(tag.props.size).toBe('m');
});
it('should render with default props', () => {
const {getByTestId} = renderWithIntlAndTheme(
<GuestTag
testID='guest-tag'
/>,
);
const tag = getByTestId('guest-tag');
expect(tag.props.message.id).toBe('post_info.guest');
expect(tag.props.message.defaultMessage).toBe('Guest');
expect(tag.props.uppercase).toBe(true);
expect(tag.props.size).toBeUndefined();
});
});

View file

@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type ComponentProps} from 'react';
import {defineMessage} from 'react-intl';
import Tag from './base_tag';
const botMessage = defineMessage({
id: 'post_info.guest',
defaultMessage: 'Guest',
});
type GuestTagProps = Omit<ComponentProps<typeof Tag>, 'message' | 'icon' | 'type'>;
const GuestTag = ({
testID,
size,
}: GuestTagProps) => {
return (
<Tag
message={botMessage}
testID={testID}
uppercase={true}
size={size}
/>
);
};
export default GuestTag;

View file

@ -1,116 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {type StyleProp, type TextStyle, View, type ViewStyle} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type TagProps = {
id: string;
defaultMessage: string;
inTitle?: boolean;
show?: boolean;
style?: StyleProp<ViewStyle>;
testID?: string;
textStyle?: StyleProp<TextStyle>;
variant?: 'default' | 'subtle';
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
alignSelf: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
borderRadius: 4,
paddingVertical: 2,
paddingHorizontal: 4,
},
text: {
color: theme.centerChannelColor,
fontFamily: 'OpenSans-SemiBold',
fontSize: 10,
textTransform: 'uppercase',
},
title: {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.15),
color: changeOpacity(theme.sidebarHeaderTextColor, 0.6),
},
// Variant styles
subtleContainer: {
borderRadius: 6,
paddingVertical: 4,
paddingHorizontal: 8,
marginRight: 8,
marginBottom: 8,
},
subtleText: {
fontSize: 12,
textTransform: 'none',
},
};
});
export function BotTag(props: Omit<TagProps, 'id' | 'defaultMessage'>) {
const id = t('post_info.bot');
const defaultMessage = 'Bot';
return (
<Tag
{...props}
id={id}
defaultMessage={defaultMessage}
/>
);
}
export function GuestTag(props: Omit<TagProps, 'id' | 'defaultMessage'>) {
const id = t('post_info.guest');
const defaultMessage = 'Guest';
return (
<Tag
{...props}
id={id}
defaultMessage={defaultMessage}
/>
);
}
const Tag = ({id, defaultMessage, inTitle, show = true, style, testID, textStyle, variant = 'default'}: TagProps) => {
const theme = useTheme();
if (!show) {
return null;
}
const styles = getStyleFromTheme(theme);
return (
<View
style={[
styles.container,
variant === 'subtle' && styles.subtleContainer,
style,
]}
>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={[
styles.text,
inTitle ? styles.title : null,
variant === 'subtle' && styles.subtleText,
textStyle,
]}
testID={testID}
/>
</View>
);
};
import Tag from './base_tag';
import BotTag from './bot_tag';
import GuestTag from './guest_tag';
export default Tag;
export {
GuestTag,
BotTag,
};

View file

@ -76,16 +76,12 @@ const nonThemedStyles = StyleSheet.create({
rowInfoContainer: {
flex: 1,
flexDirection: 'row',
},
icon: {
marginLeft: 4,
alignItems: 'center',
gap: 4,
},
profile: {
marginRight: 12,
},
tag: {
marginLeft: 6,
},
flex: {
flex: 1,
},
@ -201,29 +197,19 @@ const UserItem = ({
)}
</Text>
{showBadges && bot && (
<BotTag
testID={`${userItemTestId}.bot.tag`}
style={nonThemedStyles.tag}
/>
<BotTag testID={`${userItemTestId}.bot.tag`}/>
)}
{showBadges && guest && !hideGuestTags && (
<GuestTag
testID={`${userItemTestId}.guest.tag`}
style={nonThemedStyles.tag}
/>
<GuestTag testID={`${userItemTestId}.guest.tag`}/>
)}
{Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && (
<CustomStatusEmoji
customStatus={customStatus!}
style={nonThemedStyles.icon}
/>
<CustomStatusEmoji customStatus={customStatus!}/>
)}
{shared && (
<CompassIcon
name={'circle-multiple-outline'}
size={16}
color={theme.centerChannelColor}
style={nonThemedStyles.icon}
/>
)}
<View style={nonThemedStyles.flex}/>

View file

@ -814,8 +814,10 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
<View
style={
{
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"gap": 4,
}
}
>
@ -1134,8 +1136,10 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
<View
style={
{
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"gap": 4,
}
}
>
@ -1492,8 +1496,10 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
<View
style={
{
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"gap": 4,
}
}
>
@ -1711,8 +1717,10 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
<View
style={
{
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"gap": 4,
}
}
>

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {defineMessage, useIntl} from 'react-intl';
import {Platform, Text, TouchableOpacity, View} from 'react-native';
import {useCurrentCall} from '@calls/state';
@ -65,9 +65,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
unmutedIcon: {
color: changeOpacity(theme.centerChannelColor, 0.56),
},
hostTag: {
paddingVertical: 4,
},
raiseHandIcon: {
color: theme.awayIndicator,
},
@ -76,6 +73,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const hostMessage = defineMessage({
id: 'mobile.calls_host',
defaultMessage: 'host',
});
export const Participant = ({sess, teammateNameDisplay, onPress}: Props) => {
const intl = useIntl();
const currentCall = useCurrentCall();
@ -122,9 +124,8 @@ export const Participant = ({sess, teammateNameDisplay, onPress}: Props) => {
}
{sess.userId === currentCall.hostId &&
<Tag
id={'mobile.calls_host'}
defaultMessage={'host'}
style={styles.hostTag}
message={hostMessage}
uppercase={true}
/>
}
</View>

View file

@ -34,13 +34,10 @@ type Props = {
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
botContainer: {
alignSelf: 'flex-end',
bottom: 7.5,
height: 20,
marginBottom: 0,
marginLeft: 4,
paddingVertical: 0,
displayNameContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
botText: {
fontSize: 14,
@ -164,7 +161,7 @@ const DirectChannel = ({
{getGMIntroMessageSpecificPart(userNotifyProps, channelNotifyProps, styles.boldText)}
</Text>
);
}, [channel.displayName, theme, channelNotifyProps, userNotifyProps]);
}, [channel.type, channel.displayName, hasGMasDMFeature, styles.message, styles.boldText, userNotifyProps, channelNotifyProps]);
const profiles = useMemo(() => {
if (channel.type === General.DM_CHANNEL) {
@ -195,14 +192,14 @@ const DirectChannel = ({
userIds={channelMembers.map((cm) => cm.userId)}
/>
);
}, [members, theme]);
}, [channel.id, channel.name, channel.type, currentUserId, members, theme]);
return (
<View style={styles.container}>
<View style={styles.profilesContainer}>
{profiles}
</View>
<View style={{flexDirection: 'row'}}>
<View style={styles.displayNameContainer}>
<Text
style={[styles.title, channel.type === General.GM_CHANNEL ? styles.titleGroup : undefined]}
testID='channel_post_list.intro.display_name'
@ -210,10 +207,7 @@ const DirectChannel = ({
{channel.displayName}
</Text>
{isBot &&
<BotTag
style={styles.botContainer}
textStyle={styles.botText}
/>
<BotTag size={'m'}/>
}
</View>
{message}

View file

@ -288,9 +288,7 @@ export default function ChannelAddMembers({
onDismissClick={handleDismissBanner}
location={Screens.CHANNEL_ADD_MEMBERS}
testID={`${TEST_ID}.notice`}
containerStyle={style.flatBottomBanner}
iconSize={24}
tagsVariant='subtle'
squareCorners={true}
/>
)}
<View style={style.searchBar}>

View file

@ -25,18 +25,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
displayName: {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
},
position: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 200),
},
tagContainer: {
marginLeft: 12,
},
tag: {
color: theme.centerChannelColor,
...typography('Body', 100, 'SemiBold'),
},
titleContainer: {
flex: 1,
marginLeft: 16,
@ -81,15 +76,13 @@ const DirectMessage = ({
</Text>
{user?.isGuest && !hideGuestTags &&
<GuestTag
textStyle={styles.tag}
style={styles.tagContainer}
size={'m'}
testID={`${directMessageUserTestId}.guest.tag`}
/>
}
{user?.isBot &&
<BotTag
textStyle={styles.tag}
style={styles.tagContainer}
size={'m'}
testID={`${directMessageUserTestId}.bot.tag`}
/>
}

View file

@ -14,8 +14,6 @@ const propPossibilities = {};
const buttonSizeValues = ['xs', 's', 'm', 'lg'];
const buttonEmphasisValues = ['primary', 'secondary', 'tertiary', 'link'];
const buttonTypeValues = ['default', 'destructive', 'inverted', 'disabled'];
const buttonStateValues = ['default', 'hover', 'active', 'focus'];
const onPress = () => Alert.alert('Button pressed!');
@ -25,28 +23,32 @@ const ButtonComponentLibrary = () => {
const [disabled, disabledSelector] = useBooleanProp('disabled', false);
const [buttonSize, buttonSizePosibilities, buttonSizeSelector] = useDropdownProp('size', 'm', buttonSizeValues, true);
const [buttonEmphasis, buttonEmphasisPosibilities, buttonEmphasisSelector] = useDropdownProp('emphasis', 'primary', buttonEmphasisValues, true);
const [buttonType, buttonTypePosibilities, buttonTypeSelector] = useDropdownProp('buttonType', 'default', buttonTypeValues, true);
const [buttonState, buttonStatePosibilities, buttonStateSelector] = useDropdownProp('buttonState', 'default', buttonStateValues, true);
const [iconName, iconNameSelector] = useStringProp('iconName', '', true);
const [isIconOnTheRight, isIconOnTheRightSelector] = useBooleanProp('isIconOnTheRight', false);
const [showLoader, showLoaderSelector] = useBooleanProp('showLoader', false);
const [isInverted, isInvertedSelector] = useBooleanProp('isInverted', false);
const [isDestructive, isDestructiveSelector] = useBooleanProp('isDestructive', false);
const components = useMemo(
() => buildComponent(Button, propPossibilities, [
buttonSizePosibilities,
buttonEmphasisPosibilities,
buttonTypePosibilities,
buttonStatePosibilities,
], [
text,
disabled,
buttonSize,
buttonEmphasis,
buttonType,
buttonState,
iconName,
isIconOnTheRight,
showLoader,
isInverted,
isDestructive,
{
theme,
onPress,
},
]),
[buttonEmphasis, buttonEmphasisPosibilities, buttonSize, buttonSizePosibilities, buttonState, buttonStatePosibilities, buttonType, buttonTypePosibilities, disabled, text, theme],
[buttonEmphasis, buttonEmphasisPosibilities, buttonSize, buttonSizePosibilities, disabled, text, theme, iconName, isIconOnTheRight, showLoader, isInverted, isDestructive],
);
return (
@ -55,8 +57,11 @@ const ButtonComponentLibrary = () => {
{disabledSelector}
{buttonSizeSelector}
{buttonEmphasisSelector}
{buttonTypeSelector}
{buttonStateSelector}
{iconNameSelector}
{isIconOnTheRightSelector}
{showLoaderSelector}
{isInvertedSelector}
{isDestructiveSelector}
<View>{components}</View>
</>
);

View file

@ -0,0 +1,55 @@
// 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 BaseChip from '@components/chips/base_chip';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {useBooleanProp, useStringProp} from './hooks';
import {buildComponent} from './utils';
const propPossibilities = {};
const onPress = () => Alert.alert('Button pressed!');
const ChipComponentLibrary = () => {
const theme = useTheme();
const [label, labelSelector] = useStringProp('label', 'Chip text', false);
const [showRemoveOption, showRemoveOptionSelector] = useBooleanProp('showRemoveOption', false);
const [hasPrefix, hasPrefixSelector] = useBooleanProp('hasPrefix', false);
const components = useMemo(
() => buildComponent(BaseChip, propPossibilities, [], [
label,
showRemoveOption,
{
onPress,
prefix: hasPrefix.hasPrefix ? (
<CompassIcon
name='account-outline'
size={16}
color={theme.centerChannelColor}
style={{marginLeft: 6}}
/>
) : undefined,
testID: 'chip-example',
theme,
},
]),
[label, showRemoveOption, hasPrefix.hasPrefix, theme],
);
return (
<>
{labelSelector}
{showRemoveOptionSelector}
{hasPrefixSelector}
<View style={{flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 16}}>{components}</View>
</>
);
};
export default ChipComponentLibrary;

View file

@ -12,11 +12,17 @@ import SecurityManager from '@managers/security_manager';
import {popTopScreen} from '@screens/navigation';
import ButtonComponentLibrary from './button.cl';
import ChipComponentLibrary from './chip.cl';
import SectionNoticeComponentLibrary from './section_notice.cl';
import TagComponentLibrary from './tag.cl';
import type {AvailableScreens} from '@typings/screens/navigation';
const componentMap = {
Button: ButtonComponentLibrary,
Chip: ChipComponentLibrary,
Tag: TagComponentLibrary,
SectionNotice: SectionNoticeComponentLibrary,
};
type ComponentName = keyof typeof componentMap

View file

@ -0,0 +1,102 @@
// 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 SectionNotice from '@components/section_notice';
import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
import {buildComponent} from './utils';
const propPossibilities = {};
const sectionNoticeTypeValues = ['info', 'success', 'danger', 'welcome', 'warning', 'hint'];
const onButtonPress = (name: string) => Alert.alert(`Button pressed: ${name}`);
const onDismissPress = () => Alert.alert('Notice dismissed!');
const PRIMARY_BUTTON = {
onClick: () => onButtonPress('primary'),
text: 'Primary Action',
};
const SECONDARY_BUTTON = {
onClick: () => onButtonPress('secondary'),
text: 'Secondary Action',
};
const LINK_BUTTON = {
onClick: () => onButtonPress('link'),
text: 'Link Action',
};
const TAGS = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7', 'tag8', 'tag9', 'tag10'];
const SectionNoticeComponentLibrary = () => {
const [title, titleSelector] = useStringProp('title', 'Notice Title', false);
const [text, textSelector] = useStringProp('text', 'This is the notice text content with **markdown**.', false);
const [isDismissable, isDismissableSelector] = useBooleanProp('isDismissable', false);
const [squareCorners, squareCornersSelector] = useBooleanProp('squareCorners', false);
const [hasPrimaryButton, hasPrimaryButtonSelector] = useBooleanProp('hasPrimaryButton', false);
const [hasSecondaryButton, hasSecondaryButtonSelector] = useBooleanProp('hasSecondaryButton', false);
const [hasLinkButton, hasLinkButtonSelector] = useBooleanProp('hasLinkButton', false);
const [hasTags, hasTagsSelector] = useBooleanProp('hasTags', false);
const [sectionNoticeType, sectionNoticeTypePossibilities, sectionNoticeTypeSelector] = useDropdownProp('type', 'info', sectionNoticeTypeValues, true);
const primaryButton = hasPrimaryButton.hasPrimaryButton ? PRIMARY_BUTTON : undefined;
const secondaryButton = hasSecondaryButton.hasSecondaryButton ? SECONDARY_BUTTON : undefined;
const linkButton = hasLinkButton.hasLinkButton ? LINK_BUTTON : undefined;
const tags = hasTags.hasTags ? TAGS : undefined;
const components = useMemo(
() => buildComponent(SectionNotice, propPossibilities, [
sectionNoticeTypePossibilities,
], [
title,
text,
isDismissable,
squareCorners,
{
primaryButton,
secondaryButton,
linkButton,
tags,
onDismissClick: isDismissable.isDismissable ? onDismissPress : undefined,
location: 'ComponentLibrary',
testID: 'section-notice-example',
},
sectionNoticeType,
]),
[
sectionNoticeTypePossibilities,
title,
text,
isDismissable,
squareCorners,
primaryButton,
secondaryButton,
linkButton,
tags,
sectionNoticeType,
],
);
return (
<>
{titleSelector}
{textSelector}
{isDismissableSelector}
{squareCornersSelector}
{hasPrimaryButtonSelector}
{hasSecondaryButtonSelector}
{hasLinkButtonSelector}
{hasTagsSelector}
{sectionNoticeTypeSelector}
<View style={{gap: 16, marginTop: 16}}>{components}</View>
</>
);
};
export default SectionNoticeComponentLibrary;

View file

@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {View} from 'react-native';
import Tag from '@components/tag/base_tag';
import {useTheme} from '@context/theme';
import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
import {buildComponent} from './utils';
const propPossibilities = {};
const tagTypeValues = ['general', 'info', 'danger', 'success', 'warning', 'infoDim'];
const tagSizeValues = ['xs', 's', 'm'];
const TagComponentLibrary = () => {
const theme = useTheme();
const [message, messageSelector] = useStringProp('message', 'Tag text', false);
const [icon, iconSelector] = useStringProp('icon', 'check', false);
const [useIcon, useIconSelector] = useBooleanProp('useIcon', true);
const [uppercase, uppercaseSelector] = useBooleanProp('uppercase', true);
const [tagType, tagTypePossibilities, tagTypeSelector] = useDropdownProp('type', 'general', tagTypeValues, true);
const [tagSize, tagSizePossibilities, tagSizeSelector] = useDropdownProp('size', 's', tagSizeValues, true);
const components = useMemo(
() => buildComponent(Tag, propPossibilities, [
tagTypePossibilities,
tagSizePossibilities,
], [
message,
uppercase,
{
icon: useIcon.useIcon ? icon.icon : undefined,
theme,
testID: 'tag-example',
},
tagType,
tagSize,
]),
[tagTypePossibilities, tagSizePossibilities, message, uppercase, useIcon.useIcon, icon.icon, theme, tagType, tagSize],
);
return (
<>
{messageSelector}
{iconSelector}
{useIconSelector}
{uppercaseSelector}
{tagTypeSelector}
{tagSizeSelector}
<View style={{gap: 8, alignItems: 'flex-start'}}>{components}</View>
</>
);
};
export default TagComponentLibrary;

View file

@ -96,6 +96,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
flexWrap: 'wrap',
marginHorizontal: 20,
marginVertical: 16,
gap: 8,
},
};
});

View file

@ -316,9 +316,7 @@ export default function ManageChannelMembers({
tags={attributeTags.length > 0 ? attributeTags : undefined}
location={Screens.MANAGE_CHANNEL_MEMBERS}
testID={`${TEST_ID}.notice`}
containerStyle={styles.flatBottomBanner}
iconSize={24}
tagsVariant='subtle'
squareCorners={true}
/>
)}
<View style={styles.searchBar}>

View file

@ -14,7 +14,6 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
style={
[
{
"borderRadius": 4,
"borderStyle": "solid",
"borderWidth": 1,
},
@ -22,7 +21,9 @@ exports[`SendTestNotificationNotice should match snapshot 1`] = `
"backgroundColor": "rgba(93,137,234,0.08)",
"borderColor": "rgba(93,137,234,0.16)",
},
undefined,
{
"borderRadius": 4,
},
]
}
testID="sectionNoticeContainer"

View file

@ -48,6 +48,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
details: {
marginLeft: 24,
justifyContent: 'center',
alignItems: 'flex-start',
gap: 4,
flex: 1,
},
displayName: {

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {StyleSheet} from 'react-native';
import {defineMessages} from 'react-intl';
import Tag, {BotTag, GuestTag} from '@components/tag';
@ -14,37 +14,37 @@ type Props = {
isTeamAdmin: boolean;
}
const styles = StyleSheet.create({
tag: {
alignSelf: 'flex-start',
paddingHorizontal: 6,
marginBottom: 4,
const messages = defineMessages({
sysAdmin: {
id: 'user_profile.system_admin',
defaultMessage: 'System Admin',
},
teamAdmin: {
id: 'user_profile.team_admin',
defaultMessage: 'Team Admin',
},
channelAdmin: {
id: 'user_profile.channel_admin',
defaultMessage: 'Channel Admin',
},
});
const UserProfileTag = ({isBot, isChannelAdmin, showGuestTag, isSystemAdmin, isTeamAdmin}: Props) => {
if (isBot) {
return (
<BotTag
style={styles.tag}
testID='user_profile.bot.tag'
/>);
<BotTag testID='user_profile.bot.tag'/>);
}
if (showGuestTag) {
return (
<GuestTag
style={styles.tag}
testID='user_profile.guest.tag'
/>);
<GuestTag testID='user_profile.guest.tag'/>);
}
if (isSystemAdmin) {
return (
<Tag
id='user_profile.system_admin'
defaultMessage='System Admin'
style={styles.tag}
message={messages.sysAdmin}
uppercase={true}
testID='user_profile.system_admin.tag'
/>
);
@ -53,10 +53,9 @@ const UserProfileTag = ({isBot, isChannelAdmin, showGuestTag, isSystemAdmin, isT
if (isTeamAdmin) {
return (
<Tag
id='user_profile.team_admin'
defaultMessage='Team Admin'
style={styles.tag}
message={messages.teamAdmin}
testID='user_profile.team_admin.tag'
uppercase={true}
/>
);
}
@ -64,10 +63,9 @@ const UserProfileTag = ({isBot, isChannelAdmin, showGuestTag, isSystemAdmin, isT
if (isChannelAdmin) {
return (
<Tag
id='user_profile.channel_admin'
defaultMessage='Channel Admin'
style={styles.tag}
message={messages.channelAdmin}
testID='user_profile.channel_admin.tag'
uppercase={true}
/>
);
}

View file

@ -198,7 +198,7 @@ export const getBackgroundStyles = (theme: Theme): BackgroundStyles => {
},
inverted: {
default: {
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
backgroundColor: changeOpacity(theme.buttonColor, 0.12),
},
hover: {
backgroundColor: changeOpacity(theme.sidebarText, 0.16),

View file

@ -1272,7 +1272,10 @@
"unreads.empty.paragraph": "Turn off the unread filter to show all your channels.",
"unreads.empty.show_all": "Show all",
"unreads.empty.title": "No more unreads",
"user_profile.channel_admin": "Channel Admin",
"user_profile.custom_status": "Custom Status",
"user_profile.system_admin": "System Admin",
"user_profile.team_admin": "Team Admin",
"user_settings.notifications.test_notification.body": "Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.",
"user_settings.notifications.test_notification.go_to_docs": "Troubleshooting docs",
"user_settings.notifications.test_notification.send_button.error": "Error sending test notification",