From 6e23fbe7a78dcf05debf4925aafcc2fc9bffa6b7 Mon Sep 17 00:00:00 2001 From: Anurag Shivarathri Date: Mon, 24 May 2021 16:07:47 +0530 Subject: [PATCH] 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 * 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 Co-authored-by: Mattermod --- .../src/main/assets/fonts/compass-icons.ttf | Bin 61240 -> 41676 bytes app/client/rest/base.ts | 4 + app/client/rest/index.ts | 3 + app/client/rest/shared_channels.ts | 35 +++ .../at_mention_item/at_mention_item.tsx | 76 ++++-- .../autocomplete/at_mention_item/index.js | 2 + .../channel_mention_item.js | 5 +- .../channel_mention_item/index.js | 1 + app/components/channel_icon.tsx | 16 +- .../channel_list_row/channel_list_row.js | 8 +- app/components/custom_list/custom_list_row.js | 4 +- .../__snapshots__/user_list_test.test.js.snap | 183 ++++++++++++++ .../user_list_row/user_list_row.js | 30 ++- .../user_list_row/user_list_test.test.js | 16 ++ app/components/post_header/index.js | 3 +- app/components/post_header/post_header.js | 21 +- .../channel_item/channel_item.js | 1 + app/mm-redux/action_types/index.ts | 2 + app/mm-redux/action_types/remote_cluster.ts | 8 + app/mm-redux/actions/channels.ts | 39 +++ app/mm-redux/actions/index.ts | 2 + app/mm-redux/actions/remote_cluster.ts | 15 ++ app/mm-redux/reducers/entities/index.ts | 2 + .../reducers/entities/remote_cluster.ts | 24 ++ app/mm-redux/types/channels.ts | 15 ++ app/mm-redux/types/remote_cluster.ts | 7 + app/mm-redux/types/store.ts | 6 + app/mm-redux/types/users.ts | 1 + app/mm-redux/utils/user_utils.ts | 4 + .../__snapshots__/channel_title.test.js.snap | 83 ++++++- .../channel_title/channel_title.js | 40 ++- .../channel_title/channel_title.test.js | 15 ++ .../channel_nav_bar/channel_title/index.js | 15 +- app/screens/channel_info/channel_info.js | 1 + .../channel_info/channel_info_header.js | 3 + .../channel_members/channel_members.js | 4 +- app/screens/more_channels/index.js | 28 +-- app/screens/more_channels/more_channels.js | 76 ++++-- .../more_channels/more_channels.test.js | 31 +++ .../__snapshots__/user_profile.test.js.snap | 231 ++++++++++++++++++ app/screens/user_profile/index.js | 6 +- app/screens/user_profile/user_profile.js | 49 +++- app/screens/user_profile/user_profile.test.js | 20 ++ app/selectors/channel.js | 30 ++- assets/base/compass-icons.json | 28 +++ assets/base/i18n/en.json | 3 + assets/fonts/compass-icons.ttf | Bin 61240 -> 41676 bytes 47 files changed, 1102 insertions(+), 94 deletions(-) create mode 100644 app/client/rest/shared_channels.ts create mode 100644 app/mm-redux/action_types/remote_cluster.ts create mode 100644 app/mm-redux/actions/remote_cluster.ts create mode 100644 app/mm-redux/reducers/entities/remote_cluster.ts create mode 100644 app/mm-redux/types/remote_cluster.ts diff --git a/android/app/src/main/assets/fonts/compass-icons.ttf b/android/app/src/main/assets/fonts/compass-icons.ttf index 2a2743fa9a281f7445bc982b106ea1fdf544386a..35a5333af0bef17c7bda1c5b7f23adcae9cd8846 100644 GIT binary patch delta 9171 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 delta 29153 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 diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index acf0d9666..58d3df9da 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -186,6 +186,10 @@ export default class ClientBase { return `${this.getPostsRoute()}/${postId}`; } + getSharedChannelsRoute() { + return `${this.getBaseRoute()}/sharedchannels`; + } + getReactionsRoute() { return `${this.getBaseRoute()}/reactions`; } diff --git a/app/client/rest/index.ts b/app/client/rest/index.ts index 611648c4e..7cebb0369 100644 --- a/app/client/rest/index.ts +++ b/app/client/rest/index.ts @@ -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, diff --git a/app/client/rest/shared_channels.ts b/app/client/rest/shared_channels.ts new file mode 100644 index 000000000..e8107a114 --- /dev/null +++ b/app/client/rest/shared_channels.ts @@ -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; + getSharedChannels: (teamId: string, page: number, perPage?: number) => Promise; +} + +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; diff --git a/app/components/autocomplete/at_mention_item/at_mention_item.tsx b/app/components/autocomplete/at_mention_item/at_mention_item.tsx index ec14f8622..ae771e93d 100644 --- a/app/components/autocomplete/at_mention_item/at_mention_item.tsx +++ b/app/components/autocomplete/at_mention_item/at_mention_item.tsx @@ -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`} /> - - - {Boolean(name.length) && - - {name} - - } - - {isCurrentUser && - } - {` @${username}`} - + + + + {Boolean(name.length) && + + {name} {name} + + } + + {isCurrentUser && + } + {` @${username}`} + + + {isShared && ( + + )} ); diff --git a/app/components/autocomplete/at_mention_item/index.js b/app/components/autocomplete/at_mention_item/index.js index 2c995cc86..ce10b465c 100644 --- a/app/components/autocomplete/at_mention_item/index.js +++ b/app/components/autocomplete/at_mention_item/index.js @@ -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, }; diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.js b/app/components/autocomplete/channel_mention_item/channel_mention_item.js index a5065bfed..3af63eadc 100644 --- a/app/components/autocomplete/channel_mention_item/channel_mention_item.js +++ b/app/components/autocomplete/channel_mention_item/channel_mention_item.js @@ -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'; } diff --git a/app/components/autocomplete/channel_mention_item/index.js b/app/components/autocomplete/channel_mention_item/index.js index 7b8b7cc34..5ae940528 100644 --- a/app/components/autocomplete/channel_mention_item/index.js +++ b/app/components/autocomplete/channel_mention_item/index.js @@ -30,6 +30,7 @@ function mapStateToProps(state, ownProps) { return { displayName, name: channel?.name, + shared: channel?.shared, type: channel?.type, isBot, isGuest, diff --git a/app/components/channel_icon.tsx b/app/components/channel_icon.tsx index 1a9b63379..a019e6506 100644 --- a/app/components/channel_icon.tsx +++ b/app/components/channel_icon.tsx @@ -18,8 +18,10 @@ type Props = { isInfo: boolean; isUnread: boolean; membersCount: number; + shared: boolean; size: number; statusStyle?: StyleProp; + style?: StyleProp; testID?: string; theme: Theme; type: string; @@ -105,7 +107,6 @@ const ChannelIcon = (props: Props) => { } let icon; - let extraStyle; if (props.isArchived) { icon = ( { 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 = ( + + ); } else if (props.type === General.OPEN_CHANNEL) { icon = ( { ); } else if (props.type === General.DM_CHANNEL) { - // extraStyle = {marginRight: 6}; icon = ( { } return ( - + {icon} ); diff --git a/app/components/custom_list/channel_list_row/channel_list_row.js b/app/components/custom_list/channel_list_row/channel_list_row.js index 6736407bc..cae0503f6 100644 --- a/app/components/custom_list/channel_list_row/channel_list_row.js +++ b/app/components/custom_list/channel_list_row/channel_list_row.js @@ -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 ( `; + +exports[`UserListRow should match snapshot for remote user 1`] = ` + + + + + + + + + + @user + + + + + + + + + +`; diff --git a/app/components/custom_list/user_list_row/user_list_row.js b/app/components/custom_list/user_list_row/user_list_row.js index 3e27e3007..f83bcae4d 100644 --- a/app/components/custom_list/user_list_row/user_list_row.js +++ b/app/components/custom_list/user_list_row/user_list_row.js @@ -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 ( + + ); + }; + render() { const {formatMessage} = this.context.intl; const { @@ -131,6 +154,7 @@ export default class UserListRow extends React.PureComponent { } + {this.renderIcon(style)} ); @@ -172,5 +196,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { fontSize: 12, color: changeOpacity(theme.centerChannelColor, 0.5), }, + sharedUserIcon: { + alignSelf: 'center', + opacity: 0.75, + }, }; }); diff --git a/app/components/custom_list/user_list_row/user_list_test.test.js b/app/components/custom_list/user_list_row/user_list_test.test.js index 594b49e65..7a337fa89 100644 --- a/app/components/custom_list/user_list_row/user_list_test.test.js +++ b/app/components/custom_list/user_list_row/user_list_test.test.js @@ -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( + , + {context: {intl: {formatMessage}}}, + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + test('should match snapshot for currentUser with (you) populated in list', () => { const newProps = { ...baseProps, diff --git a/app/components/post_header/index.js b/app/components/post_header/index.js index a419c4ab6..03657267b 100644 --- a/app/components/post_header/index.js +++ b/app/components/post_header/index.js @@ -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, }; }; diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js index 05499ee11..404905ede 100644 --- a/app/components/post_header/post_header.js +++ b/app/components/post_header/post_header.js @@ -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 ( + + ); + }; + renderReply = () => { const { commentCount, @@ -328,6 +342,7 @@ export default class PostHeader extends PureComponent { {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, + }, }; }); diff --git a/app/components/sidebars/main/channels_list/channel_item/channel_item.js b/app/components/sidebars/main/channels_list/channel_item/channel_item.js index 93c5b1693..2317422bc 100644 --- a/app/components/sidebars/main/channels_list/channel_item/channel_item.js +++ b/app/components/sidebars/main/channels_list/channel_item/channel_item.js @@ -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} diff --git a/app/mm-redux/action_types/index.ts b/app/mm-redux/action_types/index.ts index 9d3753db6..070aa8448 100644 --- a/app/mm-redux/action_types/index.ts +++ b/app/mm-redux/action_types/index.ts @@ -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, }; diff --git a/app/mm-redux/action_types/remote_cluster.ts b/app/mm-redux/action_types/remote_cluster.ts new file mode 100644 index 000000000..7fbf28d69 --- /dev/null +++ b/app/mm-redux/action_types/remote_cluster.ts @@ -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, +}); diff --git a/app/mm-redux/actions/channels.ts b/app/mm-redux/actions/channels.ts index 0a5bf92e1..5147bd85b 100644 --- a/app/mm-redux/actions/channels.ts +++ b/app/mm-redux/actions/channels.ts @@ -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) => { + 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}); diff --git a/app/mm-redux/actions/index.ts b/app/mm-redux/actions/index.ts index abbb464a2..f62d99f42 100644 --- a/app/mm-redux/actions/index.ts +++ b/app/mm-redux/actions/index.ts @@ -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, }; diff --git a/app/mm-redux/actions/remote_cluster.ts b/app/mm-redux/actions/remote_cluster.ts new file mode 100644 index 000000000..e267b285d --- /dev/null +++ b/app/mm-redux/actions/remote_cluster.ts @@ -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, + ], + }); +} diff --git a/app/mm-redux/reducers/entities/index.ts b/app/mm-redux/reducers/entities/index.ts index f21f2d422..a59ce82ea 100644 --- a/app/mm-redux/reducers/entities/index.ts +++ b/app/mm-redux/reducers/entities/index.ts @@ -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, }); diff --git a/app/mm-redux/reducers/entities/remote_cluster.ts b/app/mm-redux/reducers/entities/remote_cluster.ts new file mode 100644 index 000000000..4e7402d27 --- /dev/null +++ b/app/mm-redux/reducers/entities/remote_cluster.ts @@ -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 = {}, 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, +}); diff --git a/app/mm-redux/types/channels.ts b/app/mm-redux/types/channels.ts index a74afcf65..4eb3b2633 100644 --- a/app/mm-redux/types/channels.ts +++ b/app/mm-redux/types/channels.ts @@ -106,3 +106,18 @@ export type ChannelMemberCountByGroup = { }; export type ChannelMemberCountsByGroup = Record; + +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; +}; diff --git a/app/mm-redux/types/remote_cluster.ts b/app/mm-redux/types/remote_cluster.ts new file mode 100644 index 000000000..28f05df2c --- /dev/null +++ b/app/mm-redux/types/remote_cluster.ts @@ -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; +}; diff --git a/app/mm-redux/types/store.ts b/app/mm-redux/types/store.ts index 4671be495..c8bc67a4b 100644 --- a/app/mm-redux/types/store.ts +++ b/app/mm-redux/types/store.ts @@ -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; diff --git a/app/mm-redux/types/users.ts b/app/mm-redux/types/users.ts index c9620b785..59d9ecff5 100644 --- a/app/mm-redux/types/users.ts +++ b/app/mm-redux/types/users.ts @@ -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; diff --git a/app/mm-redux/utils/user_utils.ts b/app/mm-redux/utils/user_utils.ts index e480d8179..38ebb66f0 100644 --- a/app/mm-redux/utils/user_utils.ts +++ b/app/mm-redux/utils/user_utils.ts @@ -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); } diff --git a/app/screens/channel/channel_nav_bar/channel_title/__snapshots__/channel_title.test.js.snap b/app/screens/channel/channel_nav_bar/channel_title/__snapshots__/channel_title.test.js.snap index 498d394ef..ba01af993 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/__snapshots__/channel_title.test.js.snap +++ b/app/screens/channel/channel_nav_bar/channel_title/__snapshots__/channel_title.test.js.snap @@ -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 `; +exports[`ChannelTitle should match snapshot when isChannelShared is true 1`] = ` + + + + My User + + + + + +`; + exports[`ChannelTitle should match snapshot when isSelfDMChannel is true 1`] = ` @@ -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", diff --git a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js index e4bd80683..3a510dc95 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js +++ b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js @@ -12,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 = ( + + ); + } + return ( {channelDisplayName} + {channelIcon} {icon} {mutedIcon} @@ -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, diff --git a/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js b/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js index 2b253751d..b7afe41df 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js +++ b/app/screens/channel/channel_nav_bar/channel_title/channel_title.test.js @@ -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( + , + {context: {intl: {formatMessage: (intlId) => intlId.defaultMessage}}}, + ); + + expect(wrapper.getElement()).toMatchSnapshot(); + }); }); diff --git a/app/screens/channel/channel_nav_bar/channel_title/index.js b/app/screens/channel/channel_nav_bar/channel_title/index.js index eb749515c..cc3633bc8 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/index.js +++ b/app/screens/channel/channel_nav_bar/channel_title/index.js @@ -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), }; } diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js index 2286dab48..53c9ea737 100644 --- a/app/screens/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -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} diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js index 666d8403a..fd5d5ed29 100644 --- a/app/screens/channel_info/channel_info_header.js +++ b/app/screens/channel_info/channel_info_header.js @@ -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} diff --git a/app/screens/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js index 3cd2e9638..45bc47f12 100644 --- a/app/screens/channel_members/channel_members.js +++ b/app/screens/channel_members/channel_members.js @@ -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); diff --git a/app/screens/more_channels/index.js b/app/screens/more_channels/index.js index 56635184f..2251099f0 100644 --- a/app/screens/more_channels/index.js +++ b/app/screens/more_channels/index.js @@ -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, diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js index 8b951ba55..d773107f4 100644 --- a/app/screens/more_channels/more_channels.js +++ b/app/screens/more_channels/more_channels.js @@ -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 = (); @@ -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 = ( - {typeOfChannels === 'public' ? publicChannelsText : archivedChannelsText} + {channelDropdownText} {' '} { 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( + , + {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( + , + {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); + }); }); diff --git a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap index 712e44ee6..9753765c4 100644 --- a/app/screens/user_profile/__snapshots__/user_profile.test.js.snap +++ b/app/screens/user_profile/__snapshots__/user_profile.test.js.snap @@ -154,3 +154,234 @@ exports[`user_profile should match snapshot 1`] = ` `; + +exports[`user_profile should match snapshot when user is from remote 1`] = ` + + + + + + + @fred + + + + + + + + nick + + + + + + + + Remote Organization + + + + + + + + +`; diff --git a/app/screens/user_profile/index.js b/app/screens/user_profile/index.js index dcd297a53..5262e8b4c 100644 --- a/app/screens/user_profile/index.js +++ b/app/screens/user_profile/index.js @@ -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), }; } diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js index aa2e32d44..c8b37a0b3 100644 --- a/app/screens/user_profile/user_profile.js +++ b/app/screens/user_profile/user_profile.js @@ -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 ( + + + + + {remoteClusterInfo.display_name} + + + ); + } + 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()} @@ -451,6 +492,10 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => { marginRight: 22, backgroundColor: '#EBEBEC', }, + organizationDataContainer: { + alignItems: 'center', + flexDirection: 'row', + }, }; }); diff --git a/app/screens/user_profile/user_profile.test.js b/app/screens/user_profile/user_profile.test.js index 6d7d2f663..7f4c3bbb3 100644 --- a/app/screens/user_profile/user_profile.test.js +++ b/app/screens/user_profile/user_profile.test.js @@ -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( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + expect(wrapper.getElement()).toMatchSnapshot(); + }); + test('should call goToEditProfile', () => { const goToScreen = jest.spyOn(NavigationActions, 'goToScreen'); diff --git a/app/selectors/channel.js b/app/selectors/channel.js index 1df59139f..c5fd649e8 100644 --- a/app/selectors/channel.js +++ b/app/selectors/channel.js @@ -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); + }, +); diff --git a/assets/base/compass-icons.json b/assets/base/compass-icons.json index 9e3729b0b..a6e20aa16 100644 --- a/assets/base/compass-icons.json +++ b/assets/base/compass-icons.json @@ -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", diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 5dea6edcb..057920f88 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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...", diff --git a/assets/fonts/compass-icons.ttf b/assets/fonts/compass-icons.ttf index 2a2743fa9a281f7445bc982b106ea1fdf544386a..35a5333af0bef17c7bda1c5b7f23adcae9cd8846 100644 GIT binary patch delta 9171 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 delta 29153 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