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(