From ee93e4fa59af41ff418109ff78d1bd66784e014f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 22 Jul 2019 22:14:39 +0200 Subject: [PATCH] Adding guest accounts feature (#2990) * MM-15059: Restict the permissions for guests (#2791) * MM-15057: Adding guest badge to identify guest users (#2774) * MM-15057: Adding guest badge to identify guest users * Adding Guest tags in the channel title * Adding i18n translations * Adding DM and GM guest warnings * Fixing check-style * Adding and fixing tests * Adding i18n text * Only showing the subtitle when there is enough space * Addressing new design changes * Fixing eslint * Addressing PR comments * Moving getChannelStats to the handleSelectChannel action * Adding the guest info in android landscape channel headers * simplified the guest warning text generation * Fixing i18n * Fixing leaving channel behavior for guests (#2989) * Fixing leaving channel behavior for guests * Fixing tests and adding a new one * fixing typo * Addressing PR comments * Addressing PR comments * Fixing tests --- app/actions/views/channel.js | 2 + .../at_mention_item/at_mention_item.js | 7 + .../autocomplete/at_mention_item/index.js | 3 + .../channel_mention_item.js | 7 + app/components/channel_intro/channel_intro.js | 11 +- .../__snapshots__/user_list_test.test.js.snap | 222 ++++++++++++++++++ .../user_list_row/user_list_row.js | 6 + .../user_list_row/user_list_test.test.js | 16 ++ app/components/guest_tag.js | 55 +++++ app/components/post_header/index.js | 2 + app/components/post_header/post_header.js | 16 ++ .../main/channels_list/channel_item/index.js | 4 + .../sidebars/main/channels_list/list/index.js | 13 +- .../sidebars/main/channels_list/list/list.js | 11 +- .../channel_nav_bar/channel_nav_bar.js | 8 +- .../channel_title/channel_title.js | 61 ++++- .../channel_nav_bar/channel_title/index.js | 20 +- app/screens/channel_info/channel_info.js | 11 +- .../channel_info/channel_info_header.js | 35 +++ app/screens/channel_info/index.js | 8 + .../__snapshots__/select_team.test.js.snap | 34 +++ app/screens/select_team/index.js | 5 + app/screens/select_team/select_team.js | 16 ++ app/screens/select_team/select_team.test.js | 22 ++ .../__snapshots__/user_profile.test.js.snap | 31 +++ app/screens/user_profile/user_profile.js | 6 + app/screens/user_profile/user_profile.test.js | 27 +++ app/utils/users.js | 23 ++ assets/base/i18n/en.json | 5 + package-lock.json | 4 +- package.json | 4 +- 31 files changed, 679 insertions(+), 16 deletions(-) create mode 100644 app/components/guest_tag.js create mode 100644 app/utils/users.js diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index ee1db2d48..be8ebc0d9 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -12,6 +12,7 @@ import { markChannelAsRead, leaveChannel as serviceLeaveChannel, markChannelAsViewed, selectChannel, + getChannelStats, } from 'mattermost-redux/actions/channels'; import { getPosts, @@ -380,6 +381,7 @@ export function handleSelectChannel(channelId, fromPushNotification = false) { const actions = [ selectChannel(channelId), + getChannelStats(channelId), setChannelDisplayName(channel.display_name), { type: ViewTypes.SET_INITIAL_POST_VISIBILITY, diff --git a/app/components/autocomplete/at_mention_item/at_mention_item.js b/app/components/autocomplete/at_mention_item/at_mention_item.js index 1d83fdbc1..b24a83290 100644 --- a/app/components/autocomplete/at_mention_item/at_mention_item.js +++ b/app/components/autocomplete/at_mention_item/at_mention_item.js @@ -11,6 +11,7 @@ import { import ProfilePicture from 'app/components/profile_picture'; import BotTag from 'app/components/bot_tag'; +import GuestTag from 'app/components/guest_tag'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; export default class AtMentionItem extends PureComponent { @@ -20,6 +21,7 @@ export default class AtMentionItem extends PureComponent { onPress: PropTypes.func.isRequired, userId: PropTypes.string.isRequired, username: PropTypes.string, + isGuest: PropTypes.bool, isBot: PropTypes.bool, theme: PropTypes.object.isRequired, }; @@ -42,6 +44,7 @@ export default class AtMentionItem extends PureComponent { username, theme, isBot, + isGuest, } = this.props; const style = getStyleFromTheme(theme); @@ -66,6 +69,10 @@ export default class AtMentionItem extends PureComponent { show={isBot} theme={theme} /> + {hasFullName && {' - '}} {hasFullName && {`${firstName} ${lastName}`}} diff --git a/app/components/autocomplete/at_mention_item/index.js b/app/components/autocomplete/at_mention_item/index.js index 355d7dce2..382537563 100644 --- a/app/components/autocomplete/at_mention_item/index.js +++ b/app/components/autocomplete/at_mention_item/index.js @@ -9,6 +9,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import AtMentionItem from './at_mention_item'; +import {isGuest} from 'app/utils/users'; + function mapStateToProps(state, ownProps) { const user = getUser(state, ownProps.userId); @@ -17,6 +19,7 @@ function mapStateToProps(state, ownProps) { lastName: user.last_name, username: user.username, isBot: Boolean(user.is_bot), + isGuest: isGuest(user), theme: getTheme(state), }; } diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.js b/app/components/autocomplete/channel_mention_item/channel_mention_item.js index 867b14d5f..606de102f 100644 --- a/app/components/autocomplete/channel_mention_item/channel_mention_item.js +++ b/app/components/autocomplete/channel_mention_item/channel_mention_item.js @@ -10,6 +10,7 @@ import { import {General} from 'mattermost-redux/constants'; import BotTag from 'app/components/bot_tag'; +import GuestTag from 'app/components/guest_tag'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -20,6 +21,7 @@ export default class ChannelMentionItem extends PureComponent { name: PropTypes.string, type: PropTypes.string, isBot: PropTypes.bool.isRequired, + isGuest: PropTypes.bool.isRequired, onPress: PropTypes.func.isRequired, theme: PropTypes.object.isRequired, }; @@ -41,6 +43,7 @@ export default class ChannelMentionItem extends PureComponent { theme, type, isBot, + isGuest, } = this.props; const style = getStyleFromTheme(theme); @@ -60,6 +63,10 @@ export default class ChannelMentionItem extends PureComponent { show={isBot} theme={theme} /> + ); } diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js index b9d0200ea..658b53177 100644 --- a/app/components/channel_intro/channel_intro.js +++ b/app/components/channel_intro/channel_intro.js @@ -14,9 +14,11 @@ import {injectIntl, intlShape} from 'react-intl'; import ProfilePicture from 'app/components/profile_picture'; import BotTag from 'app/components/bot_tag'; +import GuestTag from 'app/components/guest_tag'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {t} from 'app/utils/i18n'; +import {isGuest} from 'app/utils/users'; class ChannelIntro extends PureComponent { static propTypes = { @@ -91,12 +93,19 @@ class ChannelIntro extends PureComponent { > - {index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `} + {this.getDisplayName(member)} + + + {index === currentChannelMembers.length - 1 ? '' : ', '} + ); diff --git a/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap b/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap index da2d83785..741210df8 100644 --- a/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap +++ b/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap @@ -91,6 +91,37 @@ exports[`UserListRow should match snapshot 1`] = ` } } /> + @@ -187,6 +218,37 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated } } /> + @@ -285,6 +347,37 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = ` } } /> + @@ -302,3 +395,132 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = ` `; + +exports[`UserListRow should match snapshot for guest user 1`] = ` + + + + + + + + + + @user + + + + + + + + +`; diff --git a/app/components/custom_list/user_list_row/user_list_row.js b/app/components/custom_list/user_list_row/user_list_row.js index c9763a338..8cf57fc42 100644 --- a/app/components/custom_list/user_list_row/user_list_row.js +++ b/app/components/custom_list/user_list_row/user_list_row.js @@ -13,7 +13,9 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils'; import ProfilePicture from 'app/components/profile_picture'; import BotTag from 'app/components/bot_tag'; +import GuestTag from 'app/components/guest_tag'; import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; +import {isGuest} from 'app/utils/users'; import CustomListRow from 'app/components/custom_list/custom_list_row'; export default class UserListRow extends React.PureComponent { @@ -91,6 +93,10 @@ export default class UserListRow extends React.PureComponent { show={Boolean(user.is_bot)} theme={theme} /> + {showTeammateDisplay && diff --git a/app/components/custom_list/user_list_row/user_list_test.test.js b/app/components/custom_list/user_list_row/user_list_test.test.js index 74b717a9a..c88ea250d 100644 --- a/app/components/custom_list/user_list_row/user_list_test.test.js +++ b/app/components/custom_list/user_list_row/user_list_test.test.js @@ -58,6 +58,22 @@ describe('UserListRow', () => { expect(wrapper.getElement()).toMatchSnapshot(); }); + test('should match snapshot for guest user', () => { + const newProps = { + ...baseProps, + user: { + ...baseProps.user, + roles: 'system_guest', + }, + }; + + const wrapper = shallow( + , + {context: {intl: {formatMessage}}}, + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + test('should match snapshot for currentUser with (you) populated in list', () => { const newProps = { ...baseProps, diff --git a/app/components/guest_tag.js b/app/components/guest_tag.js new file mode 100644 index 000000000..007a71c43 --- /dev/null +++ b/app/components/guest_tag.js @@ -0,0 +1,55 @@ +// 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 GuestTag extends PureComponent { + static defaultProps = { + show: true, + }; + + static propTypes = { + show: PropTypes.bool, + theme: PropTypes.object.isRequired, + inTitle: PropTypes.bool, + }; + + render() { + if (!this.props.show) { + return null; + } + const style = createStyleSheet(this.props.theme); + + return ( + + ); + } +} + +const createStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + guest: { + 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, + }, + guestTitle: { + backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.15), + color: changeOpacity(theme.sidebarHeaderTextColor, 0.6), + }, + }; +}); diff --git a/app/components/post_header/index.js b/app/components/post_header/index.js index f25124542..960a3d4df 100644 --- a/app/components/post_header/index.js +++ b/app/components/post_header/index.js @@ -14,6 +14,7 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {fromAutoResponder} from 'app/utils/general'; +import {isGuest} from 'app/utils/users'; import PostHeader from './post_header'; @@ -45,6 +46,7 @@ function makeMapStateToProps() { theme: getTheme(state), username: user.username, isBot: user.is_bot || false, + isGuest: isGuest(user), userTimezone, }; }; diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js index f5bde134e..374bb355f 100644 --- a/app/components/post_header/post_header.js +++ b/app/components/post_header/post_header.js @@ -15,6 +15,7 @@ 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 GuestTag from 'app/components/guest_tag'; import {emptyFunction} from 'app/utils/general'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -40,6 +41,7 @@ export default class PostHeader extends PureComponent { theme: PropTypes.object.isRequired, username: PropTypes.string, isBot: PropTypes.bool, + isGuest: PropTypes.bool, userTimezone: PropTypes.string, enableTimezone: PropTypes.bool, }; @@ -64,6 +66,7 @@ export default class PostHeader extends PureComponent { fromAutoResponder, overrideUsername, isBot, + isGuest, } = this.props; if (fromWebHook) { @@ -95,6 +98,19 @@ export default class PostHeader extends PureComponent { ); + } else if (isGuest) { + return ( + + + + {this.props.displayName} + + + + + ); } else if (fromAutoResponder) { let name = this.props.displayName; if (overrideUsername && enablePostUsernameOverride) { 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 afac2ad29..0649de1af 100644 --- a/app/components/sidebars/main/channels_list/channel_item/index.js +++ b/app/components/sidebars/main/channels_list/channel_item/index.js @@ -16,6 +16,7 @@ import {getUserIdFromChannelName, isChannelMuted} from 'mattermost-redux/utils/c import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {getDraftForChannel} from 'app/selectors/views'; +import {isGuest as isGuestUser} from 'app/utils/users'; import ChannelItem from './channel_item'; @@ -30,6 +31,7 @@ function makeMapStateToProps() { let displayName = channel.display_name; let isBot = false; + let isGuest = false; if (channel.type === General.DM_CHANNEL) { if (ownProps.isSearchResult) { @@ -42,6 +44,7 @@ function makeMapStateToProps() { if (teammate && teammate.is_bot) { isBot = true; } + isGuest = isGuestUser(teammate); } } @@ -81,6 +84,7 @@ function makeMapStateToProps() { theme: getTheme(state), unreadMsgs, isBot, + isGuest, }; }; } diff --git a/app/components/sidebars/main/channels_list/list/index.js b/app/components/sidebars/main/channels_list/list/index.js index eaca23964..01aa8ab45 100644 --- a/app/components/sidebars/main/channels_list/list/index.js +++ b/app/components/sidebars/main/channels_list/list/index.js @@ -16,7 +16,9 @@ import {getTheme, getFavoritesPreferences, getSidebarPreferences} from 'mattermo import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; import {memoizeResult} from 'mattermost-redux/utils/helpers'; import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; +import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles'; +import Permissions from 'mattermost-redux/constants/permissions'; import {showModal} from 'app/actions/navigation'; @@ -53,7 +55,16 @@ function mapStateToProps(state) { sidebarPrefs.favorite_at_top === 'true' && favoriteChannelIds.length, )); + let canJoinPublicChannels = true; + if (hasNewPermissions(state)) { + canJoinPublicChannels = haveITeamPermission(state, { + team: currentTeamId, + permission: Permissions.JOIN_PUBLIC_CHANNELS, + }); + } + return { + canJoinPublicChannels, canCreatePrivateChannels: showCreateOption( state, config, diff --git a/app/components/sidebars/main/channels_list/list/list.js b/app/components/sidebars/main/channels_list/list/list.js index defbb4b2e..c117574d9 100644 --- a/app/components/sidebars/main/channels_list/list/list.js +++ b/app/components/sidebars/main/channels_list/list/list.js @@ -36,9 +36,7 @@ let UnreadIndicator = null; export default class List extends PureComponent { static propTypes = { - actions: PropTypes.shape({ - showModal: PropTypes.func.isRequired, - }).isRequired, + canJoinPublicChannels: PropTypes.bool.isRequired, canCreatePrivateChannels: PropTypes.bool.isRequired, favoriteChannelIds: PropTypes.array.isRequired, onSelectChannel: PropTypes.func.isRequired, @@ -47,6 +45,9 @@ export default class List extends PureComponent { theme: PropTypes.object.isRequired, orderedChannelIds: PropTypes.array.isRequired, previewChannel: PropTypes.func, + actions: PropTypes.shape({ + showModal: PropTypes.func.isRequired, + }).isRequired, }; static contextTypes = { @@ -96,7 +97,7 @@ export default class List extends PureComponent { } getSectionConfigByType = (props, sectionType) => { - const {canCreatePrivateChannels} = props; + const {canCreatePrivateChannels, canJoinPublicChannels} = props; switch (sectionType) { case SidebarSectionTypes.UNREADS: @@ -111,7 +112,7 @@ export default class List extends PureComponent { }; case SidebarSectionTypes.PUBLIC: return { - action: this.goToMoreChannels, + action: canJoinPublicChannels ? this.goToMoreChannels : null, id: t('sidebar.channels'), defaultMessage: 'PUBLIC CHANNELS', }; diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 1ba9afc54..40b9c6e2c 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -62,6 +62,7 @@ export default class ChannelNavBar extends PureComponent { const padding = {paddingHorizontal: 0}; let height; + let canHaveSubtitle = true; switch (Platform.OS) { case 'android': height = ANDROID_TOP_PORTRAIT; @@ -75,10 +76,12 @@ export default class ChannelNavBar extends PureComponent { height -= 1; } else if (isLandscape) { height = IOS_TOP_LANDSCAPE; + canHaveSubtitle = false; } if (DeviceTypes.IS_IPHONE_X && isLandscape) { padding.paddingHorizontal = 10; + canHaveSubtitle = false; } break; } @@ -94,7 +97,10 @@ export default class ChannelNavBar extends PureComponent { openDrawer={openChannelDrawer} visible={drawerButtonVisible} /> - + diff --git a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js index f50f7f861..89de4fadf 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js +++ b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js @@ -12,15 +12,22 @@ import { import Icon from 'react-native-vector-icons/FontAwesome'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; +import {t} from 'app/utils/i18n'; +import {General} from 'mattermost-redux/constants'; +import FormattedText from 'app/components/formatted_text'; export default class ChannelTitle extends PureComponent { static propTypes = { currentChannelName: PropTypes.string, displayName: PropTypes.string, + channelType: PropTypes.string, isChannelMuted: PropTypes.bool, onPress: PropTypes.func, theme: PropTypes.object, isArchived: PropTypes.bool, + isGuest: PropTypes.bool.isRequired, + hasGuests: PropTypes.bool.isRequired, + canHaveSubtitle: PropTypes.bool.isRequired, }; static defaultProps = { @@ -42,10 +49,49 @@ export default class ChannelTitle extends PureComponent { return content; } + renderHasGuestsText = (style) => { + const {channelType, isGuest, hasGuests, canHaveSubtitle} = this.props; + if (!canHaveSubtitle) { + return null; + } + if (!isGuest && !hasGuests) { + return null; + } + + let messageId; + let defaultMessage; + if (channelType === General.DM_CHANNEL) { + messageId = t('channel.isGuest'); + defaultMessage = 'This person is a guest'; + } else if (channelType === General.GM_CHANNEL) { + messageId = t('channel.hasGuests'); + defaultMessage = 'This group message has guests'; + } else if (channelType === General.OPEN_CHANNEL || channelType === General.PRIVATE_CHANNEL) { + messageId = t('channel.channelHasGuests'); + defaultMessage = 'This channel has guests'; + } else { + return null; + } + return ( + + + + ); + } + render() { const {currentChannelName, displayName, isChannelMuted, onPress, theme} = this.props; - const channelName = displayName || currentChannelName; + const style = getStyle(theme); + + const channelName = displayName || currentChannelName; + const hasGuestsText = this.renderHasGuestsText(style); let icon; if (channelName) { icon = ( @@ -85,6 +131,7 @@ export default class ChannelTitle extends PureComponent { {icon} {mutedIcon} + {hasGuestsText} ); } @@ -124,5 +171,17 @@ const getStyle = makeStyleSheetFromTheme((theme) => { color: theme.sidebarHeaderTextColor, paddingRight: 7, }, + guestsWrapper: { + alignItems: 'flex-start', + flex: 1, + position: 'relative', + top: -1, + width: '90%', + }, + guestsText: { + color: theme.sidebarHeaderTextColor, + fontSize: 14, + opacity: 0.6, + }, }; }); diff --git a/app/screens/channel/channel_nav_bar/channel_title/index.js b/app/screens/channel/channel_nav_bar/channel_title/index.js index cdf290a20..23bd5407f 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/index.js +++ b/app/screens/channel/channel_nav_bar/channel_title/index.js @@ -3,22 +3,38 @@ import {connect} from 'react-redux'; -import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; +import {General} from 'mattermost-redux/constants'; +import {getCurrentChannel, getMyCurrentChannelMembership, getCurrentChannelStats} from 'mattermost-redux/selectors/entities/channels'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isChannelMuted} from 'mattermost-redux/utils/channel_utils'; +import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; +import {getUserIdFromChannelName, isChannelMuted} from 'mattermost-redux/utils/channel_utils'; + +import {isGuest} from 'app/utils/users'; import ChannelTitle from './channel_title'; function mapStateToProps(state) { const currentChannel = getCurrentChannel(state); + const currentUserId = getCurrentUserId(state); const myChannelMember = getMyCurrentChannelMembership(state); + const stats = getCurrentChannelStats(state) || {member_count: 0, guest_count: 0}; + + let isTeammateGuest = false; + if (currentChannel && currentChannel.type === General.DM_CHANNEL) { + const teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name); + const teammate = getUser(state, teammateId); + isTeammateGuest = isGuest(teammate); + } return { currentChannelName: currentChannel ? currentChannel.display_name : '', isArchived: currentChannel ? currentChannel.delete_at !== 0 : false, displayName: state.views.channel.displayName, + channelType: currentChannel?.type, isChannelMuted: isChannelMuted(myChannelMember), theme: getTheme(state), + isGuest: isTeammateGuest, + hasGuests: stats.guest_count > 0, }; } diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index c2a07a787..027661a2c 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -53,7 +53,9 @@ export default class ChannelInfo extends PureComponent { currentChannel: PropTypes.object.isRequired, currentChannelCreatorName: PropTypes.string, currentChannelMemberCount: PropTypes.number, + currentChannelGuestCount: PropTypes.number, currentUserId: PropTypes.string, + currentUserIsGuest: PropTypes.bool, status: PropTypes.string, theme: PropTypes.object.isRequired, isChannelMuted: PropTypes.bool.isRequired, @@ -65,6 +67,10 @@ export default class ChannelInfo extends PureComponent { isBot: PropTypes.bool.isRequired, }; + static defaultProps = { + currentChannelGuestCount: 0, + } + static contextTypes = { intl: intlShape.isRequired, }; @@ -333,11 +339,12 @@ export default class ChannelInfo extends PureComponent { renderLeaveOrDeleteChannelRow = () => { const channel = this.props.currentChannel; + const isGuest = this.props.currentUserIsGuest; const isDefaultChannel = channel.name === General.DEFAULT_CHANNEL; const isDirectMessage = channel.type === General.DM_CHANNEL; const isGroupMessage = channel.type === General.GM_CHANNEL; - return !isDefaultChannel && !isDirectMessage && !isGroupMessage; + return (!isDefaultChannel && !isDirectMessage && !isGroupMessage) || (isDefaultChannel && isGuest); }; renderCloseDirect = () => { @@ -471,6 +478,7 @@ export default class ChannelInfo extends PureComponent { currentChannel, currentChannelCreatorName, currentChannelMemberCount, + currentChannelGuestCount, status, theme, isBot, @@ -512,6 +520,7 @@ export default class ChannelInfo extends PureComponent { type={currentChannel.type} isArchived={currentChannel.delete_at !== 0} isBot={isBot} + hasGuests={currentChannelGuestCount > 0} isGroupConstrained={currentChannel.group_constrained} /> } diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index f5ae75dd1..d24921c9f 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -12,6 +12,8 @@ import { } from 'react-native'; import {intlShape} from 'react-intl'; +import {General} from 'mattermost-redux/constants'; + import ChannelIcon from 'app/components/channel_icon'; import FormattedDate from 'app/components/formatted_date'; import FormattedText from 'app/components/formatted_text'; @@ -20,6 +22,7 @@ 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'; export default class ChannelInfoHeader extends React.PureComponent { static propTypes = { @@ -35,6 +38,7 @@ export default class ChannelInfoHeader extends React.PureComponent { type: PropTypes.string.isRequired, isArchived: PropTypes.bool.isRequired, isBot: PropTypes.bool.isRequired, + hasGuests: PropTypes.bool.isRequired, isGroupConstrained: PropTypes.bool, }; @@ -42,6 +46,36 @@ export default class ChannelInfoHeader extends React.PureComponent { intl: intlShape.isRequired, }; + renderHasGuestText = (style) => { + const {type, hasGuests} = this.props; + if (!hasGuests) { + return null; + } + + let messageId; + let defaultMessage; + + if (type === General.GM_CHANNEL) { + messageId = t('channel.hasGuests'); + defaultMessage = 'This group message has guests'; + } else if (type === General.DM_CHANNEL) { + messageId = t('channel.isGuest'); + defaultMessage = 'This person is a guest'; + } else { + messageId = t('channel.channelHasGuests'); + defaultMessage = 'This channel has guests'; + } + return ( + + + + ); + } + handleLongPress = (text, actionText) => { const {formatMessage} = this.context.intl; @@ -128,6 +162,7 @@ export default class ChannelInfoHeader extends React.PureComponent { {displayName} + {this.renderHasGuestText(style)} {purpose.length > 0 && `; + +exports[`SelectTeam should match snapshot when user is a guest 1`] = ` + + + + + + +`; diff --git a/app/screens/select_team/index.js b/app/screens/select_team/index.js index cfbe925f1..7ff9a7eca 100644 --- a/app/screens/select_team/index.js +++ b/app/screens/select_team/index.js @@ -7,16 +7,21 @@ import {connect} from 'react-redux'; import {getTeams, joinTeam} from 'mattermost-redux/actions/teams'; import {logout} from 'mattermost-redux/actions/users'; import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {resetToChannel, dismissModal} from 'app/actions/navigation'; import {handleTeamChange} from 'app/actions/views/select_team'; +import {isGuest} from 'app/utils/users'; import SelectTeam from './select_team.js'; function mapStateToProps(state) { + const currentUser = getCurrentUser(state); + const currentUserIsGuest = isGuest(currentUser); return { teamsRequest: state.requests.teams.getTeams, teams: getJoinableTeams(state), + currentUserIsGuest, }; } diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js index 2e0866aca..4f1b443b8 100644 --- a/app/screens/select_team/select_team.js +++ b/app/screens/select_team/select_team.js @@ -50,6 +50,7 @@ export default class SelectTeam extends PureComponent { }).isRequired, componentId: PropTypes.string.isRequired, currentUrl: PropTypes.string.isRequired, + currentUserIsGuest: PropTypes.bool.isRequired, userWithoutTeams: PropTypes.bool, teams: PropTypes.array.isRequired, theme: PropTypes.object, @@ -241,6 +242,21 @@ export default class SelectTeam extends PureComponent { ); } + if (this.props.currentUserIsGuest) { + return ( + + + + + + + ); + } + return ( diff --git a/app/screens/select_team/select_team.test.js b/app/screens/select_team/select_team.test.js index 7e3a1219b..1a2ddac7a 100644 --- a/app/screens/select_team/select_team.test.js +++ b/app/screens/select_team/select_team.test.js @@ -36,6 +36,7 @@ describe('SelectTeam', () => { const baseProps = { actions, currentChannelId: 'someId', + currentUserIsGuest: false, currentUrl: 'test', userWithoutTeams: false, teams: [], @@ -79,4 +80,25 @@ describe('SelectTeam', () => { wrapper.update(); expect(wrapper.getElement()).toMatchSnapshot(); }); + + test('should match snapshot when user is a guest', async () => { + const props = { + ...baseProps, + currentUserIsGuest: true, + teams: [{ + id: 'kemjcpu9bi877yegqjs18ndp4r', + invite_id: 'ojsnudhqzbfzpk6e4n6ip1hwae', + name: 'test', + }], + teamsRequest: { + status: RequestStatus.SUCCESS, + }, + }; + + const wrapper = shallow( + , + ); + await getTeams(); + expect(wrapper.getElement()).toMatchSnapshot(); + }); }); diff --git a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap index ba003b418..15f733c44 100644 --- a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap +++ b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap @@ -82,6 +82,37 @@ exports[`user_profile should match snapshot 1`] = ` } } /> + + ); } diff --git a/app/screens/user_profile/user_profile.test.js b/app/screens/user_profile/user_profile.test.js index 5c425058f..a8dba3122 100644 --- a/app/screens/user_profile/user_profile.test.js +++ b/app/screens/user_profile/user_profile.test.js @@ -7,6 +7,7 @@ import Preferences from 'mattermost-redux/constants/preferences'; import UserProfile from './user_profile.js'; import BotTag from 'app/components/bot_tag'; +import GuestTag from 'app/components/guest_tag'; jest.mock('react-intl'); jest.mock('app/utils/theme', () => { @@ -88,6 +89,32 @@ describe('user_profile', () => { )).toEqual(true); }); + test('should contain guest tag', async () => { + const guestUser = { + email: 'test@test.com', + first_name: 'test', + id: '4hzdnk6mg7gepe7yze6m3domnc', + last_name: 'fake', + nickname: 'nick', + username: 'fred', + roles: 'system_guest', + }; + + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + expect(wrapper.containsMatchingElement( + + )).toEqual(true); + }); + test('should push EditProfile', async () => { const wrapper = shallow(