[MM-42586] Reaction toggle behavior (#25412)

* add toggle_reaction action

* change to use toggle_reaction action instead of add_reaction

* add submit_reaction action

* add a selector to check if a reaction has already been added

* update test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
KyeongSoo Kim
2023-11-17 20:07:03 +09:00
коммит произвёл GitHub
родитель 2184876c77
Коммит 34ce0d00d4
17 изменённых файлов: 210 добавлений и 75 удалений

Просмотреть файл

@@ -12,10 +12,12 @@ import * as Actions from 'actions/post_actions';
import mockStore from 'tests/test_store'; import mockStore from 'tests/test_store';
import {Constants, ActionTypes, RHSStates} from 'utils/constants'; import {Constants, ActionTypes, RHSStates} from 'utils/constants';
import * as PostUtils from 'utils/post_utils';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
jest.mock('mattermost-redux/actions/posts', () => ({ jest.mock('mattermost-redux/actions/posts', () => ({
removeReaction: (...args: any[]) => ({type: 'MOCK_REMOVE_REACTION', args}),
addReaction: (...args: any[]) => ({type: 'MOCK_ADD_REACTION', args}), addReaction: (...args: any[]) => ({type: 'MOCK_ADD_REACTION', args}),
createPost: (...args: any[]) => ({type: 'MOCK_CREATE_POST', args}), createPost: (...args: any[]) => ({type: 'MOCK_CREATE_POST', args}),
createPostImmediately: (...args: any[]) => ({type: 'MOCK_CREATE_POST_IMMEDIATELY', args}), createPostImmediately: (...args: any[]) => ({type: 'MOCK_CREATE_POST_IMMEDIATELY', args}),
@@ -48,6 +50,8 @@ jest.mock('utils/user_agent', () => ({
isDesktopApp: jest.fn().mockReturnValue(false), isDesktopApp: jest.fn().mockReturnValue(false),
})); }));
const mockMakeGetIsReactionAlreadyAddedToPost = jest.spyOn(PostUtils, 'makeGetIsReactionAlreadyAddedToPost');
const POST_CREATED_TIME = Date.now(); const POST_CREATED_TIME = Date.now();
// This mocks the Date.now() function so it returns a constant value // This mocks the Date.now() function so it returns a constant value
@@ -433,6 +437,84 @@ describe('Actions.Posts', () => {
}); });
}); });
describe('submitReaction', () => {
describe('addReaction', () => {
test('should add reaction when the action is + and the reaction is not added', async () => {
const testStore = mockStore(initialState);
mockMakeGetIsReactionAlreadyAddedToPost.mockReturnValueOnce(() => false);
testStore.dispatch(Actions.submitReaction('post_id_1', '+', 'emoji_name_1'));
expect(testStore.getActions()).toEqual([
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
{args: ['emoji_name_1'], type: 'MOCK_ADD_RECENT_EMOJI'},
]);
});
test('should take no action when the action is + and the reaction has already been added', async () => {
const testStore = mockStore(initialState);
mockMakeGetIsReactionAlreadyAddedToPost.mockReturnValueOnce(() => true);
testStore.dispatch(Actions.submitReaction('post_id_1', '+', 'emoji_name_1'));
expect(testStore.getActions()).toEqual([]);
});
});
describe('removeReaction', () => {
test('should remove reaction when the action is - and the reaction has already been added', async () => {
const testStore = mockStore(initialState);
mockMakeGetIsReactionAlreadyAddedToPost.mockReturnValueOnce(() => true);
testStore.dispatch(Actions.submitReaction('post_id_1', '-', 'emoji_name_1'));
expect(testStore.getActions()).toEqual([
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_REMOVE_REACTION'},
]);
});
test('should take no action when the action is - and the reaction is not added', async () => {
const testStore = mockStore(initialState);
mockMakeGetIsReactionAlreadyAddedToPost.mockReturnValueOnce(() => false);
testStore.dispatch(Actions.submitReaction('post_id_1', '-', 'emoji_name_1'));
expect(testStore.getActions()).toEqual([]);
});
});
});
describe('toggleReaction', () => {
test('should add reaction when the reaction is not added', async () => {
const testStore = mockStore(initialState);
mockMakeGetIsReactionAlreadyAddedToPost.mockReturnValueOnce(() => false);
testStore.dispatch(Actions.toggleReaction('post_id_1', 'emoji_name_1'));
expect(testStore.getActions()).toEqual([
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
{args: ['emoji_name_1'], type: 'MOCK_ADD_RECENT_EMOJI'},
]);
});
test('should remove reaction when the reaction has already been added', async () => {
const testStore = mockStore(initialState);
mockMakeGetIsReactionAlreadyAddedToPost.mockReturnValueOnce(() => true);
testStore.dispatch(Actions.toggleReaction('post_id_1', 'emoji_name_1'));
expect(testStore.getActions()).toEqual([
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_REMOVE_REACTION'},
]);
});
});
test('addReaction', async () => { test('addReaction', async () => {
const testStore = mockStore(initialState); const testStore = mockStore(initialState);

Просмотреть файл

@@ -34,6 +34,7 @@ import {
StoragePrefixes, StoragePrefixes,
} from 'utils/constants'; } from 'utils/constants';
import {matchEmoticons} from 'utils/emoticons'; import {matchEmoticons} from 'utils/emoticons';
import {makeGetIsReactionAlreadyAddedToPost} from 'utils/post_utils';
import * as UserAgent from 'utils/user_agent'; import * as UserAgent from 'utils/user_agent';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
@@ -140,6 +141,36 @@ function storeCommentDraft(rootPostId: string, draft: null) {
}; };
} }
export function submitReaction(postId: string, action: string, emojiName: string) {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState() as GlobalState;
const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost();
const isReactionAlreadyAddedToPost = getIsReactionAlreadyAddedToPost(state, postId, emojiName);
if (action === '+' && !isReactionAlreadyAddedToPost) {
dispatch(addReaction(postId, emojiName));
} else if (action === '-' && isReactionAlreadyAddedToPost) {
dispatch(PostActions.removeReaction(postId, emojiName));
}
return {data: true};
};
}
export function toggleReaction(postId: string, emojiName: string) {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState() as GlobalState;
const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost();
const isReactionAlreadyAddedToPost = getIsReactionAlreadyAddedToPost(state, postId, emojiName);
if (isReactionAlreadyAddedToPost) {
return dispatch(PostActions.removeReaction(postId, emojiName));
}
return dispatch(addReaction(postId, emojiName));
};
}
export function addReaction(postId: string, emojiName: string) { export function addReaction(postId: string, emojiName: string) {
return (dispatch: DispatchFunc) => { return (dispatch: DispatchFunc) => {
dispatch(PostActions.addReaction(postId, emojiName)); dispatch(PostActions.addReaction(postId, emojiName));

Просмотреть файл

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import { import {
removeReaction,
addMessageIntoHistory, addMessageIntoHistory,
moveHistoryIndexBack, moveHistoryIndexBack,
} from 'mattermost-redux/actions/posts'; } from 'mattermost-redux/actions/posts';
@@ -17,7 +16,6 @@ import {
updateCommentDraft, updateCommentDraft,
makeOnMoveHistoryIndex, makeOnMoveHistoryIndex,
submitPost, submitPost,
submitReaction,
submitCommand, submitCommand,
makeOnSubmit, makeOnSubmit,
makeOnEditLatestPost, makeOnEditLatestPost,
@@ -63,6 +61,7 @@ jest.mock('actions/hooks', () => ({
})); }));
jest.mock('actions/post_actions', () => ({ jest.mock('actions/post_actions', () => ({
submitReaction: (...args) => ({type: 'MOCK_SUBMIT_REACTION', args}),
addReaction: (...args) => ({type: 'MOCK_ADD_REACTION', args}), addReaction: (...args) => ({type: 'MOCK_ADD_REACTION', args}),
createPost: jest.fn(() => ({type: 'MOCK_CREATE_POST'})), createPost: jest.fn(() => ({type: 'MOCK_CREATE_POST'})),
setEditingPost: (...args) => ({type: 'MOCK_SET_EDITING_POST', args}), setEditingPost: (...args) => ({type: 'MOCK_SET_EDITING_POST', args}),
@@ -293,25 +292,6 @@ describe('rhs view actions', () => {
}); });
}); });
describe('submitReaction', () => {
test('it adds a reaction when action is +', () => {
store.dispatch(submitReaction('post_id_1', '+', 'emoji_name_1'));
const testStore = mockStore(initialState);
testStore.dispatch(PostActions.addReaction('post_id_1', 'emoji_name_1'));
expect(store.getActions()).toEqual(testStore.getActions());
});
test('it removes a reaction when action is -', () => {
store.dispatch(submitReaction('post_id_1', '-', 'emoji_name_1'));
const testStore = mockStore(initialState);
testStore.dispatch(removeReaction('post_id_1', 'emoji_name_1'));
expect(store.getActions()).toEqual(testStore.getActions());
});
});
describe('submitCommand', () => { describe('submitCommand', () => {
const args = { const args = {
channel_id: channelId, channel_id: channelId,
@@ -400,7 +380,7 @@ describe('rhs view actions', () => {
})); }));
const testStore = mockStore(initialState); const testStore = mockStore(initialState);
testStore.dispatch(submitReaction(latestPostId, '+', 'smile')); testStore.dispatch(PostActions.submitReaction(latestPostId, '+', 'smile'));
expect(store.getActions()).toEqual( expect(store.getActions()).toEqual(
expect.arrayContaining(testStore.getActions()), expect.arrayContaining(testStore.getActions()),

Просмотреть файл

@@ -4,7 +4,6 @@
import type {Post} from '@mattermost/types/posts'; import type {Post} from '@mattermost/types/posts';
import { import {
removeReaction,
addMessageIntoHistory, addMessageIntoHistory,
moveHistoryIndexBack, moveHistoryIndexBack,
moveHistoryIndexForward, moveHistoryIndexForward,
@@ -106,17 +105,6 @@ export function submitPost(channelId: string, rootId: string, draft: PostDraft)
}; };
} }
export function submitReaction(postId: string, action: string, emojiName: string) {
return (dispatch: DispatchFunc) => {
if (action === '+') {
dispatch(PostActions.addReaction(postId, emojiName));
} else if (action === '-') {
dispatch(removeReaction(postId, emojiName));
}
return {data: true};
};
}
export function submitCommand(channelId: string, rootId: string, draft: PostDraft) { export function submitCommand(channelId: string, rootId: string, draft: PostDraft) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => { return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState(); const state = getState();
@@ -170,7 +158,7 @@ export function makeOnSubmit(channelId: string, rootId: string, latestPostId: st
const emojiMap = new EmojiMap(emojis); const emojiMap = new EmojiMap(emojis);
if (isReaction && emojiMap.has(isReaction[2])) { if (isReaction && emojiMap.has(isReaction[2])) {
dispatch(submitReaction(latestPostId, isReaction[1], isReaction[2])); dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2]));
} else if (message.indexOf('/') === 0 && !options.ignoreSlash) { } else if (message.indexOf('/') === 0 && !options.ignoreSlash) {
try { try {
await dispatch(submitCommand(channelId, rootId, draft)); await dispatch(submitCommand(channelId, rootId, draft));

Просмотреть файл

@@ -76,6 +76,7 @@ const baseProp: Props = {
addMessageIntoHistory: jest.fn(), addMessageIntoHistory: jest.fn(),
moveHistoryIndexBack: jest.fn(), moveHistoryIndexBack: jest.fn(),
moveHistoryIndexForward: jest.fn(), moveHistoryIndexForward: jest.fn(),
submitReaction: jest.fn(),
addReaction: jest.fn(), addReaction: jest.fn(),
removeReaction: jest.fn(), removeReaction: jest.fn(),
clearDraftUploads: jest.fn(), clearDraftUploads: jest.fn(),
@@ -728,13 +729,13 @@ describe('components/advanced_create_post', () => {
}); });
it('onSubmit test for addReaction message', async () => { it('onSubmit test for addReaction message', async () => {
const addReaction = jest.fn(); const submitReaction = jest.fn();
const wrapper = shallow( const wrapper = shallow(
advancedCreatePost({ advancedCreatePost({
actions: { actions: {
...baseProp.actions, ...baseProp.actions,
addReaction, submitReaction,
}, },
}), }),
); );
@@ -744,17 +745,17 @@ describe('components/advanced_create_post', () => {
}); });
await (wrapper.instance() as AdvancedCreatePost).handleSubmit(submitEvent); await (wrapper.instance() as AdvancedCreatePost).handleSubmit(submitEvent);
expect(addReaction).toHaveBeenCalledWith('a', 'smile'); expect(submitReaction).toHaveBeenCalledWith('a', '+', 'smile');
}); });
it('onSubmit test for removeReaction message', () => { it('onSubmit test for removeReaction message', async () => {
const removeReaction = jest.fn(); const submitReaction = jest.fn();
const wrapper = shallow( const wrapper = shallow(
advancedCreatePost({ advancedCreatePost({
actions: { actions: {
...baseProp.actions, ...baseProp.actions,
removeReaction, submitReaction,
}, },
}), }),
); );
@@ -763,9 +764,8 @@ describe('components/advanced_create_post', () => {
message: '-:smile:', message: '-:smile:',
}); });
const form = wrapper.find('#create_post'); await (wrapper.instance() as AdvancedCreatePost).handleSubmit(submitEvent);
form.simulate('Submit', {preventDefault: jest.fn()}); expect(submitReaction).toHaveBeenCalledWith('a', '-', 'smile');
expect(removeReaction).toHaveBeenCalledWith('a', 'smile');
}); });
/*it('check for postError state on handlePostError callback', () => { /*it('check for postError state on handlePostError callback', () => {

Просмотреть файл

@@ -174,6 +174,8 @@ export type Props = {
// func called for navigation through messages by Down arrow // func called for navigation through messages by Down arrow
moveHistoryIndexForward: (index: string) => Promise<void>; moveHistoryIndexForward: (index: string) => Promise<void>;
submitReaction: (postId: string, action: string, emojiName: string) => void;
// func called for adding a reaction // func called for adding a reaction
addReaction: (postId: string, emojiName: string) => void; addReaction: (postId: string, emojiName: string) => void;
@@ -799,10 +801,8 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
const emojiName = isReaction[2]; const emojiName = isReaction[2];
const postId = this.props.latestReplyablePostId; const postId = this.props.latestReplyablePostId;
if (postId && action === '+') { if (postId) {
this.props.actions.addReaction(postId, emojiName); this.props.actions.submitReaction(postId, action, emojiName);
} else if (postId && action === '-') {
this.props.actions.removeReaction(postId, emojiName);
} }
this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, null, channelId); this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, null, channelId);

Просмотреть файл

@@ -36,7 +36,7 @@ import type {ActionResult, GetStateFunc, DispatchFunc} from 'mattermost-redux/ty
import {executeCommand} from 'actions/command'; import {executeCommand} from 'actions/command';
import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks'; import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks';
import {addReaction, createPost, setEditingPost, emitShortcutReactToLastPostFrom} from 'actions/post_actions'; import {addReaction, createPost, setEditingPost, emitShortcutReactToLastPostFrom, submitReaction} from 'actions/post_actions';
import {actionOnGlobalItemsWithPrefix} from 'actions/storage'; import {actionOnGlobalItemsWithPrefix} from 'actions/storage';
import {scrollPostListToBottom} from 'actions/views/channel'; import {scrollPostListToBottom} from 'actions/views/channel';
import {removeDraft, updateDraft} from 'actions/views/drafts'; import {removeDraft, updateDraft} from 'actions/views/drafts';
@@ -153,6 +153,7 @@ type Actions = {
addReaction: (postId: string, emojiName: string) => void; addReaction: (postId: string, emojiName: string) => void;
onSubmitPost: (post: Post, fileInfos: FileInfo[]) => void; onSubmitPost: (post: Post, fileInfos: FileInfo[]) => void;
removeReaction: (postId: string, emojiName: string) => void; removeReaction: (postId: string, emojiName: string) => void;
submitReaction: (postId: string, action: string, emojiName: string) => void;
clearDraftUploads: () => void; clearDraftUploads: () => void;
runMessageWillBePostedHooks: (originalPost: Post) => ActionResult; runMessageWillBePostedHooks: (originalPost: Post) => ActionResult;
runSlashCommandWillBePostedHooks: (originalMessage: string, originalArgs: CommandArgs) => ActionResult; runSlashCommandWillBePostedHooks: (originalMessage: string, originalArgs: CommandArgs) => ActionResult;
@@ -202,6 +203,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
onSubmitPost, onSubmitPost,
moveHistoryIndexBack, moveHistoryIndexBack,
moveHistoryIndexForward, moveHistoryIndexForward,
submitReaction,
addReaction, addReaction,
removeReaction, removeReaction,
setDraft, setDraft,

Просмотреть файл

@@ -7,7 +7,7 @@ import type {ActionCreatorsMapObject, Dispatch} from 'redux';
import type {Action} from 'mattermost-redux/types/actions'; import type {Action} from 'mattermost-redux/types/actions';
import {addReaction} from 'actions/post_actions'; import {toggleReaction} from 'actions/post_actions';
import PostReaction from './post_reaction'; import PostReaction from './post_reaction';
import type {Props} from './post_reaction'; import type {Props} from './post_reaction';
@@ -15,7 +15,7 @@ import type {Props} from './post_reaction';
function mapDispatchToProps(dispatch: Dispatch) { function mapDispatchToProps(dispatch: Dispatch) {
return { return {
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Props['actions']>({ actions: bindActionCreators<ActionCreatorsMapObject<Action>, Props['actions']>({
addReaction, toggleReaction,
}, dispatch), }, dispatch),
}; };
} }

Просмотреть файл

@@ -18,7 +18,7 @@ describe('components/post_view/PostReaction', () => {
showEmojiPicker: false, showEmojiPicker: false,
toggleEmojiPicker: jest.fn(), toggleEmojiPicker: jest.fn(),
actions: { actions: {
addReaction: jest.fn(), toggleReaction: jest.fn(),
}, },
}; };
@@ -27,13 +27,13 @@ describe('components/post_view/PostReaction', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('should call addReaction and toggleEmojiPicker on handleAddEmoji', () => { test('should call toggleReaction and toggleEmojiPicker on handleToggleEmoji', () => {
const wrapper = shallow(<PostReaction {...baseProps}/>); const wrapper = shallow(<PostReaction {...baseProps}/>);
const instance = wrapper.instance() as PostReaction; const instance = wrapper.instance() as PostReaction;
instance.handleAddEmoji({name: 'smile'} as Emoji); instance.handleToggleEmoji({name: 'smile'} as Emoji);
expect(baseProps.actions.addReaction).toHaveBeenCalledTimes(1); expect(baseProps.actions.toggleReaction).toHaveBeenCalledTimes(1);
expect(baseProps.actions.addReaction).toHaveBeenCalledWith('post_id_1', 'smile'); expect(baseProps.actions.toggleReaction).toHaveBeenCalledWith('post_id_1', 'smile');
expect(baseProps.toggleEmojiPicker).toHaveBeenCalledTimes(1); expect(baseProps.toggleEmojiPicker).toHaveBeenCalledTimes(1);
}); });
}); });

Просмотреть файл

@@ -30,7 +30,7 @@ export type Props = {
showEmojiPicker: boolean; showEmojiPicker: boolean;
toggleEmojiPicker: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; toggleEmojiPicker: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
actions: { actions: {
addReaction: (postId: string, emojiName: string) => (dispatch: Dispatch) => {data: boolean}; toggleReaction: (postId: string, emojiName: string) => (dispatch: Dispatch) => {data: boolean};
}; };
} }
@@ -45,10 +45,10 @@ export default class PostReaction extends React.PureComponent<Props, State> {
showEmojiPicker: false, showEmojiPicker: false,
}; };
handleAddEmoji = (emoji: Emoji): void => { handleToggleEmoji = (emoji: Emoji): void => {
this.setState({showEmojiPicker: false}); this.setState({showEmojiPicker: false});
const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name; const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name;
this.props.actions.addReaction(this.props.postId, emojiName); this.props.actions.toggleReaction(this.props.postId, emojiName);
this.props.toggleEmojiPicker(); this.props.toggleEmojiPicker();
}; };
@@ -79,7 +79,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
show={showEmojiPicker} show={showEmojiPicker}
target={this.props.getDotMenuRef} target={this.props.getDotMenuRef}
onHide={this.props.toggleEmojiPicker} onHide={this.props.toggleEmojiPicker}
onEmojiClick={this.handleAddEmoji} onEmojiClick={this.handleToggleEmoji}
topOffset={TOP_OFFSET} topOffset={TOP_OFFSET}
spaceRequiredAbove={spaceRequiredAbove} spaceRequiredAbove={spaceRequiredAbove}
spaceRequiredBelow={spaceRequiredBelow} spaceRequiredBelow={spaceRequiredBelow}

Просмотреть файл

@@ -9,7 +9,7 @@ import type {Emoji} from '@mattermost/types/emojis';
import type {GenericAction} from 'mattermost-redux/types/actions'; import type {GenericAction} from 'mattermost-redux/types/actions';
import {addReaction} from 'actions/post_actions'; import {toggleReaction} from 'actions/post_actions';
import {getEmojiMap} from 'selectors/emojis'; import {getEmojiMap} from 'selectors/emojis';
import {getCurrentLocale} from 'selectors/i18n'; import {getCurrentLocale} from 'selectors/i18n';
@@ -20,7 +20,7 @@ import PostReaction from './post_recent_reactions';
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) { function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return { return {
actions: bindActionCreators({ actions: bindActionCreators({
addReaction, toggleReaction,
}, dispatch), }, dispatch),
}; };
} }

Просмотреть файл

@@ -27,7 +27,7 @@ type Props = {
size: number; size: number;
defaultEmojis: Emoji[]; defaultEmojis: Emoji[];
actions: { actions: {
addReaction: (postId: string, emojiName: string) => void; toggleReaction: (postId: string, emojiName: string) => void;
}; };
} }
@@ -41,9 +41,9 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
size: 3, size: 3,
}; };
handleAddEmoji = (emoji: Emoji): void => { handleToggleEmoji = (emoji: Emoji): void => {
const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name; const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name;
this.props.actions.addReaction(this.props.postId, emojiName); this.props.actions.toggleReaction(this.props.postId, emojiName);
}; };
complementEmojis = (emojis: Emoji[]): (Emoji[]) => { complementEmojis = (emojis: Emoji[]): (Emoji[]) => {
@@ -108,7 +108,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
<React.Fragment> <React.Fragment>
<EmojiItem <EmojiItem
emoji={emoji} emoji={emoji}
onItemClick={this.handleAddEmoji} onItemClick={this.handleToggleEmoji}
order={n} order={n}
/> />
</React.Fragment> </React.Fragment>

Просмотреть файл

@@ -11,7 +11,7 @@ import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {canAddReactions} from 'mattermost-redux/selectors/entities/reactions'; import {canAddReactions} from 'mattermost-redux/selectors/entities/reactions';
import type {GenericAction} from 'mattermost-redux/types/actions'; import type {GenericAction} from 'mattermost-redux/types/actions';
import {addReaction} from 'actions/post_actions'; import {toggleReaction} from 'actions/post_actions';
import {makeGetUniqueReactionsToPost} from 'utils/post_utils'; import {makeGetUniqueReactionsToPost} from 'utils/post_utils';
@@ -43,7 +43,7 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) { function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return { return {
actions: bindActionCreators({ actions: bindActionCreators({
addReaction, toggleReaction,
}, dispatch), }, dispatch),
}; };
} }

Просмотреть файл

@@ -51,7 +51,7 @@ type Props = {
/** /**
* Function to add a reaction to the post * Function to add a reaction to the post
*/ */
addReaction: (postId: string, emojiName: string) => void; toggleReaction: (postId: string, emojiName: string) => void;
}; };
}; };
@@ -91,7 +91,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
handleEmojiClick = (emoji: Emoji): void => { handleEmojiClick = (emoji: Emoji): void => {
this.setState({showEmojiPicker: false}); this.setState({showEmojiPicker: false});
const emojiName = isSystemEmoji(emoji) ? emoji.short_names[0] : emoji.name; const emojiName = isSystemEmoji(emoji) ? emoji.short_names[0] : emoji.name;
this.props.actions.addReaction(this.props.post.id, emojiName); this.props.actions.toggleReaction(this.props.post.id, emojiName);
}; };
hideEmojiPicker = (): void => { hideEmojiPicker = (): void => {

Просмотреть файл

@@ -27,7 +27,7 @@ describe('components/ReactionList', () => {
const teamId = 'teamId'; const teamId = 'teamId';
const actions = { const actions = {
addReaction: jest.fn(), toggleReaction: jest.fn(),
}; };
const baseProps = { const baseProps = {

Просмотреть файл

@@ -1240,10 +1240,10 @@ describe('PostUtils.isWithinCodeBlock', () => {
it('should handle whitespace within and around code blocks', () => { it('should handle whitespace within and around code blocks', () => {
const [caretPosition, message] = getCaretAndMsg(` const [caretPosition, message] = getCaretAndMsg(`
|${TRIPLE_BACKTICKS} |${TRIPLE_BACKTICKS}
| Test text asd 1 | Test text asd 1
| ${CARET_MARKER} | ${CARET_MARKER}
|${TRIPLE_BACKTICKS} |${TRIPLE_BACKTICKS}
`); `);
expect(PostUtils.isWithinCodeBlock(message, caretPosition)).toBe(true); expect(PostUtils.isWithinCodeBlock(message, caretPosition)).toBe(true);
}); });
@@ -1319,3 +1319,37 @@ describe('PostUtils.getUserOrGroupFromMentionName', () => {
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
}); });
describe('makeGetIsReactionAlreadyAddedToPost', () => {
const currentUserId = 'current_user_id';
const baseState = {
entities: {
users: {
currentUserId,
},
posts: {
reactions: {
post_id_1: {
'current_user_id-smile': {
emoji_name: 'smile',
user_id: currentUserId,
post_id: 'post_id_1',
},
},
},
},
general: {
config: {},
},
emojis: {},
}} as unknown as GlobalState;
test('should return true if the post has an emoji that the user has reacted to.', () => {
const getIsReactionAlreadyAddedToPost = PostUtils.makeGetIsReactionAlreadyAddedToPost();
expect(getIsReactionAlreadyAddedToPost(baseState, 'post_id_1', 'sad')).toBeFalsy();
expect(getIsReactionAlreadyAddedToPost(baseState, 'post_id_1', 'smile')).toBeTruthy();
});
});

Просмотреть файл

@@ -710,6 +710,24 @@ export function makeGetUniqueReactionsToPost(): (state: GlobalState, postId: Pos
); );
} }
export function makeGetIsReactionAlreadyAddedToPost(): (state: GlobalState, postId: Post['id'], emojiName: string) => boolean {
const getUniqueReactionsToPost = makeGetUniqueReactionsToPost();
return createSelector(
'makeGetIsReactionAlreadyAddedToPost',
(state: GlobalState, postId: string) => getUniqueReactionsToPost(state, postId),
getCurrentUserId,
(state: GlobalState, postId: string, emojiName: string) => emojiName,
(reactions, currentUserId, emojiName) => {
const reactionsForPost = reactions || {};
const isReactionAlreadyAddedToPost = Object.values(reactionsForPost).some((reaction) => reaction.user_id === currentUserId && reaction.emoji_name === emojiName);
return isReactionAlreadyAddedToPost;
},
);
}
export function getMentionDetails(usersByUsername: Record<string, UserProfile | Group>, mentionName: string): UserProfile | Group | undefined { export function getMentionDetails(usersByUsername: Record<string, UserProfile | Group>, mentionName: string): UserProfile | Group | undefined {
let mentionNameToLowerCase = mentionName.toLowerCase(); let mentionNameToLowerCase = mentionName.toLowerCase();