MM-13618 Adding bot tags. (#2669)

* Adding bot tags.

* Snapshot update.

* Moving bot tag to own component.

* Snapshots.

* Adding missing bot tags, fixing bot profile, allowing tap of bot username to open profile.

* Snapshots and tests.
This commit is contained in:
Christopher Speller 2019-03-26 16:59:45 -07:00 committed by GitHub
parent 6cb2bdbb0f
commit ec9e46fb62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 537 additions and 91 deletions

View file

@ -10,6 +10,7 @@ import {
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
@ -19,6 +20,7 @@ export default class AtMentionItem extends PureComponent {
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
isBot: PropTypes.bool,
theme: PropTypes.object.isRequired,
};
@ -34,6 +36,7 @@ export default class AtMentionItem extends PureComponent {
userId,
username,
theme,
isBot,
} = this.props;
const style = getStyleFromTheme(theme);
@ -54,6 +57,10 @@ export default class AtMentionItem extends PureComponent {
/>
</View>
<Text style={style.rowUsername}>{`@${username}`}</Text>
<BotTag
show={isBot}
theme={theme}
/>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
</TouchableOpacity>

View file

@ -16,6 +16,7 @@ function mapStateToProps(state, ownProps) {
firstName: user.first_name,
lastName: user.last_name,
username: user.username,
isBot: Boolean(user.is_bot),
theme: getTheme(state),
};
}

View file

@ -9,6 +9,7 @@ import {
} from 'react-native';
import {General} from 'mattermost-redux/constants';
import BotTag from 'app/components/bot_tag';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@ -18,6 +19,7 @@ export default class ChannelMentionItem extends PureComponent {
displayName: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
isBot: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
@ -38,6 +40,7 @@ export default class ChannelMentionItem extends PureComponent {
name,
theme,
type,
isBot,
} = this.props;
const style = getStyleFromTheme(theme);
@ -53,6 +56,10 @@ export default class ChannelMentionItem extends PureComponent {
style={style.row}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
<BotTag
show={isBot}
theme={theme}
/>
</TouchableOpacity>
);
}

View file

@ -3,6 +3,8 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@ -11,14 +13,25 @@ import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
import ChannelMentionItem from './channel_mention_item';
import {getUser} from 'mattermost-redux/selectors/entities/users';
function mapStateToProps(state, ownProps) {
const channel = getChannel(state, ownProps.channelId);
const displayName = getChannelNameForSearchAutocomplete(state, ownProps.channelId);
let isBot = false;
if (channel.type === General.DM_CHANNEL) {
const teammate = getUser(state, channel.teammate_id);
if (teammate && teammate.is_bot) {
isBot = true;
}
}
return {
displayName,
name: channel.name,
type: channel.type,
isBot,
theme: getTheme(state),
};
}

50
app/components/bot_tag.js Normal file
View file

@ -0,0 +1,50 @@
// 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 {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
export default class BotTag extends PureComponent {
static defaultProps = {
show: true,
};
static propTypes = {
show: PropTypes.bool,
theme: PropTypes.object.isRequired,
};
render() {
if (!this.props.show) {
return null;
}
const style = createStyleSheet(this.props.theme);
return (
<FormattedText
id='post_info.bot'
defaultMessage='BOT'
style={style.bot}
/>
);
}
}
const createStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
bot: {
alignSelf: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 2,
color: theme.centerChannelColor,
fontSize: 10,
fontWeight: '600',
marginRight: 5,
marginLeft: 5,
paddingVertical: 2,
paddingHorizontal: 4,
},
};
});

View file

@ -8,7 +8,7 @@ import {
Text,
View,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import Icon from 'react-native-vector-icons/FontAwesome5';
import {General} from 'mattermost-redux/constants';
@ -26,6 +26,7 @@ export default class ChannelIcon extends React.PureComponent {
theme: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
isArchived: PropTypes.bool.isRequired,
isBot: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -47,6 +48,7 @@ export default class ChannelIcon extends React.PureComponent {
theme,
type,
isArchived,
isBot,
} = this.props;
const style = getStyleSheet(theme);
@ -85,6 +87,13 @@ export default class ChannelIcon extends React.PureComponent {
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
/>
);
} else if (isBot) {
icon = (
<Icon
name='robot'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}, style.iconBot]}
/>
);
} else if (hasDraft) {
icon = (
<Icon
@ -177,6 +186,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
iconInfo: {
color: theme.centerChannelColor,
},
iconBot: {
marginLeft: -5,
},
groupBox: {
alignSelf: 'flex-start',
alignItems: 'center',

View file

@ -14,6 +14,7 @@ import {General} from 'mattermost-redux/constants';
import {injectIntl, intlShape} from 'react-intl';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
@ -91,16 +92,24 @@ class ChannelIntro extends PureComponent {
const {currentChannelMembers, theme} = this.props;
const style = getStyleSheet(theme);
return currentChannelMembers.map((member, index) => (
<TouchableOpacity
key={member.id}
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
>
<Text style={style.displayName}>
{index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `}
</Text>
</TouchableOpacity>
));
return currentChannelMembers.map((member, index) => {
return (
<TouchableOpacity
key={member.id}
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
>
<View style={style.indicatorContainer}>
<Text style={style.displayName}>
{index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `}
</Text>
<BotTag
show={Boolean(member.is_bot)}
theme={theme}
/>
</View>
</TouchableOpacity>
);
});
};
buildDMContent = () => {
@ -382,6 +391,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexWrap: 'wrap',
justifyContent: 'flex-start',
},
indicatorContainer: {
flexDirection: 'row',
},
};
});

View file

@ -41,18 +41,57 @@ exports[`UserListRow should match snapshot 1`] = `
}
>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
<View
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
"flexDirection": "row",
}
}
>
@user
</Text>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
>
@user
</Text>
<BotTag
show={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
</View>
</View>
</View>
</CustomListRow>
@ -100,16 +139,55 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
}
>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
<View
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
"flexDirection": "row",
}
}
/>
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
/>
<BotTag
show={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
</View>
</View>
</View>
</CustomListRow>
@ -157,18 +235,57 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
}
>
<View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
<View
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
"flexDirection": "row",
}
}
>
@user
</Text>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
>
@user
</Text>
<BotTag
show={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
</View>
</View>
<View>
<Text

View file

@ -12,6 +12,7 @@ import {
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import CustomListRow from 'app/components/custom_list/custom_list_row';
@ -78,13 +79,19 @@ export default class UserListRow extends React.PureComponent {
</View>
<View style={style.textContainer}>
<View>
<Text
style={style.username}
ellipsizeMode='tail'
numberOfLines={1}
>
{usernameDisplay}
</Text>
<View style={style.indicatorContainer}>
<Text
style={style.username}
ellipsizeMode='tail'
numberOfLines={1}
>
{usernameDisplay}
</Text>
<BotTag
show={Boolean(user.is_bot)}
theme={theme}
/>
</View>
</View>
{showTeammateDisplay &&
<View>
@ -140,6 +147,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 15,
color: theme.centerChannelColor,
},
indicatorContainer: {
flexDirection: 'row',
},
deactivated: {
marginTop: 2,
fontSize: 12,

View file

@ -44,6 +44,7 @@ function makeMapStateToProps() {
overrideUsername: post?.props?.override_username, // eslint-disable-line camelcase
theme: getTheme(state),
username: user.username,
isBot: user.is_bot || false,
userTimezone,
};
};

View file

@ -14,6 +14,7 @@ import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import FormattedDate from 'app/components/formatted_date';
import ReplyIcon from 'app/components/reply_icon';
import BotTag from 'app/components/bot_tag';
import {emptyFunction} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -38,6 +39,7 @@ export default class PostHeader extends PureComponent {
showFullDate: PropTypes.bool,
theme: PropTypes.object.isRequired,
username: PropTypes.string,
isBot: PropTypes.bool,
userTimezone: PropTypes.string,
enableTimezone: PropTypes.bool,
};
@ -61,6 +63,7 @@ export default class PostHeader extends PureComponent {
isSystemMessage,
fromAutoResponder,
overrideUsername,
isBot,
} = this.props;
if (fromWebHook) {
@ -74,13 +77,24 @@ export default class PostHeader extends PureComponent {
<Text style={style.displayName}>
{name}
</Text>
<FormattedText
id='post_info.bot'
defaultMessage='BOT'
style={style.bot}
<BotTag
theme={this.props.theme}
/>
</View>
);
} else if (isBot) {
return (
<TouchableOpacity onPress={this.handleUsernamePress}>
<View style={style.indicatorContainer}>
<Text style={style.displayName}>
{this.props.displayName}
</Text>
<BotTag
theme={this.props.theme}
/>
</View>
</TouchableOpacity>
);
} else if (fromAutoResponder) {
let name = this.props.displayName;
if (overrideUsername && enablePostUsernameOverride) {
@ -291,17 +305,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
indicatorContainer: {
flexDirection: 'row',
},
bot: {
alignSelf: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 2,
color: theme.centerChannelColor,
fontSize: 10,
fontWeight: '600',
marginRight: 5,
paddingVertical: 2,
paddingHorizontal: 4,
},
displayName: {
color: theme.centerChannelColor,
fontSize: 15,

View file

@ -111,7 +111,7 @@ export default class ProfilePicture extends PureComponent {
}
render() {
const {edit, showStatus, theme} = this.props;
const {edit, showStatus, theme, user} = this.props;
const {pictureUrl} = this.state;
const style = getStyleSheet(theme);
@ -172,7 +172,7 @@ export default class ProfilePicture extends PureComponent {
return (
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
{image}
{(showStatus || edit) &&
{(showStatus || edit) && (user && !user.is_bot) &&
<View style={[style.statusWrapper, statusStyle, {borderRadius: this.props.statusSize / 2}]}>
{statusIcon}
</View>

View file

@ -39,6 +39,7 @@ exports[`ChannelItem should match snapshot 1`] = `
hasDraft={false}
isActive={false}
isArchived={false}
isBot={false}
isInfo={false}
isUnread={true}
membersCount={1}
@ -153,6 +154,7 @@ 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}
@ -267,6 +269,7 @@ 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}
@ -381,6 +384,7 @@ 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}
@ -484,6 +488,7 @@ exports[`ChannelItem should match snapshot for deactivated user and is searchRes
hasDraft={false}
isActive={false}
isArchived={true}
isBot={false}
isInfo={false}
isUnread={true}
membersCount={1}
@ -593,6 +598,7 @@ exports[`ChannelItem should match snapshot with draft 1`] = `
hasDraft={true}
isActive={false}
isArchived={false}
isBot={false}
isInfo={false}
isUnread={true}
membersCount={1}
@ -698,6 +704,7 @@ exports[`ChannelItem should match snapshot with mentions and muted 1`] = `
hasDraft={false}
isActive={false}
isArchived={false}
isBot={false}
isInfo={false}
isUnread={true}
membersCount={1}

View file

@ -39,6 +39,7 @@ export default class ChannelItem extends PureComponent {
theme: PropTypes.object.isRequired,
unreadMsgs: PropTypes.number.isRequired,
isSearchResult: PropTypes.bool,
isBot: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -99,6 +100,7 @@ export default class ChannelItem extends PureComponent {
theme,
isSearchResult,
channel,
isBot,
} = this.props;
const isArchived = channel.delete_at > 0;
@ -183,6 +185,7 @@ export default class ChannelItem extends PureComponent {
theme={theme}
type={channel.type}
isArchived={isArchived}
isBot={isBot}
/>
);

View file

@ -38,6 +38,7 @@ describe('ChannelItem', () => {
theme: Preferences.THEMES.default,
unreadMsgs: 1,
isSearchResult: false,
isBot: false,
};
test('should match snapshot', () => {

View file

@ -12,7 +12,7 @@ import {
} from 'mattermost-redux/selectors/entities/channels';
import {getTheme, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import {getUserIdFromChannelName, isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {getDraftForChannel} from 'app/selectors/views';
@ -29,12 +29,19 @@ function makeMapStateToProps() {
const channelDraft = getDraftForChannel(state, channel.id);
let displayName = channel.display_name;
let isBot = false;
if (channel.type === General.DM_CHANNEL) {
if (!ownProps.isSearchResult) {
const teammate = getUser(state, channel.teammate_id);
if (ownProps.isSearchResult) {
isBot = channel.isBot;
} else {
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
const teammate = getUser(state, teammateId);
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
displayName = displayUsername(teammate, teammateNameDisplay, false);
if (teammate && teammate.is_bot) {
isBot = true;
}
}
}
@ -73,6 +80,7 @@ function makeMapStateToProps() {
showUnreadForMsgs,
theme: getTheme(state),
unreadMsgs,
isBot,
};
};
}

View file

@ -218,6 +218,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,
@ -263,6 +264,7 @@ class FilteredList extends Component {
nickname: u.nickname,
fullname: `${u.first_name} ${u.last_name}`,
delete_at: u.delete_at,
isBot: u.is_bot,
};
});

View file

@ -58,6 +58,7 @@ export default class ChannelInfo extends PureComponent {
canManageUsers: PropTypes.bool.isRequired,
canEditChannel: PropTypes.bool.isRequired,
ignoreChannelMentions: PropTypes.bool.isRequired,
isBot: PropTypes.bool.isRequired,
};
static contextTypes = {
@ -510,6 +511,7 @@ export default class ChannelInfo extends PureComponent {
navigator,
status,
theme,
isBot,
} = this.props;
const style = getStyleSheet(theme);
@ -548,6 +550,7 @@ export default class ChannelInfo extends PureComponent {
theme={theme}
type={currentChannel.type}
isArchived={currentChannel.delete_at !== 0}
isBot={isBot}
/>
}
<View style={style.rowsContainer}>

View file

@ -29,6 +29,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
theme: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
isArchived: PropTypes.bool.isRequired,
isBot: PropTypes.bool.isRequired,
};
render() {
@ -45,6 +46,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
theme,
type,
isArchived,
isBot,
} = this.props;
const style = getStyleSheet(theme);
@ -62,6 +64,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
theme={theme}
type={type}
isArchived={isArchived}
isBot={isBot}
/>
<Text
ellipsizeMode='tail'

View file

@ -60,9 +60,14 @@ function mapStateToProps(state) {
const currentUser = getUser(state, currentUserId);
let status;
let isBot = false;
if (currentChannel.type === General.DM_CHANNEL) {
const teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name);
const teammate = getUser(state, teammateId);
status = getStatusForUserId(state, teammateId);
if (teammate && teammate.is_bot) {
isBot = true;
}
}
const isAdmin = checkIsAdmin(roles);
@ -88,6 +93,7 @@ function mapStateToProps(state) {
status,
theme: getTheme(state),
canManageUsers,
isBot,
};
}

View file

@ -32,6 +32,57 @@ exports[`user_profile should match snapshot 1`] = `
statusSize={40}
userId="4hzdnk6mg7gepe7yze6m3domnc"
/>
<View
style={
Object {
"flexDirection": "row",
"marginTop": 15,
}
}
>
<Text
style={
Object {
"color": "#3d3c40",
"fontSize": 17,
"fontWeight": "600",
}
}
>
fred
</Text>
<BotTag
show={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
/>
</View>
<Text
style={
Object {
@ -41,7 +92,7 @@ exports[`user_profile should match snapshot 1`] = `
}
}
>
@undefined
@fred
</Text>
</View>
<View
@ -52,6 +103,31 @@ exports[`user_profile should match snapshot 1`] = `
}
}
>
<View>
<Text
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
"marginBottom": 10,
"marginTop": 25,
}
}
>
USERNAME
</Text>
<Text
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
>
fred
</Text>
</View>
<View>
<Text
style={

View file

@ -10,6 +10,8 @@ import {makeDirectChannel} from 'app/actions/views/more_dms';
import {getTeammateNameDisplaySetting, getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import Preferences from 'mattermost-redux/constants/preferences';
import {loadBot} from 'mattermost-redux/actions/bots';
import {getBotAccounts} from 'mattermost-redux/selectors/entities/bots';
import {isTimezoneEnabled} from 'app/utils/timezone';
@ -26,6 +28,7 @@ function mapStateToProps(state, ownProps) {
createChannelRequest,
currentDisplayName: state.views.channel.displayName,
user: state.entities.users.profiles[ownProps.userId],
bot: getBotAccounts(state)[ownProps.userId],
teammateNameDisplay: getTeammateNameDisplaySetting(state),
enableTimezone,
militaryTime,
@ -38,6 +41,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
makeDirectChannel,
setChannelDisplayName,
loadBot,
}, dispatch),
};
}

View file

@ -18,6 +18,7 @@ import ProfilePicture from 'app/components/profile_picture';
import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import StatusBar from 'app/components/status_bar';
import BotTag from 'app/components/bot_tag';
import {alertErrorWithFallback} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
@ -30,6 +31,7 @@ export default class UserProfile extends PureComponent {
actions: PropTypes.shape({
makeDirectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
loadBot: PropTypes.func.isRequired,
}).isRequired,
config: PropTypes.object.isRequired,
currentDisplayName: PropTypes.string,
@ -37,6 +39,7 @@ export default class UserProfile extends PureComponent {
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
bot: PropTypes.object,
militaryTime: PropTypes.bool.isRequired,
enableTimezone: PropTypes.bool.isRequired,
};
@ -51,6 +54,12 @@ export default class UserProfile extends PureComponent {
}
}
componentDidMount() {
if (this.props.user && this.props.user.is_bot) {
this.props.actions.loadBot(this.props.user.id);
}
}
close = () => {
const {navigator, theme} = this.props;
@ -78,7 +87,17 @@ export default class UserProfile extends PureComponent {
const displayName = displayUsername(user, teammateNameDisplay);
if (displayName) {
return <Text style={style.displayName}>{displayName}</Text>;
return (
<View style={style.indicatorContainer}>
<Text style={style.displayName}>
{displayName}
</Text>
<BotTag
show={Boolean(user.is_bot)}
theme={theme}
/>
</View>
);
}
return null;
@ -198,8 +217,34 @@ export default class UserProfile extends PureComponent {
return additionalOptions;
};
renderDetailsBlock = (style) => {
if (this.props.user.is_bot) {
if (!this.props.bot) {
return null;
}
return (
<View style={style.content}>
<View>
<Text style={style.header}>{'DESCRIPTION'}</Text>
<Text style={style.text}>{this.props.bot.description || ''}</Text>
</View>
</View>
);
}
return (
<View style={style.content}>
{this.props.enableTimezone && this.buildTimezoneBlock()}
{this.buildDisplayBlock('username')}
{this.props.config.ShowEmailAddress === 'true' && this.buildDisplayBlock('email')}
{this.buildDisplayBlock('nickname')}
{this.buildDisplayBlock('position')}
</View>
);
}
render() {
const {config, theme, user, enableTimezone} = this.props;
const {theme, user} = this.props;
const style = createStyleSheet(theme);
if (!user) {
@ -222,13 +267,7 @@ export default class UserProfile extends PureComponent {
{this.getDisplayName()}
<Text style={style.username}>{`@${user.username}`}</Text>
</View>
<View style={style.content}>
{enableTimezone && this.buildTimezoneBlock()}
{this.buildDisplayBlock('username')}
{config.ShowEmailAddress === 'true' && this.buildDisplayBlock('email')}
{this.buildDisplayBlock('nickname')}
{this.buildDisplayBlock('position')}
</View>
{this.renderDetailsBlock(style)}
<UserProfileRow
action={this.sendMessage}
defaultMessage='Send Message'
@ -254,7 +293,6 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
marginHorizontal: 15,
},
displayName: {
marginTop: 15,
color: theme.centerChannelColor,
fontSize: 17,
fontWeight: '600',
@ -284,6 +322,10 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
fontSize: 15,
},
indicatorContainer: {
marginTop: 15,
flexDirection: 'row',
},
};
});

View file

@ -6,6 +6,7 @@ import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import UserProfile from './user_profile.js';
import BotTag from 'app/components/bot_tag';
jest.mock('react-intl');
jest.mock('app/utils/theme', () => {
@ -20,6 +21,7 @@ describe('user_profile', () => {
const actions = {
setChannelDisplayName: jest.fn(),
makeDirectChannel: jest.fn(),
loadBot: jest.fn(),
};
const baseProps = {
actions,
@ -33,21 +35,53 @@ describe('user_profile', () => {
teams: [],
theme: Preferences.THEMES.default,
enableTimezone: false,
user: {
militaryTime: false,
};
const user = {
email: 'test@test.com',
first_name: 'test',
id: '4hzdnk6mg7gepe7yze6m3domnc',
last_name: 'fake',
nickname: 'nick',
username: 'fred',
is_bot: false,
};
test('should match snapshot', async () => {
const wrapper = shallow(
<UserProfile
{...baseProps}
user={user}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should contain bot tag', async () => {
const botUser = {
email: 'test@test.com',
first_name: 'test',
id: '4hzdnk6mg7gepe7yze6m3domnc',
last_name: 'fake',
nickname: 'nick',
},
militaryTime: false,
};
username: 'fred',
is_bot: true,
};
test('should match snapshot', async () => {
const wrapper = shallow(
<UserProfile {...baseProps}/>,
<UserProfile
{...baseProps}
user={botUser}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.containsMatchingElement(
<BotTag
show={true}
theme={baseProps.theme}
/>
)).toEqual(true);
});
});

51
package-lock.json generated
View file

@ -2688,6 +2688,7 @@
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"optional": true,
"requires": {
"is-extendable": "^0.1.0"
}
@ -2732,7 +2733,8 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
"dev": true,
"optional": true
},
"is-glob": {
"version": "4.0.0",
@ -4359,7 +4361,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -4377,11 +4380,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4394,15 +4399,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -4505,7 +4513,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -4515,6 +4524,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4527,17 +4537,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -4554,6 +4567,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -4626,7 +4640,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -4636,6 +4651,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -4711,7 +4727,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -4741,6 +4758,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -4758,6 +4776,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -4796,11 +4815,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},
@ -12338,7 +12359,8 @@
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
"dev": true
"dev": true,
"optional": true
},
"braces": {
"version": "2.3.2",
@ -12624,7 +12646,8 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true
"dev": true,
"optional": true
},
"micromatch": {
"version": "3.1.10",

View file

@ -109,7 +109,8 @@
"postinstall": "make post-install",
"test": "jest --forceExit --detectOpenHandles",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
"test:coverage": "jest --coverage",
"updatesnapshot": "jest --updateSnapshot"
},
"rnpm": {
"assets": [