MM-32921 shared channels (#5241)

* Unable to open previews from search, pinned and mentions

* Updated Compass Icons

* Added Icon to LHS and Channel Info

* Added Icons to Manage/View Members list

* Added Icon to shared users in posts

* Added icon to channel header, fixed header style

* Added Icons in autocomplete

* WIP: Add shared shannels to browse

* Adding Shared Channels string to i18n

* Added remote organization to remote user profile

* Updated snapshot for channel header

* Added browsing shared channels

* Removed the exmpty line

* Added snapshot when user is remote

* Reverted compass icons and added icons only needed for shared channels

* Fixed i18n swapped

* Copied compass-icons from webapp

* Fixed showing shared channels as deleted after browsing them

* Added new compass ttf to android & fixed icon style for browse channel listing

* Fixed search for shared channels

* user profile snapshot updated with testId

* User list row snapshot updated with testID

* Moved shared user check to util function

* Fixed required props warning for ChannelIcon component

* Update app/screens/user_profile/index.js

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Removed Request related redux

* Reverted client4

* Added rest calls

* Updated snapshot

* Reverted files

* Adding back shared channels stuff to channel icon & showing shared channels only when enabled in browse channels

* Fixed misc issues

* moved empty array outside the function to avoid re-render

* Removed renaming fields

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Anurag Shivarathri 2021-05-24 16:07:47 +05:30 committed by GitHub
parent 6ea6366e1c
commit 6e23fbe7a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1102 additions and 94 deletions

View file

@ -186,6 +186,10 @@ export default class ClientBase {
return `${this.getPostsRoute()}/${postId}`;
}
getSharedChannelsRoute() {
return `${this.getBaseRoute()}/sharedchannels`;
}
getReactionsRoute() {
return `${this.getBaseRoute()}/reactions`;
}

View file

@ -15,6 +15,7 @@ import ClientGroups, {ClientGroupsMix} from './groups';
import ClientIntegrations, {ClientIntegrationsMix} from './integrations';
import ClientPosts, {ClientPostsMix} from './posts';
import ClientPreferences, {ClientPreferencesMix} from './preferences';
import ClientSharedChannels, {ClientSharedChannelsMix} from './shared_channels';
import ClientTeams, {ClientTeamsMix} from './teams';
import ClientTos, {ClientTosMix} from './tos';
import ClientUsers, {ClientUsersMix} from './users';
@ -30,6 +31,7 @@ interface Client extends ClientBase,
ClientIntegrationsMix,
ClientPostsMix,
ClientPreferencesMix,
ClientSharedChannelsMix,
ClientTeamsMix,
ClientTosMix,
ClientUsersMix
@ -46,6 +48,7 @@ class Client extends mix(ClientBase).with(
ClientIntegrations,
ClientPosts,
ClientPreferences,
ClientSharedChannels,
ClientTeams,
ClientTos,
ClientUsers,

View file

@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {SharedChannel} from '@mm-redux/types/channels';
import {RemoteCluster} from '@mm-redux/types/remote_cluster';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientSharedChannelsMix {
getRemoteClusterInfo: (remote_id: string) => Promise<RemoteCluster>;
getSharedChannels: (teamId: string, page: number, perPage?: number) => Promise<SharedChannel[]>;
}
const ClientSharedChannels = (superclass: any) => class extends superclass {
getRemoteClusterInfo = async (remote_id: string) => {
const response = await this.doFetch(
`${this.getSharedChannelsRoute()}/remote_info/${remote_id}`,
{method: 'get'},
);
return {
...response,
remote_id,
};
};
getSharedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getSharedChannelsRoute()}/${teamId}${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
};
export default ClientSharedChannels;

View file

@ -5,10 +5,12 @@ import React from 'react';
import {Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import ChannelIcon from '@components/channel_icon';
import FormattedText from '@components/formatted_text';
import ProfilePicture from '@components/profile_picture';
import {BotTag, GuestTag} from '@components/tag';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {General} from '@mm-redux/constants';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import type {Theme} from '@mm-redux/types/preferences';
@ -18,6 +20,7 @@ interface AtMentionItemProps {
isBot: boolean;
isCurrentUser: boolean;
isGuest: boolean;
isShared: boolean;
lastName: string;
nickname: string;
onPress: (username: string) => void;
@ -45,6 +48,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
alignItems: 'center',
justifyContent: 'center',
},
rowInfo: {
flexDirection: 'row',
flex: 1,
},
rowFullname: {
fontSize: 15,
color: theme.centerChannelColor,
@ -66,6 +73,7 @@ const AtMentionItem = (props: AtMentionItemProps) => {
isBot,
isCurrentUser,
isGuest,
isShared,
lastName,
nickname,
onPress,
@ -118,33 +126,47 @@ const AtMentionItem = (props: AtMentionItemProps) => {
testID={`${testID}.profile_picture`}
/>
</View>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
{Boolean(name.length) &&
<Text
style={style.rowFullname}
numberOfLines={1}
>
{name}
</Text>
}
<Text
style={style.rowUsername}
numberOfLines={1}
>
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage='(you)'
/>}
{` @${username}`}
</Text>
<View style={style.rowInfo}>
<BotTag
show={isBot}
theme={theme}
/>
<GuestTag
show={isGuest}
theme={theme}
/>
{Boolean(name.length) &&
<Text
style={style.rowFullname}
numberOfLines={1}
>
{name} {name}
</Text>
}
<Text
style={style.rowUsername}
numberOfLines={1}
>
{isCurrentUser &&
<FormattedText
id='suggestion.mention.you'
defaultMessage='(you)'
/>}
{` @${username}`}
</Text>
</View>
{isShared && (
<ChannelIcon
isActive={false}
isArchived={false}
isInfo={true}
isUnread={true}
size={18}
shared={true}
theme={theme}
type={General.DM_CHANNEL}
/>
)}
</View>
</TouchableWithFeedback>
);

View file

@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {isShared} from '@mm-redux/utils/user_utils';
import {isGuest} from '@utils/users';
import AtMentionItem from './at_mention_item';
@ -21,6 +22,7 @@ function mapStateToProps(state, ownProps) {
showFullName: config.ShowFullName,
isBot: Boolean(user.is_bot),
isGuest: isGuest(user),
isShared: isShared(user),
theme: getTheme(state),
isCurrentUser: getCurrentUserId(state) === user.id,
};

View file

@ -47,6 +47,7 @@ const ChannelMentionItem = (props) => {
isGuest,
name,
onPress,
shared,
testID,
theme,
type,
@ -64,7 +65,9 @@ const ChannelMentionItem = (props) => {
const margins = {marginLeft: insets.left, marginRight: insets.right};
let iconName = 'globe';
let component;
if (type === General.PRIVATE_CHANNEL) {
if (shared) {
iconName = type === General.PRIVATE_CHANNEL ? 'circle-multiple-outline-lock' : 'circle-multiple-outline';
} else if (type === General.PRIVATE_CHANNEL) {
iconName = 'lock';
}

View file

@ -30,6 +30,7 @@ function mapStateToProps(state, ownProps) {
return {
displayName,
name: channel?.name,
shared: channel?.shared,
type: channel?.type,
isBot,
isGuest,

View file

@ -18,8 +18,10 @@ type Props = {
isInfo: boolean;
isUnread: boolean;
membersCount: number;
shared: boolean;
size: number;
statusStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
testID?: string;
theme: Theme;
type: string;
@ -105,7 +107,6 @@ const ChannelIcon = (props: Props) => {
}
let icon;
let extraStyle;
if (props.isArchived) {
icon = (
<CompassIcon
@ -122,6 +123,16 @@ const ChannelIcon = (props: Props) => {
testID={`${props.testID}.draft`}
/>
);
} else if (props.shared) {
const iconName = props.type === General.PRIVATE_CHANNEL ? 'circle-multiple-outline-lock' : 'circle-multiple-outline';
const sharedTestID = props.type === General.PRIVATE_CHANNEL ? 'channel_icon.shared_private' : 'channel_icon.shared_open';
icon = (
<CompassIcon
name={iconName}
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 0.5}]}
testID={sharedTestID}
/>
);
} else if (props.type === General.OPEN_CHANNEL) {
icon = (
<CompassIcon
@ -152,7 +163,6 @@ const ChannelIcon = (props: Props) => {
</View>
);
} else if (props.type === General.DM_CHANNEL) {
// extraStyle = {marginRight: 6};
icon = (
<ProfilePicture
size={props.size}
@ -165,7 +175,7 @@ const ChannelIcon = (props: Props) => {
}
return (
<View style={[style.container, extraStyle, {width: props.size, height: props.size}]}>
<View style={[style.container, {width: props.size, height: props.size}, props.style]}>
{icon}
</View>
);

View file

@ -44,6 +44,12 @@ export default class ChannelListRow extends React.PureComponent {
const testID = this.props.testID;
const itemTestID = `${testID}.${this.props.id}`;
const channelDisplayNameTestID = `${testID}.display_name`;
let icon = 'globe';
if (this.props.isArchived) {
icon = 'archive-outline';
} else if (this.props.channel?.shared) {
icon = 'circle-multiple-outline';
}
return (
<CustomListRow
@ -60,7 +66,7 @@ export default class ChannelListRow extends React.PureComponent {
>
<View style={style.titleContainer}>
<CompassIcon
name={this.props.isArchived ? 'archive-outline' : 'globe'}
name={icon}
style={style.icon}
/>
<Text

View file

@ -76,7 +76,7 @@ const style = StyleSheet.create({
width: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: '#888',
borderColor: 'rgba(61, 60, 64, 0.32)',
alignItems: 'center',
justifyContent: 'center',
},
@ -87,7 +87,7 @@ const style = StyleSheet.create({
justifyContent: 'center',
},
selectorDisabled: {
backgroundColor: '#888',
borderColor: 'rgba(61, 60, 64, 0.16)',
},
selectorFilled: {
backgroundColor: '#166DE0',

View file

@ -552,3 +552,186 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
</CustomListRow>
</View>
`;
exports[`UserListRow should match snapshot for remote user 1`] = `
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
"overflow": "hidden",
}
}
>
<CustomListRow
enabled={true}
id="21345"
onPress={[Function]}
testID="custom_list.user_item"
>
<View
style={
Object {
"alignItems": "center",
"color": "#3d3c40",
"flexDirection": "row",
}
}
>
<Connect(ProfilePicture)
iconSize={24}
size={32}
testID="custom_list.user_item.21345.profile_picture"
userId="21345"
/>
</View>
<View
style={
Object {
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"marginLeft": 10,
}
}
testID="custom_list.user_item.21345"
>
<View>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
testID="custom_list.user_item.display_username"
>
@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",
"mentionBg": "#ffffff",
"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={false}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"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>
<ChannelIcon
hasDraft={false}
isActive={false}
isArchived={false}
isBot={false}
isInfo={true}
isUnread={true}
membersCount={0}
shared={true}
size={18}
style={
Object {
"alignSelf": "center",
"opacity": 0.75,
}
}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"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",
}
}
type="D"
/>
</CustomListRow>
</View>
`;

View file

@ -9,7 +9,9 @@ import {
View,
} from 'react-native';
import {displayUsername} from '@mm-redux/utils/user_utils';
import ChannelIcon from '@components/channel_icon';
import {General} from '@mm-redux/constants';
import {displayUsername, isShared} from '@mm-redux/utils/user_utils';
import CustomListRow from 'app/components/custom_list/custom_list_row';
import ProfilePicture from 'app/components/profile_picture';
@ -38,6 +40,27 @@ export default class UserListRow extends React.PureComponent {
}
};
renderIcon = (style) => {
const {theme, user} = this.props;
if (!isShared(user)) {
return null;
}
return (
<ChannelIcon
isActive={false}
isArchived={false}
isBot={false}
isUnread={true}
isInfo={true}
size={18}
shared={true}
style={style.sharedUserIcon}
theme={theme}
type={General.DM_CHANNEL}
/>
);
};
render() {
const {formatMessage} = this.context.intl;
const {
@ -131,6 +154,7 @@ export default class UserListRow extends React.PureComponent {
</View>
}
</View>
{this.renderIcon(style)}
</CustomListRow>
</View>
);
@ -172,5 +196,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
sharedUserIcon: {
alignSelf: 'center',
opacity: 0.75,
},
};
});

View file

@ -75,6 +75,22 @@ describe('UserListRow', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot for remote user', () => {
const newProps = {
...baseProps,
user: {
...baseProps.user,
remote_id: 'abc123',
},
};
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

@ -10,7 +10,7 @@ import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
import {getUser, getCurrentUser} from '@mm-redux/selectors/entities/users';
import {isPostPendingOrFailed, isSystemMessage, isFromWebhook} from '@mm-redux/utils/post_utils';
import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {displayUsername, isShared} from '@mm-redux/utils/user_utils';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {fromAutoResponder} from 'app/utils/general';
@ -59,6 +59,7 @@ function makeMapStateToProps() {
isBot: user.is_bot || false,
isGuest: isGuest(user),
isLandscape: isLandscape(state),
isShared: isShared(user),
userTimezone,
};
};

View file

@ -41,6 +41,7 @@ export default class PostHeader extends PureComponent {
username: PropTypes.string,
isBot: PropTypes.bool,
isGuest: PropTypes.bool,
isShared: PropTypes.bool,
userTimezone: PropTypes.string,
enableTimezone: PropTypes.bool,
previousPostExists: PropTypes.bool,
@ -214,6 +215,19 @@ export default class PostHeader extends PureComponent {
);
};
renderMemberTypeIcon = (style) => {
if (!this.props.isShared) {
return null;
}
return (
<CompassIcon
name='circle-multiple-outline'
size={17}
style={style.sharedUserIcon}
/>
);
};
renderReply = () => {
const {
commentCount,
@ -328,6 +342,7 @@ export default class PostHeader extends PureComponent {
<View style={[style.container, (isPendingOrFailedPost && style.pendingPost)]}>
<View style={style.wrapper}>
{this.renderDisplayName()}
{this.renderMemberTypeIcon(style)}
{this.renderTag()}
{dateComponent}
{this.renderReply()}
@ -409,6 +424,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
displayNameContainerLandscapeBotReplyWidth: {
maxWidth: '70%',
},
sharedUserIcon: {
color: changeOpacity(theme.centerChannelColor, 0.75),
top: 3,
marginRight: 6,
},
};
});

View file

@ -161,6 +161,7 @@ export default class ChannelItem extends PureComponent {
isUnread={isUnread}
hasDraft={hasDraft && channelId !== currentChannelId}
membersCount={displayName.split(',').length}
shared={channel.shared && channel.type !== General.DM_CHANNEL}
statusStyle={{backgroundColor: theme.sidebarBg, borderColor: 'transparent'}}
size={24}
theme={theme}

View file

@ -18,6 +18,7 @@ import GroupTypes from './groups';
import BotTypes from './bots';
import PluginTypes from './plugins';
import ChannelCategoryTypes from './channel_categories';
import RemoteClusterTypes from './remote_cluster';
import AppsTypes from './apps';
export {
@ -38,5 +39,6 @@ export {
BotTypes,
PluginTypes,
ChannelCategoryTypes,
RemoteClusterTypes,
AppsTypes,
};

View file

@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import keyMirror from '@mm-redux/utils/key_mirror';
export default keyMirror({
REMOTE_CLUSTER_INFO_RECEIVED: null,
});

View file

@ -19,6 +19,7 @@ import {Action, ActionFunc, batchActions, DispatchFunc, GetStateFunc} from '@mm-
import {Channel, ChannelNotifyProps, ChannelMembership} from '@mm-redux/types/channels';
import {PreferenceType} from '@mm-redux/types/preferences';
import {Dictionary} from '@mm-redux/types/utilities';
import {logError} from './errors';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
@ -939,6 +940,44 @@ export function getArchivedChannels(teamId: string, page = 0, perPage: number =
};
}
// Returns all public shared channels
export function getSharedChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let channels;
try {
channels = await Client4.getSharedChannels(teamId, page, perPage);
channels = (channels || []).map((channel: Dictionary<string|boolean|number>) => {
if (channel.delete_at === undefined) {
channel.delete_at = 0;
}
channel.type = General.OPEN_CHANNEL;
channel.shared = true;
return channel;
});
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(batchActions([
{type: ChannelTypes.GET_CHANNELS_FAILURE, error},
logError(error),
]));
return {error};
}
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_CHANNELS,
teamId,
data: channels,
},
{
type: ChannelTypes.GET_CHANNELS_SUCCESS,
},
]));
return {data: channels};
};
}
export function getAllChannelsWithCount(page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE, notAssociatedToGroup = '', excludeDefaultChannels = false): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

View file

@ -17,6 +17,7 @@ import * as search from './search';
import * as teams from './teams';
import * as timezone from './timezone';
import * as users from './users';
import * as remoteCluster from './remote_cluster';
export {
bots,
@ -35,5 +36,6 @@ export {
teams,
timezone,
users,
remoteCluster,
};

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
import {RemoteClusterTypes} from '@mm-redux/action_types';
import {bindClientFunc} from './helpers';
export function getRemoteClusterInfo(remote_id: string) {
return bindClientFunc({
clientFunc: Client4.getRemoteClusterInfo,
onSuccess: [RemoteClusterTypes.REMOTE_CLUSTER_INFO_RECEIVED],
params: [
remote_id,
],
});
}

View file

@ -19,6 +19,7 @@ import roles from './roles';
import groups from './groups';
import bots from './bots';
import channelCategories from './channel_categories';
import remoteCluster from './remote_cluster';
import apps from './apps';
export default combineReducers({
@ -38,5 +39,6 @@ export default combineReducers({
groups,
bots,
channelCategories,
remoteCluster,
apps,
});

View file

@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {RemoteClusterTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {Dictionary} from '@mm-redux/types/utilities';
import {RemoteCluster} from '@mm-redux/types/remote_cluster';
function info(state: Dictionary<RemoteCluster> = {}, action: GenericAction) {
switch (action.type) {
case RemoteClusterTypes.REMOTE_CLUSTER_INFO_RECEIVED: {
const nextState = {...state};
nextState[action.data.remote_id] = action.data;
return nextState;
}
default:
return state;
}
}
export default combineReducers({
info,
});

View file

@ -106,3 +106,18 @@ export type ChannelMemberCountByGroup = {
};
export type ChannelMemberCountsByGroup = Record<string, ChannelMemberCountByGroup>;
export type SharedChannel = {
channel_id: string;
team_id: string;
home: boolean;
readonly: boolean;
share_name: string;
share_displayname: string;
share_purpose: string;
share_header: string;
creator_id: string;
create_at: number;
update_at: number;
remote_id: string;
};

View file

@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type RemoteCluster = {
display_name: string;
create_at: number;
remote_id: string;
};

View file

@ -17,6 +17,7 @@ import {Role} from './roles';
import {PreferenceType} from './preferences';
import {Bot} from './bots';
import {ChannelCategoriesState} from './channel_categories';
import {RemoteCluster} from './remote_cluster';
import {Dictionary} from './utilities';
import {AppsState} from './apps';
@ -49,6 +50,11 @@ export type GlobalState = {
gifs: any;
groups: GroupsState;
channelCategories: ChannelCategoriesState;
remoteCluster: {
info: {
[x: string]: RemoteCluster;
};
};
apps: AppsState;
};
errors: Array<any>;

View file

@ -35,6 +35,7 @@ export type UserProfile = {
last_name: string;
position: string;
roles: string;
remote_id?: string;
locale: string;
notify_props: UserNotifyProps;
terms_of_service_id: string;

View file

@ -64,6 +64,10 @@ export function isChannelAdmin(roles: string): boolean {
return rolesIncludePermission(roles, General.CHANNEL_ADMIN_ROLE);
}
export function isShared(user: UserProfile): boolean {
return Boolean(user.remote_id);
}
export function hasUserAccessTokenRole(roles: string): boolean {
return rolesIncludePermission(roles, General.SYSTEM_USER_ACCESS_TOKEN_ROLE);
}

View file

@ -18,7 +18,6 @@ exports[`ChannelTitle should match snapshot 1`] = `
"justifyContent": "flex-start",
"position": "relative",
"top": -1,
"width": "90%",
}
}
>
@ -28,6 +27,8 @@ exports[`ChannelTitle should match snapshot 1`] = `
style={
Object {
"color": undefined,
"flex": 0,
"flexShrink": 1,
"fontSize": 18,
"fontWeight": "bold",
"textAlign": "center",
@ -69,7 +70,6 @@ exports[`ChannelTitle should match snapshot when is DM and has guests and the te
"justifyContent": "flex-start",
"position": "relative",
"top": -1,
"width": "90%",
}
}
>
@ -79,6 +79,8 @@ exports[`ChannelTitle should match snapshot when is DM and has guests and the te
style={
Object {
"color": undefined,
"flex": 0,
"flexShrink": 1,
"fontSize": 18,
"fontWeight": "bold",
"textAlign": "center",
@ -145,7 +147,6 @@ exports[`ChannelTitle should match snapshot when is DM and has guests but the te
"justifyContent": "flex-start",
"position": "relative",
"top": -1,
"width": "90%",
}
}
>
@ -155,6 +156,8 @@ exports[`ChannelTitle should match snapshot when is DM and has guests but the te
style={
Object {
"color": undefined,
"flex": 0,
"flexShrink": 1,
"fontSize": 18,
"fontWeight": "bold",
"textAlign": "center",
@ -178,6 +181,77 @@ exports[`ChannelTitle should match snapshot when is DM and has guests but the te
</ForwardRef>
`;
exports[`ChannelTitle should match snapshot when isChannelShared is true 1`] = `
<ForwardRef
style={
Object {
"flex": 1,
}
}
testID="channel.title.button"
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "flex-start",
"position": "relative",
"top": -1,
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": undefined,
"flex": 0,
"flexShrink": 1,
"fontSize": 18,
"fontWeight": "bold",
"textAlign": "center",
}
}
testID="channel.nav_bar.title"
>
My User
</Text>
<ChannelIcon
hasDraft={false}
isActive={true}
isArchived={false}
isBot={false}
isInfo={false}
isUnread={false}
membersCount={0}
shared={true}
size={18}
style={
Object {
"marginLeft": 3,
"marginRight": 0,
}
}
theme={Object {}}
type="P"
/>
<CompassIcon
name="chevron-down"
size={24}
style={
Object {
"color": undefined,
"marginHorizontal": 1,
}
}
/>
</View>
</ForwardRef>
`;
exports[`ChannelTitle should match snapshot when isSelfDMChannel is true 1`] = `
<ForwardRef
style={
@ -196,7 +270,6 @@ exports[`ChannelTitle should match snapshot when isSelfDMChannel is true 1`] = `
"justifyContent": "flex-start",
"position": "relative",
"top": -1,
"width": "90%",
}
}
>
@ -206,6 +279,8 @@ exports[`ChannelTitle should match snapshot when isSelfDMChannel is true 1`] = `
style={
Object {
"color": undefined,
"flex": 0,
"flexShrink": 1,
"fontSize": 18,
"fontWeight": "bold",
"textAlign": "center",

View file

@ -12,6 +12,7 @@ import {
import {General} from '@mm-redux/constants';
import ChannelIcon from '@components/channel_icon';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {makeStyleSheetFromTheme} from '@utils/theme';
@ -19,17 +20,18 @@ import {t} from '@utils/i18n';
export default class ChannelTitle extends PureComponent {
static propTypes = {
canHaveSubtitle: PropTypes.bool.isRequired,
channelType: PropTypes.string,
currentChannelName: PropTypes.string,
displayName: PropTypes.string,
channelType: PropTypes.string,
hasGuests: PropTypes.bool.isRequired,
isArchived: PropTypes.bool,
isChannelMuted: PropTypes.bool,
isChannelShared: PropTypes.bool,
isGuest: PropTypes.bool.isRequired,
isSelfDMChannel: PropTypes.bool.isRequired,
onPress: PropTypes.func,
theme: PropTypes.object,
isArchived: PropTypes.bool,
isGuest: PropTypes.bool.isRequired,
hasGuests: PropTypes.bool.isRequired,
canHaveSubtitle: PropTypes.bool.isRequired,
isSelfDMChannel: PropTypes.bool.isRequired,
};
static defaultProps = {
@ -119,7 +121,9 @@ export default class ChannelTitle extends PureComponent {
render() {
const {
channelType,
isChannelMuted,
isChannelShared,
onPress,
theme,
} = this.props;
@ -150,6 +154,22 @@ export default class ChannelTitle extends PureComponent {
);
}
let channelIcon;
if (isChannelShared) {
channelIcon = (
<ChannelIcon
isActive={true}
isArchived={false}
isBot={false}
size={18}
shared={isChannelShared}
style={style.channelIconContainer}
theme={theme}
type={channelType}
/>
);
}
return (
<TouchableOpacity
testID={'channel.title.button'}
@ -166,6 +186,7 @@ export default class ChannelTitle extends PureComponent {
>
{channelDisplayName}
</Text>
{channelIcon}
{icon}
{mutedIcon}
</View>
@ -187,7 +208,6 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
top: -1,
flexDirection: 'row',
justifyContent: 'flex-start',
width: '90%',
},
icon: {
color: theme.sidebarHeaderTextColor,
@ -198,6 +218,12 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
flex: 0,
flexShrink: 1,
},
channelIconContainer: {
marginLeft: 3,
marginRight: 0,
},
muted: {
marginTop: 1,

View file

@ -73,4 +73,19 @@ describe('ChannelTitle', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when isChannelShared is true', () => {
const props = {
...baseProps,
displayName: 'My User',
isChannelShared: true,
channelType: General.PRIVATE_CHANNEL,
};
const wrapper = shallow(
<ChannelTitle {...props}/>,
{context: {intl: {formatMessage: (intlId) => intlId.defaultMessage}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View file

@ -29,15 +29,16 @@ function mapStateToProps(state) {
}
return {
isSelfDMChannel,
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,
currentChannelName: currentChannel ? currentChannel.display_name : '',
displayName: state.views.channel.displayName,
hasGuests: stats.guest_count > 0,
isArchived: currentChannel ? currentChannel.delete_at !== 0 : false,
isChannelMuted: isChannelMuted(myChannelMember),
isChannelShared: currentChannel?.shared,
isGuest: isTeammateGuest,
isSelfDMChannel,
theme: getTheme(state),
};
}

View file

@ -198,6 +198,7 @@ export default class ChannelInfo extends PureComponent {
memberCount={currentChannelMemberCount}
onPermalinkPress={this.handlePermalinkPress}
purpose={currentChannel.purpose}
shared={currentChannel.shared}
teammateId={teammateId}
theme={theme}
type={currentChannel.type}

View file

@ -33,6 +33,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
header: PropTypes.string,
onPermalinkPress: PropTypes.func,
purpose: PropTypes.string,
shared: PropTypes.bool,
teammateId: PropTypes.string,
theme: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
@ -133,6 +134,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
memberCount,
onPermalinkPress,
purpose,
shared,
teammateId,
theme,
type,
@ -158,6 +160,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
membersCount={memberCount}
size={24}
userId={teammateId}
shared={shared}
theme={theme}
type={type}
isArchived={isArchived}

View file

@ -18,7 +18,7 @@ import SearchBar from '@components/search_bar';
import StatusBar from '@components/status_bar';
import {debounce} from '@mm-redux/actions/helpers';
import {General} from '@mm-redux/constants';
import {filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils';
import {filterProfilesMatchingTerm, isShared} from '@mm-redux/utils/user_utils';
import {alertErrorIfInvalidPermissions} from '@utils/general';
import {createProfilesSections, loadingText} from '@utils/member_list';
import {
@ -243,7 +243,7 @@ export default class ChannelMembers extends PureComponent {
const selectProps = {
selectable: true,
selected: this.state.selectedIds[props.id],
enabled: props.id !== this.props.currentUserId,
enabled: props.id !== this.props.currentUserId && !isShared(props.item),
};
return this.renderItem(props, selectProps);

View file

@ -3,12 +3,10 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {handleSelectChannel, setChannelDisplayName} from '@actions/views/channel';
import {General} from '@mm-redux/constants';
import {getArchivedChannels, getChannels, joinChannel, searchChannels} from '@mm-redux/actions/channels';
import {getChannelsInCurrentTeam, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
import {getArchivedChannels, getChannels, getSharedChannels, joinChannel, searchChannels} from '@mm-redux/actions/channels';
import {getCurrentUserId, getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {showCreateOption} from '@mm-redux/utils/channel_utils';
@ -17,30 +15,19 @@ import {isAdmin, isSystemAdmin} from '@mm-redux/utils/user_utils';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
import {teamArchivedChannels, joinablePublicChannels, joinableSharedChannels} from '@selectors/channel';
import MoreChannels from './more_channels';
const joinablePublicChannels = createSelector(
getChannelsInCurrentTeam,
getMyChannelMemberships,
(channels, myMembers) => {
return channels.filter((c) => {
return (!myMembers[c.id] && c.type === General.OPEN_CHANNEL && c.delete_at === 0);
});
},
);
const teamArchivedChannels = createSelector(
getChannelsInCurrentTeam,
(channels) => {
return channels.filter((c) => c.delete_at !== 0);
},
);
const defaultSharedChannels = [];
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const sharedChannelsEnabled = config.ExperimentalSharedChannels === 'true';
const roles = getCurrentUserRoles(state);
const channels = joinablePublicChannels(state);
const sharedChannels = sharedChannelsEnabled ? joinableSharedChannels(state) : defaultSharedChannels;
const archivedChannels = teamArchivedChannels(state);
const currentTeamId = getCurrentTeamId(state);
const canShowArchivedChannels = config.ExperimentalViewArchivedChannels === 'true' &&
@ -51,6 +38,8 @@ function mapStateToProps(state) {
currentUserId: getCurrentUserId(state),
currentTeamId,
channels,
sharedChannels,
sharedChannelsEnabled,
archivedChannels,
theme: getTheme(state),
canShowArchivedChannels,
@ -62,6 +51,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
getArchivedChannels,
getChannels,
getSharedChannels,
handleSelectChannel,
joinChannel,
searchChannels,

View file

@ -34,6 +34,7 @@ export default class MoreChannels extends PureComponent {
actions: PropTypes.shape({
getArchivedChannels: PropTypes.func.isRequired,
getChannels: PropTypes.func.isRequired,
getSharedChannels: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
searchChannels: PropTypes.func.isRequired,
@ -42,6 +43,8 @@ export default class MoreChannels extends PureComponent {
componentId: PropTypes.string,
canCreateChannels: PropTypes.bool.isRequired,
channels: PropTypes.array,
sharedChannels: PropTypes.array,
sharedChannelsEnabled: PropTypes.bool,
archivedChannels: PropTypes.array,
closeButton: PropTypes.object,
currentUserId: PropTypes.string.isRequired,
@ -63,13 +66,16 @@ export default class MoreChannels extends PureComponent {
this.searchTimeoutId = 0;
this.publicPage = -1;
this.sharedPage = -1;
this.archivedPage = -1;
this.nextPublic = true;
this.nextShared = true;
this.nextArchived = true;
this.mounted = false;
this.state = {
channels: props.channels.slice(0, General.CHANNELS_CHUNK_SIZE),
sharedChannels: props.sharedChannels.slice(0, General.CHANNELS_CHUNK_SIZE),
archivedChannels: props.archivedChannels.slice(0, General.CHANNELS_CHUNK_SIZE),
typeOfChannels: 'public',
loading: false,
@ -186,6 +192,15 @@ export default class MoreChannels extends PureComponent {
).then(this.loadedChannels);
}
break;
case 'shared':
if (this.nextShared) {
actions.getSharedChannels(
currentTeamId,
this.sharedPage + 1,
General.CHANNELS_CHUNK_SIZE,
).then(this.loadedChannels);
}
break;
case 'archived':
if (canShowArchivedChannels && this.nextArchived) {
actions.getArchivedChannels(
@ -226,8 +241,12 @@ export default class MoreChannels extends PureComponent {
if (this.mounted) {
const {typeOfChannels} = this.state;
const isPublic = typeOfChannels === 'public';
const isShared = typeOfChannels === 'shared';
if (isPublic) {
if (isShared) {
this.sharedPage += 1;
this.nextShared = data?.length > 0;
} else if (isPublic) {
this.publicPage += 1;
this.nextPublic = data?.length > 0;
} else {
@ -355,7 +374,7 @@ export default class MoreChannels extends PureComponent {
}
searchChannels = (text) => {
const {actions, channels, archivedChannels, currentTeamId, canShowArchivedChannels} = this.props;
const {actions, channels, sharedChannels, archivedChannels, currentTeamId, canShowArchivedChannels} = this.props;
const {typeOfChannels} = this.state;
if (text) {
@ -366,6 +385,13 @@ export default class MoreChannels extends PureComponent {
term: text,
});
clearTimeout(this.searchTimeoutId);
} else if (typeOfChannels === 'shared') {
const filtered = this.filterChannels(sharedChannels, text);
this.setState({
sharedChannels: filtered,
term: text,
});
clearTimeout(this.searchTimeoutId);
} else if (typeOfChannels === 'archived' && canShowArchivedChannels) {
const filtered = this.filterChannels(archivedChannels, text);
this.setState({
@ -384,13 +410,25 @@ export default class MoreChannels extends PureComponent {
handleDropdownClick = () => {
const {formatMessage} = this.context.intl;
const titleText = formatMessage({id: 'more_channels.dropdownTitle', defaultMessage: 'Show'});
const publicChannelsText = formatMessage({id: 'more_channels.publicChannels', defaultMessage: 'Public Channels'});
const archivedChannelsText = formatMessage({id: 'more_channels.archivedChannels', defaultMessage: 'Archived Channels'});
const titleText = formatMessage({id: 'more_channels.dropdownTitle', defaultMessage: 'Show'});
const cancelText = 'Cancel';
const options = [publicChannelsText, archivedChannelsText, cancelText];
let archivedChannelsIndex = 1;
let sharedChannelsIndex;
if (this.props.sharedChannelsEnabled) {
const sharedChannelsText = formatMessage({id: 'more_channels.sharedChannels', defaultMessage: 'Shared Channels'});
options.splice(1, 0, sharedChannelsText);
sharedChannelsIndex = 1;
archivedChannelsIndex = 2;
}
BottomSheet.showBottomSheetWithOptions({
options: [publicChannelsText, archivedChannelsText, cancelText],
cancelButtonIndex: 2,
options,
cancelButtonIndex: 3,
title: titleText,
}, (value) => {
let typeOfChannels;
@ -398,7 +436,10 @@ export default class MoreChannels extends PureComponent {
case 0:
typeOfChannels = 'public';
break;
case 1:
case sharedChannelsIndex:
typeOfChannels = 'shared';
break;
case archivedChannelsIndex:
typeOfChannels = 'archived';
break;
default:
@ -413,14 +454,11 @@ export default class MoreChannels extends PureComponent {
render() {
const {formatMessage} = this.context.intl;
const {theme, canShowArchivedChannels} = this.props;
const {adding, channels, archivedChannels, loading, term, typeOfChannels} = this.state;
const {canShowArchivedChannels, sharedChannelsEnabled, theme} = this.props;
const {adding, channels, sharedChannels, archivedChannels, loading, term, typeOfChannels} = this.state;
const more = term ? emptyFunction : this.getChannels;
const style = getStyleFromTheme(theme);
const publicChannelsText = formatMessage({id: 'more_channels.showPublicChannels', defaultMessage: 'Show: Public Channels'});
const archivedChannelsText = formatMessage({id: 'more_channels.showArchivedChannels', defaultMessage: 'Show: Archived Channels'});
let content;
if (adding) {
content = (<Loading color={theme.centerChannelColor}/>);
@ -433,12 +471,22 @@ export default class MoreChannels extends PureComponent {
let activeChannels = channels;
if (canShowArchivedChannels && typeOfChannels === 'archived') {
if (typeOfChannels === 'shared') {
activeChannels = sharedChannels;
} else if (canShowArchivedChannels && typeOfChannels === 'archived') {
activeChannels = archivedChannels;
}
let channelDropdown;
if (canShowArchivedChannels) {
if (canShowArchivedChannels || sharedChannelsEnabled) {
let channelDropdownText;
if (typeOfChannels === 'public') {
channelDropdownText = formatMessage({id: 'more_channels.showPublicChannels', defaultMessage: 'Show: Public Channels'});
} else if (typeOfChannels === 'shared') {
channelDropdownText = formatMessage({id: 'more_channels.showSharedChannels', defaultMessage: 'Show: Shared Channels'});
} else {
channelDropdownText = formatMessage({id: 'more_channels.showArchivedChannels', defaultMessage: 'Show: Archived Channels'});
}
channelDropdown = (
<View
style={style.titleContainer}
@ -450,7 +498,7 @@ export default class MoreChannels extends PureComponent {
onPress={this.handleDropdownClick}
testID={`more_channels.channel.dropdown.${typeOfChannels}`}
>
{typeOfChannels === 'public' ? publicChannelsText : archivedChannelsText}
{channelDropdownText}
{' '}
<CompassIcon
name='menu-down'

View file

@ -18,6 +18,7 @@ describe('MoreChannels', () => {
joinChannel: jest.fn(),
getArchivedChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2', delete_at: 123}]}),
getChannels: jest.fn().mockResolvedValue({data: [{id: 'id', name: 'name', display_name: 'display_name'}]}),
getSharedChannels: jest.fn().mockResolvedValue({data: [{id: 'id3', name: 'shared_channel', display_name: 'shared_channel_name', shared: true}]}),
searchChannels: jest.fn(),
setChannelDisplayName: jest.fn(),
};
@ -26,6 +27,7 @@ describe('MoreChannels', () => {
actions,
canCreateChannels: true,
channels: [{id: 'id', name: 'name', display_name: 'display_name'}],
sharedChannels: [{id: 'id3', name: 'shared_channel', display_name: 'shared_channel_name'}],
archivedChannels: [{id: 'id2', name: 'archived', display_name: 'archived channel', delete_at: 123}],
closeButton: {},
currentUserId: 'current_user_id',
@ -112,6 +114,10 @@ describe('MoreChannels', () => {
wrapper.setState({typeOfChannels: 'archived'});
instance.searchChannels('archived channel');
expect(wrapper.state('archivedChannels')).toEqual(baseProps.archivedChannels);
wrapper.setState({typeOfChannels: 'shared'});
instance.searchChannels('shared');
expect(wrapper.state('sharedChannels')).toEqual(baseProps.sharedChannels);
});
test('Allow load more public channels', () => {
@ -163,4 +169,29 @@ describe('MoreChannels', () => {
instance.loadedChannels({data: []});
expect(instance.nextArchived).toBe(false);
});
test('Allow load more shared channels', () => {
const wrapper = shallow(
<MoreChannels {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
const instance = wrapper.instance();
wrapper.setState({typeOfChannels: 'shared'});
instance.loadedChannels({data: ['shared-1', 'shared-2']});
expect(instance.nextShared).toBe(true);
});
test('Prevent load more shared channels', () => {
const wrapper = shallow(
<MoreChannels {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
const instance = wrapper.instance();
wrapper.setState({typeOfChannels: 'shared'});
instance.loadedChannels({data: null});
expect(instance.nextShared).toBe(false);
instance.loadedChannels({data: []});
expect(instance.nextShared).toBe(false);
});
});

View file

@ -154,3 +154,234 @@ exports[`user_profile should match snapshot 1`] = `
</ScrollView>
</RNCSafeAreaView>
`;
exports[`user_profile should match snapshot when user is from remote 1`] = `
<RNCSafeAreaView
style={
Object {
"flex": 1,
}
}
testID="user_profile.screen"
>
<Connect(StatusBar) />
<ScrollView
contentContainerStyle={
Object {
"paddingBottom": 48,
}
}
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
testID="user_profile.scroll_view"
>
<View
style={
Object {
"alignItems": "center",
"justifyContent": "center",
"padding": 25,
}
}
>
<Connect(ProfilePicture)
iconSize={104}
size={153}
statusBorderWidth={6}
statusSize={36}
testID="user_profile.profile_picture"
userId="4hzdnk6mg7gepe7yze6m3domnc"
/>
<Text
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
"marginTop": 15,
}
}
testID="user_profile.username"
>
@fred
</Text>
</View>
<View
style={
Object {
"backgroundColor": "#EBEBEC",
"height": 1,
"marginLeft": 16,
"marginRight": 22,
}
}
/>
<View
style={
Object {
"marginBottom": 25,
"marginHorizontal": 15,
}
}
>
<View
testID="user_profile.display_block"
>
<Text
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
"marginBottom": 10,
"marginTop": 25,
"textTransform": "uppercase",
}
}
testID="user_profile.display_block.nickname.label"
/>
<Text
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
testID="user_profile.display_block.nickname.value"
>
nick
</Text>
</View>
<View>
<FormattedText
defaultMessage="ORGANIZATION"
id="mobile.routes.user_profile.organization"
style={
Object {
"color": undefined,
"fontSize": 13,
"fontWeight": "600",
"marginBottom": 10,
"marginTop": 25,
"textTransform": "uppercase",
}
}
/>
<View
style={
Object {
"alignItems": "center",
"flexDirection": "row",
}
}
>
<ChannelIcon
hasDraft={false}
isActive={true}
isArchived={false}
isBot={false}
isInfo={true}
isUnread={false}
membersCount={0}
shared={true}
size={16}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"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",
}
}
type="O"
/>
<Text
style={
Object {
"color": "#3d3c40",
"fontSize": 15,
}
}
>
Remote Organization
</Text>
</View>
</View>
</View>
<View
style={
Object {
"backgroundColor": "#EBEBEC",
"height": 1,
"marginLeft": 16,
"marginRight": 22,
}
}
/>
<userProfileRow
action={[Function]}
defaultMessage="Send Message"
icon="send"
iconColor="rgba(0, 0, 0, 0.7)"
iconSize={24}
testID="user_profile.send_message.action"
textColor="#000"
textId="mobile.routes.user_profile.send_message"
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"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",
}
}
togglable={false}
/>
</ScrollView>
</RNCSafeAreaView>
`;

View file

@ -11,6 +11,7 @@ import {getTeammateNameDisplaySetting, getTheme, getBool} from '@mm-redux/select
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
import Preferences from '@mm-redux/constants/preferences';
import {loadBot} from '@mm-redux/actions/bots';
import {getRemoteClusterInfo} from '@mm-redux/actions/remote_cluster';
import {getBotAccounts} from '@mm-redux/selectors/entities/bots';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
@ -21,18 +22,20 @@ function mapStateToProps(state, ownProps) {
const {createChannel: createChannelRequest} = state.requests.channels;
const militaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
const enableTimezone = isTimezoneEnabled(state);
const user = state.entities.users.profiles[ownProps.userId];
return {
config,
createChannelRequest,
currentDisplayName: state.views.channel.displayName,
user: state.entities.users.profiles[ownProps.userId],
user,
bot: getBotAccounts(state)[ownProps.userId],
teammateNameDisplay: getTeammateNameDisplaySetting(state),
enableTimezone,
militaryTime,
theme: getTheme(state),
isMyUser: getCurrentUserId(state) === ownProps.userId,
remoteClusterInfo: state.entities.remoteCluster.info[user?.remote_id],
};
}
@ -42,6 +45,7 @@ function mapDispatchToProps(dispatch) {
makeDirectChannel,
setChannelDisplayName,
loadBot,
getRemoteClusterInfo,
}, dispatch),
};
}

View file

@ -20,11 +20,13 @@ import {
dismissAllModalsAndPopToRoot,
} from '@actions/navigation';
import Config from '@assets/config';
import ChannelIcon from '@components/channel_icon';
import FormattedTime from '@components/formatted_time';
import ProfilePicture from '@components/profile_picture';
import FormattedText from '@components/formatted_text';
import StatusBar from '@components/status_bar';
import {BotTag, GuestTag} from '@components/tag';
import {General} from '@mm-redux/constants';
import {displayUsername} from '@mm-redux/utils/user_utils';
import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils';
import {alertErrorWithFallback} from '@utils/general';
@ -41,6 +43,7 @@ export default class UserProfile extends PureComponent {
makeDirectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
loadBot: PropTypes.func.isRequired,
getRemoteClusterInfo: PropTypes.func.isRequired,
}).isRequired,
componentId: PropTypes.string,
config: PropTypes.object.isRequired,
@ -52,6 +55,7 @@ export default class UserProfile extends PureComponent {
militaryTime: PropTypes.bool.isRequired,
enableTimezone: PropTypes.bool.isRequired,
isMyUser: PropTypes.bool.isRequired,
remoteClusterInfo: PropTypes.object,
};
static contextTypes = {
@ -82,8 +86,14 @@ export default class UserProfile extends PureComponent {
componentDidMount() {
this.navigationEventListener = Navigation.events().bindComponent(this);
if (this.props.user && this.props.user.is_bot) {
this.props.actions.loadBot(this.props.user.id);
const {user} = this.props;
if (user) {
if (user.is_bot) {
this.props.actions.loadBot(user.id);
}
if (user.remote_id) {
this.props.actions.getRemoteClusterInfo(user.remote_id);
}
}
}
@ -186,6 +196,36 @@ export default class UserProfile extends PureComponent {
return null;
};
buildOrganizationBlock = () => {
const {theme, remoteClusterInfo} = this.props;
if (!remoteClusterInfo) {
return null;
}
const style = createStyleSheet(theme);
return (
<View>
<FormattedText
id='mobile.routes.user_profile.organization'
defaultMessage='ORGANIZATION'
style={style.header}
/>
<View style={style.organizationDataContainer}>
<ChannelIcon
isActive={true}
isArchived={false}
isBot={false}
isInfo={true}
size={16}
shared={true}
theme={theme}
type={General.OPEN_CHANNEL}
/>
<Text style={style.text}>{remoteClusterInfo.display_name}</Text>
</View>
</View>
);
}
buildTimezoneBlock = () => {
const {theme, user, militaryTime} = this.props;
const style = createStyleSheet(theme);
@ -337,6 +377,7 @@ export default class UserProfile extends PureComponent {
{this.props.config.ShowFullName === 'true' && this.buildDisplayBlock('last_name')}
{this.props.config.ShowEmailAddress === 'true' && this.buildDisplayBlock('email')}
{this.buildDisplayBlock('nickname')}
{this.buildOrganizationBlock()}
{this.buildDisplayBlock('position')}
{this.props.enableTimezone && this.buildTimezoneBlock()}
</View>
@ -451,6 +492,10 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
marginRight: 22,
backgroundColor: '#EBEBEC',
},
organizationDataContainer: {
alignItems: 'center',
flexDirection: 'row',
},
};
});

View file

@ -24,6 +24,7 @@ describe('user_profile', () => {
setChannelDisplayName: jest.fn(),
makeDirectChannel: jest.fn(),
loadBot: jest.fn(),
getRemoteClusterInfo: jest.fn(),
};
const baseProps = {
actions,
@ -128,6 +129,25 @@ describe('user_profile', () => {
expect(goToScreen).toHaveBeenCalledTimes(1);
});
test('should match snapshot when user is from remote', () => {
const remoteUser = {
...user,
remote_id: 'sr23g5h456',
};
const clusterInfo = {
display_name: 'Remote Organization',
};
const wrapper = shallow(
<UserProfile
{...baseProps}
remoteClusterInfo={clusterInfo}
user={remoteUser}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call goToEditProfile', () => {
const goToScreen = jest.spyOn(NavigationActions, 'goToScreen');

View file

@ -3,8 +3,9 @@
import {createSelector} from 'reselect';
import {General} from '@mm-redux/constants';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {getChannelByName} from '@mm-redux/selectors/entities/channels';
import {getChannelByName, getChannelsInCurrentTeam, getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
import {getTeamByName} from '@mm-redux/selectors/entities/teams';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {isArchivedChannel} from '@mm-redux/utils/channel_utils';
@ -60,3 +61,30 @@ export const getChannelReachable = createSelector(
return true;
},
);
export const joinablePublicChannels = createSelector(
getChannelsInCurrentTeam,
getMyChannelMemberships,
(channels, myMembers) => {
return channels.filter((c) => {
return (!myMembers[c.id] && c.type === General.OPEN_CHANNEL && c.delete_at === 0);
});
},
);
export const joinableSharedChannels = createSelector(
getChannelsInCurrentTeam,
getMyChannelMemberships,
(channels, myMembers) => {
return channels.filter((c) => {
return (!myMembers[c.id] && c.type === General.OPEN_CHANNEL && c.shared === true && c.delete_at === 0);
});
},
);
export const teamArchivedChannels = createSelector(
getChannelsInCurrentTeam,
(channels) => {
return channels.filter((c) => c.delete_at !== 0);
},
);

View file

@ -454,6 +454,34 @@
"send-outline"
]
},
{
"uid": "4dae60c666dc2fac9f861d9e70166d25",
"css": "circle-multiple-outline-lock",
"code": 59422,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M916.7 666.7V604.2C916.7 523.6 851.4 458.3 770.8 458.3S625 523.6 625 604.2V666.7C602 666.7 583.3 685.3 583.3 708.3V916.7C583.3 939.7 602 958.3 625 958.3H916.7C939.7 958.3 958.3 939.7 958.3 916.7V708.3C958.3 685.3 939.7 666.7 916.7 666.7ZM687.5 604.2C687.5 558.2 724.9 520.8 770.8 520.8S854.2 558.2 854.2 604.2V666.7H687.5V604.2ZM464.4 214.3C503 184.4 551.5 166.7 604.2 166.7 726.7 166.7 826.4 262.8 832.7 383.7 863.2 392.3 891.2 406.9 915.1 426.3 916.1 416.3 916.7 406.1 916.7 395.8 916.7 223.2 776.7 83.3 604.2 83.3 500.9 83.3 409.3 133.5 352.4 210.7 366.7 209.1 381.2 208.3 395.8 208.3 419.2 208.3 442.1 210.4 464.4 214.3ZM500 807.9C468.7 824 433.4 833.3 395.8 833.3 269.3 833.3 166.7 730.8 166.7 604.2S269.3 375 395.8 375C417.7 375 438.8 378 458.8 383.8 510 398.3 553.4 430.5 583.2 473 599.4 449.9 619.6 429.8 643 414.1 630.4 397.8 616.8 382.3 601.3 368.7 546.4 320.8 474.5 291.7 395.8 291.7 223.2 291.7 83.3 431.6 83.3 604.2S223.2 916.7 395.8 916.7C432.4 916.7 467.4 910.1 500 898.5V807.9ZM413.8 523.5C400.6 503.9 390.4 482.1 383.8 458.8 354.5 461.2 327.7 472.2 305.9 489.4 317.2 525.5 334.9 558.8 357.6 587.9 394.3 635 444.4 670.7 501.9 690.6 506.1 661.3 520 635.2 540.8 616.1 538.3 615.4 535.7 614.6 533.2 613.8 484 597.8 442.1 565.6 413.8 523.5Z",
"width": 1000
},
"search": [
"circle-multiple-outline-lock"
]
},
{
"uid": "a0d8bdcf347c632caf032dc70c78bb2a",
"css": "circle-multiple-outline",
"code": 984725,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M604.2 708.3C776.7 708.3 916.7 568.4 916.7 395.8 916.7 223.2 776.7 83.3 604.2 83.3 500.9 83.3 409.3 133.5 352.4 210.7 366.7 209.1 381.2 208.3 395.8 208.3 419.2 208.3 442.1 210.4 464.4 214.3 503 184.4 551.5 166.7 604.2 166.7 730.8 166.7 833.3 269.3 833.3 395.8 833.3 522.4 730.8 625 604.2 625 582.3 625 561.2 622 541.2 616.2 538.5 615.5 535.8 614.7 533.2 613.8 484 597.8 442.1 565.6 413.8 523.5 400.6 503.9 390.4 482.1 383.8 458.8 354.5 461.2 327.7 472.2 305.9 489.4 317.2 525.5 334.9 558.8 357.6 587.9 396.2 637.4 449.3 675 510.6 694.1 540.2 703.3 571.6 708.3 604.2 708.3ZM604.2 791.7C618.8 791.7 633.3 790.9 647.6 789.3 590.7 866.5 499.1 916.7 395.8 916.7 223.2 916.7 83.3 776.7 83.3 604.2 83.3 431.6 223.2 291.7 395.8 291.7 474.5 291.7 546.4 320.8 601.3 368.7L601.3 368.7C644.2 406.1 676.7 455 694.1 510.6 672.3 527.8 645.5 538.8 616.2 541.2 594.6 465.3 534.8 405.4 458.8 383.8V383.8C438.8 378 417.7 375 395.8 375 269.3 375 166.7 477.6 166.7 604.2 166.7 730.8 269.3 833.3 395.8 833.3 448.5 833.3 497 815.6 535.7 785.8 557.9 789.6 580.8 791.7 604.2 791.7Z",
"width": 1000
},
"search": [
"circle-multiple-outline"
]
},
{
"uid": "3f2ee509f7e283a723125cb00de0b0ec",
"css": "circle-outline",

View file

@ -485,6 +485,7 @@
"mobile.routes.user_profile": "Profile",
"mobile.routes.user_profile.edit": "Edit",
"mobile.routes.user_profile.local_time": "LOCAL TIME",
"mobile.routes.user_profile.organization": "ORGANIZATION",
"mobile.routes.user_profile.send_message": "Send Message",
"mobile.search.after_modifier_description": "to find posts after a specific date",
"mobile.search.before_modifier_description": "to find posts before a specific date",
@ -567,8 +568,10 @@
"more_channels.dropdownTitle": "Show",
"more_channels.noMore": "No more channels to join",
"more_channels.publicChannels": "Public Channels",
"more_channels.sharedChannels": "Shared Channels",
"more_channels.showArchivedChannels": "Show: Archived Channels",
"more_channels.showPublicChannels": "Show: Public Channels",
"more_channels.showSharedChannels": "Show: Shared Channels",
"more_channels.title": "More Channels",
"msg_typing.areTyping": "{users} and {last} are typing...",
"msg_typing.isTyping": "{user} is typing...",

Binary file not shown.