[MM-23160][MM-24633][MM-24697] Highlight Group Names 2 (#4551)

* Initial Commit for Group Highlights

* Fix some stuff

* Get my groups

* update channel.js

* Address PR comments

* Address PR comments

* Update app/actions/views/channel.js

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

* update selector stuff

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
This commit is contained in:
Hossein Ahmadian-Yazdi 2020-07-20 10:19:18 -04:00 committed by GitHub
parent 69240c6c30
commit 2840a42a00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 350 additions and 45 deletions

View file

@ -586,6 +586,7 @@ function loadGroupData() {
const state = getState();
const actions = [];
const team = getCurrentTeam(state);
const currentUserId = getCurrentUserId(state);
const serverVersion = state.entities.general.serverVersion;
const license = getLicense(state);
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
@ -632,10 +633,25 @@ function loadGroupData() {
});
}
}
break;
} catch (err) {
return {error: err};
if (i === MAX_RETRIES) {
return {error: err};
}
}
}
try {
const myGroups = await Client4.getGroupsByUserId(currentUserId);
if (myGroups.length) {
actions.push({
type: GroupTypes.RECEIVED_MY_GROUPS,
data: myGroups,
});
}
} catch {
// do nothing
}
}
if (actions.length) {

View file

@ -25,6 +25,7 @@ export default class AtMention extends React.PureComponent {
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
usersByUsername: PropTypes.object.isRequired,
groupsByName: PropTypes.object,
};
static contextTypes = {
@ -93,6 +94,12 @@ export default class AtMention extends React.PureComponent {
};
}
getGroupFromMentionName() {
const {groupsByName, mentionName} = this.props;
const mentionNameTrimmed = mentionName.toLowerCase().replace(/[._-]*$/, '');
return groupsByName?.[mentionNameTrimmed] || {};
}
handleLongPress = async () => {
const {formatMessage} = this.context.intl;
@ -134,13 +141,28 @@ export default class AtMention extends React.PureComponent {
render() {
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys} = this.props;
const {user} = this.state;
let highlighted;
if (!user.username) {
const group = this.getGroupFromMentionName();
if (group.allow_reference) {
highlighted = mentionKeys.some((item) => item.key === group.name);
return (
<Text
style={textStyle}
>
<Text style={highlighted ? null : mentionStyle}>
{`@${group.name}`}
</Text>
</Text>
);
}
return <Text style={textStyle}>{'@' + mentionName}</Text>;
}
const suffix = this.props.mentionName.substring(user.username.length);
const highlighted = mentionKeys.some((item) => item.key === user.username);
highlighted = mentionKeys.some((item) => item.key === user.username);
return (
<Text

View file

@ -3,18 +3,23 @@
import {connect} from 'react-redux';
import {getUsersByUsername, getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users';
import {getUsersByUsername} from '@mm-redux/selectors/entities/users';
import {getAllUserMentionKeys} from '@mm-redux/selectors/entities/search';
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
import {getGroupsByName} from '@mm-redux/selectors/entities/groups';
import AtMention from './at_mention';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
usersByUsername: getUsersByUsername(state),
mentionKeys: getCurrentUserMentionKeys(state),
mentionKeys: ownProps.mentionKeys || getAllUserMentionKeys(state),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
groupsByName: getGroupsByName(state),
};
}

View file

@ -49,7 +49,7 @@ exports[`Markdown should match with disableAtChannelMentionHighlight 1`] = `
first={true}
last={true}
literal={null}
nodeKey="6"
nodeKey="8"
>
<Unknown
context={
@ -64,21 +64,23 @@ exports[`Markdown should match with disableAtChannelMentionHighlight 1`] = `
context={
Array [
"paragraph",
"mention_highlight",
]
}
literal={null}
mentionName="all"
nodeKey="5"
nodeKey="6"
>
<Unknown
context={
Array [
"paragraph",
"mention_highlight",
"at_mention",
]
}
literal="@all"
nodeKey="4"
nodeKey="5"
/>
</Unknown>
</Unknown>

View file

@ -5,16 +5,16 @@ import {connect} from 'react-redux';
import {getAutolinkedUrlSchemes, getConfig} from '@mm-redux/selectors/entities/general';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users';
import {getAllUserMentionKeys} from '@mm-redux/selectors/entities/search';
import Markdown from './markdown';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {MinimumHashtagLength} = getConfig(state);
return {
autolinkedUrlSchemes: getAutolinkedUrlSchemes(state),
mentionKeys: getCurrentUserMentionKeys(state),
mentionKeys: ownProps.mentionKeys || getAllUserMentionKeys(state),
minimumHashtagLength: MinimumHashtagLength ? parseInt(MinimumHashtagLength, 10) : 3,
theme: getTheme(state),
};

View file

@ -92,14 +92,6 @@ export default class Markdown extends PureComponent {
return !scheme || this.props.autolinkedUrlSchemes.indexOf(scheme) !== -1;
};
getMentionKeys = () => {
const mentionKeys = this.props.mentionKeys;
if (this.props.disableAtChannelMentionHighlight) {
return mentionKeys.filter((mention) => !['@all', '@channel', '@here'].includes(mention.key));
}
return mentionKeys;
}
createRenderer = () => {
return new Renderer({
renderers: {
@ -223,6 +215,7 @@ export default class Markdown extends PureComponent {
isSearchResult={this.props.isSearchResult}
mentionName={mentionName}
onPostPress={this.props.onPostPress}
mentionKeys={this.props.mentionKeys}
/>
);
};
@ -439,7 +432,7 @@ export default class Markdown extends PureComponent {
ast = combineTextNodes(ast);
ast = addListItemIndices(ast);
ast = pullOutImages(ast);
ast = highlightMentions(ast, this.getMentionKeys());
ast = highlightMentions(ast, this.props.mentionKeys);
if (this.props.isEdited) {
const editIndicatorNode = new Node('edited_indicator');

View file

@ -33,24 +33,4 @@ describe('Markdown', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
describe('getMentionKeys', () => {
let wrapper;
beforeAll(() => {
wrapper = shallow(
<Markdown
{...baseProps}
/>,
);
});
it('should return base mentionKey props when disableAtChannelMentionHighlight not present', () => {
expect(wrapper.instance().getMentionKeys()).toEqual(baseProps.mentionKeys);
});
it('should filter channel mentions from mentionKey props when disableAtChannelMentionHighlight is true', () => {
wrapper.setProps({disableAtChannelMentionHighlight: true});
expect(wrapper.instance().getMentionKeys()).toEqual([{key: 'user.name'}]);
});
});
});

View file

@ -12,6 +12,7 @@ import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
import {makeGetReactionsForPost} from '@mm-redux/selectors/entities/posts';
import {memoizeResult} from '@mm-redux/utils/helpers';
import {makeGetMentionKeysForPost} from '@mm-redux/selectors/entities/search';
import {
isEdited,
@ -103,6 +104,7 @@ export function makeMapStateToProps() {
isEmojiOnly,
shouldRenderJumboEmoji,
theme: getTheme(state),
mentionKeys: makeGetMentionKeysForPost(state, postProps?.disable_group_highlight, postProps?.mentionHighlightDisabled),
canDelete,
...getDimensions(state),
};

View file

@ -77,6 +77,13 @@ describe('makeMapStateToProps', () => {
general: {
serverVersion: '',
},
users: {
profiles: {},
},
groups: {
groups: {},
myGroups: {},
},
},
};
const defaultOwnProps = {

View file

@ -71,6 +71,7 @@ export default class PostBody extends PureComponent {
shouldRenderJumboEmoji: PropTypes.bool.isRequired,
theme: PropTypes.object,
location: PropTypes.string,
mentionKeys: PropTypes.array.isRequired,
};
static defaultProps = {
@ -345,6 +346,7 @@ export default class PostBody extends PureComponent {
shouldRenderJumboEmoji,
showLongPost,
theme,
mentionKeys,
} = this.props;
const {isLongPost, maxHeight} = this.state;
const style = getStyleSheet(theme);
@ -413,7 +415,7 @@ export default class PostBody extends PureComponent {
onPostPress={onPress}
textStyles={textStyles}
value={message}
disableAtChannelMentionHighlight={postProps.mentionHighlightDisabled}
mentionKeys={mentionKeys}
/>
</View>
);

View file

@ -2861,6 +2861,13 @@ export default class Client4 {
);
};
getGroupsByUserId = async (userID: string) => {
return this.doFetch(
`${this.getUsersRoute()}/${userID}/groups`,
{method: 'get'},
);
}
getGroupsNotAssociatedToTeam = async (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
this.trackEvent('api', 'api_groups_get_not_associated_to_team', {team_id: teamID});
return this.doFetch(

View file

@ -163,6 +163,20 @@ function syncables(state: Dictionary<GroupSyncables> = {}, action: GenericAction
}
}
function myGroups(state: any = {}, action: GenericAction) {
switch (action.type) {
case GroupTypes.RECEIVED_MY_GROUPS: {
const nextState = {...state};
for (const group of action.data) {
nextState[group.id] = group;
}
return nextState;
}
default:
return state;
}
}
function members(state: any = {}, action: GenericAction) {
switch (action.type) {
case GroupTypes.RECEIVED_GROUP_MEMBERS: {
@ -221,6 +235,7 @@ function groups(state: Dictionary<Group> = {}, action: GenericAction) {
}
export default combineReducers({
myGroups,
syncables,
members,
groups,

View file

@ -2,10 +2,12 @@
// See LICENSE.txt for license information.
import * as reselect from 'reselect';
import {GlobalState} from '@mm-redux/types/store';
import {Dictionary} from '@mm-redux/types/utilities';
import {Group} from '@mm-redux/types/groups';
import {filterGroupsMatchingTerm} from '@mm-redux/utils/group_utils';
import {getCurrentUserLocale} from '@mm-redux/selectors/entities/i18n';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {UserMentionKey} from '@mm-redux/selectors/entities/users';
import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles';
import {getTeam} from '@mm-redux/selectors/entities/teams';
import {Permissions} from '@mm-redux/constants';
@ -17,7 +19,11 @@ const emptySyncables = {
};
export function getAllGroups(state: GlobalState) {
return state.entities.groups?.groups || [];
return state.entities.groups?.groups || {};
}
export function getMyGroups(state: GlobalState) {
return state.entities.groups?.myGroups || {};
}
export function getGroup(state: GlobalState, id: string) {
@ -166,3 +172,32 @@ export const getAllAssociatedGroupsForReference = reselect.createSelector(
return Object.values(allGroups).filter((group) => group.allow_reference && group.delete_at === 0);
},
);
export const getMyAllowReferencedGroups = reselect.createSelector(
getMyGroups,
(myGroups) => {
return Object.values(myGroups).filter((group) => group.allow_reference && group.delete_at === 0);
},
);
export const getCurrentUserGroupMentionKeys = reselect.createSelector(
getMyAllowReferencedGroups,
(groups: Array<Group>) => {
const keys: UserMentionKey[] = [];
groups.forEach((group) => keys.push({key: `@${group.name}`}));
return keys;
},
);
export const getGroupsByName = reselect.createSelector(
getAllGroups,
(groups) => {
const groupsByName: Dictionary<Group> = {};
Object.values(groups).forEach((group) => {
groupsByName[group.name] = group;
});
return groupsByName;
},
);

View file

@ -26,4 +26,184 @@ describe('Selectors.Search', () => {
it('should return current search for current team', () => {
assert.deepEqual(Selectors.getCurrentSearchForCurrentTeam(testState), team1CurrentSearch);
});
it('getAllUserMentionKeys', () => {
const userId = '1234';
const notifyProps = {
first_name: 'true',
};
const state = {
entities: {
users: {
currentUserId: userId,
profiles: {
[userId]: {id: userId, username: 'user', first_name: 'First', last_name: 'Last', notify_props: notifyProps},
},
},
groups: {
myGroups: {
test1: {
name: 'I-AM-THE-BEST!',
delete_at: 0,
allow_reference: true,
},
test2: {
name: 'Do-you-love-me?',
delete_at: 0,
allow_reference: true,
},
test3: {
name: 'Maybe?-A-little-bit-I-guess....',
delete_at: 0,
allow_reference: false,
},
},
},
},
};
assert.deepEqual(Selectors.getAllUserMentionKeys(state), [{key: 'First', caseSensitive: true}, {key: '@user'}, {key: '@I-AM-THE-BEST!'}, {key: '@Do-you-love-me?'}]);
});
describe('makeGetMentionKeysForPost', () => {
it('should return all mentionKeys', () => {
const postProps = {
disable_group_highlight: false,
mentionHighlightDisabled: false,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@a123'}, {key: '@developers'}];
assert.deepEqual(results, expected);
});
it('should return mentionKeys without groups', () => {
const postProps = {
disable_group_highlight: true,
mentionHighlightDisabled: false,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@channel'}, {key: '@all'}, {key: '@here'}, {key: '@a123'}];
assert.deepEqual(results, expected);
});
it('should return group mentions and all mentions without channel mentions', () => {
const postProps = {
disable_group_highlight: false,
mentionHighlightDisabled: true,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@a123'}, {key: '@developers'}];
assert.deepEqual(results, expected);
});
it('should return all mentions without group mentions and channel mentions', () => {
const postProps = {
disable_group_highlight: true,
mentionHighlightDisabled: true,
};
const state = {
entities: {
users: {
currentUserId: 'a123',
profiles: {
a123: {
username: 'a123',
notify_props: {
channel: 'true',
},
},
},
},
groups: {
myGroups: {
developers: {
id: 123,
name: 'developers',
allow_reference: true,
delete_at: 0,
},
},
},
},
};
const results = Selectors.makeGetMentionKeysForPost(state, postProps.disable_group_highlight, postProps.mentionHighlightDisabled);
const expected = [{key: '@a123'}];
assert.deepEqual(results, expected);
});
});
});

View file

@ -2,15 +2,46 @@
// See LICENSE.txt for license information.
import * as reselect from 'reselect';
import {GlobalState} from '@mm-redux/types/store';
import {UserMentionKey} from './users';
import {getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users';
import {getCurrentUserGroupMentionKeys} from '@mm-redux/selectors/entities/groups';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import * as types from '@mm-redux/types';
export const getCurrentSearchForCurrentTeam = reselect.createSelector(
(state: types.store.GlobalState) => state.entities.search.current,
(state: GlobalState) => state.entities.search.current,
getCurrentTeamId,
(current, teamId) => {
return current[teamId];
},
);
export const getAllUserMentionKeys: (state: GlobalState) => UserMentionKey[] = reselect.createSelector(
getCurrentUserMentionKeys,
(state: GlobalState) => getCurrentUserGroupMentionKeys(state),
(userMentionKeys, groupMentionKeys) => {
return userMentionKeys.concat(groupMentionKeys);
},
);
export const makeGetMentionKeysForPost: (state: GlobalState, disableGroupHighlight: boolean, mentionHighlightDisabled: boolean) => UserMentionKey[] = reselect.createSelector(
getAllUserMentionKeys,
getCurrentUserMentionKeys,
(state: GlobalState, disableGroupHighlight: boolean) => disableGroupHighlight,
(state: GlobalState, disableGroupHighlight: boolean, mentionHighlightDisabled: boolean) => mentionHighlightDisabled,
(allMentionKeys, mentionKeysWithoutGroups, disableGroupHighlight = false, mentionHighlightDisabled = false) => {
let mentionKeys = allMentionKeys;
if (disableGroupHighlight) {
mentionKeys = mentionKeysWithoutGroups;
}
if (mentionHighlightDisabled) {
const CHANNEL_MENTIONS = ['@all', '@channel', '@here'];
mentionKeys = mentionKeys.filter((value) => !CHANNEL_MENTIONS.includes(value.key));
}
return mentionKeys;
},
);

View file

@ -57,6 +57,9 @@ export type GroupsState = {
groups: {
[x: string]: Group;
};
myGroups: {
[x: string]: Group;
};
};
export type GroupSearchOpts = {
q: string;

View file

@ -110,3 +110,8 @@ export type PostsState = {
messagesHistory: MessageHistory;
expandedURLs: Dictionary<string>;
};
export type PostProps = {
disable_group_highlight?: boolean;
mentionHighlightDisabled: boolean;
}