[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
This commit is contained in:
Daniel Espino García
2021-09-24 14:35:01 +02:00
committed by GitHub
parent 6dbb537f22
commit edfd743699
7 changed files with 377 additions and 9 deletions

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchResultPost should match snapshot when no team is provided 1`] = `
<View
style={
Object {
"alignItems": "baseline",
"flexDirection": "row",
"marginTop": 5,
"paddingHorizontal": 16,
}
}
>
<Text
numberOfLines={1}
style={
Object {
"color": "rgba(63,67,80,0.8)",
"flexShrink": 1,
"fontSize": 14,
"fontWeight": "600",
}
}
>
channel
</Text>
</View>
`;
exports[`SearchResultPost should match snapshot when team is provided 1`] = `
<View
style={
Object {
"alignItems": "baseline",
"flexDirection": "row",
"marginTop": 5,
"paddingHorizontal": 16,
}
}
>
<Text
numberOfLines={1}
style={
Object {
"color": "rgba(63,67,80,0.8)",
"flexShrink": 1,
"fontSize": 14,
"fontWeight": "600",
}
}
>
channel
</Text>
<React.Fragment>
<View
style={
Object {
"alignSelf": "stretch",
"borderLeftColor": "rgba(63,67,80,0.2)",
"borderLeftWidth": 1,
"borderStyle": "solid",
"height": 16,
"marginLeft": 8,
"marginRight": 8,
}
}
/>
<Text
numberOfLines={1}
style={
Object {
"color": "rgba(63,67,80,0.5)",
"flexShrink": 2,
"fontSize": 12,
"fontWeight": "400",
}
}
>
team
</Text>
</React.Fragment>
</View>
`;

View File

@@ -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 (
<Text style={styles.channelName}>{displayName}</Text>
<View style={styles.container}>
<Text
style={styles.channelName}
numberOfLines={1}
>{displayName}</Text>
{Boolean(teamName) &&
<>
<View style={styles.separator}/>
<Text
style={styles.teamName}
numberOfLines={1}
>{teamName}</Text>
</>
}
</View>
);
}
}
@@ -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',
},
};
});

View File

@@ -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(<ChannelDisplayName {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot when team is provided', async () => {
const props = {
...baseProps,
teamName: 'team',
};
const wrapper = shallow(<ChannelDisplayName {...props}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View File

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

View File

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