diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index a5064df3d..10d6f7789 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -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) {
diff --git a/app/components/at_mention/at_mention.js b/app/components/at_mention/at_mention.js
index 0193171ac..bb79289de 100644
--- a/app/components/at_mention/at_mention.js
+++ b/app/components/at_mention/at_mention.js
@@ -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 (
+
+
+ {`@${group.name}`}
+
+
+ );
+ }
+
return {'@' + mentionName};
}
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 (
diff --git a/app/components/markdown/index.js b/app/components/markdown/index.js
index 2e484bf2a..c4f61fb06 100644
--- a/app/components/markdown/index.js
+++ b/app/components/markdown/index.js
@@ -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),
};
diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index 1cf5571a4..58187e4bf 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -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');
diff --git a/app/components/markdown/markdown.test.js b/app/components/markdown/markdown.test.js
index 437ad2e31..e5ae99326 100644
--- a/app/components/markdown/markdown.test.js
+++ b/app/components/markdown/markdown.test.js
@@ -33,24 +33,4 @@ describe('Markdown', () => {
expect(wrapper.getElement()).toMatchSnapshot();
});
-
- describe('getMentionKeys', () => {
- let wrapper;
- beforeAll(() => {
- wrapper = shallow(
- ,
- );
- });
-
- 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'}]);
- });
- });
});
diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js
index 8d7cb1fdb..508e617ef 100644
--- a/app/components/post_body/index.js
+++ b/app/components/post_body/index.js
@@ -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),
};
diff --git a/app/components/post_body/index.test.js b/app/components/post_body/index.test.js
index ca9e4484f..b5e87ad1a 100644
--- a/app/components/post_body/index.test.js
+++ b/app/components/post_body/index.test.js
@@ -77,6 +77,13 @@ describe('makeMapStateToProps', () => {
general: {
serverVersion: '',
},
+ users: {
+ profiles: {},
+ },
+ groups: {
+ groups: {},
+ myGroups: {},
+ },
},
};
const defaultOwnProps = {
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index 0ad73b874..edf7dbd77 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -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}
/>
);
diff --git a/app/mm-redux/client/client4.ts b/app/mm-redux/client/client4.ts
index df38bb185..e2925a73f 100644
--- a/app/mm-redux/client/client4.ts
+++ b/app/mm-redux/client/client4.ts
@@ -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(
diff --git a/app/mm-redux/reducers/entities/groups.ts b/app/mm-redux/reducers/entities/groups.ts
index f54b281ad..50d4e84c9 100644
--- a/app/mm-redux/reducers/entities/groups.ts
+++ b/app/mm-redux/reducers/entities/groups.ts
@@ -163,6 +163,20 @@ function syncables(state: Dictionary = {}, 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 = {}, action: GenericAction) {
}
export default combineReducers({
+ myGroups,
syncables,
members,
groups,
diff --git a/app/mm-redux/selectors/entities/groups.ts b/app/mm-redux/selectors/entities/groups.ts
index 0f3fb74f6..e46beb8aa 100644
--- a/app/mm-redux/selectors/entities/groups.ts
+++ b/app/mm-redux/selectors/entities/groups.ts
@@ -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) => {
+ const keys: UserMentionKey[] = [];
+ groups.forEach((group) => keys.push({key: `@${group.name}`}));
+ return keys;
+ },
+);
+
+export const getGroupsByName = reselect.createSelector(
+ getAllGroups,
+ (groups) => {
+ const groupsByName: Dictionary = {};
+
+ Object.values(groups).forEach((group) => {
+ groupsByName[group.name] = group;
+ });
+
+ return groupsByName;
+ },
+);
diff --git a/app/mm-redux/selectors/entities/search.test.js b/app/mm-redux/selectors/entities/search.test.js
index 1e736ff0d..5312508ed 100644
--- a/app/mm-redux/selectors/entities/search.test.js
+++ b/app/mm-redux/selectors/entities/search.test.js
@@ -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);
+ });
+ });
});
diff --git a/app/mm-redux/selectors/entities/search.ts b/app/mm-redux/selectors/entities/search.ts
index b5a8481d2..92356b394 100644
--- a/app/mm-redux/selectors/entities/search.ts
+++ b/app/mm-redux/selectors/entities/search.ts
@@ -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;
+ },
+);
+
diff --git a/app/mm-redux/types/groups.ts b/app/mm-redux/types/groups.ts
index 15279921d..ffe25417a 100644
--- a/app/mm-redux/types/groups.ts
+++ b/app/mm-redux/types/groups.ts
@@ -57,6 +57,9 @@ export type GroupsState = {
groups: {
[x: string]: Group;
};
+ myGroups: {
+ [x: string]: Group;
+ };
};
export type GroupSearchOpts = {
q: string;
diff --git a/app/mm-redux/types/posts.ts b/app/mm-redux/types/posts.ts
index 27f53afa2..36bd476f2 100644
--- a/app/mm-redux/types/posts.ts
+++ b/app/mm-redux/types/posts.ts
@@ -110,3 +110,8 @@ export type PostsState = {
messagesHistory: MessageHistory;
expandedURLs: Dictionary;
};
+
+export type PostProps = {
+ disable_group_highlight?: boolean;
+ mentionHighlightDisabled: boolean;
+}