From edfd74369923b524b0d83cbb511472d0bfcd1dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 24 Sep 2021 14:35:01 +0200 Subject: [PATCH] [MM-38216] Add multiteam mentions and saved posts (#5677) * Add multiteam mentions and saved posts * Minor fix * Fix long names * Add tests, improve separation styling, revert changes on the flagged posts client, and omit the team name when the user only belongs to one team * Fix separator on iOS * Update snapshot * Fix separator * Fix snapshot * Differentiate styling between iOS and Android * Change channelTeamName to teamName --- app/client/rest/posts.ts | 7 +- app/mm-redux/actions/search.ts | 6 +- .../channel_display_name.test.js.snap | 83 +++++++ .../channel_display_name.js | 41 +++- .../channel_display_name.test.js | 33 +++ .../search/channel_display_name/index.js | 14 +- .../search/channel_display_name/index.test.js | 202 ++++++++++++++++++ 7 files changed, 377 insertions(+), 9 deletions(-) create mode 100644 app/screens/search/channel_display_name/__snapshots__/channel_display_name.test.js.snap create mode 100644 app/screens/search/channel_display_name/channel_display_name.test.js create mode 100644 app/screens/search/channel_display_name/index.test.js diff --git a/app/client/rest/posts.ts b/app/client/rest/posts.ts index b18966aae..864400e7f 100644 --- a/app/client/rest/posts.ts +++ b/app/client/rest/posts.ts @@ -250,8 +250,13 @@ const ClientPosts = (superclass: any) => class extends superclass { searchPostsWithParams = async (teamId: string, params: any) => { analytics.trackAPI('api_posts_search', {team_id: teamId}); + let route = `${this.getPostsRoute()}/search`; + if (teamId) { + route = `${this.getTeamRoute(teamId)}/posts/search`; + } + return this.doFetch( - `${this.getTeamRoute(teamId)}/posts/search`, + route, {method: 'post', body: JSON.stringify(params)}, ); }; diff --git a/app/mm-redux/actions/search.ts b/app/mm-redux/actions/search.ts index ecf905c01..912250d64 100644 --- a/app/mm-redux/actions/search.ts +++ b/app/mm-redux/actions/search.ts @@ -114,13 +114,12 @@ export function getFlaggedPosts(): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const userId = getCurrentUserId(state); - const teamId = getCurrentTeamId(state); dispatch({type: SearchTypes.SEARCH_FLAGGED_POSTS_REQUEST}); let posts; try { - posts = await Client4.getFlaggedPosts(userId, '', teamId); + posts = await Client4.getFlaggedPosts(userId); await Promise.all([getProfilesAndStatusesForPosts(posts.posts, dispatch, getState) as any, dispatch(getMissingChannelsFromPosts(posts.posts)) as any]); } catch (error) { @@ -202,7 +201,6 @@ export function clearPinnedPosts(channelId: string): ActionFunc { export function getRecentMentions(): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); - const teamId = getCurrentTeamId(state); let posts; try { @@ -213,7 +211,7 @@ export function getRecentMentions(): ActionFunc { const terms = termKeys.map(({key}) => key).join(' ').trim() + ' '; analytics.trackAPI('api_posts_search_mention'); - posts = await Client4.searchPosts(teamId, terms, true); + posts = await Client4.searchPosts('', terms, true); const profilesAndStatuses = getProfilesAndStatusesForPosts(posts.posts, dispatch, getState); const missingChannels = dispatch(getMissingChannelsFromPosts(posts.posts)); diff --git a/app/screens/search/channel_display_name/__snapshots__/channel_display_name.test.js.snap b/app/screens/search/channel_display_name/__snapshots__/channel_display_name.test.js.snap new file mode 100644 index 000000000..d83018d11 --- /dev/null +++ b/app/screens/search/channel_display_name/__snapshots__/channel_display_name.test.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchResultPost should match snapshot when no team is provided 1`] = ` + + + channel + + +`; + +exports[`SearchResultPost should match snapshot when team is provided 1`] = ` + + + channel + + + + + team + + + +`; diff --git a/app/screens/search/channel_display_name/channel_display_name.js b/app/screens/search/channel_display_name/channel_display_name.js index 6841ec1e5..d01a3d530 100644 --- a/app/screens/search/channel_display_name/channel_display_name.js +++ b/app/screens/search/channel_display_name/channel_display_name.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; -import {Text} from 'react-native'; +import {Platform, Text, View} from 'react-native'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -11,14 +11,29 @@ export default class ChannelDisplayName extends PureComponent { static propTypes = { displayName: PropTypes.string, theme: PropTypes.object.isRequired, + teamName: PropTypes.string, }; render() { - const {displayName, theme} = this.props; + const {displayName, theme, teamName} = this.props; const styles = getStyleFromTheme(theme); return ( - {displayName} + + {displayName} + {Boolean(teamName) && + <> + + {teamName} + + } + ); } } @@ -29,8 +44,28 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { color: changeOpacity(theme.centerChannelColor, 0.8), fontSize: 14, fontWeight: '600', + flexShrink: 1, + }, + teamName: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 12, + fontWeight: '400', + flexShrink: 2, + }, + separator: { + borderStyle: 'solid', + borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2), + borderLeftWidth: 1, + height: 16, + marginRight: 8, + marginLeft: 8, + alignSelf: Platform.select({ios: 'stretch'}), + }, + container: { + flexDirection: 'row', marginTop: 5, paddingHorizontal: 16, + alignItems: 'baseline', }, }; }); diff --git a/app/screens/search/channel_display_name/channel_display_name.test.js b/app/screens/search/channel_display_name/channel_display_name.test.js new file mode 100644 index 000000000..5f577923f --- /dev/null +++ b/app/screens/search/channel_display_name/channel_display_name.test.js @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {shallow} from 'enzyme'; +import React from 'react'; + +import {Preferences} from '@mm-redux/constants'; + +import ChannelDisplayName from './channel_display_name'; + +describe('SearchResultPost', () => { + const baseProps = { + displayName: 'channel', + teamName: '', + theme: Preferences.THEMES.denim, + }; + + test('should match snapshot when no team is provided', async () => { + const wrapper = shallow(); + + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should match snapshot when team is provided', async () => { + const props = { + ...baseProps, + teamName: 'team', + }; + const wrapper = shallow(); + + expect(wrapper.getElement()).toMatchSnapshot(); + }); +}); diff --git a/app/screens/search/channel_display_name/index.js b/app/screens/search/channel_display_name/index.js index 9bdf012d3..6e8452bed 100644 --- a/app/screens/search/channel_display_name/index.js +++ b/app/screens/search/channel_display_name/index.js @@ -3,21 +3,33 @@ import {connect} from 'react-redux'; +import {General} from '@mm-redux/constants'; import {makeGetChannel} from '@mm-redux/selectors/entities/channels'; import {getPost} from '@mm-redux/selectors/entities/posts'; import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getTeam, getTeamMemberships} from '@mm-redux/selectors/entities/teams'; import ChannelDisplayName from './channel_display_name'; -function makeMapStateToProps() { +// Exported for testing +export function makeMapStateToProps() { const getChannel = makeGetChannel(); return (state, ownProps) => { const post = getPost(state, ownProps.postId); const channel = post ? getChannel(state, {id: post.channel_id}) : null; + let teamName = ''; + if (channel) { + const isDMorGM = channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL; + const memberships = getTeamMemberships(state); + if (!isDMorGM && memberships && Object.values(memberships).length > 1) { + teamName = getTeam(state, channel.team_id)?.display_name; + } + } return { displayName: channel ? channel.display_name : '', theme: getTheme(state), + teamName, }; }; } diff --git a/app/screens/search/channel_display_name/index.test.js b/app/screens/search/channel_display_name/index.test.js new file mode 100644 index 000000000..dc1e8c7ba --- /dev/null +++ b/app/screens/search/channel_display_name/index.test.js @@ -0,0 +1,202 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {General} from '@mm-redux/constants'; + +import {makeMapStateToProps} from './index'; + +describe('components/SearchResultsItem/WithStore', () => { + const team = { + id: 'team_id', + display_name: 'team display name', + name: 'team_name', + }; + + const otherTeam = { + id: 'other_team_id', + display_name: 'other team display name', + name: 'other_team_name', + }; + + const currentUserID = 'other'; + const user = { + id: 'user_id', + username: 'username', + is_bot: false, + }; + const channel = { + id: 'channel_id_open', + type: General.OPEN_CHANNEL, + team_id: team.id, + name: 'open channel', + display_name: 'open channel', + }; + + const dmChannel = { + id: 'channel_id_dm', + type: General.DM_CHANNEL, + name: `${currentUserID}__${user.id}`, + display_name: `${currentUserID}__${user.id}`, + }; + + const post = { + channel_id: channel.id, + create_at: 1502715365009, + delete_at: 0, + edit_at: 1502715372443, + id: 'id', + is_pinned: false, + message: 'post message', + original_id: '', + pending_post_id: '', + props: {}, + root_id: '', + type: '', + update_at: 1502715372443, + user_id: 'user_id', + reply_count: 0, + }; + + const defaultState = { + entities: { + general: { + config: { + EnablePostUsernameOverride: 'true', + }, + license: {}, + }, + users: { + profiles: { + [user.id]: user, + }, + currentUserId: currentUserID, + statuses: {}, + profilesInChannel: {}, + }, + channels: { + channels: { + [channel.id]: channel, + [dmChannel.id]: dmChannel, + }, + }, + teams: { + myMembers: { + [team.id]: {}, + }, + teams: { + [team.id]: team, + [otherTeam.id]: otherTeam, + }, + currentTeamId: team.id, + }, + posts: { + posts: { + [post.id]: post, + }, + postsInThread: {}, + }, + preferences: { + myPreferences: { + hasOwnProperty: () => true, + }, + }, + }, + }; + + const defaultProps = { + postId: post.id, + }; + + const mstp = makeMapStateToProps(); + + test('should not show team name if user only belongs to one team', () => { + const newProps = mstp(defaultState, defaultProps); + expect(newProps.teamName).toBe(''); + }); + + test('should show team name for open and private channels when user belongs to more than one team', () => { + let state = { + ...defaultState, + entities: { + ...defaultState.entities, + teams: { + ...defaultState.entities.teams, + myMembers: { + ...defaultState.entities.teams.myMembers, + [otherTeam.id]: {}, + }, + }, + }, + }; + let newProps = mstp(state, defaultProps); + expect(newProps.teamName).toBe(team.display_name); + + state = { + ...state, + entities: { + ...state.entities, + channels: { + ...state.entities.channels, + channels: { + ...state.entities.channels.channels, + [channel.id]: { + ...channel, + type: General.PRIVATE_CHANNEL, + }, + }, + }, + }, + }; + + newProps = mstp(state, defaultProps); + expect(newProps.teamName).toBe(team.display_name); + }); + + test('should not show team name for dm and group channels when user belongs to more than one team', () => { + let state = { + ...defaultState, + entities: { + ...defaultState.entities, + teams: { + ...defaultState.entities.teams, + myMembers: { + ...defaultState.entities.teams.myMembers, + [otherTeam.id]: {}, + }, + }, + posts: { + ...defaultState.entities.posts, + posts: { + ...defaultState.entities.posts.posts, + [post.id]: { + ...defaultState.entities.posts.posts[post.id], + channel_id: dmChannel.id, + }, + }, + }, + }, + }; + + let newProps = mstp(state, defaultProps); + expect(newProps.teamName).toBe(''); + + state = { + ...state, + entities: { + ...state.entities, + channels: { + ...state.entities.channels, + channels: { + ...state.entities.channels.channels, + [dmChannel.id]: { + ...dmChannel, + type: General.GM_CHANNEL, + }, + }, + }, + }, + }; + newProps = mstp(state, defaultProps); + expect(newProps.teamName).toBe(''); + }); +});