From a8ee3a1b5a489889f8a91c615b7ff831dc95e1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 19 Apr 2023 10:13:14 +0200 Subject: [PATCH] Fix and unify channel and user list items (#7175) * Fix channel and user list items * Fixes on members and create a dm lists * Fix tutorial and ipad * Fix test * Address feedback * Several fixes on Android * Fix tests * Address feedback * Add more non breaking strings --------- Co-authored-by: Daniel Espino --- .../at_mention_group/at_mention_group.tsx | 1 - .../autocomplete/at_mention_item/index.tsx | 30 +- app/components/autocomplete/autocomplete.tsx | 1 + .../autocomplete_section_header.tsx | 5 +- .../channel_mention/channel_mention.tsx | 30 +- .../autocomplete/channel_mention/index.ts | 3 +- .../channel_mention_item.tsx | 153 --- .../channel_mention_item/index.ts | 41 - .../emoji_suggestion/emoji_suggestion.tsx | 1 - .../app_slash_suggestion.tsx | 8 +- .../slash_suggestion_item.tsx | 1 - .../autocomplete/special_mention_item.tsx | 3 +- .../autocomplete_selector/index.tsx | 2 +- .../channel_icon/dm_avatar/dm_avatar.tsx | 34 +- app/components/channel_icon/index.tsx | 74 +- .../__snapshots__/channel_item.test.tsx.snap | 427 +++---- app/components/channel_item/channel_body.tsx | 125 ++ .../channel_item/channel_item.test.tsx | 3 - app/components/channel_item/channel_item.tsx | 263 ++-- .../custom_status/custom_status.tsx | 23 +- app/components/channel_item/index.ts | 87 +- .../custom_status_emoji.test.tsx.snap | 58 +- .../custom_status_emoji.test.tsx | 2 - .../custom_status/custom_status_emoji.tsx | 23 +- app/components/emoji/emoji.tsx | 11 +- .../post/header/display_name/index.tsx | 1 - .../channel_info/channel_info.tsx | 4 +- app/components/profile_picture/index.tsx | 33 +- app/components/profile_picture/status.tsx | 11 +- app/components/selected_chip/index.tsx | 20 +- .../selected_users/selected_user.tsx | 4 +- app/components/server_user_list/index.tsx | 3 - app/components/tag/index.tsx | 3 - .../threads_button.test.tsx.snap | 281 ++--- .../threads_button/threads_button.test.tsx | 4 +- .../threads_button/threads_button.tsx | 69 +- .../user_avatars_stack/users_list/index.tsx | 43 +- app/components/user_item/index.ts | 12 +- app/components/user_item/user_item.tsx | 266 ++-- .../__snapshots__/index.test.tsx.snap | 1090 ++++++++--------- app/components/user_list/index.test.tsx | 4 - app/components/user_list/index.tsx | 45 +- app/components/user_list_row/index.tsx | 161 +-- app/constants/view.ts | 5 + app/screens/channel/header/header.tsx | 1 - .../channel_add_members.tsx | 1 - .../options/add_members/add_members.tsx | 2 +- .../muted_banner.tsx | 14 +- .../create_direct_message.tsx | 1 - .../filtered_list/filtered_list.tsx | 33 +- .../remote_channel_item/index.ts | 34 - .../remote_channel_item.tsx | 123 -- .../filtered_list/user_item/index.ts | 26 - .../filtered_list/user_item/user_item.tsx | 99 -- .../unfiltered_list/unfiltered_list.tsx | 7 +- .../threads_list/thread/thread.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 10 +- .../categories/body/category_body.tsx | 14 +- .../categories_list/categories/categories.tsx | 13 +- .../categories/unreads/unreads.tsx | 18 +- .../header/__snapshots__/header.test.tsx.snap | 2 + .../categories_list/header/header.tsx | 3 +- .../channel_list/categories_list/index.tsx | 13 +- .../categories_list/subheader/subheader.tsx | 3 + .../integration_selector.tsx | 4 +- app/screens/invite/invite.tsx | 3 +- app/screens/invite/selection.tsx | 43 +- app/screens/invite/summary_report.tsx | 6 - app/screens/invite/text_item.tsx | 2 - app/screens/manage_channel_members/index.tsx | 3 +- .../manage_channel_members.tsx | 3 - .../reactors_list/reactor/reactor.tsx | 28 +- app/screens/user_profile/custom_status.tsx | 1 - app/utils/strings.ts | 6 + types/components/emoji.ts | 7 +- 75 files changed, 1682 insertions(+), 2311 deletions(-) delete mode 100644 app/components/autocomplete/channel_mention_item/channel_mention_item.tsx delete mode 100644 app/components/autocomplete/channel_mention_item/index.ts create mode 100644 app/components/channel_item/channel_body.tsx delete mode 100644 app/screens/find_channels/filtered_list/remote_channel_item/index.ts delete mode 100644 app/screens/find_channels/filtered_list/remote_channel_item/remote_channel_item.tsx delete mode 100644 app/screens/find_channels/filtered_list/user_item/index.ts delete mode 100644 app/screens/find_channels/filtered_list/user_item/user_item.tsx create mode 100644 app/utils/strings.ts diff --git a/app/components/autocomplete/at_mention_group/at_mention_group.tsx b/app/components/autocomplete/at_mention_group/at_mention_group.tsx index 1bd9880c89..7e25af9552 100644 --- a/app/components/autocomplete/at_mention_group/at_mention_group.tsx +++ b/app/components/autocomplete/at_mention_group/at_mention_group.tsx @@ -20,7 +20,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { height: 40, paddingVertical: 8, paddingTop: 4, - paddingHorizontal: 16, flexDirection: 'row', alignItems: 'center', }, diff --git a/app/components/autocomplete/at_mention_item/index.tsx b/app/components/autocomplete/at_mention_item/index.tsx index f4b9684a03..1b73b6a386 100644 --- a/app/components/autocomplete/at_mention_item/index.tsx +++ b/app/components/autocomplete/at_mention_item/index.tsx @@ -2,12 +2,8 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import TouchableWithFeedback from '@components/touchable_with_feedback'; import UserItem from '@components/user_item'; -import {useTheme} from '@context/theme'; -import {changeOpacity} from '@utils/theme'; import type UserModel from '@typings/database/models/servers/user'; @@ -22,26 +18,16 @@ const AtMentionItem = ({ onPress, testID, }: AtMentionItemProps) => { - const insets = useSafeAreaInsets(); - const theme = useTheme(); - - const completeMention = useCallback(() => { - onPress?.(user.username); - }, [user.username]); + const completeMention = useCallback((u: UserModel | UserProfile) => { + onPress?.(u.username); + }, []); return ( - - - + ); }; diff --git a/app/components/autocomplete/autocomplete.tsx b/app/components/autocomplete/autocomplete.tsx index 2f7998d71e..64b7f48e6a 100644 --- a/app/components/autocomplete/autocomplete.tsx +++ b/app/components/autocomplete/autocomplete.tsx @@ -43,6 +43,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { listStyle: { backgroundColor: theme.centerChannelBg, borderRadius: 4, + paddingHorizontal: 16, }, }; }); diff --git a/app/components/autocomplete/autocomplete_section_header.tsx b/app/components/autocomplete/autocomplete_section_header.tsx index 581e254985..00be90073b 100644 --- a/app/components/autocomplete/autocomplete_section_header.tsx +++ b/app/components/autocomplete/autocomplete_section_header.tsx @@ -8,16 +8,15 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'; import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; +import {typography} from '@utils/typography'; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { section: { flexDirection: 'row', - paddingHorizontal: 16, }, sectionText: { - fontSize: 12, - fontWeight: '600', + ...typography('Body', 75, 'SemiBold'), textTransform: 'uppercase', color: changeOpacity(theme.centerChannelColor, 0.56), paddingTop: 16, diff --git a/app/components/autocomplete/channel_mention/channel_mention.tsx b/app/components/autocomplete/channel_mention/channel_mention.tsx index 84b01652f5..e742d0749d 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.tsx +++ b/app/components/autocomplete/channel_mention/channel_mention.tsx @@ -7,13 +7,16 @@ import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, Style import {searchChannels} from '@actions/remote/channel'; import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header'; -import ChannelMentionItem from '@components/autocomplete/channel_mention_item'; +import ChannelItem from '@components/channel_item'; import {General} from '@constants'; import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete'; import {useServerUrl} from '@context/server'; +import DatabaseManager from '@database/manager'; import useDidUpdate from '@hooks/did_update'; import {t} from '@i18n'; +import {getUserById} from '@queries/servers/user'; import {hasTrailingSpaces} from '@utils/helpers'; +import {getUserIdFromChannelName} from '@utils/user'; import type ChannelModel from '@typings/database/models/servers/channel'; import type MyChannelModel from '@typings/database/models/servers/my_channel'; @@ -138,6 +141,7 @@ type Props = { matchTerm: string; localChannels: ChannelModel[]; teamId: string; + currentUserId: string; } const emptySections: Array> = []; @@ -155,6 +159,7 @@ const ChannelMention = ({ matchTerm, localChannels, teamId, + currentUserId, }: Props) => { const serverUrl = useServerUrl(); @@ -193,7 +198,22 @@ const ChannelMention = ({ setLoading(false); }; - const completeMention = useCallback((mention: string) => { + const completeMention = useCallback(async (c: ChannelModel | Channel) => { + let mention = c.name; + const teammateId = getUserIdFromChannelName(currentUserId, c.name); + + if (c.type === General.DM_CHANNEL && teammateId) { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const user = await getUserById(database, teammateId); + if (user) { + mention = `@${user.username}`; + } + } catch (err) { + // Do nothing + } + } + const mentionPart = value.substring(0, localCursorPosition); let completedDraft: string; @@ -231,14 +251,16 @@ const ChannelMention = ({ setSections(emptySections); setRemoteChannels(emptyChannels); latestSearchAt.current = Date.now(); - }, [value, localCursorPosition, isSearch]); + }, [value, localCursorPosition, isSearch, currentUserId]); const renderItem = useCallback(({item}: SectionListRenderItemInfo) => { return ( - ); }, [completeMention]); diff --git a/app/components/autocomplete/channel_mention/index.ts b/app/components/autocomplete/channel_mention/index.ts index 17eb57b252..316d42c06a 100644 --- a/app/components/autocomplete/channel_mention/index.ts +++ b/app/components/autocomplete/channel_mention/index.ts @@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators'; import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete'; import {observeChannel, queryAllMyChannel, queryChannelsForAutocomplete} from '@queries/servers/channel'; -import {observeCurrentTeamId} from '@queries/servers/system'; +import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system'; import ChannelMention from './channel_mention'; @@ -58,6 +58,7 @@ const emptyChannelList: ChannelModel[] = []; const withMembers = withObservables([], ({database}: WithDatabaseArgs) => { return { myMembers: queryAllMyChannel(database).observe(), + currentUserId: observeCurrentUserId(database), }; }); diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.tsx b/app/components/autocomplete/channel_mention_item/channel_mention_item.tsx deleted file mode 100644 index 9ded73355c..0000000000 --- a/app/components/autocomplete/channel_mention_item/channel_mention_item.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// 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 {useSafeAreaInsets} from 'react-native-safe-area-context'; - -import ChannelIcon from '@components/channel_icon'; -import {BotTag, GuestTag} from '@components/tag'; -import TouchableWithFeedback from '@components/touchable_with_feedback'; -import {useTheme} from '@context/theme'; -import {isDMorGM} from '@utils/channel'; -import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; - -import type ChannelModel from '@typings/database/models/servers/channel'; - -const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { - return { - icon: { - marginRight: 11, - opacity: 0.56, - }, - row: { - paddingHorizontal: 16, - height: 40, - flexDirection: 'row', - alignItems: 'center', - }, - rowDisplayName: { - flex: 1, - fontSize: 15, - color: theme.centerChannelColor, - }, - rowName: { - fontSize: 15, - color: theme.centerChannelColor, - opacity: 0.56, - }, - }; -}); - -type Props = { - channel: Channel | ChannelModel; - displayName?: string; - isBot: boolean; - isGuest: boolean; - onPress: (name?: string) => void; - testID?: string; -}; - -const ChannelMentionItem = ({ - channel, - displayName, - isBot, - isGuest, - onPress, - testID, -}: Props) => { - const insets = useSafeAreaInsets(); - const theme = useTheme(); - - const completeMention = () => { - if (isDMorGM(channel)) { - onPress('@' + displayName?.replace(/ /g, '')); - } else { - onPress(channel.name); - } - }; - - const style = getStyleFromTheme(theme); - const margins = useMemo(() => { - return {marginLeft: insets.left, marginRight: insets.right}; - }, [insets]); - const rowStyle = useMemo(() => { - return [style.row, margins]; - }, [margins, style]); - - let component; - - const isArchived = ('delete_at' in channel ? channel.delete_at : channel.deleteAt) > 0; - const channelMentionItemTestId = `${testID}.${channel.name}`; - - if (isDMorGM(channel)) { - if (!displayName) { - return null; - } - - component = ( - - - {'@' + displayName} - - - - - ); - } else { - component = ( - - - - - {displayName} - - {` ~${channel.name}`} - - - - - ); - } - - return component; -}; - -export default ChannelMentionItem; diff --git a/app/components/autocomplete/channel_mention_item/index.ts b/app/components/autocomplete/channel_mention_item/index.ts deleted file mode 100644 index 662cbb6928..0000000000 --- a/app/components/autocomplete/channel_mention_item/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; -import withObservables from '@nozbe/with-observables'; -import {of as of$} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; - -import {General} from '@constants'; -import {observeUser} from '@queries/servers/user'; - -import ChannelMentionItem from './channel_mention_item'; - -import type {WithDatabaseArgs} from '@typings/database/database'; -import type ChannelModel from '@typings/database/models/servers/channel'; -import type UserModel from '@typings/database/models/servers/user'; - -type OwnProps = { - channel: Channel | ChannelModel; -} - -const enhanced = withObservables([], ({database, channel}: WithDatabaseArgs & OwnProps) => { - let user = of$(undefined); - const teammateId = 'teammate_id' in channel ? channel.teammate_id : ''; - const channelDisplayName = 'display_name' in channel ? channel.display_name : channel.displayName; - if (channel.type === General.DM_CHANNEL && teammateId) { - user = observeUser(database, teammateId!); - } - - const isBot = user.pipe(switchMap((u) => of$(u ? u.isBot : false))); - const isGuest = user.pipe(switchMap((u) => of$(u ? u.isGuest : false))); - const displayName = user.pipe(switchMap((u) => of$(u ? u.username : channelDisplayName))); - - return { - isBot, - isGuest, - displayName, - }; -}); - -export default withDatabase(enhanced(ChannelMentionItem)); diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx index 1b38f442d9..d07c79b063 100644 --- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx +++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.tsx @@ -54,7 +54,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { alignItems: 'center', overflow: 'hidden', paddingBottom: 8, - paddingHorizontal: 16, height: 40, }, }; diff --git a/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx b/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx index 3abae6636d..680b961217 100644 --- a/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx +++ b/app/components/autocomplete/slash_suggestion/app_slash_suggestion/app_slash_suggestion.tsx @@ -7,7 +7,7 @@ import {useIntl} from 'react-intl'; import {FlatList, Platform, StyleProp, ViewStyle} from 'react-native'; import AtMentionItem from '@components/autocomplete/at_mention_item'; -import ChannelMentionItem from '@components/autocomplete/channel_mention_item'; +import ChannelItem from '@components/channel_item'; import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@constants/apps'; import {useServerUrl} from '@context/server'; import analytics from '@managers/analytics'; @@ -98,7 +98,7 @@ const AppSlashSuggestion = ({ } }, [serverUrl, updateValue]); - const completeIgnoringSuggestion = useCallback((base: string): (toIgnore: string) => void => { + const completeIgnoringSuggestion = useCallback((base: string): () => void => { return () => { completeSuggestion(base); }; @@ -127,10 +127,12 @@ const AppSlashSuggestion = ({ } return ( - ); } diff --git a/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx b/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx index 6dbd4ec54f..c1b01c136f 100644 --- a/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx +++ b/app/components/autocomplete/slash_suggestion/slash_suggestion_item.tsx @@ -37,7 +37,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { flexDirection: 'row', alignItems: 'center', paddingBottom: 8, - paddingHorizontal: 16, overflow: 'hidden', }, slashIcon: { diff --git a/app/components/autocomplete/special_mention_item.tsx b/app/components/autocomplete/special_mention_item.tsx index 479dee291b..1a9f4279b3 100644 --- a/app/components/autocomplete/special_mention_item.tsx +++ b/app/components/autocomplete/special_mention_item.tsx @@ -18,12 +18,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { row: { height: 40, paddingVertical: 8, - paddingHorizontal: 9, flexDirection: 'row', alignItems: 'center', }, rowPicture: { - marginHorizontal: 8, + marginRight: 8, width: 20, alignItems: 'center', justifyContent: 'center', diff --git a/app/components/autocomplete_selector/index.tsx b/app/components/autocomplete_selector/index.tsx index fa786087e0..361ddcc7ac 100644 --- a/app/components/autocomplete_selector/index.tsx +++ b/app/components/autocomplete_selector/index.tsx @@ -143,7 +143,7 @@ function AutoCompleteSelector({ const goToSelectorScreen = useCallback(preventDoubleTap(() => { const screen = Screens.INTEGRATION_SELECTOR; - goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect, teammateNameDisplay}); + goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect}); }), [dataSource, options, getDynamicOptions]); const handleSelect = useCallback((newSelection?: Selection) => { diff --git a/app/components/channel_icon/dm_avatar/dm_avatar.tsx b/app/components/channel_icon/dm_avatar/dm_avatar.tsx index 992013326d..04db4f3c00 100644 --- a/app/components/channel_icon/dm_avatar/dm_avatar.tsx +++ b/app/components/channel_icon/dm_avatar/dm_avatar.tsx @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import React from 'react'; -import {View} from 'react-native'; import CompassIcon from '@components/compass_icon'; import ProfilePicture from '@components/profile_picture'; @@ -10,54 +9,55 @@ import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import type UserModel from '@typings/database/models/servers/user'; +import type {StyleProp, ViewStyle} from 'react-native'; type Props = { author?: UserModel; - isInfo?: boolean; + isOnCenterBg?: boolean; + style: StyleProp; + size: number; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: {marginLeft: 4}, status: { backgroundColor: theme.sidebarBg, borderWidth: 0, }, - statusInfo: { + statusOnCenterBg: { backgroundColor: theme.centerChannelBg, }, icon: { color: changeOpacity(theme.sidebarText, 0.4), left: 1, }, - iconInfo: { + iconOnCenterBg: { color: changeOpacity(theme.centerChannelColor, 0.72), }, })); -const DmAvatar = ({author, isInfo}: Props) => { +const DmAvatar = ({author, isOnCenterBg, style, size}: Props) => { const theme = useTheme(); - const style = getStyleSheet(theme); + const styles = getStyleSheet(theme); if (author?.deleteAt) { return ( ); } return ( - - - + ); }; diff --git a/app/components/channel_icon/index.tsx b/app/components/channel_icon/index.tsx index 96386c5f1d..2c2170bed2 100644 --- a/app/components/channel_icon/index.tsx +++ b/app/components/channel_icon/index.tsx @@ -16,7 +16,7 @@ type ChannelIconProps = { hasDraft?: boolean; isActive?: boolean; isArchived?: boolean; - isInfo?: boolean; + isOnCenterBg?: boolean; isUnread?: boolean; isMuted?: boolean; membersCount?: number; @@ -43,10 +43,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { iconUnread: { color: theme.sidebarUnreadText, }, - iconInfo: { + iconOnCenterBg: { color: changeOpacity(theme.centerChannelColor, 0.72), }, - iconInfoUnread: { + iconUnreadOnCenterBg: { color: theme.centerChannelColor, }, groupBox: { @@ -61,7 +61,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { groupBoxUnread: { backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3), }, - groupBoxInfo: { + groupBoxOnCenterBg: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.3), }, group: { @@ -74,10 +74,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { groupUnread: { color: theme.sidebarUnreadText, }, - groupInfo: { + groupOnCenterBg: { color: changeOpacity(theme.centerChannelColor, 0.72), }, - groupInfoUnread: { + groupUnreadOnCenterBg: { color: theme.centerChannelColor, }, muted: { @@ -88,7 +88,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const ChannelIcon = ({ hasDraft = false, isActive = false, isArchived = false, - isInfo = false, isUnread = false, isMuted = false, + isOnCenterBg = false, isUnread = false, isMuted = false, membersCount = 0, name, shared, size = 12, style, testID, type, }: ChannelIconProps) => { @@ -115,22 +115,38 @@ const ChannelIcon = ({ activeGroup = styles.groupActive; } - if (isInfo) { - activeIcon = isUnread && !isMuted ? styles.iconInfoUnread : styles.iconInfo; - activeGroupBox = styles.groupBoxInfo; - activeGroup = isUnread ? styles.groupInfoUnread : styles.groupInfo; + if (isOnCenterBg) { + activeIcon = isUnread && !isMuted ? styles.iconUnreadOnCenterBg : styles.iconOnCenterBg; + activeGroupBox = styles.groupBoxOnCenterBg; + activeGroup = isUnread ? styles.groupUnreadOnCenterBg : styles.groupOnCenterBg; } if (isMuted) { mutedStyle = styles.muted; } - let icon; + const commonStyles = [ + style, + mutedStyle, + ]; + + const commonIconStyles = [ + styles.icon, + unreadIcon, + activeIcon, + commonStyles, + {fontSize: size}, + ]; + + let icon = null; if (isArchived) { icon = ( ); @@ -138,7 +154,10 @@ const ChannelIcon = ({ icon = ( ); @@ -148,7 +167,10 @@ const ChannelIcon = ({ icon = ( ); @@ -156,7 +178,10 @@ const ChannelIcon = ({ icon = ( ); @@ -164,7 +189,10 @@ const ChannelIcon = ({ icon = ( ); @@ -172,7 +200,7 @@ const ChannelIcon = ({ const fontSize = size - 12; icon = ( ); } - return ( - - {icon} - - ); + return icon; }; export default React.memo(ChannelIcon); diff --git a/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap b/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap index ea25aef92e..4db607bcde 100644 --- a/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap +++ b/app/components/channel_item/__snapshots__/channel_item.test.tsx.snap @@ -41,11 +41,10 @@ exports[`components/channel_list/categories/body/channel_item should match snaps { "alignItems": "center", "flexDirection": "row", - "minHeight": 40, - "paddingHorizontal": 20, }, false, - undefined, + false, + false, { "minHeight": 40, }, @@ -53,79 +52,71 @@ exports[`components/channel_list/categories/body/channel_item should match snaps } testID="channel_item.hello" > + + + Hello! + - - - - - - Hello! - - - + /> `; @@ -171,11 +162,10 @@ exports[`components/channel_list/categories/body/channel_item should match snaps { "alignItems": "center", "flexDirection": "row", - "minHeight": 40, - "paddingHorizontal": 20, }, false, - undefined, + false, + false, { "minHeight": 40, }, @@ -183,103 +173,93 @@ exports[`components/channel_list/categories/body/channel_item should match snaps } testID="channel_item.hello" > + + + Hello! + - - - - - - Hello! - - - + /> + + + Hello! + - - - - - - Hello! - - - + /> `; diff --git a/app/components/channel_item/channel_body.tsx b/app/components/channel_item/channel_body.tsx new file mode 100644 index 0000000000..1146f9b60f --- /dev/null +++ b/app/components/channel_item/channel_body.tsx @@ -0,0 +1,125 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {StyleProp, Text, TextStyle, View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {useIsTablet} from '@hooks/device'; +import {nonBreakingString} from '@utils/strings'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import CustomStatus from './custom_status'; + +type Props = { + displayName: string; + teamDisplayName: string; + teammateId?: string; + isMuted: boolean; + textStyles: StyleProp; + testId: string; + channelName: string; +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + teamName: { + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 75), + }, + teamNameMuted: { + color: changeOpacity(theme.centerChannelColor, 0.32), + }, + flex: { + flex: 0, + flexShrink: 1, + }, + channelName: { + ...typography('Body', 200), + color: changeOpacity(theme.centerChannelColor, 0.64), + }, + customStatus: { + marginLeft: 4, + }, + }; +}); +export const ChannelBody = ({ + displayName, + channelName, + teamDisplayName, + teammateId, + isMuted, + textStyles, + testId, +}: Props) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + const isTablet = useIsTablet(); + const nonBreakingDisplayName = nonBreakingString(displayName); + const channelText = ( + + {nonBreakingDisplayName} + {Boolean(channelName) && ( + + {nonBreakingString(` ~${channelName}`)} + + )} + + ); + + if (teamDisplayName) { + const teamText = ( + + {nonBreakingString(`${isTablet ? ' ' : ''}${teamDisplayName}`)} + + ); + + if (isTablet) { + return ( + + {nonBreakingDisplayName} + {teamText} + + ); + } + + return ( + + {channelText} + {teamText} + + ); + } + + if (teammateId) { + const customStatus = ( + + ); + return ( + <> + {channelText} + {customStatus} + + ); + } + + return channelText; +}; diff --git a/app/components/channel_item/channel_item.test.tsx b/app/components/channel_item/channel_item.test.tsx index 3f4ff215f7..bd42f43bf5 100644 --- a/app/components/channel_item/channel_item.test.tsx +++ b/app/components/channel_item/channel_item.test.tsx @@ -41,7 +41,6 @@ describe('components/channel_list/categories/body/channel_item', () => { onPress={() => undefined} isUnread={myChannel.isUnread} mentionsCount={myChannel.mentionsCount} - hasMember={Boolean(myChannel)} hasCall={false} />, ); @@ -62,7 +61,6 @@ describe('components/channel_list/categories/body/channel_item', () => { onPress={() => undefined} isUnread={myChannel.isUnread} mentionsCount={myChannel.mentionsCount} - hasMember={Boolean(myChannel)} hasCall={false} />, ); @@ -83,7 +81,6 @@ describe('components/channel_list/categories/body/channel_item', () => { onPress={() => undefined} isUnread={myChannel.isUnread} mentionsCount={myChannel.mentionsCount} - hasMember={Boolean(myChannel)} hasCall={true} />, ); diff --git a/app/components/channel_item/channel_item.tsx b/app/components/channel_item/channel_item.tsx index 89a68985a1..e8f3f19b9f 100644 --- a/app/components/channel_item/channel_item.tsx +++ b/app/components/channel_item/channel_item.tsx @@ -3,85 +3,79 @@ import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import {StyleSheet, TouchableOpacity, View} from 'react-native'; import Badge from '@components/badge'; import ChannelIcon from '@components/channel_icon'; import CompassIcon from '@components/compass_icon'; import {General} from '@constants'; +import {HOME_PADDING} from '@constants/view'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {isDMorGM} from '@utils/channel'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {getUserIdFromChannelName} from '@utils/user'; -import CustomStatus from './custom_status'; +import {ChannelBody} from './channel_body'; import type ChannelModel from '@typings/database/models/servers/channel'; type Props = { - channel: ChannelModel; + channel: ChannelModel | Channel; currentUserId: string; hasDraft: boolean; isActive: boolean; - isInfo?: boolean; isMuted: boolean; membersCount: number; isUnread: boolean; mentionsCount: number; - onPress: (channelId: string) => void; - hasMember: boolean; + onPress: (channel: ChannelModel | Channel) => void; teamDisplayName?: string; testID?: string; hasCall: boolean; + isOnCenterBg?: boolean; + showChannelName?: boolean; + isOnHome?: boolean; } +export const ROW_HEIGHT = 40; +export const ROW_HEIGHT_WITH_TEAM = 58; + export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { flexDirection: 'row', - paddingHorizontal: 20, - minHeight: 40, alignItems: 'center', }, - infoItem: { - paddingHorizontal: 0, - }, - wrapper: { - flex: 1, - flexDirection: 'row', - }, icon: { - fontSize: 24, - lineHeight: 28, - color: changeOpacity(theme.sidebarText, 0.72), + marginRight: 12, }, text: { - marginTop: -1, color: changeOpacity(theme.sidebarText, 0.72), - paddingLeft: 12, - paddingRight: 20, }, highlight: { color: theme.sidebarUnreadText, }, - textInfo: { + textOnCenterBg: { color: theme.centerChannelColor, - paddingRight: 20, }, muted: { color: changeOpacity(theme.sidebarText, 0.32), }, - mutedInfo: { + mutedOnCenterBg: { color: changeOpacity(theme.centerChannelColor, 0.32), }, badge: { borderColor: theme.sidebarBg, - position: 'relative', - left: 0, - top: -2, + marginLeft: 4, + + //Overwrite default badge styles + position: undefined, + top: undefined, + left: undefined, alignSelf: undefined, }, - infoBadge: { + badgeOnCenterBg: { color: theme.buttonColor, backgroundColor: theme.buttonBg, borderColor: theme.centerChannelBg, @@ -93,31 +87,15 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1), borderLeftColor: theme.sidebarTextActiveBorder, borderLeftWidth: 5, - marginLeft: 0, - paddingLeft: 14, }, textActive: { color: theme.sidebarText, }, - teamName: { - color: changeOpacity(theme.centerChannelColor, 0.64), - paddingLeft: 12, - marginTop: 4, - ...typography('Body', 75), - }, - teamNameMuted: { - color: changeOpacity(theme.centerChannelColor, 0.32), - }, - teamNameTablet: { - marginLeft: -12, - paddingLeft: 0, - marginTop: 0, - paddingBottom: 0, - top: 5, - }, hasCall: { textAlign: 'right', - paddingRight: 0, + }, + filler: { + flex: 1, }, })); @@ -126,137 +104,118 @@ export const textStyle = StyleSheet.create({ regular: typography('Body', 200, 'Regular'), }); -const ChannelListItem = ({ - channel, currentUserId, hasDraft, - isActive, isInfo, isMuted, membersCount, hasMember, - isUnread, mentionsCount, onPress, teamDisplayName, testID, hasCall}: Props) => { +const ChannelItem = ({ + channel, + currentUserId, + hasDraft, + isActive, + isMuted, + membersCount, + isUnread, + mentionsCount, + onPress, + teamDisplayName = '', + testID, + hasCall, + isOnCenterBg = false, + showChannelName = false, + isOnHome = false, +}: Props) => { const {formatMessage} = useIntl(); const theme = useTheme(); const isTablet = useIsTablet(); const styles = getStyleSheet(theme); + const channelName = (showChannelName && !isDMorGM(channel)) ? channel.name : ''; + // Make it bolded if it has unreads or mentions const isBolded = isUnread || mentionsCount > 0; + const showActive = isActive && isTablet; + + const teammateId = (channel.type === General.DM_CHANNEL) ? getUserIdFromChannelName(currentUserId, channel.name) : undefined; + const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === teammateId; + + let displayName = 'displayName' in channel ? channel.displayName : channel.display_name; + if (isOwnDirectMessage) { + displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}); + } + + const deleteAt = 'deleteAt' in channel ? channel.deleteAt : channel.delete_at; + const channelItemTestId = `${testID}.${channel.name}`; const height = useMemo(() => { - let h = 40; - if (isInfo) { - h = (teamDisplayName && !isTablet) ? 58 : 44; - } - return h; - }, [teamDisplayName, isInfo, isTablet]); + return (teamDisplayName && !isTablet) ? ROW_HEIGHT_WITH_TEAM : ROW_HEIGHT; + }, [teamDisplayName, isTablet]); const handleOnPress = useCallback(() => { - onPress(channel.id); + onPress(channel); }, [channel.id]); const textStyles = useMemo(() => [ isBolded && !isMuted ? textStyle.bold : textStyle.regular, styles.text, isBolded && styles.highlight, - isActive && isTablet && !isInfo ? styles.textActive : null, - isInfo ? styles.textInfo : null, + showActive ? styles.textActive : null, + isOnCenterBg ? styles.textOnCenterBg : null, isMuted && styles.muted, - isMuted && isInfo && styles.mutedInfo, - ], [isBolded, styles, isMuted, isActive, isInfo, isTablet]); + isMuted && isOnCenterBg && styles.mutedOnCenterBg, + ], [isBolded, styles, isMuted, showActive, isOnCenterBg]); const containerStyle = useMemo(() => [ styles.container, - isActive && isTablet && !isInfo && styles.activeItem, - isInfo && styles.infoItem, + isOnHome && HOME_PADDING, + showActive && styles.activeItem, + showActive && isOnHome && { + paddingLeft: HOME_PADDING.paddingLeft - styles.activeItem.borderLeftWidth, + }, {minHeight: height}, - ], - [height, isActive, isTablet, isInfo, styles]); - - if (!hasMember) { - return null; - } - - const teammateId = (channel.type === General.DM_CHANNEL) ? getUserIdFromChannelName(currentUserId, channel.name) : undefined; - const isOwnDirectMessage = (channel.type === General.DM_CHANNEL) && currentUserId === teammateId; - - let displayName = channel.displayName; - if (isOwnDirectMessage) { - displayName = formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}); - } - - const channelItemTestId = `${testID}.${channel.name}`; + ], [height, showActive, styles, isOnHome]); return ( - <> - - - 0} - membersCount={membersCount} - name={channel.name} - shared={channel.shared} - size={24} - type={channel.type} - isMuted={isMuted} - /> - - - {displayName} - - {isInfo && Boolean(teamDisplayName) && !isTablet && - - {teamDisplayName} - - } - - {Boolean(teammateId) && - - } - {isInfo && Boolean(teamDisplayName) && isTablet && - - {teamDisplayName} - - } - - 0} - value={mentionsCount} - style={[styles.badge, isMuted && styles.mutedBadge, isInfo && styles.infoBadge]} - /> - {hasCall && - - } - - + + 0} + membersCount={membersCount} + name={channel.name} + shared={channel.shared} + size={24} + type={channel.type} + isMuted={isMuted} + style={styles.icon} + /> + + + 0} + value={mentionsCount} + style={[styles.badge, isMuted && styles.mutedBadge, isOnCenterBg && styles.badgeOnCenterBg]} + /> + {hasCall && + + } + ); }; -export default ChannelListItem; +export default ChannelItem; diff --git a/app/components/channel_item/custom_status/custom_status.tsx b/app/components/channel_item/custom_status/custom_status.tsx index b0503b7e95..4b24f41cbb 100644 --- a/app/components/channel_item/custom_status/custom_status.tsx +++ b/app/components/channel_item/custom_status/custom_status.tsx @@ -2,30 +2,20 @@ // See LICENSE.txt for license information. import React from 'react'; -import {StyleSheet} from 'react-native'; import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; +import type {EmojiCommonStyle} from '@typings/components/emoji'; +import type {StyleProp} from 'react-native'; + type Props = { customStatus?: UserCustomStatus; customStatusExpired: boolean; isCustomStatusEnabled: boolean; - isInfo?: boolean; - testID?: string; + style: StyleProp; } -const style = StyleSheet.create({ - customStatusEmoji: { - color: '#000', - marginHorizontal: -5, - top: 3, - }, - info: { - marginHorizontal: -15, - }, -}); - -const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, isInfo, testID}: Props) => { +const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, style}: Props) => { const showCustomStatusEmoji = Boolean(isCustomStatusEnabled && customStatus?.emoji && !customStatusExpired); if (!showCustomStatusEmoji) { @@ -35,8 +25,7 @@ const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, return ( ); }; diff --git a/app/components/channel_item/index.ts b/app/components/channel_item/index.ts index 9e07bf3404..9683b61aa5 100644 --- a/app/components/channel_item/index.ts +++ b/app/components/channel_item/index.ts @@ -21,66 +21,68 @@ import type {WithDatabaseArgs} from '@typings/database/database'; import type ChannelModel from '@typings/database/models/servers/channel'; type EnhanceProps = WithDatabaseArgs & { - channel: ChannelModel; + channel: ChannelModel | Channel; showTeamName?: boolean; serverUrl?: string; + shouldHighlightActive?: boolean; + shouldHighlightState?: boolean; } -const enhance = withObservables(['channel', 'showTeamName'], ({ +const enhance = withObservables(['channel', 'showTeamName', 'shouldHighlightActive', 'shouldHighlightState'], ({ channel, database, - showTeamName, serverUrl, + showTeamName = false, + shouldHighlightActive = false, + shouldHighlightState = false, }: EnhanceProps) => { const currentUserId = observeCurrentUserId(database); const myChannel = observeMyChannel(database, channel.id); - const hasDraft = queryDraft(database, channel.id).observeWithColumns(['message', 'files']).pipe( - switchMap((draft) => of$(draft.length > 0)), - distinctUntilChanged(), - ); + const hasDraft = shouldHighlightState ? + queryDraft(database, channel.id).observeWithColumns(['message', 'files']).pipe( + switchMap((draft) => of$(draft.length > 0)), + distinctUntilChanged(), + ) : of$(false); - const isActive = observeCurrentChannelId(database).pipe( - switchMap((id) => of$(id ? id === channel.id : false)), - distinctUntilChanged(), - ); + const isActive = shouldHighlightActive ? + observeCurrentChannelId(database).pipe( + switchMap((id) => of$(id ? id === channel.id : false)), + distinctUntilChanged(), + ) : of$(false); - const isMuted = myChannel.pipe( - switchMap((mc) => { - if (!mc) { - return of$(false); - } - return observeIsMutedSetting(database, mc.id); - }), - ); + const isMuted = shouldHighlightState ? + myChannel.pipe( + switchMap((mc) => { + if (!mc) { + return of$(false); + } + return observeIsMutedSetting(database, mc.id); + }), + ) : of$(false); - let teamDisplayName = of$(''); - if (channel.teamId && showTeamName) { - teamDisplayName = observeTeam(database, channel.teamId).pipe( + const teamId = 'teamId' in channel ? channel.teamId : channel.team_id; + const teamDisplayName = (teamId && showTeamName) ? + observeTeam(database, teamId).pipe( switchMap((team) => of$(team?.displayName || '')), distinctUntilChanged(), - ); - } + ) : of$(''); - let membersCount = of$(0); - if (channel.type === General.GM_CHANNEL) { - membersCount = queryChannelMembers(database, channel.id).observeCount(false); - } + const membersCount = channel.type === General.GM_CHANNEL ? + queryChannelMembers(database, channel.id).observeCount(false) : + of$(0); - const isUnread = myChannel.pipe( - switchMap((mc) => of$(mc?.isUnread)), - distinctUntilChanged(), - ); + const isUnread = shouldHighlightState ? + myChannel.pipe( + switchMap((mc) => of$(mc?.isUnread)), + distinctUntilChanged(), + ) : of$(false); - const mentionsCount = myChannel.pipe( - switchMap((mc) => of$(mc?.mentionsCount)), - distinctUntilChanged(), - ); - - const hasMember = myChannel.pipe( - switchMap((mc) => of$(Boolean(mc))), - distinctUntilChanged(), - ); + const mentionsCount = shouldHighlightState ? + myChannel.pipe( + switchMap((mc) => of$(mc?.mentionsCount)), + distinctUntilChanged(), + ) : of$(0); const hasCall = observeChannelsWithCalls(serverUrl || '').pipe( switchMap((calls) => of$(Boolean(calls[channel.id]))), @@ -88,7 +90,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({ ); return { - channel: channel.observe(), + channel: 'observe' in channel ? channel.observe() : of$(channel), currentUserId, hasDraft, isActive, @@ -97,7 +99,6 @@ const enhance = withObservables(['channel', 'showTeamName'], ({ isUnread, mentionsCount, teamDisplayName, - hasMember, hasCall, }; }); diff --git a/app/components/custom_status/__snapshots__/custom_status_emoji.test.tsx.snap b/app/components/custom_status/__snapshots__/custom_status_emoji.test.tsx.snap index db71fd5862..a3f1ff4b5f 100644 --- a/app/components/custom_status/__snapshots__/custom_status_emoji.test.tsx.snap +++ b/app/components/custom_status/__snapshots__/custom_status_emoji.test.tsx.snap @@ -1,41 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/custom_status/custom_status_emoji should match snapshot 1`] = ` - - - 📆 - - + 📆 + `; exports[`components/custom_status/custom_status_emoji should match snapshot with props 1`] = ` - - - 📆 - - + 📆 + `; diff --git a/app/components/custom_status/custom_status_emoji.test.tsx b/app/components/custom_status/custom_status_emoji.test.tsx index b235760845..77f1e89a60 100644 --- a/app/components/custom_status/custom_status_emoji.test.tsx +++ b/app/components/custom_status/custom_status_emoji.test.tsx @@ -26,7 +26,6 @@ describe('components/custom_status/custom_status_emoji', () => { const wrapper = renderWithEverything( , {database}, ); @@ -38,7 +37,6 @@ describe('components/custom_status/custom_status_emoji', () => { , {database}, ); diff --git a/app/components/custom_status/custom_status_emoji.tsx b/app/components/custom_status/custom_status_emoji.tsx index fea1f6985d..a0140ff45a 100644 --- a/app/components/custom_status/custom_status_emoji.tsx +++ b/app/components/custom_status/custom_status_emoji.tsx @@ -2,29 +2,26 @@ // See LICENSE.txt for license information. import React from 'react'; -import {StyleProp, TextStyle, View} from 'react-native'; import Emoji from '@components/emoji'; +import type {EmojiCommonStyle} from '@typings/components/emoji'; +import type {StyleProp} from 'react-native'; + interface ComponentProps { customStatus: UserCustomStatus; emojiSize?: number; - style?: StyleProp; - testID?: string; + style?: StyleProp; } -const CustomStatusEmoji = ({customStatus, emojiSize = 16, style, testID}: ComponentProps) => { +const CustomStatusEmoji = ({customStatus, emojiSize = 16, style}: ComponentProps) => { if (customStatus.emoji) { return ( - - - + ); } diff --git a/app/components/emoji/emoji.tsx b/app/components/emoji/emoji.tsx index 2998213551..65d48a5eac 100644 --- a/app/components/emoji/emoji.tsx +++ b/app/components/emoji/emoji.tsx @@ -29,12 +29,13 @@ const assetImages = new Map([['mattermost.png', require('@assets/images/emojis/m const Emoji = (props: EmojiProps) => { const { customEmojis, - customEmojiStyle, + imageStyle, displayTextOnly, emojiName, literal = '', testID, textStyle, + commonStyle, } = props; const serverUrl = useServerUrl(); let assetImage = ''; @@ -73,7 +74,7 @@ const Emoji = (props: EmojiProps) => { if (displayTextOnly || (!imageUrl && !assetImage && !unicode)) { return ( {literal} @@ -91,7 +92,7 @@ const Emoji = (props: EmojiProps) => { return ( {code} @@ -110,7 +111,7 @@ const Emoji = (props: EmojiProps) => { @@ -128,7 +129,7 @@ const Emoji = (props: EmojiProps) => { return ( )} diff --git a/app/components/post_with_channel_info/channel_info/channel_info.tsx b/app/components/post_with_channel_info/channel_info/channel_info.tsx index 7673e38657..bf165362d2 100644 --- a/app/components/post_with_channel_info/channel_info/channel_info.tsx +++ b/app/components/post_with_channel_info/channel_info/channel_info.tsx @@ -5,8 +5,8 @@ import React, {useCallback, useMemo, useState} from 'react'; import {View, Text, StyleSheet} from 'react-native'; import {switchToChannelById} from '@actions/remote/channel'; -import TouchableWithFeedback from '@app/components/touchable_with_feedback'; -import {useServerUrl} from '@app/context/server'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; diff --git a/app/components/profile_picture/index.tsx b/app/components/profile_picture/index.tsx index 1a96a8822b..453ede9ecf 100644 --- a/app/components/profile_picture/index.tsx +++ b/app/components/profile_picture/index.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useEffect, useMemo} from 'react'; -import {Platform, StyleProp, View, ViewStyle} from 'react-native'; +import {StyleProp, View, ViewStyle} from 'react-native'; import {fetchStatusInBatch} from '@actions/remote/user'; import {useServerUrl} from '@context/server'; @@ -15,11 +15,6 @@ import Status from './status'; import type UserModel from '@typings/database/models/servers/user'; import type {Source} from 'react-native-fast-image'; -const STATUS_BUFFER = Platform.select({ - ios: 3, - android: 2, -}); - type ProfilePictureProps = { author?: UserModel | UserProfile; forwardRef?: React.RefObject; @@ -27,6 +22,7 @@ type ProfilePictureProps = { showStatus?: boolean; size: number; statusSize?: number; + containerStyle?: StyleProp; statusStyle?: StyleProp; testID?: string; source?: Source | string; @@ -38,7 +34,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { container: { justifyContent: 'center', alignItems: 'center', - borderRadius: 80, }, icon: { color: changeOpacity(theme.centerChannelColor, 0.48), @@ -67,6 +62,7 @@ const ProfilePicture = ({ showStatus = true, size = 64, statusSize = 14, + containerStyle, statusStyle, testID, source, @@ -77,7 +73,6 @@ const ProfilePicture = ({ serverUrl = url || serverUrl; const style = getStyleSheet(theme); - const buffer = showStatus ? STATUS_BUFFER || 0 : 0; const isBot = author && (('isBot' in author) ? author.isBot : author.is_bot); useEffect(() => { @@ -86,26 +81,14 @@ const ProfilePicture = ({ } }, []); - const containerStyle = useMemo(() => { - if (author) { - return { - width: size + (buffer - 1), - height: size + (buffer - 1), - borderRadius: (size + buffer) / 2, - }; - } - - return { - ...style.container, - width: size + buffer, - height: size + buffer, - borderRadius: (size + buffer) / 2, - }; - }, [author, size]); + const viewStyle = useMemo( + () => [style.container, {width: size, height: size}, containerStyle], + [style, size, containerStyle], + ); return ( { return { statusWrapper: { position: 'absolute', - bottom: 0, - right: 0, + bottom: -STATUS_BUFFER, + right: -STATUS_BUFFER, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', diff --git a/app/components/selected_chip/index.tsx b/app/components/selected_chip/index.tsx index 3ea8e221dd..0b78e7dc20 100644 --- a/app/components/selected_chip/index.tsx +++ b/app/components/selected_chip/index.tsx @@ -2,11 +2,12 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {Text, TouchableOpacity, View} from 'react-native'; +import {Text, TouchableOpacity, useWindowDimensions} from 'react-native'; import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import CompassIcon from '@components/compass_icon'; import {useTheme} from '@context/theme'; +import {nonBreakingString} from '@utils/strings'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -35,11 +36,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { marginRight: 8, paddingHorizontal: 7, }, - extraContent: { - flexDirection: 'row', - alignItems: 'center', - color: theme.centerChannelColor, - }, text: { marginLeft: 8, color: theme.centerChannelColor, @@ -61,6 +57,7 @@ export default function SelectedChip({ }: SelectedChipProps) { const theme = useTheme(); const style = getStyleFromTheme(theme); + const dimensions = useWindowDimensions(); const onPress = useCallback(() => { onRemove(id); @@ -73,16 +70,13 @@ export default function SelectedChip({ style={style.container} testID={testID} > - {extra && ( - - {extra} - - )} + {extra} - {text} + {nonBreakingString(text)} void; term: string; @@ -24,7 +23,6 @@ type Props = { export default function ServerUserList({ currentUserId, - teammateNameDisplay, tutorialWatched, handleSelectProfile, term, @@ -121,7 +119,6 @@ export default function ServerUserList({ profiles={data} selectedIds={selectedIds} showNoResults={!loading && page.current !== -1} - teammateNameDisplay={teammateNameDisplay} fetchMore={getProfiles} term={term} testID={testID} diff --git a/app/components/tag/index.tsx b/app/components/tag/index.tsx index 1b376d3302..2f87a10767 100644 --- a/app/components/tag/index.tsx +++ b/app/components/tag/index.tsx @@ -25,9 +25,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { alignSelf: 'center', backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), borderRadius: 4, - marginRight: 2, - marginBottom: 1, - marginLeft: 2, paddingVertical: 2, paddingHorizontal: 4, }, diff --git a/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap b/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap index c07f7a3829..8eb4071c91 100644 --- a/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap +++ b/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap @@ -38,70 +38,62 @@ exports[`Thread item in the channel list Threads Component should match snapshot > - + - - - Threads - - + Threads + `; -exports[`Thread item in the channel list Threads Component should match snapshot with isInfo 1`] = ` +exports[`Thread item in the channel list Threads Component should match snapshot with onCenterBg 1`] = ` - + - - - Threads - - + Threads + `; @@ -247,65 +230,57 @@ exports[`Thread item in the channel list Threads Component should match snapshot > - + - - - Threads - - + Threads + `; diff --git a/app/components/threads_button/threads_button.test.tsx b/app/components/threads_button/threads_button.test.tsx index cbc90ecd2e..ebea2b58f5 100644 --- a/app/components/threads_button/threads_button.test.tsx +++ b/app/components/threads_button/threads_button.test.tsx @@ -36,11 +36,11 @@ describe('Thread item in the channel list', () => { expect(toJSON()).toMatchSnapshot(); }); - test('Threads Component should match snapshot with isInfo', () => { + test('Threads Component should match snapshot with onCenterBg', () => { const {toJSON} = renderWithIntlAndTheme( , ); diff --git a/app/components/threads_button/threads_button.tsx b/app/components/threads_button/threads_button.tsx index b543ee8ae4..6a150cbae3 100644 --- a/app/components/threads_button/threads_button.tsx +++ b/app/components/threads_button/threads_button.tsx @@ -8,10 +8,12 @@ import {switchToGlobalThreads} from '@actions/local/thread'; import Badge from '@components/badge'; import { getStyleSheet as getChannelItemStyleSheet, + ROW_HEIGHT, textStyle as channelItemTextStyle, } from '@components/channel_item/channel_item'; import CompassIcon from '@components/compass_icon'; import FormattedText from '@components/formatted_text'; +import {HOME_PADDING} from '@constants/view'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; @@ -19,13 +21,10 @@ import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - baseContainer: { - marginLeft: -18, - marginRight: -20, - }, icon: { color: changeOpacity(theme.sidebarText, 0.5), fontSize: 24, + marginRight: 12, }, iconActive: { color: theme.sidebarText, @@ -41,16 +40,27 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ type Props = { currentChannelId: string; groupUnreadsSeparately: boolean; - isInfo?: boolean; + onCenterBg?: boolean; onlyUnreads: boolean; onPress?: () => void; + shouldHighlighActive?: boolean; unreadsAndMentions: { unreads: boolean; mentions: number; }; + isOnHome?: boolean; }; -const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUnreads, onPress, unreadsAndMentions}: Props) => { +const ThreadsButton = ({ + currentChannelId, + groupUnreadsSeparately, + onCenterBg, + onlyUnreads, + onPress, + unreadsAndMentions, + shouldHighlighActive = false, + isOnHome = false, +}: Props) => { const isTablet = useIsTablet(); const serverUrl = useServerUrl(); @@ -67,18 +77,23 @@ const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUn }), [onPress, serverUrl]); const {unreads, mentions} = unreadsAndMentions; - const isActive = isTablet && !currentChannelId; + const isActive = isTablet && shouldHighlighActive && !currentChannelId; const [containerStyle, iconStyle, textStyle, badgeStyle] = useMemo(() => { const container = [ styles.container, + isOnHome && HOME_PADDING, isActive && styles.activeItem, + isActive && isOnHome && { + paddingLeft: HOME_PADDING.paddingLeft - styles.activeItem.borderLeftWidth, + }, + {minHeight: ROW_HEIGHT}, ]; const icon = [ customStyles.icon, (isActive || unreads) && customStyles.iconActive, - isInfo && customStyles.iconInfo, + onCenterBg && customStyles.iconInfo, ]; const text = [ @@ -87,16 +102,16 @@ const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUn styles.text, unreads && styles.highlight, isActive && styles.textActive, - isInfo && styles.textInfo, + onCenterBg && styles.textOnCenterBg, ]; const badge = [ styles.badge, - isInfo && styles.infoBadge, + onCenterBg && styles.badgeOnCenterBg, ]; return [container, icon, text, badge]; - }, [customStyles, isActive, isInfo, styles, unreads]); + }, [customStyles, isActive, onCenterBg, styles, unreads, isOnHome]); if (groupUnreadsSeparately && (onlyUnreads && !isActive && !unreads && !mentions)) { return null; @@ -107,23 +122,21 @@ const ThreadsButton = ({currentChannelId, groupUnreadsSeparately, isInfo, onlyUn onPress={handlePress} testID='channel_list.threads.button' > - - - - - 0} - /> - + + + + 0} + /> ); diff --git a/app/components/user_avatars_stack/users_list/index.tsx b/app/components/user_avatars_stack/users_list/index.tsx index 3a8490cb27..518ad0b21f 100644 --- a/app/components/user_avatars_stack/users_list/index.tsx +++ b/app/components/user_avatars_stack/users_list/index.tsx @@ -4,7 +4,7 @@ import {BottomSheetFlatList} from '@gorhom/bottom-sheet'; import React, {useCallback, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import {Keyboard, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder, StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'; +import {Keyboard, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder} from 'react-native'; import {FlatList} from 'react-native-gesture-handler'; import UserItem from '@components/user_item'; @@ -23,40 +23,30 @@ type Props = { type ItemProps = { channelId: string; - containerStyle: StyleProp; location: string; user: UserModel; } -const style = StyleSheet.create({ - container: { - paddingLeft: 0, - }, -}); - -const Item = ({channelId, containerStyle, location, user}: ItemProps) => { +const Item = ({channelId, location, user}: ItemProps) => { const intl = useIntl(); const theme = useTheme(); - const openUserProfile = async () => { - if (user) { - await dismissBottomSheet(Screens.BOTTOM_SHEET); - const screen = Screens.USER_PROFILE; - const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); - const closeButtonId = 'close-user-profile'; - const props = {closeButtonId, location, userId: user.id, channelId}; - Keyboard.dismiss(); - openAsBottomSheet({screen, title, theme, closeButtonId, props}); - } - }; + const openUserProfile = useCallback(async (u: UserModel | UserProfile) => { + await dismissBottomSheet(Screens.BOTTOM_SHEET); + const screen = Screens.USER_PROFILE; + const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const closeButtonId = 'close-user-profile'; + const props = {closeButtonId, location, userId: u.id, channelId}; + + Keyboard.dismiss(); + openAsBottomSheet({screen, title, theme, closeButtonId, props}); + }, [location, channelId, theme, intl]); return ( - - - + ); }; @@ -89,7 +79,6 @@ const UsersList = ({channelId, location, type = 'FlatList', users}: Props) => { channelId={channelId} location={location} user={item} - containerStyle={style.container} /> ), [channelId, location]); diff --git a/app/components/user_item/index.ts b/app/components/user_item/index.ts index fa9f733861..df41e980b6 100644 --- a/app/components/user_item/index.ts +++ b/app/components/user_item/index.ts @@ -3,8 +3,11 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {distinctUntilChanged, switchMap} from 'rxjs/operators'; import {observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system'; +import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user'; import UserItem from './user_item'; @@ -12,12 +15,17 @@ import type {WithDatabaseArgs} from '@typings/database/database'; const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { const isCustomStatusEnabled = observeConfigBooleanValue(database, 'EnableCustomUserStatuses'); - const showFullName = observeConfigBooleanValue(database, 'ShowFullName'); const currentUserId = observeCurrentUserId(database); + const locale = observeCurrentUser(database).pipe( + switchMap((u) => of$(u?.locale)), + distinctUntilChanged(), + ); + const teammateNameDisplay = observeTeammateNameDisplay(database); return { isCustomStatusEnabled, - showFullName, currentUserId, + locale, + teammateNameDisplay, }; }); diff --git a/app/components/user_item/user_item.tsx b/app/components/user_item/user_item.tsx index 24c0812d78..656bd9b33e 100644 --- a/app/components/user_item/user_item.tsx +++ b/app/components/user_item/user_item.tsx @@ -1,104 +1,92 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useMemo} from 'react'; -import {IntlShape, useIntl} from 'react-intl'; -import {StyleProp, Text, View, ViewStyle} from 'react-native'; +import React, {useCallback, useMemo} from 'react'; +import {useIntl} from 'react-intl'; +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; -import ChannelIcon from '@components/channel_icon'; +import CompassIcon from '@components/compass_icon'; import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; -import FormattedText from '@components/formatted_text'; import ProfilePicture from '@components/profile_picture'; import {BotTag, GuestTag} from '@components/tag'; -import {General} from '@constants'; import {useTheme} from '@context/theme'; +import {nonBreakingString} from '@utils/strings'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {typography} from '@utils/typography'; -import {getUserCustomStatus, isBot, isCustomStatusExpired, isGuest, isShared} from '@utils/user'; +import {displayUsername, getUserCustomStatus, isBot, isCustomStatusExpired, isGuest, isShared} from '@utils/user'; import type UserModel from '@typings/database/models/servers/user'; type AtMentionItemProps = { - user?: UserProfile | UserModel; - containerStyle?: StyleProp; + user: UserProfile | UserModel; currentUserId: string; - showFullName: boolean; testID?: string; isCustomStatusEnabled: boolean; - pictureContainerStyle?: StyleProp; + showBadges?: boolean; + locale?: string; + teammateNameDisplay: string; + rightDecorator?: React.ReactNode; + onUserPress?: (user: UserProfile | UserModel) => void; + onUserLongPress?: (user: UserProfile | UserModel) => void; + disabled?: boolean; + viewRef?: React.LegacyRef; + padding?: number; } -const getName = (user: UserProfile | UserModel | undefined, showFullName: boolean, isCurrentUser: boolean, intl: IntlShape) => { - let name = ''; - if (!user) { - return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'}); - } - - const hasNickname = user.nickname.length > 0; - - if (showFullName) { - const first = 'first_name' in user ? user.first_name : user.firstName; - const last = 'last_name' in user ? user.last_name : user.lastName; - name += `${first} ${last} `; - } - - if (hasNickname && !isCurrentUser) { - name += name.length > 0 ? `(${user.nickname})` : user.nickname; - } - - return name.trim(); -}; - -const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { +const getThemedStyles = makeStyleSheetFromTheme((theme: Theme) => { return { - row: { - height: 40, - paddingVertical: 8, - paddingTop: 4, - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center', - }, - rowPicture: { - marginRight: 10, - marginLeft: 2, - width: 24, - alignItems: 'center', - justifyContent: 'center', - }, - rowInfo: { - flexDirection: 'row', - overflow: 'hidden', - }, rowFullname: { ...typography('Body', 200), color: theme.centerChannelColor, - paddingLeft: 4, + flex: 0, flexShrink: 1, }, rowUsername: { - ...typography('Body', 200), + ...typography('Body', 100), color: changeOpacity(theme.centerChannelColor, 0.64), - fontSize: 15, - fontFamily: 'OpenSans', - }, - icon: { - marginLeft: 4, }, }; }); +const nonThemedStyles = StyleSheet.create({ + row: { + height: 40, + paddingVertical: 8, + paddingTop: 4, + flexDirection: 'row', + alignItems: 'center', + }, + icon: { + marginLeft: 4, + }, + profile: { + marginRight: 12, + }, + tag: { + marginLeft: 6, + }, + flex: { + flex: 1, + }, +}); + const UserItem = ({ - containerStyle, user, currentUserId, - showFullName, testID, isCustomStatusEnabled, - pictureContainerStyle, + showBadges = false, + locale, + teammateNameDisplay, + rightDecorator, + onUserPress, + onUserLongPress, + disabled = false, + viewRef, + padding, }: AtMentionItemProps) => { const theme = useTheme(); - const style = getStyleFromTheme(theme); + const style = getThemedStyles(theme); const intl = useIntl(); const bot = user ? isBot(user) : false; @@ -106,91 +94,109 @@ const UserItem = ({ const shared = user ? isShared(user) : false; const isCurrentUser = currentUserId === user?.id; - const name = getName(user, showFullName, isCurrentUser, intl); const customStatus = getUserCustomStatus(user); const customStatusExpired = isCustomStatusExpired(user); - const userItemTestId = `${testID}.${user?.id}`; + const deleteAt = 'deleteAt' in user ? user.deleteAt : user.delete_at; - let rowUsernameFlexShrink = 1; - if (user) { - for (const rowInfoElem of [bot, guest, Boolean(name.length), isCurrentUser]) { - if (rowInfoElem) { - rowUsernameFlexShrink++; - } - } + let displayName = displayUsername(user, locale, teammateNameDisplay); + const showTeammateDisplay = displayName !== user?.username; + if (isCurrentUser) { + displayName = intl.formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}); } - const usernameTextStyle = useMemo(() => { - return [style.rowUsername, {flexShrink: rowUsernameFlexShrink}]; - }, [user, rowUsernameFlexShrink]); + const userItemTestId = `${testID}.${user?.id}`; + + const containerStyle = useMemo(() => { + return [ + nonThemedStyles.row, + { + opacity: disabled ? 0.32 : 1, + paddingHorizontal: padding || undefined, + }, + ]; + }, [disabled, padding]); + + const onPress = useCallback(() => { + onUserPress?.(user); + }, [user, onUserPress]); + + const onLongPress = useCallback(() => { + onUserLongPress?.(user); + }, [user, onUserLongPress]); return ( - - + - - - {bot && } - {guest && } - {Boolean(name.length) && - - {name} - - } - {isCurrentUser && - + {nonBreakingString(displayName)} + {Boolean(showTeammateDisplay) && ( + + {nonBreakingString(` @${user!.username}`)} + + )} + {Boolean(deleteAt) && ( + + {nonBreakingString(` ${intl.formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})}`)} + + )} + + {showBadges && bot && ( + - } - {Boolean(user) && ( - - {` @${user!.username}`} - )} + {showBadges && guest && ( + + )} + {Boolean(isCustomStatusEnabled && !bot && customStatus?.emoji && !customStatusExpired) && ( + + )} + {shared && ( + + )} + + {Boolean(rightDecorator) && rightDecorator} - {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 45fc81c90d..74c833e9d9 100644 --- a/app/components/user_list/__snapshots__/index.test.tsx.snap +++ b/app/components/user_list/__snapshots__/index.test.tsx.snap @@ -117,141 +117,123 @@ exports[`components/channel_list_row should show no results 1`] = ` style={null} > + + + + + johndoe + + - - - - - - - - - johndoe - - - - - - + @@ -400,141 +382,123 @@ exports[`components/channel_list_row should show results and tutorial 1`] = ` style={null} > + + + + + johndoe + + - - - - - - - - - johndoe - - - - - - + @@ -763,141 +727,123 @@ exports[`components/channel_list_row should show results no tutorial 1`] = ` style={null} > + + + + + johndoe + + - - - - - - - - - johndoe - - - - - - + @@ -1078,141 +1024,123 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`] style={null} > + + + + + johndoe + + - - - - - - - - - johndoe - - - - - - + @@ -1266,141 +1194,123 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`] style={null} > + + + + + rocky + + - - - - - - - - - rocky - - - - - - + diff --git a/app/components/user_list/index.test.tsx b/app/components/user_list/index.test.tsx index fd0ae5d99c..8ae4fbaf65 100644 --- a/app/components/user_list/index.test.tsx +++ b/app/components/user_list/index.test.tsx @@ -77,7 +77,6 @@ describe('components/channel_list_row', () => { profiles={[user]} testID='UserListRow' currentUserId={'1'} - teammateNameDisplay={'johndoe'} handleSelectProfile={() => { // noop }} @@ -101,7 +100,6 @@ describe('components/channel_list_row', () => { profiles={[user]} testID='UserListRow' currentUserId={'1'} - teammateNameDisplay={'johndoe'} handleSelectProfile={() => { // noop }} @@ -125,7 +123,6 @@ describe('components/channel_list_row', () => { profiles={[user, user2]} testID='UserListRow' currentUserId={'1'} - teammateNameDisplay={'johndoe'} handleSelectProfile={() => { // noop }} @@ -149,7 +146,6 @@ describe('components/channel_list_row', () => { profiles={[user]} testID='UserListRow' currentUserId={'1'} - teammateNameDisplay={'johndoe'} handleSelectProfile={() => { // noop }} diff --git a/app/components/user_list/index.tsx b/app/components/user_list/index.tsx index 9383e1cda3..a78436d1e1 100644 --- a/app/components/user_list/index.tsx +++ b/app/components/user_list/index.tsx @@ -21,6 +21,8 @@ import { } from '@utils/theme'; import {typography} from '@utils/typography'; +import type UserModel from '@typings/database/models/servers/user'; + type UserProfileWithChannelAdmin = UserProfile & {scheme_admin?: boolean} type RenderItemType = ListRenderItemInfo & {section?: SectionListData} @@ -147,8 +149,7 @@ type Props = { profiles: UserProfile[]; channelMembers?: ChannelMembership[]; currentUserId: string; - teammateNameDisplay: string; - handleSelectProfile: (user: UserProfile) => void; + handleSelectProfile: (user: UserProfile | UserModel) => void; fetchMore?: () => void; loading: boolean; manageMode?: boolean; @@ -165,7 +166,6 @@ export default function UserList({ channelMembers, selectedIds, currentUserId, - teammateNameDisplay, handleSelectProfile, fetchMore, loading, @@ -198,21 +198,29 @@ export default function UserList({ return createProfilesSections(intl, profiles, channelMembers); }, [channelMembers, loading, profiles, term]); - const openUserProfile = useCallback(async (profile: UserProfile) => { - const {user} = await storeProfile(serverUrl, profile); - if (user) { - const screen = Screens.USER_PROFILE; - const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); - const closeButtonId = 'close-user-profile'; - const props = { - closeButtonId, - userId: user.id, - location: Screens.USER_PROFILE, - }; - - Keyboard.dismiss(); - openAsBottomSheet({screen, title, theme, closeButtonId, props}); + const openUserProfile = useCallback(async (profile: UserProfile | UserModel) => { + let user: UserModel; + if ('create_at' in profile) { + const res = await storeProfile(serverUrl, profile); + if (!res.user) { + return; + } + user = res.user; + } else { + user = profile; } + + const screen = Screens.USER_PROFILE; + const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}); + const closeButtonId = 'close-user-profile'; + const props = { + closeButtonId, + userId: user.id, + location: Screens.USER_PROFILE, + }; + + Keyboard.dismiss(); + openAsBottomSheet({screen, title, theme, closeButtonId, props}); }, []); const renderItem = useCallback(({item, index, section}: RenderItemType) => { @@ -237,12 +245,11 @@ export default function UserList({ selected={selected} showManageMode={showManageMode} testID='create_direct_message.user_list.user_item' - teammateNameDisplay={teammateNameDisplay} tutorialWatched={tutorialWatched} user={item} /> ); - }, [selectedIds, handleSelectProfile, showManageMode, manageMode, teammateNameDisplay, tutorialWatched]); + }, [selectedIds, handleSelectProfile, showManageMode, manageMode, tutorialWatched]); const renderLoading = useCallback(() => { if (!loading) { diff --git a/app/components/user_list_row/index.tsx b/app/components/user_list_row/index.tsx index 85bcc2b919..bf9faa9c73 100644 --- a/app/components/user_list_row/index.tsx +++ b/app/components/user_list_row/index.tsx @@ -6,24 +6,22 @@ import {useIntl} from 'react-intl'; import { InteractionManager, Platform, - Text, View, } from 'react-native'; import {storeProfileLongPressTutorial} from '@actions/app/global'; import CompassIcon from '@components/compass_icon'; import FormattedText from '@components/formatted_text'; -import ProfilePicture from '@components/profile_picture'; -import {BotTag, GuestTag} from '@components/tag'; -import TouchableWithFeedback from '@components/touchable_with_feedback'; import TutorialHighlight from '@components/tutorial_highlight'; import TutorialLongPress from '@components/tutorial_highlight/long_press'; +import UserItem from '@components/user_item'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {t} from '@i18n'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {typography} from '@utils/typography'; -import {displayUsername, isGuest} from '@utils/user'; + +import type UserModel from '@typings/database/models/servers/user'; type Props = { highlight?: boolean; @@ -31,13 +29,12 @@ type Props = { isMyUser: boolean; isChannelAdmin: boolean; manageMode: boolean; - onLongPress: (user: UserProfile) => void; - onPress?: (user: UserProfile) => void; + onLongPress: (user: UserProfile | UserModel) => void; + onPress?: (user: UserProfile | UserModel) => void; selectable: boolean; disabled?: boolean; selected: boolean; showManageMode: boolean; - teammateNameDisplay: string; testID: string; tutorialWatched?: boolean; user: UserProfile; @@ -45,54 +42,16 @@ type Props = { const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { - container: { - flex: 1, - flexDirection: 'row', - paddingHorizontal: 20, - height: 58, - overflow: 'hidden', - }, - profileContainer: { - flexDirection: 'row', - alignItems: 'center', - color: theme.centerChannelColor, - }, - textContainer: { - paddingHorizontal: 10, - justifyContent: 'center', - flexDirection: 'column', - flex: 1, - }, - username: { - color: changeOpacity(theme.centerChannelColor, 0.64), - ...typography('Body', 75, 'Regular'), - }, - displayName: { - height: 24, - color: theme.centerChannelColor, - maxWidth: '80%', - ...typography('Body', 200, 'Regular'), - }, - indicatorContainer: { - flexDirection: 'row', - }, - deactivated: { - marginTop: 2, - fontSize: 12, - color: changeOpacity(theme.centerChannelColor, 0.64), - }, - sharedUserIcon: { - alignSelf: 'center', - opacity: 0.75, - }, selector: { alignItems: 'center', justifyContent: 'center', + marginLeft: 12, }, selectorManage: { alignItems: 'center', flexDirection: 'row', justifyContent: 'center', + marginLeft: 12, }, manageText: { color: changeOpacity(theme.centerChannelColor, 0.64), @@ -107,7 +66,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { }; }); -const DISABLED_OPACITY = 0.32; const DEFAULT_ICON_OPACITY = 0.32; function UserListRow({ @@ -122,7 +80,6 @@ function UserListRow({ disabled, selected, showManageMode = false, - teammateNameDisplay, testID, tutorialWatched = false, user, @@ -133,8 +90,7 @@ function UserListRow({ const [itemBounds, setItemBounds] = useState({startX: 0, startY: 0, endX: 0, endY: 0}); const viewRef = useRef(null); const style = getStyleFromTheme(theme); - const {formatMessage, locale} = useIntl(); - const {username} = user; + const {formatMessage} = useIntl(); const startTutorial = () => { viewRef.current?.measureInWindow((x, y, w, h) => { @@ -167,16 +123,12 @@ function UserListRow({ } }, [highlight, tutorialWatched, isTablet]); - const handlePress = useCallback(() => { + const handlePress = useCallback((u: UserModel | UserProfile) => { if (isMyUser && manageMode) { return; } - onPress?.(user); - }, [onPress, isMyUser, manageMode, user]); - - const handleLongPress = useCallback(() => { - onLongPress?.(user); - }, [onLongPress, user]); + onPress?.(u); + }, [onPress, isMyUser, manageMode]); const manageModeIcon = useMemo(() => { if (!showManageMode || isMyUser) { @@ -208,12 +160,11 @@ function UserListRow({ }, []); const icon = useMemo(() => { - if (!selectable) { + if (!selectable && !selected) { return null; } - const iconOpacity = DEFAULT_ICON_OPACITY * (disabled ? DISABLED_OPACITY : 1); - const color = selected ? theme.buttonBg : changeOpacity(theme.centerChannelColor, iconOpacity); + const color = selected ? theme.buttonBg : changeOpacity(theme.centerChannelColor, DEFAULT_ICON_OPACITY); return ( - - - - - - - - - {teammateDisplay} - - - - - {showTeammateDisplay && - - - {usernameDisplay} - - - } - {user.delete_at > 0 && - - - {formatMessage({id: 'mobile.user_list.deactivated', defaultMessage: 'Deactivated'})} - - - } - - {manageMode ? manageModeIcon : icon} - - + {showTutorial && } diff --git a/app/screens/channel_add_members/channel_add_members.tsx b/app/screens/channel_add_members/channel_add_members.tsx index 30e9ddc3fa..dbd6687200 100644 --- a/app/screens/channel_add_members/channel_add_members.tsx +++ b/app/screens/channel_add_members/channel_add_members.tsx @@ -273,7 +273,6 @@ export default function ChannelAddMembers({ currentUserId={currentUserId} handleSelectProfile={handleSelectProfile} selectedIds={selectedIds} - teammateNameDisplay={teammateNameDisplay} term={term} testID={`${TEST_ID}.user_list`} tutorialWatched={tutorialWatched} diff --git a/app/screens/channel_info/options/add_members/add_members.tsx b/app/screens/channel_info/options/add_members/add_members.tsx index 106c4af67a..b031841ca4 100644 --- a/app/screens/channel_info/options/add_members/add_members.tsx +++ b/app/screens/channel_info/options/add_members/add_members.tsx @@ -5,10 +5,10 @@ import React from 'react'; import {useIntl} from 'react-intl'; import {Platform} from 'react-native'; -import {getHeaderOptions} from '@app/screens/channel_add_members/channel_add_members'; import OptionItem from '@components/option_item'; import {Screens} from '@constants'; import {useTheme} from '@context/theme'; +import {getHeaderOptions} from '@screens/channel_add_members/channel_add_members'; import {goToScreen} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; diff --git a/app/screens/channel_notification_preferences/muted_banner.tsx b/app/screens/channel_notification_preferences/muted_banner.tsx index 0b7451ef4b..f315c9ff52 100644 --- a/app/screens/channel_notification_preferences/muted_banner.tsx +++ b/app/screens/channel_notification_preferences/muted_banner.tsx @@ -7,13 +7,13 @@ import {View} from 'react-native'; import Animated, {FlipOutXUp} from 'react-native-reanimated'; import {toggleMuteChannel} from '@actions/remote/channel'; -import Button from '@app/components/button'; -import CompassIcon from '@app/components/compass_icon'; -import FormattedText from '@app/components/formatted_text'; -import {useServerUrl} from '@app/context/server'; -import {useTheme} from '@app/context/theme'; -import {preventDoubleTap} from '@app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme'; +import Button from '@components/button'; +import CompassIcon from '@components/compass_icon'; +import FormattedText from '@components/formatted_text'; +import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { diff --git a/app/screens/create_direct_message/create_direct_message.tsx b/app/screens/create_direct_message/create_direct_message.tsx index 4e4dd36138..5ea8f0ac35 100644 --- a/app/screens/create_direct_message/create_direct_message.tsx +++ b/app/screens/create_direct_message/create_direct_message.tsx @@ -320,7 +320,6 @@ export default function CreateDirectMessage({ currentUserId={currentUserId} handleSelectProfile={handleSelectProfile} selectedIds={selectedIds} - teammateNameDisplay={teammateNameDisplay} term={term} testID='create_direct_message.user_list' tutorialWatched={tutorialWatched} diff --git a/app/screens/find_channels/filtered_list/filtered_list.tsx b/app/screens/find_channels/filtered_list/filtered_list.tsx index 7cc19de2ca..96343d6e06 100644 --- a/app/screens/find_channels/filtered_list/filtered_list.tsx +++ b/app/screens/find_channels/filtered_list/filtered_list.tsx @@ -14,14 +14,12 @@ import ChannelItem from '@components/channel_item'; import Loading from '@components/loading'; import NoResultsWithTerm from '@components/no_results_with_term'; import ThreadsButton from '@components/threads_button'; +import UserItem from '@components/user_item'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {sortChannelsByDisplayName} from '@utils/channel'; import {displayUsername} from '@utils/user'; -import RemoteChannelItem from './remote_channel_item'; -import UserItem from './user_item'; - import type ChannelModel from '@typings/database/models/servers/channel'; import type UserModel from '@typings/database/models/servers/user'; @@ -144,8 +142,9 @@ const FilteredList = ({ onLoading(false); }; - const onJoinChannel = useCallback(async (channelId: string, displayName: string) => { - const {error} = await joinChannelIfNeeded(serverUrl, channelId); + const onJoinChannel = useCallback(async (c: Channel | ChannelModel) => { + const {error} = await joinChannelIfNeeded(serverUrl, c.id); + const displayName = 'display_name' in c ? c.display_name : c.displayName; if (error) { Alert.alert( '', @@ -158,11 +157,12 @@ const FilteredList = ({ } await close(); - switchToChannelById(serverUrl, channelId, undefined, true); + switchToChannelById(serverUrl, c.id, undefined, true); }, [serverUrl, close, locale]); - const onOpenDirectMessage = useCallback(async (teammateId: string, displayName: string) => { - const {data, error} = await makeDirectChannel(serverUrl, teammateId, displayName, false); + const onOpenDirectMessage = useCallback(async (u: UserProfile | UserModel) => { + const displayName = displayUsername(u, locale, teammateDisplayNameSetting); + const {data, error} = await makeDirectChannel(serverUrl, u.id, displayName, false); if (error || !data) { Alert.alert( '', @@ -176,11 +176,11 @@ const FilteredList = ({ await close(); switchToChannelById(serverUrl, data.id); - }, [serverUrl, close, locale]); + }, [serverUrl, close, locale, teammateDisplayNameSetting]); - const onSwitchToChannel = useCallback(async (channelId: string) => { + const onSwitchToChannel = useCallback(async (c: Channel | ChannelModel) => { await close(); - switchToChannelById(serverUrl, channelId); + switchToChannelById(serverUrl, c.id); }, [serverUrl, close]); const onSwitchToThreads = useCallback(async () => { @@ -214,7 +214,7 @@ const FilteredList = ({ if (item === 'thread') { return ( ); @@ -223,27 +223,30 @@ const FilteredList = ({ return ( ); } else if ('username' in item) { return ( ); } return ( - ); diff --git a/app/screens/find_channels/filtered_list/remote_channel_item/index.ts b/app/screens/find_channels/filtered_list/remote_channel_item/index.ts deleted file mode 100644 index 63676e0620..0000000000 --- a/app/screens/find_channels/filtered_list/remote_channel_item/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; -import withObservables from '@nozbe/with-observables'; -import React from 'react'; -import {of as of$} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; - -import {observeTeam} from '@queries/servers/team'; - -import RemoteChannelItem from './remote_channel_item'; - -import type {WithDatabaseArgs} from '@typings/database/database'; - -type EnhanceProps = WithDatabaseArgs & { - channel: Channel; - showTeamName?: boolean; -} - -const enhance = withObservables(['channel', 'showTeamName'], ({channel, database, showTeamName}: EnhanceProps) => { - let teamDisplayName = of$(''); - if (channel.team_id && showTeamName) { - teamDisplayName = observeTeam(database, channel.team_id).pipe( - switchMap((team) => of$(team?.displayName || '')), - ); - } - - return { - teamDisplayName, - }; -}); - -export default React.memo(withDatabase(enhance(RemoteChannelItem))); diff --git a/app/screens/find_channels/filtered_list/remote_channel_item/remote_channel_item.tsx b/app/screens/find_channels/filtered_list/remote_channel_item/remote_channel_item.tsx deleted file mode 100644 index 7a88214f00..0000000000 --- a/app/screens/find_channels/filtered_list/remote_channel_item/remote_channel_item.tsx +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useCallback, useMemo} from 'react'; -import {Text, TouchableOpacity, View} from 'react-native'; - -import ChannelIcon from '@components/channel_icon'; -import {useTheme} from '@context/theme'; -import {useIsTablet} from '@hooks/device'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; - -type Props = { - onPress: (channelId: string, displayName: string) => void; - channel: Channel; - teamDisplayName?: string; - testID?: string; -} - -export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - flexDirection: 'row', - paddingHorizontal: 0, - minHeight: 44, - alignItems: 'center', - marginVertical: 2, - }, - wrapper: { - flex: 1, - flexDirection: 'row', - }, - text: { - marginTop: -1, - color: theme.centerChannelColor, - paddingLeft: 12, - paddingRight: 20, - ...typography('Body', 200, 'Regular'), - }, - teamName: { - color: changeOpacity(theme.centerChannelColor, 0.64), - paddingLeft: 12, - marginTop: 4, - ...typography('Body', 75), - }, - teamNameTablet: { - marginLeft: -12, - paddingLeft: 0, - marginTop: 0, - paddingBottom: 0, - top: 5, - }, -})); - -const RemoteChannelItem = ({onPress, channel, teamDisplayName, testID}: Props) => { - const theme = useTheme(); - const isTablet = useIsTablet(); - const styles = getStyleSheet(theme); - const height = (teamDisplayName && !isTablet) ? 58 : 44; - - const handleOnPress = useCallback(() => { - onPress(channel.id, channel.display_name); - }, [channel]); - - const containerStyle = useMemo(() => [ - styles.container, - {minHeight: height}, - ], - [height, styles]); - - return ( - - <> - - - 0} - name={channel.name} - shared={channel.shared} - size={24} - type={channel.type} - /> - - - {channel.display_name} - - {Boolean(teamDisplayName) && !isTablet && - - {teamDisplayName} - - } - - {Boolean(teamDisplayName) && isTablet && - - {teamDisplayName} - - } - - - - - ); -}; - -export default RemoteChannelItem; diff --git a/app/screens/find_channels/filtered_list/user_item/index.ts b/app/screens/find_channels/filtered_list/user_item/index.ts deleted file mode 100644 index 3eab004ecf..0000000000 --- a/app/screens/find_channels/filtered_list/user_item/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; -import withObservables from '@nozbe/with-observables'; -import React from 'react'; - -import {observeCurrentUserId} from '@queries/servers/system'; -import {observeTeammateNameDisplay} from '@queries/servers/user'; - -import UserItem from './user_item'; - -import type {WithDatabaseArgs} from '@typings/database/database'; -import type UserModel from '@typings/database/models/servers/user'; - -type EnhanceProps = WithDatabaseArgs & { - user: UserModel; -} - -const enhance = withObservables(['user'], ({database, user}: EnhanceProps) => ({ - currentUserId: observeCurrentUserId(database), - teammateDisplayNameSetting: observeTeammateNameDisplay(database), - user: user.observe(), -})); - -export default React.memo(withDatabase(enhance(UserItem))); diff --git a/app/screens/find_channels/filtered_list/user_item/user_item.tsx b/app/screens/find_channels/filtered_list/user_item/user_item.tsx deleted file mode 100644 index 4efcec50df..0000000000 --- a/app/screens/find_channels/filtered_list/user_item/user_item.tsx +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useCallback} from 'react'; -import {useIntl} from 'react-intl'; -import {Text, TouchableOpacity, View} from 'react-native'; - -import CustomStatus from '@components/channel_item/custom_status'; -import ProfilePicture from '@components/profile_picture'; -import {useTheme} from '@context/theme'; -import {makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; -import {displayUsername} from '@utils/user'; - -import type UserModel from '@typings/database/models/servers/user'; - -type Props = { - currentUserId: string; - onPress: (channelId: string, displayName: string) => void; - teammateDisplayNameSetting?: string; - testID?: string; - user: UserModel; -} - -export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - flexDirection: 'row', - paddingHorizontal: 0, - height: 44, - alignItems: 'center', - marginVertical: 2, - }, - wrapper: { - flex: 1, - flexDirection: 'row', - }, - text: { - marginTop: -1, - color: theme.centerChannelColor, - paddingLeft: 12, - paddingRight: 20, - ...typography('Body', 200, 'Regular'), - }, - avatar: {marginLeft: 4}, - status: { - backgroundColor: theme.centerChannelBg, - borderWidth: 0, - }, -})); - -const UserItem = ({currentUserId, onPress, teammateDisplayNameSetting, testID, user}: Props) => { - const {formatMessage, locale} = useIntl(); - const theme = useTheme(); - const styles = getStyleSheet(theme); - const isOwnDirectMessage = currentUserId === user.id; - const displayName = displayUsername(user, locale, teammateDisplayNameSetting); - const userItemTestId = `${testID}.${user.id}`; - - const handleOnPress = useCallback(() => { - onPress(user.id, displayName); - }, [user.id, displayName, onPress]); - - return ( - - <> - - - - - - - - {isOwnDirectMessage ? formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}) : displayName} - - - - - - - - ); -}; - -export default UserItem; diff --git a/app/screens/find_channels/unfiltered_list/unfiltered_list.tsx b/app/screens/find_channels/unfiltered_list/unfiltered_list.tsx index fc0ea767ae..dce037b174 100644 --- a/app/screens/find_channels/unfiltered_list/unfiltered_list.tsx +++ b/app/screens/find_channels/unfiltered_list/unfiltered_list.tsx @@ -52,9 +52,9 @@ const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, te const [sections, setSections] = useState(buildSections(recentChannels)); const sectionListStyle = useMemo(() => ({paddingBottom: keyboardHeight}), [keyboardHeight]); - const onPress = useCallback(async (channelId: string) => { + const onPress = useCallback(async (c: Channel | ChannelModel) => { await close(); - switchToChannelById(serverUrl, channelId); + switchToChannelById(serverUrl, c.id); }, [serverUrl, close]); const renderSectionHeader = useCallback(({section}: SectionListRenderItemInfo) => ( @@ -65,9 +65,10 @@ const UnfilteredList = ({close, keyboardHeight, recentChannels, showTeamName, te return ( ); diff --git a/app/screens/global_threads/threads_list/thread/thread.tsx b/app/screens/global_threads/threads_list/thread/thread.tsx index 5a4cbbf93d..a96197f588 100644 --- a/app/screens/global_threads/threads_list/thread/thread.tsx +++ b/app/screens/global_threads/threads_list/thread/thread.tsx @@ -7,10 +7,10 @@ import {Text, TouchableHighlight, View} from 'react-native'; import {switchToChannelById} from '@actions/remote/channel'; import {fetchAndSwitchToThread} from '@actions/remote/thread'; -import TouchableWithFeedback from '@app/components/touchable_with_feedback'; import FormattedText from '@components/formatted_text'; import FriendlyDate from '@components/friendly_date'; import RemoveMarkdown from '@components/remove_markdown'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; diff --git a/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap b/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap index 0c36ae6be6..9767f9a211 100644 --- a/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap +++ b/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap @@ -15,8 +15,6 @@ exports[`components/categories_list should render channels error 1`] = ` "backgroundColor": "#1e325c", "flex": 1, "maxWidth": 750, - "paddingLeft": 18, - "paddingRight": 20, "paddingTop": 10, } } @@ -33,6 +31,8 @@ exports[`components/categories_list should render channels error 1`] = ` style={ { "marginLeft": 0, + "paddingLeft": 18, + "paddingRight": 20, } } > @@ -350,8 +350,6 @@ exports[`components/categories_list should render team error 1`] = ` "backgroundColor": "#1e325c", "flex": 1, "maxWidth": 750, - "paddingLeft": 18, - "paddingRight": 20, "paddingTop": 10, } } @@ -368,6 +366,8 @@ exports[`components/categories_list should render team error 1`] = ` style={ { "marginLeft": 0, + "paddingLeft": 18, + "paddingRight": 20, } } > @@ -467,6 +467,8 @@ exports[`components/categories_list should render team error 1`] = ` style={ { "flexDirection": "row", + "paddingLeft": 18, + "paddingRight": 20, } } > diff --git a/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx b/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx index 89d11cdc8e..cd4bd15be2 100644 --- a/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx +++ b/app/screens/home/channel_list/categories_list/categories/body/category_body.tsx @@ -7,6 +7,7 @@ import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 're import {fetchDirectChannelsInfo} from '@actions/remote/channel'; import ChannelItem from '@components/channel_item'; +import {ROW_HEIGHT as CHANNEL_ROW_HEIGHT} from '@components/channel_item/channel_item'; import {useServerUrl} from '@context/server'; import {isDMorGM} from '@utils/channel'; @@ -16,7 +17,7 @@ import type ChannelModel from '@typings/database/models/servers/channel'; type Props = { sortedChannels: ChannelModel[]; category: CategoryModel; - onChannelSwitch: (channelId: string) => void; + onChannelSwitch: (channel: Channel | ChannelModel) => void; unreadIds: Set; unreadsOnTop: boolean; }; @@ -46,6 +47,9 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan onPress={onChannelSwitch} key={item.id} testID={`channel_list.category.${category.displayName.replace(/ /g, '_').toLocaleLowerCase()}.channel_item`} + shouldHighlightActive={true} + shouldHighlightState={true} + isOnHome={true} /> ); }, [onChannelSwitch]); @@ -62,8 +66,8 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan } }, [directChannels.length]); - const height = ids.length ? ids.length * 40 : 0; - const unreadHeight = unreadChannels.length ? unreadChannels.length * 40 : 0; + const height = ids.length ? ids.length * CHANNEL_ROW_HEIGHT : 0; + const unreadHeight = unreadChannels.length ? unreadChannels.length * CHANNEL_ROW_HEIGHT : 0; const animatedStyle = useAnimatedStyle(() => { const opacity = unreadHeight > 0 ? 1 : 0; @@ -74,7 +78,7 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan }; }, [height, unreadHeight]); - const listHeight = useMemo(() => ({ + const listStyle = useMemo(() => ({ height: category.collapsed ? unreadHeight : height, }), [category.collapsed, height, unreadHeight]); @@ -87,7 +91,7 @@ const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChan // @ts-expect-error strictMode not exposed on the types strictMode={true} - style={listHeight} + style={listStyle} /> ); diff --git a/app/screens/home/channel_list/categories_list/categories/categories.tsx b/app/screens/home/channel_list/categories_list/categories/categories.tsx index a347b35aee..b6ecbc01ca 100644 --- a/app/screens/home/channel_list/categories_list/categories/categories.tsx +++ b/app/screens/home/channel_list/categories_list/categories/categories.tsx @@ -17,6 +17,7 @@ import CategoryHeader from './header'; import UnreadCategories from './unreads'; import type CategoryModel from '@typings/database/models/servers/category'; +import type ChannelModel from '@typings/database/models/servers/channel'; type Props = { categories: CategoryModel[]; @@ -27,8 +28,6 @@ type Props = { const styles = StyleSheet.create({ mainList: { flex: 1, - marginLeft: -18, - marginRight: -20, }, loadingView: { alignItems: 'center', @@ -39,7 +38,11 @@ const styles = StyleSheet.create({ const extractKey = (item: CategoryModel | 'UNREADS') => (item === 'UNREADS' ? 'UNREADS' : item.id); -const Categories = ({categories, onlyUnreads, unreadsOnTop}: Props) => { +const Categories = ({ + categories, + onlyUnreads, + unreadsOnTop, +}: Props) => { const intl = useIntl(); const listRef = useRef(null); const serverUrl = useServerUrl(); @@ -60,8 +63,8 @@ const Categories = ({categories, onlyUnreads, unreadsOnTop}: Props) => { const [initiaLoad, setInitialLoad] = useState(!categoriesToShow.length); - const onChannelSwitch = useCallback(async (channelId: string) => { - switchToChannelById(serverUrl, channelId); + const onChannelSwitch = useCallback(async (c: Channel | ChannelModel) => { + switchToChannelById(serverUrl, c.id); }, [serverUrl]); const renderCategory = useCallback((data: {item: CategoryModel | 'UNREADS'}) => { diff --git a/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx b/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx index 106ef596c8..e55249cdf1 100644 --- a/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx +++ b/app/screens/home/channel_list/categories_list/categories/unreads/unreads.tsx @@ -1,11 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; import {FlatList, Text} from 'react-native'; import ChannelItem from '@components/channel_item'; +import {HOME_PADDING} from '@constants/view'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -25,14 +26,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ color: changeOpacity(theme.sidebarText, 0.64), ...typography('Heading', 75), textTransform: 'uppercase', - paddingLeft: 18, paddingVertical: 8, marginTop: 12, + ...HOME_PADDING, }, })); type UnreadCategoriesProps = { - onChannelSwitch: (channelId: string) => void; + onChannelSwitch: (channel: Channel | ChannelModel) => void; onlyUnreads: boolean; unreadChannels: ChannelModel[]; unreadThreads: {unreads: boolean; mentions: number}; @@ -52,11 +53,20 @@ const UnreadCategories = ({onChannelSwitch, onlyUnreads, unreadChannels, unreadT channel={item} onPress={onChannelSwitch} testID='channel_list.category.unreads.channel_item' + shouldHighlightActive={true} + shouldHighlightState={true} + isOnHome={true} /> ); }, [onChannelSwitch]); const showEmptyState = onlyUnreads && !unreadChannels.length; + const containerStyle = useMemo(() => { + return [ + showEmptyState && !isTablet && styles.empty, + ]; + }, [styles, showEmptyState, isTablet]); + const showTitle = !onlyUnreads || (onlyUnreads && !showEmptyState); const EmptyState = showEmptyState && !isTablet ? ( @@ -76,7 +86,7 @@ const UnreadCategories = ({onChannelSwitch, onlyUnreads, unreadChannels, unreadT } diff --git a/app/screens/home/channel_list/categories_list/header/header.tsx b/app/screens/home/channel_list/categories_list/header/header.tsx index ebb4fc5aa0..20a1460faa 100644 --- a/app/screens/home/channel_list/categories_list/header/header.tsx +++ b/app/screens/home/channel_list/categories_list/header/header.tsx @@ -12,6 +12,7 @@ import CompassIcon from '@components/compass_icon'; import {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; +import {HOME_PADDING} from '@constants/view'; import {useServerDisplayName, useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; @@ -266,7 +267,7 @@ const ChannelListHeader = ({ } return ( - + {header} ); diff --git a/app/screens/home/channel_list/categories_list/index.tsx b/app/screens/home/channel_list/categories_list/index.tsx index 7287ce050e..0778c5b564 100644 --- a/app/screens/home/channel_list/categories_list/index.tsx +++ b/app/screens/home/channel_list/categories_list/index.tsx @@ -20,8 +20,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { flex: 1, backgroundColor: theme.sidebarBg, - paddingLeft: 18, - paddingRight: 20, paddingTop: 10, }, })); @@ -68,7 +66,12 @@ const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: C return ( <> - {isCRTEnabled && } + {isCRTEnabled && + + } ); @@ -76,9 +79,7 @@ const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: C return ( - + {content} ); diff --git a/app/screens/home/channel_list/categories_list/subheader/subheader.tsx b/app/screens/home/channel_list/categories_list/subheader/subheader.tsx index 46a6d9ffcb..62746a80fa 100644 --- a/app/screens/home/channel_list/categories_list/subheader/subheader.tsx +++ b/app/screens/home/channel_list/categories_list/subheader/subheader.tsx @@ -5,6 +5,8 @@ import React, {useEffect} from 'react'; import {StyleSheet, View} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import {HOME_PADDING} from '@constants/view'; + import SearchField from './search_field'; import UnreadFilter from './unread_filter'; @@ -15,6 +17,7 @@ type Props = { const style = StyleSheet.create({ container: { flexDirection: 'row', + ...HOME_PADDING, }, }); diff --git a/app/screens/integration_selector/integration_selector.tsx b/app/screens/integration_selector/integration_selector.tsx index 0b16b8c9c4..434bf0d892 100644 --- a/app/screens/integration_selector/integration_selector.tsx +++ b/app/screens/integration_selector/integration_selector.tsx @@ -118,7 +118,6 @@ export type Props = { isMultiselect?: boolean; selected: SelectedDialogValue; theme: Theme; - teammateNameDisplay: string; componentId: AvailableScreens; } @@ -165,7 +164,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { function IntegrationSelector( {dataSource, data, isMultiselect = false, selected, handleSelect, - currentTeamId, currentUserId, componentId, getDynamicOptions, options, teammateNameDisplay}: Props) { + currentTeamId, currentUserId, componentId, getDynamicOptions, options}: Props) { const serverUrl = useServerUrl(); const theme = useTheme(); const searchTimeoutId = useRef(null); @@ -554,7 +553,6 @@ function IntegrationSelector( return ( { export type EmailInvite = string; -export type SearchResult = UserProfile|EmailInvite; +export type SearchResult = UserProfile|UserModel|EmailInvite; export type InviteResult = { userId: string; diff --git a/app/screens/invite/selection.tsx b/app/screens/invite/selection.tsx index b31bccbd67..7eaa262079 100644 --- a/app/screens/invite/selection.tsx +++ b/app/screens/invite/selection.tsx @@ -70,6 +70,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }, searchListPadding: { paddingVertical: 8, + flex: 1, }, searchListShadow: { shadowColor: '#000', @@ -85,6 +86,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { searchListFlatList: { backgroundColor: theme.centerChannelBg, borderRadius: 4, + paddingHorizontal: 16, }, selectedItems: { display: 'flex', @@ -222,12 +224,12 @@ export default function Selection({ style.push(styles.searchListFlatList); - if (searchResults.length) { + if (searchResults.length || (term && !loading)) { style.push(styles.searchListBorder, styles.searchListPadding); } return style; - }, [searchResults, styles]); + }, [searchResults, styles, Boolean(term && !loading)]); const renderNoResults = useCallback(() => { if (!term || loading) { @@ -235,20 +237,18 @@ export default function Selection({ } return ( - - - + ); }, [term, loading]); const renderItem = useCallback(({item}: ListRenderItemInfo) => { const key = keyExtractor(item); - return ( + return typeof item === 'string' ? ( - {typeof item === 'string' ? ( - - ) : ( - - )} + + ) : ( + ); }, [searchResults, onSelectItem]); diff --git a/app/screens/invite/summary_report.tsx b/app/screens/invite/summary_report.tsx index 874e002678..8819ca6b2b 100644 --- a/app/screens/invite/summary_report.tsx +++ b/app/screens/invite/summary_report.tsx @@ -46,11 +46,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { flexDirection: 'column', paddingVertical: 12, }, - user: { - paddingTop: 0, - paddingBottom: 0, - height: 'auto', - }, reason: { paddingLeft: 56, paddingRight: 20, @@ -135,7 +130,6 @@ export default function SummaryReport({ ) : ( )} diff --git a/app/screens/invite/text_item.tsx b/app/screens/invite/text_item.tsx index 9a14e3ae1c..593d092f2c 100644 --- a/app/screens/invite/text_item.tsx +++ b/app/screens/invite/text_item.tsx @@ -13,7 +13,6 @@ import {typography} from '@utils/typography'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { item: { - paddingHorizontal: 20, display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -21,7 +20,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { search: { height: 40, paddingVertical: 8, - paddingHorizontal: 16, }, itemText: { display: 'flex', diff --git a/app/screens/manage_channel_members/index.tsx b/app/screens/manage_channel_members/index.tsx index ae1d739069..d1b7dca7a1 100644 --- a/app/screens/manage_channel_members/index.tsx +++ b/app/screens/manage_channel_members/index.tsx @@ -10,7 +10,7 @@ import {observeTutorialWatched} from '@queries/app/global'; import {observeCurrentChannel} from '@queries/servers/channel'; import {observeCanManageChannelMembers, observePermissionForChannel} from '@queries/servers/role'; import {observeCurrentChannelId, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system'; -import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user'; +import {observeCurrentUser} from '@queries/servers/user'; import ManageChannelMembers from './manage_channel_members'; @@ -31,7 +31,6 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { currentUserId: observeCurrentUserId(database), currentTeamId: observeCurrentTeamId(database), canManageAndRemoveMembers, - teammateNameDisplay: observeTeammateNameDisplay(database), tutorialWatched: observeTutorialWatched(Tutorial.PROFILE_LONG_PRESS), canChangeMemberRoles, }; diff --git a/app/screens/manage_channel_members/manage_channel_members.tsx b/app/screens/manage_channel_members/manage_channel_members.tsx index 920bbf53ff..4cf7c8d22a 100644 --- a/app/screens/manage_channel_members/manage_channel_members.tsx +++ b/app/screens/manage_channel_members/manage_channel_members.tsx @@ -30,7 +30,6 @@ type Props = { componentId: AvailableScreens; currentTeamId: string; currentUserId: string; - teammateNameDisplay: string; tutorialWatched: boolean; } @@ -69,7 +68,6 @@ export default function ManageChannelMembers({ componentId, currentTeamId, currentUserId, - teammateNameDisplay, tutorialWatched, }: Props) { const serverUrl = useServerUrl(); @@ -271,7 +269,6 @@ export default function ManageChannelMembers({ selectedIds={EMPTY_IDS} showManageMode={canManageAndRemoveMembers && isManageMode} showNoResults={!loading} - teammateNameDisplay={teammateNameDisplay} term={term} testID='manage_members.user_list' tutorialWatched={tutorialWatched} diff --git a/app/screens/reactions/reactors_list/reactor/reactor.tsx b/app/screens/reactions/reactors_list/reactor/reactor.tsx index 61b2582bc6..7b1f733ac3 100644 --- a/app/screens/reactions/reactors_list/reactor/reactor.tsx +++ b/app/screens/reactions/reactors_list/reactor/reactor.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {Keyboard, StyleSheet, TouchableOpacity} from 'react-native'; +import {Keyboard} from 'react-native'; import UserItem from '@components/user_item'; import {Screens} from '@constants'; @@ -15,20 +15,9 @@ import type UserModel from '@typings/database/models/servers/user'; type Props = { channelId: string; location: string; - user?: UserModel; + user: UserModel; } -const style = StyleSheet.create({ - container: { - marginBottom: 8, - paddingLeft: 0, - flexDirection: 'row', - }, - picture: { - marginLeft: 0, - }, -}); - const Reactor = ({channelId, location, user}: Props) => { const intl = useIntl(); const theme = useTheme(); @@ -46,14 +35,11 @@ const Reactor = ({channelId, location, user}: Props) => { }; return ( - - - + ); }; diff --git a/app/screens/user_profile/custom_status.tsx b/app/screens/user_profile/custom_status.tsx index 5f6f82a33a..51e420e037 100644 --- a/app/screens/user_profile/custom_status.tsx +++ b/app/screens/user_profile/custom_status.tsx @@ -74,7 +74,6 @@ const UserProfileCustomStatus = ({customStatus}: Props) => { } diff --git a/app/utils/strings.ts b/app/utils/strings.ts new file mode 100644 index 0000000000..610ba37718 --- /dev/null +++ b/app/utils/strings.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export function nonBreakingString(s: string) { + return s.replace(' ', '\xa0'); +} diff --git a/types/components/emoji.ts b/types/components/emoji.ts index 8daf4074f7..77cc747495 100644 --- a/types/components/emoji.ts +++ b/types/components/emoji.ts @@ -5,13 +5,18 @@ import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji import type {StyleProp, TextStyle} from 'react-native'; import type {ImageStyle} from 'react-native-fast-image'; +// The intersection of the image styles and text styles +type ImageStyleUniques = Omit +export type EmojiCommonStyle = Omit + export type EmojiProps = { emojiName: string; displayTextOnly?: boolean; literal?: string; size?: number; textStyle?: StyleProp; - customEmojiStyle?: StyleProp; + imageStyle?: StyleProp; + commonStyle?: StyleProp; customEmojis: CustomEmojiModel[]; testID?: string; }