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
This commit is contained in:
Jesús Espino 2019-07-22 22:14:39 +02:00 committed by GitHub
parent 15989b79e6
commit ee93e4fa59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 679 additions and 16 deletions

View file

@ -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,

View file

@ -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}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
</TouchableOpacity>

View file

@ -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),
};
}

View file

@ -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}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
</TouchableOpacity>
);
}

View file

@ -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 {
>
<View style={style.indicatorContainer}>
<Text style={style.displayName}>
{index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `}
{this.getDisplayName(member)}
</Text>
<BotTag
show={Boolean(member.is_bot)}
theme={theme}
/>
<GuestTag
show={isGuest(member)}
theme={theme}
/>
<Text style={style.displayName}>
{index === currentChannelMembers.length - 1 ? '' : ', '}
</Text>
</View>
</TouchableOpacity>
);

View file

@ -91,6 +91,37 @@ exports[`UserListRow should match snapshot 1`] = `
}
}
/>
<GuestTag
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>
@ -187,6 +218,37 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
}
}
/>
<GuestTag
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>
@ -285,6 +347,37 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
}
}
/>
<GuestTag
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>
@ -302,3 +395,132 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
</CustomListRow>
</View>
`;
exports[`UserListRow should match snapshot for guest user 1`] = `
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
}
}
>
<CustomListRow
enabled={true}
id="21345"
onPress={[Function]}
>
<View
style={
Object {
"alignItems": "center",
"color": "#3d3c40",
"flexDirection": "row",
}
}
>
<Connect(ProfilePicture)
size={32}
userId="21345"
/>
</View>
<View
style={
Object {
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"marginLeft": 10,
}
}
>
<View>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<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",
}
}
/>
<GuestTag
show={true}
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>
</View>
`;

View file

@ -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}
/>
<GuestTag
show={isGuest(user)}
theme={theme}
/>
</View>
</View>
{showTeammateDisplay &&

View file

@ -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(
<UserListRow {...newProps}/>,
{context: {intl: {formatMessage}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for currentUser with (you) populated in list', () => {
const newProps = {
...baseProps,

View file

@ -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 (
<FormattedText
id='post_info.guest'
defaultMessage='GUEST'
style={[style.guest, this.props.inTitle ? style.guestTitle : null]}
/>
);
}
}
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),
},
};
});

View file

@ -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,
};
};

View file

@ -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 {
</View>
</TouchableOpacity>
);
} else if (isGuest) {
return (
<TouchableOpacity onPress={this.handleUsernamePress}>
<View style={style.indicatorContainer}>
<Text style={style.displayName}>
{this.props.displayName}
</Text>
<GuestTag
theme={this.props.theme}
/>
</View>
</TouchableOpacity>
);
} else if (fromAutoResponder) {
let name = this.props.displayName;
if (overrideUsername && enablePostUsernameOverride) {

View file

@ -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,
};
};
}

View file

@ -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,

View file

@ -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',
};

View file

@ -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}
/>
<ChannelTitle onPress={onPress}/>
<ChannelTitle
onPress={onPress}
canHaveSubtitle={canHaveSubtitle}
/>
<ChannelSearchButton
theme={theme}
/>

View file

@ -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 (
<View style={style.guestsWrapper}>
<FormattedText
numberOfLines={1}
ellipsizeMode='tail'
id={messageId}
defaultMessage={defaultMessage}
style={style.guestsText}
/>
</View>
);
}
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}
</View>
{hasGuestsText}
</TouchableOpacity>
);
}
@ -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,
},
};
});

View file

@ -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,
};
}

View file

@ -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}
/>
}

View file

@ -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 (
<View style={style.section}>
<FormattedText
style={style.header}
id={messageId}
defaultMessage={defaultMessage}
/>
</View>
);
}
handleLongPress = (text, actionText) => {
const {formatMessage} = this.context.intl;
@ -128,6 +162,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
{displayName}
</Text>
</View>
{this.renderHasGuestText(style)}
{purpose.length > 0 &&
<View style={style.section}>
<TouchableHighlight

View file

@ -29,6 +29,7 @@ import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from
import {areChannelMentionsIgnored, getUserIdFromChannelName, isChannelMuted, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils';
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isGuest} from 'app/utils/users';
import {
popTopScreen,
@ -56,6 +57,7 @@ function mapStateToProps(state) {
const currentChannelCreatorName = currentChannelCreator && currentChannelCreator.username;
const currentChannelStats = getCurrentChannelStats(state);
const currentChannelMemberCount = currentChannelStats && currentChannelStats.member_count;
let currentChannelGuestCount = (currentChannelStats && currentChannelStats.guest_count) || 0;
const currentChannelMember = getMyCurrentChannelMembership(state);
const currentUserId = getCurrentUserId(state);
const favoriteChannels = getSortedFavoriteChannelIds(state);
@ -67,6 +69,7 @@ function mapStateToProps(state) {
canManageUsers = false;
}
const currentUser = getUser(state, currentUserId);
const currentUserIsGuest = isGuest(currentUser);
let status;
let isBot = false;
@ -77,6 +80,9 @@ function mapStateToProps(state) {
if (teammate && teammate.is_bot) {
isBot = true;
}
if (isGuest(teammate)) {
currentChannelGuestCount = 1;
}
}
const isAdmin = checkIsAdmin(roles);
@ -94,7 +100,9 @@ function mapStateToProps(state) {
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,
currentChannelGuestCount,
currentUserId,
currentUserIsGuest,
isChannelMuted: isChannelMuted(currentChannelMember),
ignoreChannelMentions: areChannelMentionsIgnored(currentChannelMember && currentChannelMember.notify_props, currentUser.notify_props),
isCurrent,

View file

@ -143,3 +143,37 @@ exports[`SelectTeam should match snapshot for teams 1`] = `
/>
</View>
`;
exports[`SelectTeam should match snapshot when user is a guest 1`] = `
<View
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<Connect(StatusBar) />
<View
style={
Object {
"alignItems": "center",
"flexDirection": "row",
"marginHorizontal": 16,
"marginTop": 20,
}
}
>
<FormattedText
defaultMessage="Your guest account has no teams or channels assigned. Please contact an administrator."
id="mobile.select_team.guest_cant_join_team"
style={
Object {
"color": undefined,
"fontSize": 13,
}
}
/>
</View>
</View>
`;

View file

@ -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,
};
}

View file

@ -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 (
<View style={styles.container}>
<StatusBar/>
<View style={styles.headingContainer}>
<FormattedText
id='mobile.select_team.guest_cant_join_team'
defaultMessage='Your guest account has no teams or channels assigned. Please contact an administrator.'
style={styles.heading}
/>
</View>
</View>
);
}
return (
<View style={styles.container}>
<StatusBar/>

View file

@ -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(
<SelectTeam {...props}/>,
);
await getTeams();
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -82,6 +82,37 @@ exports[`user_profile should match snapshot 1`] = `
}
}
/>
<GuestTag
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={

View file

@ -20,9 +20,11 @@ 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 GuestTag from 'app/components/guest_tag';
import {alertErrorWithFallback} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import {isGuest} from 'app/utils/users';
import UserProfileRow from './user_profile_row';
import Config from 'assets/config';
@ -129,6 +131,10 @@ export default class UserProfile extends PureComponent {
show={Boolean(user.is_bot)}
theme={theme}
/>
<GuestTag
show={isGuest(user)}
theme={theme}
/>
</View>
);
}

View file

@ -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(
<UserProfile
{...baseProps}
user={guestUser}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.containsMatchingElement(
<GuestTag
show={true}
theme={baseProps.theme}
/>
)).toEqual(true);
});
test('should push EditProfile', async () => {
const wrapper = shallow(
<UserProfile

23
app/utils/users.js Normal file
View file

@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// @flow
export function isInRole(roles, inRole) {
if (roles) {
var parts = roles.split(' ');
for (var i = 0; i < parts.length; i++) {
if (parts[i] === inRole) {
return true;
}
}
}
return false;
}
export function isGuest(user) {
if (user && user.roles && isInRole(user.roles, 'system_guest')) {
return true;
}
return false;
}

View file

@ -35,6 +35,9 @@
"channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.ignoreChannelMentions.settings": "Ignore @channel, @here, @all",
"channel_notifications.muteChannel.settings": "Mute channel",
"channel.channelHasGuests": "This channel has guests",
"channel.hasGuests": "This group message has guests",
"channel.isGuest": "This person is a guest",
"combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.",
"combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.",
"combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.",
@ -416,6 +419,7 @@
"mobile.search.no_results": "No Results Found",
"mobile.search.on_modifier_description": "to find posts on a specific date",
"mobile.search.recent_title": "Recent Searches",
"mobile.select_team.guest_cant_join_team": "Your guest account has no teams or channels assigned. Please contact an administrator.",
"mobile.select_team.join_open": "Open teams you can join",
"mobile.select_team.no_teams": "There are no available teams for you to join.",
"mobile.server_upgrade.button": "OK",
@ -483,6 +487,7 @@
"post_info.bot": "BOT",
"post_info.del": "Delete",
"post_info.edit": "Edit",
"post_info.guest": "GUEST",
"post_info.message.show_less": "Show less",
"post_info.message.show_more": "Show more",
"post_info.system": "System",

4
package-lock.json generated
View file

@ -13150,8 +13150,8 @@
"integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A=="
},
"mattermost-redux": {
"version": "github:mattermost/mattermost-redux#3277c79c0312a6b4dd4c9421d09d6aaa1c18307a",
"from": "github:mattermost/mattermost-redux#3277c79c0312a6b4dd4c9421d09d6aaa1c18307a",
"version": "github:mattermost/mattermost-redux#3e283f1d52cd70a9323e314b9d95757b9e216aea",
"from": "github:mattermost/mattermost-redux#3e283f1d52cd70a9323e314b9d95757b9e216aea",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "3.0.0",

View file

@ -20,7 +20,7 @@
"intl": "1.2.5",
"jail-monkey": "2.2.0",
"jsc-android": "241213.2.0",
"mattermost-redux": "github:mattermost/mattermost-redux#3277c79c0312a6b4dd4c9421d09d6aaa1c18307a",
"mattermost-redux": "github:mattermost/mattermost-redux#3e283f1d52cd70a9323e314b9d95757b9e216aea",
"mime-db": "1.40.0",
"moment-timezone": "0.5.25",
"prop-types": "15.7.2",
@ -146,4 +146,4 @@
"node_modules/(?!react-native|jail-monkey)"
]
}
}
}