From 6027c850bdc198689c738d88262c4aa0209aca86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 7 Aug 2024 21:27:50 +0200 Subject: [PATCH] MM-59881 Bubble submit result and expose to plugins (#27766) Automatic Merge --- webapp/channels/src/actions/command.test.js | 16 +-- webapp/channels/src/actions/command.ts | 38 +++--- .../channels/src/actions/post_actions.test.ts | 8 +- webapp/channels/src/actions/post_actions.ts | 33 ++--- .../src/actions/views/create_comment.tsx | 38 +++--- .../advanced_create_comment.tsx | 9 ++ .../advanced_text_editor.tsx | 13 +- .../advanced_text_editor/use_submit.tsx | 6 +- .../src/actions/posts.test.ts | 32 ----- .../mattermost-redux/src/actions/posts.ts | 125 +++--------------- webapp/channels/src/plugins/export.js | 4 +- 11 files changed, 117 insertions(+), 205 deletions(-) diff --git a/webapp/channels/src/actions/command.test.js b/webapp/channels/src/actions/command.test.js index e35058051a..da0e7c159d 100644 --- a/webapp/channels/src/actions/command.test.js +++ b/webapp/channels/src/actions/command.test.js @@ -183,7 +183,7 @@ describe('executeCommand', () => { modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL, }); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); }); @@ -198,7 +198,7 @@ describe('executeCommand', () => { modalId: 'user_settings', }, ]); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); }); @@ -220,7 +220,7 @@ describe('executeCommand', () => { toHaveBeenCalledWith('/leave is not supported in reply threads. Use it in the center channel instead.', 'channel_id', 'root_id'); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); test('should show private modal if channel is private', async () => { @@ -236,7 +236,7 @@ describe('executeCommand', () => { dialogProps: {channel: {type: Constants.PRIVATE_CHANNEL}}, }); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); test('should use user id as name if channel is dm', async () => { @@ -248,7 +248,7 @@ describe('executeCommand', () => { const result = await store.dispatch(executeCommand('/leave', {})); expect(store.getActions()[0].data).toEqual([{category: 'direct_channel_show', name: 'userId', user_id: 'user123', value: 'false'}]); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); test('should use channel id as name if channel is gm', async () => { @@ -260,7 +260,7 @@ describe('executeCommand', () => { const result = await store.dispatch(executeCommand('/leave', {})); expect(store.getActions()[0].data).toEqual([{category: 'group_channel_show', name: 'channelId', user_id: 'user123', value: 'false'}]); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); }); @@ -295,7 +295,7 @@ describe('executeCommand', () => { type: ActionTypes.MODAL_OPEN, modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, }); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); test('should show error when marketpace is not enabled', async () => { @@ -395,7 +395,7 @@ describe('executeCommand', () => { query: undefined, selected_field: undefined, }, true); - expect(result).toEqual({data: true}); + expect(result.data).toBeDefined(); }); }); }); diff --git a/webapp/channels/src/actions/command.ts b/webapp/channels/src/actions/command.ts index 524e261761..72eef65896 100644 --- a/webapp/channels/src/actions/command.ts +++ b/webapp/channels/src/actions/command.ts @@ -1,7 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {CommandArgs} from '@mattermost/types/integrations'; +import type {AppCallResponse} from '@mattermost/types/apps'; +import type {CommandArgs, CommandResponse} from '@mattermost/types/integrations'; import {IntegrationTypes} from 'mattermost-redux/action_types'; import {unfavoriteChannel} from 'mattermost-redux/actions/channels'; @@ -39,7 +40,14 @@ import type {GlobalState} from 'types/store'; import {doAppSubmit, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps'; import {trackEvent} from './telemetry_actions'; -export function executeCommand(message: string, args: CommandArgs): ActionFuncAsync { +export type ExecuteCommandReturnType = { + frontendHandled?: boolean; + silentFailureReason?: Error; + commandResponse?: CommandResponse; + appResponse?: AppCallResponse; +} + +export function executeCommand(message: string, args: CommandArgs): ActionFuncAsync { return async (dispatch, getState) => { const state = getState() as GlobalState; @@ -71,7 +79,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs switch (cmd) { case '/search': dispatch(PostActions.searchForTerm(msg.substring(cmdLength + 1, msg.length))); - return {data: true}; + return {data: {frontendHandled: true}}; case '/shortcuts': if (UserAgent.isMobile()) { const error = {message: localizeMessage('create_post.shortcutsNotSupported', 'Keyboard shortcuts are not supported on your device')}; @@ -79,20 +87,20 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs } dispatch(openModal({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL, dialogType: KeyboardShortcutsModal})); - return {data: true}; + return {data: {frontendHandled: true}}; case '/leave': { // /leave command not supported in reply threads. if (args.channel_id && args.root_id) { dispatch(GlobalActions.sendEphemeralPost('/leave is not supported in reply threads. Use it in the center channel instead.', args.channel_id, args.root_id)); - return {data: true}; + return {data: {frontendHandled: true}}; } const channel = getCurrentChannel(state); if (!channel) { - return {data: false}; + return {data: {silentFailureReason: new Error('cannot find current channel')}}; } if (channel.type === Constants.PRIVATE_CHANNEL) { dispatch(openModal({modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL, dialogType: LeaveChannelModal, dialogProps: {channel}})); - return {data: true}; + return {data: {frontendHandled: true}}; } if ( channel.type === Constants.DM_CHANNEL || @@ -118,13 +126,13 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs dispatch(unfavoriteChannel(channel.id)); } - return {data: true}; + return {data: {frontendHandled: true}}; } break; } case '/settings': dispatch(openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true}})); - return {data: true}; + return {data: {frontendHandled: true}}; case '/marketplace': // check if user has permissions to access the read plugins if (!haveICurrentTeamPermission(state, Permissions.SYSCONSOLE_WRITE_PLUGINS)) { @@ -137,7 +145,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs } dispatch(openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal, dialogProps: {openedFrom: 'command'}})); - return {data: true}; + return {data: {frontendHandled: true}}; case '/collapse': case '/expand': dispatch(PostActions.resetEmbedVisibility()); @@ -173,14 +181,14 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs if (callResp.text) { dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.text, args)); } - return {data: true}; + return {data: {appResponse: callResp}}; case AppCallResponseTypes.FORM: if (callResp.form) { dispatch(openAppsModal(callResp.form, creq.context)); } - return {data: true}; + return {data: {appResponse: callResp}}; case AppCallResponseTypes.NAVIGATE: - return {data: true}; + return {data: {appResponse: callResp}}; default: return createErrorMessage(intlShim.formatMessage( { @@ -213,7 +221,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs if (msg.trim() === '/logout') { GlobalActions.emitUserLoggedOutEvent(hasGotoLocation ? data.goto_location : '/'); - return {data: true}; + return {data: {response: data}}; } if (data.trigger_id) { @@ -230,6 +238,6 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs } } - return {data: true}; + return {data: {response: data}}; }; } diff --git a/webapp/channels/src/actions/post_actions.test.ts b/webapp/channels/src/actions/post_actions.test.ts index d5e4fae73f..2d0101cb08 100644 --- a/webapp/channels/src/actions/post_actions.test.ts +++ b/webapp/channels/src/actions/post_actions.test.ts @@ -358,7 +358,7 @@ describe('Actions.Posts', () => { const immediateExpectedState = [{ args: [newPost, files], - type: 'MOCK_CREATE_POST_IMMEDIATELY', + type: 'MOCK_CREATE_POST', }, { args: ['draft_current_channel_id', null], type: 'MOCK_SET_GLOBAL_ITEM', @@ -453,8 +453,8 @@ describe('Actions.Posts', () => { 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'}, + {args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'}, ]); }); @@ -503,8 +503,8 @@ describe('Actions.Posts', () => { 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'}, + {args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'}, ]); }); @@ -529,8 +529,8 @@ describe('Actions.Posts', () => { await testStore.dispatch(Actions.addReaction('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'}, + {args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'}, ]); }); test('should not add reaction if we are over the limit', async () => { diff --git a/webapp/channels/src/actions/post_actions.ts b/webapp/channels/src/actions/post_actions.ts index 708dd35951..4ef136b91f 100644 --- a/webapp/channels/src/actions/post_actions.ts +++ b/webapp/channels/src/actions/post_actions.ts @@ -40,12 +40,12 @@ import { } from 'utils/constants'; import {matchEmoticons} from 'utils/emoticons'; import {makeGetIsReactionAlreadyAddedToPost, makeGetUniqueEmojiNameReactionsForPost} from 'utils/post_utils'; -import * as UserAgent from 'utils/user_agent'; import type {GlobalState} from 'types/store'; import {completePostReceive} from './new_post'; import type {NewPostMessageProps} from './new_post'; +import type {SubmitPostReturnType} from './views/create_comment'; export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync { return async (dispatch, getState) => { @@ -106,7 +106,7 @@ export function unflagPost(postId: string): ActionFuncAsync { }; } -export function createPost(post: Post, files: FileInfo[]): ActionFuncAsync { +export function createPost(post: Post, files: FileInfo[], afterSubmit?: (response: SubmitPostReturnType) => void): ActionFuncAsync { return async (dispatch) => { // parse message and emit emoji event const emojis = matchEmoticons(post.message); @@ -115,12 +115,7 @@ export function createPost(post: Post, files: FileInfo[]): ActionFuncAsync { dispatch(addRecentEmojis(trimmedEmojis)); } - let result; - if (UserAgent.isIosClassic()) { - result = await dispatch(PostActions.createPostImmediately(post, files)); - } else { - result = await dispatch(PostActions.createPost(post, files)); - } + const result = await dispatch(PostActions.createPost(post, files, afterSubmit)); if (post.root_id) { dispatch(storeCommentDraft(post.root_id, null)); @@ -146,23 +141,23 @@ function storeCommentDraft(rootPostId: string, draft: null): ActionFunc { }; } -export function submitReaction(postId: string, action: string, emojiName: string): ActionFunc { - return (dispatch, getState) => { +export function submitReaction(postId: string, action: string, emojiName: string): ActionFuncAsync { + return async (dispatch, getState) => { const state = getState() as GlobalState; const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost(); const isReactionAlreadyAddedToPost = getIsReactionAlreadyAddedToPost(state, postId, emojiName); if (action === '+' && !isReactionAlreadyAddedToPost) { - dispatch(addReaction(postId, emojiName)); + return dispatch(addReaction(postId, emojiName)); } else if (action === '-' && isReactionAlreadyAddedToPost) { - dispatch(PostActions.removeReaction(postId, emojiName)); + return dispatch(PostActions.removeReaction(postId, emojiName)); } - return {data: true}; + return {error: new Error(`unknown action ${action}`)}; }; } -export function toggleReaction(postId: string, emojiName: string): ActionFuncAsync { +export function toggleReaction(postId: string, emojiName: string): ActionFuncAsync { return async (dispatch, getState) => { const state = getState(); const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost(); @@ -176,9 +171,9 @@ export function toggleReaction(postId: string, emojiName: string): ActionFuncAsy }; } -export function addReaction(postId: string, emojiName: string): ActionFunc { +export function addReaction(postId: string, emojiName: string): ActionFuncAsync { const getUniqueEmojiNameReactionsForPost = makeGetUniqueEmojiNameReactionsForPost(); - return (dispatch, getState) => { + return async (dispatch, getState) => { const state = getState() as GlobalState; const config = getConfig(state); const uniqueEmojiNames = getUniqueEmojiNameReactionsForPost(state, postId) ?? []; @@ -193,12 +188,12 @@ export function addReaction(postId: string, emojiName: string): ActionFunc { onExited: () => closeModal(ModalIdentifiers.REACTION_LIMIT_REACHED), }, })); - return {data: false}; + return {error: new Error('reached reaction limit')}; } - dispatch(PostActions.addReaction(postId, emojiName)); dispatch(addRecentEmoji(emojiName)); - return {data: true}; + const result = await dispatch(PostActions.addReaction(postId, emojiName)); + return result; }; } diff --git a/webapp/channels/src/actions/views/create_comment.tsx b/webapp/channels/src/actions/views/create_comment.tsx index e7cf1d6f35..e7bce9091a 100644 --- a/webapp/channels/src/actions/views/create_comment.tsx +++ b/webapp/channels/src/actions/views/create_comment.tsx @@ -3,9 +3,8 @@ import type {Post} from '@mattermost/types/posts'; -import { - addMessageIntoHistory, -} from 'mattermost-redux/actions/posts'; +import type {CreatePostReturnType, SubmitReactionReturnType} from 'mattermost-redux/actions/posts'; +import {addMessageIntoHistory} from 'mattermost-redux/actions/posts'; import {Permissions} from 'mattermost-redux/constants'; import {createSelector} from 'mattermost-redux/selectors/create_selector'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; @@ -25,6 +24,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import type {ActionFunc, ActionFuncAsync} from 'mattermost-redux/types/actions'; import {isPostPendingOrFailed} from 'mattermost-redux/utils/post_utils'; +import type {ExecuteCommandReturnType} from 'actions/command'; import {executeCommand} from 'actions/command'; import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks'; import * as PostActions from 'actions/post_actions'; @@ -56,7 +56,7 @@ export function updateCommentDraft(rootId: string, draft?: PostDraft, save = fal return updateDraft(key, draft ?? null, rootId, save); } -export function submitPost(channelId: string, rootId: string, draft: PostDraft): ActionFuncAsync { +export function submitPost(channelId: string, rootId: string, draft: PostDraft, afterSubmit?: (response: SubmitPostReturnType) => void): ActionFuncAsync { return async (dispatch, getState) => { const state = getState(); @@ -103,11 +103,13 @@ export function submitPost(channelId: string, rootId: string, draft: PostDraft): post = hookResult.data; - return dispatch(PostActions.createPost(post, draft.fileInfos)); + return dispatch(PostActions.createPost(post, draft.fileInfos, afterSubmit)); }; } -export function submitCommand(channelId: string, rootId: string, draft: PostDraft): ActionFuncAsync { +type SubmitCommandRerturnType = ExecuteCommandReturnType & CreatePostReturnType; + +export function submitCommand(channelId: string, rootId: string, draft: PostDraft): ActionFuncAsync { return async (dispatch, getState) => { const state = getState(); @@ -126,13 +128,13 @@ export function submitCommand(channelId: string, rootId: string, draft: PostDraf return {error: hookResult.error}; } else if (!hookResult.data!.message && !hookResult.data!.args) { // do nothing with an empty return from a hook - return {}; + return {error: new Error('command not submitted due to plugin hook')}; } message = hookResult.data!.message; args = hookResult.data!.args; - const {error} = await dispatch(executeCommand(message, args)); + const {error, data} = await dispatch(executeCommand(message, args)); if (error) { if (error.sendMessage) { @@ -141,7 +143,7 @@ export function submitCommand(channelId: string, rootId: string, draft: PostDraf throw (error); } - return {}; + return {data: data!}; }; } @@ -175,7 +177,9 @@ export function makeOnSubmit(channelId: string, rootId: string, latestPostId: st }; } -export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean}): ActionFuncAsync { +export type SubmitPostReturnType = CreatePostReturnType & SubmitCommandRerturnType & SubmitReactionReturnType; + +export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean; afterSubmit?: (response: SubmitPostReturnType) => void}): ActionFuncAsync { return async (dispatch, getState) => { const {message, channelId, rootId} = draft; const state = getState(); @@ -190,14 +194,16 @@ export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean}): Ac if (isReaction && emojiMap.has(isReaction[2])) { const latestPostId = getLatestInteractablePostId(state, channelId, rootId); if (latestPostId) { - dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2])); + return dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2])); } - } else if (message.indexOf('/') === 0 && !options.ignoreSlash) { - await dispatch(submitCommand(channelId, rootId, draft)); - } else { - await dispatch(submitPost(channelId, rootId, draft)); + return {error: new Error('no post to react to')}; } - return {data: true}; + + if (message.indexOf('/') === 0 && !options.ignoreSlash) { + return dispatch(submitCommand(channelId, rootId, draft)); + } + + return dispatch(submitPost(channelId, rootId, draft, options.afterSubmit)); }; } diff --git a/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx b/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx index ce1fd99f27..6f9e181c90 100644 --- a/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx +++ b/webapp/channels/src/components/advanced_create_comment/advanced_create_comment.tsx @@ -5,6 +5,8 @@ import React from 'react'; +import type {SubmitPostReturnType} from 'actions/views/create_comment'; + import AdvancedTextEditor from 'components/advanced_text_editor/advanced_text_editor'; import {Locations} from 'utils/constants'; @@ -19,6 +21,11 @@ export type Props = { isThreadView?: boolean; placeholder?: string; + + /** + * Used by plugins to act after the post is made + */ + afterSubmit?: (response: SubmitPostReturnType) => void; } const AdvancedCreateComment = ({ @@ -26,6 +33,7 @@ const AdvancedCreateComment = ({ rootId, isThreadView, placeholder, + afterSubmit, }: Props) => { return ( ); }; diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx index 937e132a72..6265d70cbf 100644 --- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx +++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx @@ -18,6 +18,7 @@ import {getCurrentUserId, isCurrentUserGuestUser, getStatusForUserId, makeGetDis import * as GlobalActions from 'actions/global_actions'; import {actionOnGlobalItemsWithPrefix} from 'actions/storage'; +import type {SubmitPostReturnType} from 'actions/views/create_comment'; import {removeDraft, updateDraft} from 'actions/views/drafts'; import {makeGetDraft} from 'selectors/rhs'; import {connectionErrorCount} from 'selectors/views/system'; @@ -79,14 +80,20 @@ type Props = { postId: string; isThreadView?: boolean; placeholder?: string; + + /** + * Used by plugins to act after the post is made + */ + afterSubmit?: (response: SubmitPostReturnType) => void; } -const AdvanceTextEditor = ({ +const AdvancedTextEditor = ({ location, channelId, postId, isThreadView = false, placeholder, + afterSubmit, }: Props) => { const {formatMessage} = useIntl(); @@ -244,7 +251,7 @@ const AdvanceTextEditor = ({ isValidPersistentNotifications, onSubmitCheck: prioritySubmitCheck, } = usePriority(draft, handleDraftChange, focusTextbox, showPreview); - const [handleSubmit, errorClass] = useSubmit(draft, postError, channelId, postId, serverError, lastBlurAt, focusTextbox, setServerError, setPostError, setShowPreview, handleDraftChange, prioritySubmitCheck); + const [handleSubmit, errorClass] = useSubmit(draft, postError, channelId, postId, serverError, lastBlurAt, focusTextbox, setServerError, setPostError, setShowPreview, handleDraftChange, prioritySubmitCheck, afterSubmit); const [handleKeyDown, postMsgKeyPress] = useKeyHandler( draft, channelId, @@ -660,4 +667,4 @@ const AdvanceTextEditor = ({ ); }; -export default AdvanceTextEditor; +export default AdvancedTextEditor; diff --git a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx index 754725ea81..b56f8539b0 100644 --- a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx +++ b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx @@ -16,6 +16,7 @@ import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles' import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; import {scrollPostListToBottom} from 'actions/views/channel'; +import type {SubmitPostReturnType} from 'actions/views/create_comment'; import {onSubmit} from 'actions/views/create_comment'; import {openModal} from 'actions/views/modals'; @@ -60,6 +61,7 @@ const useSubmit = ( setShowPreview: (showPreview: boolean) => void, handleDraftChange: (draft: PostDraft, options?: {instant?: boolean; show?: boolean}) => void, prioritySubmitCheck: (onConfirm: () => void) => boolean, + afterSubmit?: (response: SubmitPostReturnType) => void, ): [ (e: React.FormEvent, submittingDraft?: PostDraft) => void, string | null, @@ -153,7 +155,7 @@ const useSubmit = ( setServerError(null); const ignoreSlash = isErrorInvalidSlashCommand(serverError) && serverError?.submittedMessage === submittingDraft.message; - const options = {ignoreSlash}; + const options = {ignoreSlash, afterSubmit}; try { await dispatch(onSubmit(submittingDraft, options)); @@ -188,7 +190,7 @@ const useSubmit = ( } isDraftSubmitting.current = false; - }, [handleDraftChange, dispatch, draft, focusTextbox, isRootDeleted, postError, serverError, showPostDeletedModal, channelId, postId, lastBlurAt, setPostError, setServerError]); + }, [handleDraftChange, dispatch, draft, focusTextbox, isRootDeleted, postError, serverError, showPostDeletedModal, channelId, postId, lastBlurAt, setPostError, setServerError, afterSubmit]); const showNotifyAllModal = useCallback((mentions: string[], channelTimezoneCount: number, memberNotifyCount: number) => { dispatch(openModal({ diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts index 0685d49fdf..3571c7df0c 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.test.ts @@ -89,38 +89,6 @@ describe('Actions.Posts', () => { expect(!postsInChannel[channelId]).toBeTruthy(); }); - it('maintain postReplies', async () => { - const channelId = TestHelper.basicChannel!.id; - const post = TestHelper.fakePost(channelId); - const postId = TestHelper.generateId(); - - nock(Client4.getBaseRoute()). - post('/posts'). - reply(201, {...post, id: postId}); - - await store.dispatch(Actions.createPostImmediately(post)); - - const post2 = TestHelper.fakePostWithId(channelId); - post2.root_id = postId; - - nock(Client4.getBaseRoute()). - post('/posts'). - reply(201, post2); - - await store.dispatch(Actions.createPostImmediately(post2)); - - expect(store.getState().entities.posts.postsReplies[postId]).toBe(1); - - nock(Client4.getBaseRoute()). - delete(`/posts/${post2.id}`). - reply(200, OK_RESPONSE); - - await store.dispatch(Actions.deletePost(post2)); - await store.dispatch(Actions.removePost(post2)); - - expect(store.getState().entities.posts.postsReplies[postId]).toBe(0); - }); - it('resetCreatePostRequest', async () => { const channelId = TestHelper.basicChannel!.id; const post = TestHelper.fakePost(channelId); diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts index c796e66fb1..d9f7558eca 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts @@ -8,6 +8,7 @@ import type {Channel, ChannelUnread} from '@mattermost/types/channels'; import type {FetchPaginatedThreadOptions} from '@mattermost/types/client4'; import type {Group} from '@mattermost/types/groups'; import type {Post, PostList, PostAcknowledgement} from '@mattermost/types/posts'; +import type {Reaction} from '@mattermost/types/reactions'; import type {GlobalState} from '@mattermost/types/store'; import type {UserProfile} from '@mattermost/types/users'; @@ -168,7 +169,12 @@ export function getPost(postId: string): ActionFuncAsync { }; } -export function createPost(post: Post, files: any[] = []): ActionFuncAsync { +export type CreatePostReturnType = { + created?: boolean; + pending?: string; +} + +export function createPost(post: Post, files: any[] = [], afterSubmit?: (response: any) => void): ActionFuncAsync { return async (dispatch, getState) => { const state = getState(); const currentUserId = state.entities.users.currentUserId; @@ -178,7 +184,7 @@ export function createPost(post: Post, files: any[] = []): ActionFuncAsync { let actions: AnyAction[] = []; if (PostSelectors.isPostIdSending(state, pendingPostId)) { - return {data: true}; + return {data: {pending: pendingPostId}}; } let newPost = { @@ -266,6 +272,8 @@ export function createPost(post: Post, files: any[] = []): ActionFuncAsync { } dispatch(batchActions(actions, 'BATCH_CREATE_POST')); + afterSubmit?.({created}); + return {data: {created}}; } catch (error) { const data = { ...newPost, @@ -288,107 +296,11 @@ export function createPost(post: Post, files: any[] = []): ActionFuncAsync { } dispatch(batchActions(actions, 'BATCH_CREATE_POST_FAILED')); + return {error}; } }()); - return {data: true}; - }; -} - -export function createPostImmediately(post: Post, files: any[] = []): ActionFuncAsync { - return async (dispatch, getState) => { - const state = getState(); - const currentUserId = state.entities.users.currentUserId; - const timestamp = Date.now(); - const pendingPostId = `${currentUserId}:${timestamp}`; - - let newPost: Post = { - ...post, - pending_post_id: pendingPostId, - create_at: timestamp, - update_at: timestamp, - reply_count: 0, - }; - - if (post.root_id) { - newPost.reply_count = PostSelectors.getPostRepliesCount(state, post.root_id) + 1; - } - - if (files.length) { - const fileIds = files.map((file) => file.id); - - newPost = { - ...newPost, - file_ids: fileIds, - }; - - dispatch({ - type: FileTypes.RECEIVED_FILES_FOR_POST, - postId: pendingPostId, - data: files, - }); - dispatch({ - type: ChannelTypes.INCREMENT_FILE_COUNT, - amount: files.length, - id: newPost.channel_id, - }); - } - - const crtEnabled = isCollapsedThreadsEnabled(state); - dispatch(receivedNewPost({ - ...newPost, - id: pendingPostId, - }, crtEnabled)); - - try { - const created = await Client4.createPost({...newPost, create_at: 0}); - newPost.id = created.id; - newPost.reply_count = created.reply_count; - } catch (error) { - forceLogoutIfNecessary(error, dispatch, getState); - dispatch({type: PostTypes.CREATE_POST_FAILURE, data: newPost, error}); - dispatch(removePost({ - ...newPost, - id: pendingPostId, - })); - dispatch(logError(error)); - return {error}; - } - - const actions: AnyAction[] = [ - receivedPost(newPost, crtEnabled), - { - type: PostTypes.CREATE_POST_SUCCESS, - }, - { - type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT, - data: { - channelId: newPost.channel_id, - amount: 1, - amountRoot: newPost.root_id === '' ? 1 : 0, - }, - }, - { - type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT, - data: { - channelId: newPost.channel_id, - amount: 1, - amountRoot: newPost.root_id === '' ? 1 : 0, - }, - }, - ]; - - if (files) { - actions.push({ - type: FileTypes.RECEIVED_FILES_FOR_POST, - postId: newPost.id, - data: files, - }); - } - - dispatch(batchActions(actions)); - - return {data: newPost}; + return {data: {created: true}}; }; } @@ -589,7 +501,12 @@ export function unpinPost(postId: string): ActionFuncAsync { }; } -export function addReaction(postId: string, emojiName: string): ActionFuncAsync { +export type SubmitReactionReturnType = { + reaction?: Reaction; + removedReaction?: boolean; +} + +export function addReaction(postId: string, emojiName: string): ActionFuncAsync { return async (dispatch, getState) => { const currentUserId = getState().entities.users.currentUserId; @@ -607,11 +524,11 @@ export function addReaction(postId: string, emojiName: string): ActionFuncAsync data: reaction, }); - return {data: true}; + return {data: {reaction}}; }; } -export function removeReaction(postId: string, emojiName: string): ActionFuncAsync { +export function removeReaction(postId: string, emojiName: string): ActionFuncAsync { return async (dispatch, getState) => { const currentUserId = getState().entities.users.currentUserId; @@ -628,7 +545,7 @@ export function removeReaction(postId: string, emojiName: string): ActionFuncAsy data: {user_id: currentUserId, post_id: postId, emoji_name: emojiName}, }); - return {data: true}; + return {data: {removedReaction: true}}; }; } diff --git a/webapp/channels/src/plugins/export.js b/webapp/channels/src/plugins/export.js index a1b0545b47..e651addf0a 100644 --- a/webapp/channels/src/plugins/export.js +++ b/webapp/channels/src/plugins/export.js @@ -6,6 +6,7 @@ import {openModal} from 'actions/views/modals'; import {closeRightHandSide, selectPostById} from 'actions/views/rhs'; import {getSelectedPostId, getIsRhsOpen} from 'selectors/rhs'; +import AdvancedTextEditor from 'components/advanced_text_editor/advanced_text_editor'; import ChannelInviteModal from 'components/channel_invite_modal'; import ChannelMembersModal from 'components/channel_members_modal'; import {openPricingModal} from 'components/global_header/right_controls/plan_upgrade_button'; @@ -26,7 +27,6 @@ import {formatText} from 'utils/text_formatting'; import {useWebSocket, useWebSocketClient, WebSocketContext} from 'utils/use_websocket'; import {imageURLForUser} from 'utils/utils'; -import CreatePost from './exported_create_post'; import {openInteractiveDialog} from './interactive_dialog'; // This import has intentional side effects. Do not remove without research. import Textbox from './textbox'; @@ -89,8 +89,8 @@ window.Components = { BotBadge: BotTag, StartTrialFormModal, ThreadViewer, - CreatePost, PostMessagePreview, + AdvancedTextEditor, }; // This is a prototype of the Product API for use by internal plugins only while we transition to the proper architecture