From a60d3eeb9231975df6582fe9fcaf5fd83f4158ea Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Thu, 29 Apr 2021 15:18:48 +0200 Subject: [PATCH] MM-34407 Use Avatars for Sidebar DMs (#5334) (#5358) * MM-34407 Use Avatars for Sidebar DMs * Update app/components/channel_icon.js Co-authored-by: Joseph Baylon * UI feedback * Align the private channel icon Co-authored-by: Joseph Baylon (cherry picked from commit 1b36529853b2c0f75d44e47153b51e63ae2c0e9d) Co-authored-by: Elias Nahum --- app/components/channel_icon.js | 236 ------------------ app/components/channel_icon.tsx | 184 ++++++++++++++ .../profile_picture/profile_picture.js | 221 ---------------- .../profile_picture/profile_picture.tsx | 216 ++++++++++++++++ .../__snapshots__/channel_item.test.js.snap | 101 +++++--- .../channel_item/channel_item.js | 21 +- .../main/channels_list/channel_item/index.js | 8 +- .../filtered_list/filtered_list.js | 14 +- .../list/__snapshots__/list.test.js.snap | 2 +- .../sidebars/main/channels_list/list/list.js | 2 +- .../__snapshots__/user_status.test.js.snap | 2 +- app/components/user_status/user_status.js | 57 ----- app/components/user_status/user_status.tsx | 54 ++++ app/hooks/did_update.ts | 18 ++ app/hooks/index.ts | 4 + .../__snapshots__/channel_info.test.js.snap | 2 - .../channel_info_header.test.js.snap | 24 +- app/screens/channel_info/channel_info.js | 10 +- app/screens/channel_info/channel_info.test.js | 2 - .../channel_info/channel_info_header.js | 36 ++- app/screens/channel_info/index.js | 17 +- babel.config.js | 1 + tsconfig.json | 2 + 23 files changed, 610 insertions(+), 624 deletions(-) delete mode 100644 app/components/channel_icon.js create mode 100644 app/components/channel_icon.tsx delete mode 100644 app/components/profile_picture/profile_picture.js create mode 100644 app/components/profile_picture/profile_picture.tsx delete mode 100644 app/components/user_status/user_status.js create mode 100644 app/components/user_status/user_status.tsx create mode 100644 app/hooks/did_update.ts create mode 100644 app/hooks/index.ts diff --git a/app/components/channel_icon.js b/app/components/channel_icon.js deleted file mode 100644 index e7d26299c..000000000 --- a/app/components/channel_icon.js +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Text, - View, -} from 'react-native'; - -import {General} from '@mm-redux/constants'; - -import CompassIcon from '@components/compass_icon'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; - -export default class ChannelIcon extends React.PureComponent { - static propTypes = { - isActive: PropTypes.bool, - isInfo: PropTypes.bool, - isUnread: PropTypes.bool, - hasDraft: PropTypes.bool, - membersCount: PropTypes.number, - size: PropTypes.number, - status: PropTypes.string, - theme: PropTypes.object.isRequired, - type: PropTypes.string.isRequired, - isArchived: PropTypes.bool.isRequired, - isBot: PropTypes.bool.isRequired, - testID: PropTypes.string, - }; - - static defaultProps = { - isActive: false, - isInfo: false, - isUnread: false, - size: 12, - }; - - render() { - const { - isActive, - isUnread, - isInfo, - hasDraft, - membersCount, - size, - status, - theme, - type, - isArchived, - isBot, - testID, - } = this.props; - const style = getStyleSheet(theme); - - let activeIcon; - let unreadIcon; - let activeGroupBox; - let unreadGroupBox; - let activeGroup; - let unreadGroup; - let offlineColor = changeOpacity(theme.sidebarText, 0.5); - - if (isUnread) { - unreadIcon = style.iconUnread; - unreadGroupBox = style.groupBoxUnread; - unreadGroup = style.groupUnread; - } - - if (isActive) { - activeIcon = style.iconActive; - activeGroupBox = style.groupBoxActive; - activeGroup = style.groupActive; - } - - if (isInfo) { - activeIcon = style.iconInfo; - activeGroupBox = style.groupBoxInfo; - activeGroup = style.groupInfo; - offlineColor = changeOpacity(theme.centerChannelColor, 0.5); - } - - let icon; - if (isArchived) { - icon = ( - - ); - } else if (isBot) { - icon = ( - - ); - } else if (hasDraft) { - icon = ( - - ); - } else if (type === General.OPEN_CHANNEL) { - icon = ( - - ); - } else if (type === General.PRIVATE_CHANNEL) { - icon = ( - - ); - } else if (type === General.GM_CHANNEL) { - const fontSize = (size - 10); - icon = ( - - - {membersCount} - - - ); - } else if (type === General.DM_CHANNEL) { - switch (status) { - case General.AWAY: - icon = ( - - ); - break; - case General.DND: - icon = ( - - ); - break; - case General.ONLINE: - icon = ( - - ); - break; - default: - icon = ( - - ); - break; - } - } - - return ( - - {icon} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - marginRight: 8, - alignItems: 'center', - }, - icon: { - color: changeOpacity(theme.sidebarText, 0.4), - }, - iconActive: { - color: theme.sidebarTextActiveColor, - }, - iconUnread: { - color: theme.sidebarUnreadText, - }, - iconInfo: { - color: theme.centerChannelColor, - }, - groupBox: { - alignSelf: 'flex-start', - alignItems: 'center', - backgroundColor: changeOpacity(theme.sidebarText, 0.3), - borderColor: changeOpacity(theme.sidebarText, 0.3), - borderWidth: 1, - borderRadius: 2, - justifyContent: 'center', - }, - groupBoxActive: { - backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3), - }, - groupBoxUnread: { - backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3), - }, - groupBoxInfo: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.3), - }, - group: { - color: changeOpacity(theme.sidebarText, 0.6), - fontSize: 10, - fontWeight: '600', - }, - groupActive: { - color: theme.sidebarTextActiveColor, - }, - groupUnread: { - color: theme.sidebarUnreadText, - }, - groupInfo: { - color: theme.centerChannelColor, - }, - }; -}); diff --git a/app/components/channel_icon.tsx b/app/components/channel_icon.tsx new file mode 100644 index 000000000..1a9b63379 --- /dev/null +++ b/app/components/channel_icon.tsx @@ -0,0 +1,184 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleProp, Text, View, ViewStyle} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; +import ProfilePicture from '@components/profile_picture'; +import {General} from '@mm-redux/constants'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import type {Theme} from '@mm-redux/types/preferences'; + +type Props = { + hasDraft: boolean; + isActive: boolean; + isArchived: boolean; + isInfo: boolean; + isUnread: boolean; + membersCount: number; + size: number; + statusStyle?: StyleProp; + testID?: string; + theme: Theme; + type: string; + userId?: string; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + alignItems: 'center', + justifyContent: 'center', + }, + icon: { + color: changeOpacity(theme.sidebarText, 0.4), + }, + iconActive: { + color: theme.sidebarTextActiveColor, + }, + iconUnread: { + color: theme.sidebarUnreadText, + }, + iconInfo: { + color: theme.centerChannelColor, + }, + groupBox: { + alignItems: 'center', + backgroundColor: changeOpacity(theme.sidebarText, 0.16), + borderRadius: 4, + justifyContent: 'center', + }, + groupBoxActive: { + backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3), + }, + groupBoxUnread: { + backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3), + }, + groupBoxInfo: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.3), + }, + group: { + color: theme.sidebarText, + fontSize: 10, + fontWeight: '600', + }, + groupActive: { + color: theme.sidebarTextActiveColor, + }, + groupUnread: { + color: theme.sidebarUnreadText, + }, + groupInfo: { + color: theme.centerChannelColor, + }, + }; +}); + +const ChannelIcon = (props: Props) => { + const style = getStyleSheet(props.theme); + + let activeIcon; + let unreadIcon; + let activeGroupBox; + let unreadGroupBox; + let activeGroup; + let unreadGroup; + + if (props.isUnread) { + unreadIcon = style.iconUnread; + unreadGroupBox = style.groupBoxUnread; + unreadGroup = style.groupUnread; + } + + if (props.isActive) { + activeIcon = style.iconActive; + activeGroupBox = style.groupBoxActive; + activeGroup = style.groupActive; + } + + if (props.isInfo) { + activeIcon = style.iconInfo; + activeGroupBox = style.groupBoxInfo; + activeGroup = style.groupInfo; + } + + let icon; + let extraStyle; + if (props.isArchived) { + icon = ( + + ); + } else if (props.hasDraft) { + icon = ( + + ); + } else if (props.type === General.OPEN_CHANNEL) { + icon = ( + + ); + } else if (props.type === General.PRIVATE_CHANNEL) { + icon = ( + + ); + } else if (props.type === General.GM_CHANNEL) { + const fontSize = (props.size - 12); + const boxSize = (props.size - 4); + icon = ( + + + {props.membersCount} + + + ); + } else if (props.type === General.DM_CHANNEL) { + // extraStyle = {marginRight: 6}; + icon = ( + + ); + } + + return ( + + {icon} + + ); +}; + +ChannelIcon.defaultProps = { + hasDraft: false, + isActive: false, + isArchived: false, + isInfo: false, + isUnread: false, + membersCount: 0, + size: 12, +}; + +export default ChannelIcon; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js deleted file mode 100644 index 7fc2fe53b..000000000 --- a/app/components/profile_picture/profile_picture.js +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {Platform, View} from 'react-native'; -import FastImage from 'react-native-fast-image'; - -import {Client4} from '@client/rest'; - -import CompassIcon from '@components/compass_icon'; -import UserStatus from '@components/user_status'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; - -const STATUS_BUFFER = Platform.select({ - ios: 3, - android: 2, -}); - -export default class ProfilePicture extends PureComponent { - static propTypes = { - isCurrentUser: PropTypes.bool, - size: PropTypes.number, - statusSize: PropTypes.number, - iconSize: PropTypes.number, - user: PropTypes.object, - userId: PropTypes.string, - showStatus: PropTypes.bool, - status: PropTypes.string, - edit: PropTypes.bool, - imageUri: PropTypes.string, - profileImageUri: PropTypes.string, - profileImageRemove: PropTypes.bool, - theme: PropTypes.object, - testID: PropTypes.string, - actions: PropTypes.shape({ - getStatusForId: PropTypes.func.isRequired, - setProfileImageUri: PropTypes.func.isRequired, - }), - }; - - static defaultProps = { - showStatus: true, - size: 128, - statusSize: 14, - edit: false, - }; - - state = { - pictureUrl: null, - }; - - componentDidMount() { - const {actions, edit, imageUri, profileImageUri, user, status} = this.props; - this.mounted = true; - - if (!status && user) { - actions.getStatusForId(user.id); - } - - if (profileImageUri) { - this.setImageURL(profileImageUri); - } else if (edit && imageUri) { - this.setImageURL(imageUri); - } else if (user) { - const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update); - - this.setImageURL(uri); - this.clearProfileImageUri(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - setImageURL = (pictureUrl) => { - if (this.mounted) { - this.setState({pictureUrl}); - } - }; - - clearProfileImageUri = () => { - if (this.props.isCurrentUser && this.props.profileImageUri !== '') { - this.props.actions.setProfileImageUri(''); - } - } - - componentDidUpdate(prevProps) { - if (this.props.profileImageRemove !== prevProps.profileImageRemove) { - this.setImageURL(null); - } else if (this.mounted) { - if (this.props.edit && this.props.imageUri && this.props.imageUri !== prevProps.imageUri) { - this.setImageURL(this.props.imageUri); - return; - } - - if (this.props.profileImageUri !== '' && this.props.profileImageUri !== prevProps.profileImageUri) { - this.setImageURL(this.props.profileImageUri); - } - - const url = prevProps.user ? Client4.getProfilePictureUrl(prevProps.user.id, prevProps.user.last_picture_update) : null; - const nextUrl = this.props.user ? Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update) : null; - - if (nextUrl && url !== nextUrl) { - // empty function is so that promise unhandled is not triggered in dev mode - this.setImageURL(nextUrl); - this.clearProfileImageUri(); - } - } - } - - render() { - const {edit, showStatus, theme, user, size, testID} = this.props; - const {pictureUrl} = this.state; - const style = getStyleSheet(theme); - - let statusIcon; - let statusStyle; - let containerStyle = { - width: size + STATUS_BUFFER, - height: size + STATUS_BUFFER, - }; - if (edit) { - const iconColor = changeOpacity(theme.centerChannelColor, 0.6); - statusStyle = { - width: this.props.statusSize, - height: this.props.statusSize, - backgroundColor: theme.centerChannelBg, - }; - statusIcon = ( - - ); - } else if (this.props.status && !edit) { - statusIcon = ( - - ); - } - - let source = null; - let image; - if (pictureUrl) { - let prefix = ''; - if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') && - !pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) { - prefix = 'file://'; - } - - source = { - uri: `${prefix}${pictureUrl}`, - }; - - image = ( - - ); - } else { - containerStyle = { - width: size + (STATUS_BUFFER - 1), - height: size + (STATUS_BUFFER - 1), - backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), - }; - image = ( - - ); - } - - return ( - - {image} - {(showStatus || edit) && (user && !user.is_bot) && - - {statusIcon} - - } - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - justifyContent: 'center', - alignItems: 'center', - borderRadius: 80, - }, - icon: { - color: changeOpacity(theme.centerChannelColor, 0.48), - }, - statusWrapper: { - position: 'absolute', - bottom: 0, - right: 0, - overflow: 'hidden', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.centerChannelBg, - }, - status: { - color: theme.centerChannelBg, - }, - }; -}); diff --git a/app/components/profile_picture/profile_picture.tsx b/app/components/profile_picture/profile_picture.tsx new file mode 100644 index 000000000..a23af0cd3 --- /dev/null +++ b/app/components/profile_picture/profile_picture.tsx @@ -0,0 +1,216 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useEffect, useState} from 'react'; +import {Platform, StyleProp, View, ViewProps, ViewStyle} from 'react-native'; +import FastImage from 'react-native-fast-image'; + +import {Client4} from '@client/rest'; +import CompassIcon from '@components/compass_icon'; +import UserStatus from '@components/user_status'; +import {useDidUpdate} from '@hooks'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import type {Theme} from '@mm-redux/types/preferences'; +import type {UserProfile} from '@mm-redux/types/users'; + +const STATUS_BUFFER = Platform.select({ + ios: 3, + android: 2, +}); + +type ProfilePictureProps = { + actions: { + setProfileImageUri: (imageUri?: string) => void; + getStatusForId: (id: string) => void; + }; + edit: boolean; + iconSize?: number; + imageUri?: string; + isCurrentUser: boolean; + profileImageRemove?: boolean; + profileImageUri?: string; + showStatus: boolean; + size: number; + status?: string; + statusSize: number; + statusStyle?: StyleProp | any; + user?: UserProfile; + userId: string; + theme: Theme; + testID?: string; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + justifyContent: 'center', + alignItems: 'center', + borderRadius: 80, + }, + icon: { + color: changeOpacity(theme.centerChannelColor, 0.48), + }, + statusWrapper: { + position: 'absolute', + bottom: 0, + right: 0, + overflow: 'hidden', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.centerChannelBg, + borderWidth: 1, + borderColor: theme.centerChannelBg, + }, + status: { + color: theme.centerChannelBg, + }, + }; +}); + +const ProfilePicture = (props: ProfilePictureProps) => { + const [pictureUrl, setPictureUrl] = useState(); + const style = getStyleSheet(props.theme); + const buffer = STATUS_BUFFER || 0; + + const clearProfileImageUri = () => { + if (props.isCurrentUser && props.profileImageUri !== '') { + props.actions.setProfileImageUri(''); + } + }; + + useEffect(() => { + const {edit, imageUri, profileImageUri, user, status} = props; + const {getStatusForId} = props.actions; + + if (!status && user) { + getStatusForId(user.id); + } + + if (profileImageUri) { + setPictureUrl(profileImageUri); + } else if (edit && imageUri) { + setPictureUrl(imageUri); + } else if (user) { + const uri = Client4.getProfilePictureUrl(user.id, user.last_picture_update); + + setPictureUrl(uri); + clearProfileImageUri(); + } + }, []); + + useDidUpdate(() => { + setPictureUrl(undefined); + }, [props.profileImageRemove]); + + useDidUpdate(() => { + const {edit, imageUri} = props; + if (edit && imageUri) { + setPictureUrl(imageUri); + } + }, [props.edit, props.imageUri]); + + useDidUpdate(() => { + const {profileImageUri} = props; + if (profileImageUri) { + setPictureUrl(profileImageUri); + } + }, [props.profileImageUri]); + + useDidUpdate(() => { + const {user} = props; + + const url = user ? Client4.getProfilePictureUrl(user.id, user.last_picture_update) : undefined; + if (url !== pictureUrl) { + setPictureUrl(url); + } + }, [props.user]); + + let statusIcon; + let statusStyle = props.statusStyle; + let containerStyle: StyleProp = { + width: props.size + buffer, + height: props.size + buffer, + }; + + if (props.edit) { + const iconColor = changeOpacity(props.theme.centerChannelColor, 0.6); + statusStyle = { + width: props.statusSize, + height: props.statusSize, + backgroundColor: props.theme.centerChannelBg, + }; + statusIcon = ( + + ); + } else if (props.status && !props.edit) { + statusIcon = ( + + ); + } + + let source = null; + let image; + if (pictureUrl) { + let prefix = ''; + if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') && + !pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) { + prefix = 'file://'; + } + + source = { + uri: `${prefix}${pictureUrl}`, + }; + + image = ( + + ); + } else { + containerStyle = { + width: props.size + (buffer - 1), + height: props.size + (buffer - 1), + backgroundColor: changeOpacity(props.theme.centerChannelColor, 0.08), + }; + image = ( + + ); + } + + return ( + + {image} + {(props.showStatus || props.edit) && (!props.user?.is_bot) && + + {statusIcon} + + } + + ); +}; + +ProfilePicture.defaultProps = { + showStatus: true, + size: 128, + statusSize: 14, + edit: false, +}; + +export default ProfilePicture; diff --git a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap index 83694769d..bcecf7e09 100644 --- a/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap +++ b/app/components/sidebars/main/channels_list/channel_item/__snapshots__/channel_item.test.js.snap @@ -37,12 +37,16 @@ exports[`ChannelItem should match snapshot 1`] = ` hasDraft={false} isActive={false} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -87,6 +91,7 @@ exports[`ChannelItem should match snapshot 1`] = ` "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -153,12 +158,16 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you hasDraft={false} isActive={true} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -203,6 +212,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -213,7 +223,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you } testID="main.sidebar.channels_list.list.channel_item.display_name" > - {displayname} (you) + display_name @@ -268,12 +278,16 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you hasDraft={false} isActive={true} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -318,6 +332,7 @@ exports[`ChannelItem should match snapshot for current user i.e currentUser (you "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -383,12 +398,16 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh hasDraft={false} isActive={true} isArchived={true} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -433,6 +452,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is currentCh "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -487,12 +507,16 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes hasDraft={false} isActive={false} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -537,6 +561,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -592,12 +617,16 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe hasDraft={false} isActive={false} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -642,6 +671,7 @@ exports[`ChannelItem should match snapshot for deactivated user and not searchRe "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -697,12 +727,16 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = ` hasDraft={false} isActive={false} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -747,6 +781,7 @@ exports[`ChannelItem should match snapshot for isManualUnread 1`] = ` "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -806,12 +841,16 @@ exports[`ChannelItem should match snapshot with draft 1`] = ` hasDraft={true} isActive={false} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -856,6 +895,7 @@ exports[`ChannelItem should match snapshot with draft 1`] = ` "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, @@ -913,12 +953,16 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = ` hasDraft={false} isActive={false} isArchived={false} - isBot={false} isInfo={false} isUnread={true} membersCount={1} - size={16} - status="online" + size={24} + statusStyle={ + Object { + "backgroundColor": "#145dbf", + "borderColor": "transparent", + } + } testID="main.sidebar.channels_list.list.channel_item.channel_icon" theme={ Object { @@ -963,6 +1007,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = ` "fontFamily": "Open Sans", "fontSize": 16, "lineHeight": 24, + "marginLeft": 13, "maxWidth": "80%", "paddingRight": 10, }, diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js index 07a832129..93c5b1693 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.js @@ -10,11 +10,11 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; +import Badge from '@components/badge'; +import ChannelIcon from '@components/channel_icon'; import {General} from '@mm-redux/constants'; -import Badge from 'app/components/badge'; -import ChannelIcon from 'app/components/channel_icon'; -import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; export default class ChannelItem extends PureComponent { static propTypes = { @@ -33,10 +33,10 @@ export default class ChannelItem extends PureComponent { onSelectChannel: PropTypes.func.isRequired, shouldHideChannel: PropTypes.bool, showUnreadForMsgs: PropTypes.bool.isRequired, + teammateId: PropTypes.string, theme: PropTypes.object.isRequired, unreadMsgs: PropTypes.number.isRequired, isSearchResult: PropTypes.bool, - isBot: PropTypes.bool.isRequired, }; static defaultProps = { @@ -77,7 +77,7 @@ export default class ChannelItem extends PureComponent { theme, isSearchResult, channel, - isBot, + teammateId, } = this.props; // Only ever show an archived channel if it's the currently viewed channel. @@ -103,7 +103,7 @@ export default class ChannelItem extends PureComponent { if (isSearchResult) { isCurrenUser = channel.id === currentUserId; } else { - isCurrenUser = channel.teammate_id === currentUserId; + isCurrenUser = teammateId === currentUserId; } } if (isCurrenUser) { @@ -161,13 +161,13 @@ export default class ChannelItem extends PureComponent { isUnread={isUnread} hasDraft={hasDraft && channelId !== currentChannelId} membersCount={displayName.split(',').length} - size={16} - status={channel.status} + statusStyle={{backgroundColor: theme.sidebarBg, borderColor: 'transparent'}} + size={24} theme={theme} type={channel.type} isArchived={isArchived} - isBot={isBot} testID={`${testID}.channel_icon`} + userId={teammateId} /> ); @@ -231,6 +231,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { fontSize: 16, lineHeight: 24, paddingRight: 10, + marginLeft: 13, maxWidth: '80%', flex: 1, alignSelf: 'center', diff --git a/app/components/sidebars/main/channels_list/channel_item/index.js b/app/components/sidebars/main/channels_list/channel_item/index.js index 40db734df..a470ab9f4 100644 --- a/app/components/sidebars/main/channels_list/channel_item/index.js +++ b/app/components/sidebars/main/channels_list/channel_item/index.js @@ -30,16 +30,14 @@ function makeMapStateToProps() { const channelDraft = getDraftForChannel(state, channel.id); let displayName = channel.display_name; - let isBot = false; let isGuest = false; let isArchived = channel.delete_at > 0; + let teammateId; if (channel.type === General.DM_CHANNEL) { - const teammateId = getUserIdFromChannelName(currentUserId, channel.name); + teammateId = getUserIdFromChannelName(currentUserId, channel.name); const teammate = getUser(state, teammateId); - isBot = Boolean(ownProps.isSearchResult ? channel.isBot : teammate?.is_bot); //eslint-disable-line camelcase - if (teammate) { const teammateNameDisplay = getTeammateNameDisplaySetting(state); displayName = displayUsername(teammate, teammateNameDisplay, false); @@ -78,13 +76,13 @@ function makeMapStateToProps() { displayName, hasDraft: Boolean(channelDraft.draft.trim() || channelDraft?.files?.length), isArchived, - isBot, isChannelMuted: isChannelMuted(member), isGuest, isManualUnread: isManuallyUnread(state, ownProps.channelId), mentions: member ? member.mention_count : 0, shouldHideChannel, showUnreadForMsgs, + teammateId, theme: getTheme(state), unreadMsgs, }; diff --git a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js index 266a1bcc2..b884f4bf7 100644 --- a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js +++ b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js @@ -179,7 +179,7 @@ class FilteredList extends Component { }; buildCurrentDMSForSearch = (props, term) => { - const {channels, teammateNameDisplay, profiles, statuses, pastDirectMessages, groupChannelMemberDetails} = props; + const {channels, currentUserId, teammateNameDisplay, profiles, pastDirectMessages, groupChannelMemberDetails} = props; const {favoriteChannels} = channels; const favoriteDms = favoriteChannels.filter((c) => { @@ -206,7 +206,6 @@ class FilteredList extends Component { return { id: u.id, - status: statuses[u.id], display_name: displayName, username: u.username, email: u.email, @@ -215,10 +214,7 @@ class FilteredList extends Component { nickname: u.nickname, fullname: `${u.first_name} ${u.last_name}`, delete_at: u.delete_at, - isBot: u.is_bot, - - // need name key for DM's as we use it for sortChannelsByDisplayName with same display_name - name: displayName, + name: `${currentUserId}__${u.id}`, }; }); @@ -233,7 +229,7 @@ class FilteredList extends Component { } buildMembersForSearch = (props, term) => { - const {channels, currentUserId, teammateNameDisplay, profiles, teamProfiles, statuses, pastDirectMessages, restrictDms} = props; + const {channels, currentUserId, teammateNameDisplay, profiles, teamProfiles, pastDirectMessages, restrictDms} = props; const {favoriteChannels, unreadChannels} = channels; const favoriteAndUnreadDms = [...favoriteChannels, ...unreadChannels].filter((c) => { @@ -251,17 +247,15 @@ class FilteredList extends Component { return { id: u.id, - status: statuses[u.id], display_name: displayName, username: u.username, email: u.email, - name: displayName, + name: `${currentUserId}__${u.id}`, type: General.DM_CHANNEL, fake: true, nickname: u.nickname, fullname: `${u.first_name} ${u.last_name}`, delete_at: u.delete_at, - isBot: u.is_bot, }; }); diff --git a/app/components/sidebars/main/channels_list/list/__snapshots__/list.test.js.snap b/app/components/sidebars/main/channels_list/list/__snapshots__/list.test.js.snap index 43150d13c..987271dcd 100644 --- a/app/components/sidebars/main/channels_list/list/__snapshots__/list.test.js.snap +++ b/app/components/sidebars/main/channels_list/list/__snapshots__/list.test.js.snap @@ -21,7 +21,7 @@ exports[`ChannelsList List should match snapshot 1`] = ` onEndReachedThreshold={2} onScrollBeginDrag={[Function]} onViewableItemsChanged={[Function]} - removeClippedSubviews={true} + removeClippedSubviews={false} renderItem={[Function]} renderSectionHeader={[Function]} scrollEventThrottle={50} diff --git a/app/components/sidebars/main/channels_list/list/list.js b/app/components/sidebars/main/channels_list/list/list.js index 3f1a6c277..ff34f1ff7 100644 --- a/app/components/sidebars/main/channels_list/list/list.js +++ b/app/components/sidebars/main/channels_list/list/list.js @@ -411,7 +411,7 @@ export default class List extends PureComponent { ref={this.setListRef} sections={sections} contentContainerStyle={{paddingBottom}} - removeClippedSubviews={true} + removeClippedSubviews={Platform.OS === 'android'} renderItem={this.renderItem} renderSectionHeader={this.renderSectionHeader} keyboardShouldPersistTaps={'always'} diff --git a/app/components/user_status/__snapshots__/user_status.test.js.snap b/app/components/user_status/__snapshots__/user_status.test.js.snap index 148cb501f..9a6c55a80 100644 --- a/app/components/user_status/__snapshots__/user_status.test.js.snap +++ b/app/components/user_status/__snapshots__/user_status.test.js.snap @@ -44,7 +44,7 @@ exports[`UserStatus should match snapshot, should default to offline status 1`] name="circle-outline" style={ Object { - "color": "rgba(61,60,64,0.3)", + "color": "rgba(184,184,184,0.64)", "fontSize": 32, } } diff --git a/app/components/user_status/user_status.js b/app/components/user_status/user_status.js deleted file mode 100644 index 9a88b3851..000000000 --- a/app/components/user_status/user_status.js +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; - -import {General} from '@mm-redux/constants'; - -import CompassIcon from '@components/compass_icon'; -import {changeOpacity} from '@utils/theme'; - -export default class UserStatus extends PureComponent { - static propTypes = { - isAvatar: PropTypes.bool, - size: PropTypes.number, - status: PropTypes.string, - theme: PropTypes.object.isRequired, - }; - - static defaultProps = { - size: 6, - status: General.OFFLINE, - }; - - render() { - const {size, status, theme} = this.props; - - let iconName; - let iconColor; - switch (status) { - case General.AWAY: - iconName = 'clock'; - iconColor = theme.awayIndicator; - break; - case General.DND: - iconName = 'minus-circle'; - iconColor = theme.dndIndicator; - break; - case General.ONLINE: - iconName = 'check-circle'; - iconColor = theme.onlineIndicator; - break; - default: - iconName = 'circle-outline'; - iconColor = changeOpacity(theme.centerChannelColor, 0.3); - break; - } - - return ( - - ); - } -} diff --git a/app/components/user_status/user_status.tsx b/app/components/user_status/user_status.tsx new file mode 100644 index 000000000..6c44da65b --- /dev/null +++ b/app/components/user_status/user_status.tsx @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import CompassIcon from '@components/compass_icon'; +import {General} from '@mm-redux/constants'; +import {changeOpacity} from '@utils/theme'; + +import type {Theme} from '@mm-redux/types/preferences'; + +type UserStatusProps = { + size: number; + status: string; + theme: Theme; +}; + +const UserStatus = ({size, status, theme}: UserStatusProps) => { + let iconName; + let iconColor; + switch (status) { + case General.AWAY: + iconName = 'clock'; + iconColor = theme.awayIndicator; + break; + case General.DND: + iconName = 'minus-circle'; + iconColor = theme.dndIndicator; + break; + case General.ONLINE: + iconName = 'check-circle'; + iconColor = theme.onlineIndicator; + break; + default: + iconName = 'circle-outline'; + iconColor = changeOpacity('#B8B8B8', 0.64); + break; + } + + return ( + + ); +}; + +UserStatus.defaultProps = { + size: 6, + status: General.OFFLINE, +}; + +export default UserStatus; diff --git a/app/hooks/did_update.ts b/app/hooks/did_update.ts new file mode 100644 index 000000000..e65c88c9c --- /dev/null +++ b/app/hooks/did_update.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useRef, useEffect, EffectCallback, DependencyList} from 'react'; + +function useDidUpdate(callback: EffectCallback, deps?: DependencyList) { + const hasMount = useRef(false); + + useEffect(() => { + if (hasMount.current) { + callback(); + } else { + hasMount.current = true; + } + }, deps); +} + +export default useDidUpdate; diff --git a/app/hooks/index.ts b/app/hooks/index.ts new file mode 100644 index 000000000..85799a212 --- /dev/null +++ b/app/hooks/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export {default as useDidUpdate} from './did_update'; diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap index 18e2a2c45..c7626e865 100644 --- a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap +++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap @@ -33,13 +33,11 @@ exports[`channelInfo should match snapshot 1`] = ` hasGuests={false} header="" isArchived={false} - isBot={false} isGroupConstrained={false} isTeammateGuest={false} memberCount={2} onPermalinkPress={[Function]} purpose="Purpose" - status="status" testID="channel_info.header" theme={ Object { diff --git a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap index 53ce71885..6d009a653 100644 --- a/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap +++ b/app/screens/channel_info/__snapshots__/channel_info_header.test.js.snap @@ -27,14 +27,13 @@ exports[`channel_info_header should match snapshot 1`] = ` } > 0} isGroupConstrained={currentChannel.group_constrained} diff --git a/app/screens/channel_info/channel_info.test.js b/app/screens/channel_info/channel_info.test.js index 1be121f75..914b0c2c8 100644 --- a/app/screens/channel_info/channel_info.test.js +++ b/app/screens/channel_info/channel_info.test.js @@ -44,9 +44,7 @@ describe('channelInfo', () => { currentChannelMemberCount: 2, currentChannelGuestCount: 0, currentUserId: '1234', - status: 'status', theme: Preferences.THEMES.default, - isBot: false, isTeammateGuest: false, isDirectMessage: false, actions: { diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index 8072ee7e4..666d8403a 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -12,18 +12,17 @@ import { import {intlShape} from 'react-intl'; import Clipboard from '@react-native-community/clipboard'; +import {popToRoot} from '@actions/navigation'; +import ChannelIcon from '@components/channel_icon'; +import FormattedDate from '@components/formatted_date'; +import FormattedText from '@components/formatted_text'; +import Markdown from '@components/markdown'; import {General} from '@mm-redux/constants'; - -import ChannelIcon from 'app/components/channel_icon'; -import FormattedDate from 'app/components/formatted_date'; -import FormattedText from 'app/components/formatted_text'; -import Markdown from 'app/components/markdown'; +import BottomSheet from '@utils/bottom_sheet'; +import {t} from '@utils/i18n'; +import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import mattermostManaged from 'app/mattermost_managed'; -import BottomSheet from 'app/utils/bottom_sheet'; -import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {t} from 'app/utils/i18n'; -import {popToRoot} from 'app/actions/navigation'; export default class ChannelInfoHeader extends React.PureComponent { static propTypes = { @@ -34,11 +33,10 @@ export default class ChannelInfoHeader extends React.PureComponent { header: PropTypes.string, onPermalinkPress: PropTypes.func, purpose: PropTypes.string, - status: PropTypes.string, + teammateId: PropTypes.string, theme: PropTypes.object.isRequired, type: PropTypes.string.isRequired, isArchived: PropTypes.bool.isRequired, - isBot: PropTypes.bool.isRequired, isTeammateGuest: PropTypes.bool.isRequired, hasGuests: PropTypes.bool.isRequired, isGroupConstrained: PropTypes.bool, @@ -135,11 +133,10 @@ export default class ChannelInfoHeader extends React.PureComponent { memberCount, onPermalinkPress, purpose, - status, + teammateId, theme, type, isArchived, - isBot, isGroupConstrained, testID, timeZone, @@ -148,9 +145,10 @@ export default class ChannelInfoHeader extends React.PureComponent { const style = getStyleSheet(theme); const textStyles = getMarkdownTextStyles(theme); const blockStyles = getMarkdownBlockStyles(theme); - const baseTextStyle = Platform.OS === 'ios' ? - {...style.detail, lineHeight: 20} : - style.detail; + const baseTextStyle = Platform.select({ + ios: {...style.detail, lineHeight: 20}, + android: style.detail, + }); return ( @@ -159,11 +157,10 @@ export default class ChannelInfoHeader extends React.PureComponent { isInfo={true} membersCount={memberCount} size={24} - status={status} + userId={teammateId} theme={theme} type={type} isArchived={isArchived} - isBot={isBot} testID={`${testID}.channel_icon`} /> { fontSize: 15, fontWeight: '600', color: theme.centerChannelColor, + marginLeft: 13, }, channelNameContainer: { flexDirection: 'row', diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index d409ba915..3f54901ad 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -11,10 +11,9 @@ import {getCustomEmojisInText} from '@mm-redux/actions/emojis'; import {General} from '@mm-redux/constants'; import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import {getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels'; -import {getCurrentUserId, getUser, getStatusForUserId} from '@mm-redux/selectors/entities/users'; +import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users'; import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils'; import {displayUsername} from '@mm-redux/utils/user_utils'; -import {isLandscape} from '@selectors/device'; import {isGuest} from '@utils/users'; import ChannelInfo from './channel_info'; @@ -29,17 +28,13 @@ function mapStateToProps(state) { let currentChannelGuestCount = (currentChannelStats && currentChannelStats.guest_count) || 0; const currentUserId = getCurrentUserId(state); - let status; - let isBot = false; + let teammateId; let isTeammateGuest = false; const isDirectMessage = currentChannel.type === General.DM_CHANNEL; + if (isDirectMessage) { - const teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name); + teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name); const teammate = getUser(state, teammateId); - status = getStatusForUserId(state, teammateId); - if (teammate && teammate.is_bot) { - isBot = true; - } if (isGuest(teammate)) { isTeammateGuest = true; currentChannelGuestCount = 1; @@ -56,11 +51,9 @@ function mapStateToProps(state) { currentChannelGuestCount, currentChannelMemberCount, currentUserId, - isBot, - isLandscape: isLandscape(state), isTeammateGuest, isDirectMessage, - status, + teammateId, theme: getTheme(state), }; } diff --git a/babel.config.js b/babel.config.js index 57d768854..e8c1fe91a 100644 --- a/babel.config.js +++ b/babel.config.js @@ -17,6 +17,7 @@ module.exports = { '@client': './app/client', '@components': './app/components', '@constants': './app/constants', + '@hooks': './app/hooks', '@i18n': './app/i18n', '@init': './app/init', '@mm-redux': './app/mm-redux', diff --git a/tsconfig.json b/tsconfig.json index 29588fe49..76f44fc48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,8 @@ "@components/*": ["app/components/*"], "@constants/*": ["app/constants/*"], "@constants": ["app/constants/index"], + "@hooks": ["app/hooks/index"], + "@hooks/*": ["app/hooks/*"], "@i18n": ["app/i18n/index"], "@init/*": ["app/init/*"], "@mm-redux/*": ["app/mm-redux/*"],