From cdd91dbbb8a1755a520098dd87e093281553917c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 11 Jun 2025 10:11:05 +0200 Subject: [PATCH] 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 --- app/components/chips/base_chip.test.tsx | 11 ++ app/components/chips/base_chip.tsx | 43 +++-- app/components/chips/constants.ts | 3 +- app/components/chips/user_chip.tsx | 1 + .../floating_text_chips_input/index.tsx | 1 + .../post/header/display_name/index.tsx | 1 - .../post_list/post/header/header.tsx | 17 +- .../post_list/post/header/reply/index.tsx | 10 +- .../post_list/post/header/tag/index.tsx | 26 +-- .../post_priority_label.test.tsx | 48 ++++++ .../post_priority/post_priority_label.tsx | 78 +++------ .../__snapshots__/index.test.tsx.snap | 5 +- app/components/section_notice/index.tsx | 29 ++-- app/components/selected_users/index.tsx | 6 +- app/components/tag/base_tag.test.tsx | 49 ++++++ app/components/tag/base_tag.tsx | 154 ++++++++++++++++++ app/components/tag/bot_tag.test.tsx | 46 ++++++ app/components/tag/bot_tag.tsx | 30 ++++ app/components/tag/guest_tag.test.tsx | 46 ++++++ app/components/tag/guest_tag.tsx | 30 ++++ app/components/tag/index.tsx | 118 +------------- app/components/user_item/user_item.tsx | 24 +-- .../__snapshots__/index.test.tsx.snap | 8 + .../screens/participants_list/participant.tsx | 15 +- .../intro/direct_channel/direct_channel.tsx | 22 +-- .../channel_add_members.tsx | 4 +- .../title/direct_message/direct_message.tsx | 15 +- app/screens/component_library/button.cl.tsx | 27 +-- app/screens/component_library/chip.cl.tsx | 55 +++++++ app/screens/component_library/index.tsx | 6 + .../component_library/section_notice.cl.tsx | 102 ++++++++++++ app/screens/component_library/tag.cl.tsx | 58 +++++++ app/screens/invite/selection.tsx | 1 + .../manage_channel_members.tsx | 4 +- ...end_test_notification_notice.test.tsx.snap | 5 +- app/screens/user_profile/title/index.tsx | 2 + app/screens/user_profile/title/tag.tsx | 44 +++-- app/utils/buttonStyles.ts | 2 +- assets/base/i18n/en.json | 3 + 39 files changed, 818 insertions(+), 331 deletions(-) create mode 100644 app/components/post_priority/post_priority_label.test.tsx create mode 100644 app/components/tag/base_tag.test.tsx create mode 100644 app/components/tag/base_tag.tsx create mode 100644 app/components/tag/bot_tag.test.tsx create mode 100644 app/components/tag/bot_tag.tsx create mode 100644 app/components/tag/guest_tag.test.tsx create mode 100644 app/components/tag/guest_tag.tsx create mode 100644 app/screens/component_library/chip.cl.tsx create mode 100644 app/screens/component_library/section_notice.cl.tsx create mode 100644 app/screens/component_library/tag.cl.tsx diff --git a/app/components/chips/base_chip.test.tsx b/app/components/chips/base_chip.test.tsx index d10deb547..638959c19 100644 --- a/app/components/chips/base_chip.test.tsx +++ b/app/components/chips/base_chip.test.tsx @@ -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( + , + ); + + expect(queryByTestId('base_chip.chip_button')).toBeNull(); + }); }); diff --git a/app/components/chips/base_chip.tsx b/app/components/chips/base_chip.tsx index a1fe166c3..2344289de 100644 --- a/app/components/chips/base_chip.tsx +++ b/app/components/chips/base_chip.tsx @@ -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} @@ -74,7 +87,7 @@ export default function BaseChip({ ); - let content; + let content = chipContent; if (showRemoveOption) { content = ( <> @@ -86,16 +99,16 @@ export default function BaseChip({ > ); - } else { + } else if (onPress) { content = ( diff --git a/app/components/chips/constants.ts b/app/components/chips/constants.ts index a8bdf3dc4..8dc10e6a5 100644 --- a/app/components/chips/constants.ts +++ b/app/components/chips/constants.ts @@ -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; diff --git a/app/components/chips/user_chip.tsx b/app/components/chips/user_chip.tsx index 651a30456..eda03a302 100644 --- a/app/components/chips/user_chip.tsx +++ b/app/components/chips/user_chip.tsx @@ -41,6 +41,7 @@ export default function UserChip({ size={20} iconSize={20} testID={`${testID}.profile_picture`} + showStatus={false} /> ), [testID, user]); diff --git a/app/components/floating_text_chips_input/index.tsx b/app/components/floating_text_chips_input/index.tsx index d977a48b5..1092c507d 100644 --- a/app/components/floating_text_chips_input/index.tsx +++ b/app/components/floating_text_chips_input/index.tsx @@ -224,6 +224,7 @@ const FloatingTextChipsInput = forwardRef(({ 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) { diff --git a/app/components/post_list/post/header/display_name/index.tsx b/app/components/post_list/post/header/display_name/index.tsx index 3a7b4e576..bd67117e8 100644 --- a/app/components/post_list/post/header/display_name/index.tsx +++ b/app/components/post_list/post/header/display_name/index.tsx @@ -34,7 +34,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { displayName: { color: theme.centerChannelColor, flexGrow: 1, - marginRight: 5, ...typography('Body', 200, 'SemiBold'), }, displayNameCustomEmojiWidth: { diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index df5080f99..824c6ecb0 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -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 && ( - - - + )} {!isCRTEnabled && showReply && commentCount > 0 && { 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 ( { if (isAutomation) { return ( - + ); } else if (showGuestTag) { return ( - + ); } else if (isAutoResponder) { return ( ); } diff --git a/app/components/post_priority/post_priority_label.test.tsx b/app/components/post_priority/post_priority_label.test.tsx new file mode 100644 index 000000000..476429008 --- /dev/null +++ b/app/components/post_priority/post_priority_label.test.tsx @@ -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( + ); + expect(toJSON()).toBeNull(); + }); + + it('should render urgent priority label correctly', () => { + const {getByTestId} = renderWithIntlAndTheme( + ); + + 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( + ); + + 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); + }); +}); diff --git a/app/components/post_priority/post_priority_label.tsx b/app/components/post_priority/post_priority_label.tsx index 8218757e5..fefe19516 100644 --- a/app/components/post_priority/post_priority_label.tsx +++ b/app/components/post_priority/post_priority_label.tsx @@ -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 = [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 ( - - - {labelText} - + uppercase={true} + /> ); }; diff --git a/app/components/section_notice/__snapshots__/index.test.tsx.snap b/app/components/section_notice/__snapshots__/index.test.tsx.snap index f29160dab..7eeffbc31 100644 --- a/app/components/section_notice/__snapshots__/index.test.tsx.snap +++ b/app/components/section_notice/__snapshots__/index.test.tsx.snap @@ -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" diff --git a/app/components/section_notice/index.tsx b/app/components/section_notice/index.tsx index 5410c9dc8..c07287169 100644 --- a/app/components/section_notice/index.tsx +++ b/app/components/section_notice/index.tsx @@ -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; - 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 ( )} @@ -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) => ( ))} diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx index 1cb4d3550..a6238234d 100644 --- a/app/components/selected_users/index.tsx +++ b/app/components/selected_users/index.tsx @@ -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, diff --git a/app/components/tag/base_tag.test.tsx b/app/components/tag/base_tag.test.tsx new file mode 100644 index 000000000..34deba26a --- /dev/null +++ b/app/components/tag/base_tag.test.tsx @@ -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(); + expect(getByText('Test Tag')).toBeDefined(); + }); + + it('should render icon correctly', () => { + const {getByTestId, rerender, queryByTestId} = renderWithIntlAndTheme( + , + ); + expect(getByTestId('test-tag.icon')).toBeDefined(); + + rerender( + , + ); + expect(queryByTestId('test-tag.icon')).toBeNull(); + }); + + it('should render uppercase correctly', () => { + const {getByText} = renderWithIntlAndTheme( + , + ); + expect(getByText('Test Tag')).toBeDefined(); + expect(getByText('Test Tag')).toHaveStyle({textTransform: 'uppercase'}); + }); +}); diff --git a/app/components/tag/base_tag.tsx b/app/components/tag/base_tag.tsx new file mode 100644 index 000000000..6e179f2c8 --- /dev/null +++ b/app/components/tag/base_tag.tsx @@ -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['size'], FontSizes> = { + xs: 25, + s: 75, + m: 100, +}; + +const iconSizePerSize: Record['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 = ( + + ); + } + let textComponent; + if (typeof message === 'string') { + textComponent = ( + + {message} + + ); + } else { + textComponent = ( + + ); + } + + return ( + + {/* We wrap the icon and text in a Text component to avoid + the ellipsis to go out of the box on iOS */} + + {iconComponent} + {' '} + {textComponent} + + + ); +}; + +export default Tag; diff --git a/app/components/tag/bot_tag.test.tsx b/app/components/tag/bot_tag.test.tsx new file mode 100644 index 000000000..a7d29b79e --- /dev/null +++ b/app/components/tag/bot_tag.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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(); + }); +}); diff --git a/app/components/tag/bot_tag.tsx b/app/components/tag/bot_tag.tsx new file mode 100644 index 000000000..b18dfd4fe --- /dev/null +++ b/app/components/tag/bot_tag.tsx @@ -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, 'message' | 'icon' | 'type'>; + +const BotTag = ({ + size, + testID, +}: BotTagProps) => { + return ( + + ); +}; + +export default BotTag; diff --git a/app/components/tag/guest_tag.test.tsx b/app/components/tag/guest_tag.test.tsx new file mode 100644 index 000000000..433c3fba3 --- /dev/null +++ b/app/components/tag/guest_tag.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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(); + }); +}); diff --git a/app/components/tag/guest_tag.tsx b/app/components/tag/guest_tag.tsx new file mode 100644 index 000000000..180a5e970 --- /dev/null +++ b/app/components/tag/guest_tag.tsx @@ -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, 'message' | 'icon' | 'type'>; + +const GuestTag = ({ + testID, + size, +}: GuestTagProps) => { + return ( + + ); +}; + +export default GuestTag; diff --git a/app/components/tag/index.tsx b/app/components/tag/index.tsx index aa6dba441..eff56bebf 100644 --- a/app/components/tag/index.tsx +++ b/app/components/tag/index.tsx @@ -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; - testID?: string; - textStyle?: StyleProp; - 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) { - const id = t('post_info.bot'); - const defaultMessage = 'Bot'; - - return ( - - ); -} - -export function GuestTag(props: Omit) { - const id = t('post_info.guest'); - const defaultMessage = 'Guest'; - - return ( - - ); -} - -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 ( - - - - ); -}; +import Tag from './base_tag'; +import BotTag from './bot_tag'; +import GuestTag from './guest_tag'; export default Tag; +export { + GuestTag, + BotTag, +}; diff --git a/app/components/user_item/user_item.tsx b/app/components/user_item/user_item.tsx index a3f4a9e9a..3fe23d2f0 100644 --- a/app/components/user_item/user_item.tsx +++ b/app/components/user_item/user_item.tsx @@ -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 = ({ )} {showBadges && bot && ( - + )} {showBadges && guest && !hideGuestTags && ( - + )} {Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && ( - + )} {shared && ( )} diff --git a/app/components/user_list/__snapshots__/index.test.tsx.snap b/app/components/user_list/__snapshots__/index.test.tsx.snap index a829e03ba..babe43ddc 100644 --- a/app/components/user_list/__snapshots__/index.test.tsx.snap +++ b/app/components/user_list/__snapshots__/index.test.tsx.snap @@ -814,8 +814,10 @@ exports[`components/channel_list_row should show results and tutorial 1`] = ` @@ -1134,8 +1136,10 @@ exports[`components/channel_list_row should show results no tutorial 1`] = ` @@ -1492,8 +1496,10 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`] @@ -1711,8 +1717,10 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`] diff --git a/app/products/calls/screens/participants_list/participant.tsx b/app/products/calls/screens/participants_list/participant.tsx index 373d3d572..92c1704a0 100644 --- a/app/products/calls/screens/participants_list/participant.tsx +++ b/app/products/calls/screens/participants_list/participant.tsx @@ -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 && } diff --git a/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx b/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx index a2c8d4df2..bf31d989e 100644 --- a/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx +++ b/app/screens/channel/channel_post_list/intro/direct_channel/direct_channel.tsx @@ -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)} ); - }, [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 ( {profiles} - + {isBot && - + } {message} diff --git a/app/screens/channel_add_members/channel_add_members.tsx b/app/screens/channel_add_members/channel_add_members.tsx index 29a35837f..2b93f866e 100644 --- a/app/screens/channel_add_members/channel_add_members.tsx +++ b/app/screens/channel_add_members/channel_add_members.tsx @@ -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} /> )} diff --git a/app/screens/channel_info/title/direct_message/direct_message.tsx b/app/screens/channel_info/title/direct_message/direct_message.tsx index a58eb5692..2d72362cb 100644 --- a/app/screens/channel_info/title/direct_message/direct_message.tsx +++ b/app/screens/channel_info/title/direct_message/direct_message.tsx @@ -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 = ({ {user?.isGuest && !hideGuestTags && } {user?.isBot && } diff --git a/app/screens/component_library/button.cl.tsx b/app/screens/component_library/button.cl.tsx index 1b924b4b2..905210067 100644 --- a/app/screens/component_library/button.cl.tsx +++ b/app/screens/component_library/button.cl.tsx @@ -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} {components} ); diff --git a/app/screens/component_library/chip.cl.tsx b/app/screens/component_library/chip.cl.tsx new file mode 100644 index 000000000..75b6c7b90 --- /dev/null +++ b/app/screens/component_library/chip.cl.tsx @@ -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 ? ( + + ) : undefined, + testID: 'chip-example', + theme, + }, + ]), + [label, showRemoveOption, hasPrefix.hasPrefix, theme], + ); + + return ( + <> + {labelSelector} + {showRemoveOptionSelector} + {hasPrefixSelector} + {components} + + ); +}; + +export default ChipComponentLibrary; diff --git a/app/screens/component_library/index.tsx b/app/screens/component_library/index.tsx index e4b992dba..e3c373327 100644 --- a/app/screens/component_library/index.tsx +++ b/app/screens/component_library/index.tsx @@ -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 diff --git a/app/screens/component_library/section_notice.cl.tsx b/app/screens/component_library/section_notice.cl.tsx new file mode 100644 index 000000000..7dff03c5c --- /dev/null +++ b/app/screens/component_library/section_notice.cl.tsx @@ -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} + {components} + + ); +}; + +export default SectionNoticeComponentLibrary; diff --git a/app/screens/component_library/tag.cl.tsx b/app/screens/component_library/tag.cl.tsx new file mode 100644 index 000000000..46bdb1b68 --- /dev/null +++ b/app/screens/component_library/tag.cl.tsx @@ -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} + {components} + + ); +}; + +export default TagComponentLibrary; diff --git a/app/screens/invite/selection.tsx b/app/screens/invite/selection.tsx index 600c1b061..0c4b02a7f 100644 --- a/app/screens/invite/selection.tsx +++ b/app/screens/invite/selection.tsx @@ -96,6 +96,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { flexWrap: 'wrap', marginHorizontal: 20, marginVertical: 16, + gap: 8, }, }; }); diff --git a/app/screens/manage_channel_members/manage_channel_members.tsx b/app/screens/manage_channel_members/manage_channel_members.tsx index c34eabb98..901704d70 100644 --- a/app/screens/manage_channel_members/manage_channel_members.tsx +++ b/app/screens/manage_channel_members/manage_channel_members.tsx @@ -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} /> )} diff --git a/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap b/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap index d225fb772..daaab225f 100644 --- a/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap +++ b/app/screens/settings/notifications/send_test_notification_notice/__snapshots__/send_test_notification_notice.test.tsx.snap @@ -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" diff --git a/app/screens/user_profile/title/index.tsx b/app/screens/user_profile/title/index.tsx index fd78b72b9..25c6e048f 100644 --- a/app/screens/user_profile/title/index.tsx +++ b/app/screens/user_profile/title/index.tsx @@ -48,6 +48,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ details: { marginLeft: 24, justifyContent: 'center', + alignItems: 'flex-start', + gap: 4, flex: 1, }, displayName: { diff --git a/app/screens/user_profile/title/tag.tsx b/app/screens/user_profile/title/tag.tsx index 6cdc2a4c4..12a5ff7d8 100644 --- a/app/screens/user_profile/title/tag.tsx +++ b/app/screens/user_profile/title/tag.tsx @@ -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 ( - ); + ); } if (showGuestTag) { return ( - ); + ); } if (isSystemAdmin) { return ( ); @@ -53,10 +53,9 @@ const UserProfileTag = ({isBot, isChannelAdmin, showGuestTag, isSystemAdmin, isT if (isTeamAdmin) { return ( ); } @@ -64,10 +63,9 @@ const UserProfileTag = ({isBot, isChannelAdmin, showGuestTag, isSystemAdmin, isT if (isChannelAdmin) { return ( ); } diff --git a/app/utils/buttonStyles.ts b/app/utils/buttonStyles.ts index 5f52dcd6a..069271cf6 100644 --- a/app/utils/buttonStyles.ts +++ b/app/utils/buttonStyles.ts @@ -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), diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index f7c837cd3..fdcb909dd 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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 they’re 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",