diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/up_arrow_edit_message_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/up_arrow_edit_message_spec.js index 3b8cc06468..610746be71 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/up_arrow_edit_message_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/up_arrow_edit_message_spec.js @@ -302,7 +302,10 @@ describe('Keyboard Shortcuts', () => { cy.uiGetPostTextBox().type('{uparrow}'); // # Add some text to the previous message and save - cy.get('#edit_textbox').type('Test').type('{enter}'); + // cy.get('#edit_textbox').type('Test').type('{enter}'); + cy.get('#edit_textbox').type('Test'); + cy.wait(TIMEOUTS.ONE_SEC); + cy.get('#edit_textbox').type('{enter}'); cy.wait(TIMEOUTS.ONE_SEC); cy.getLastPost().within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js index a25ec39768..0ad7cbc882 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js @@ -732,7 +732,7 @@ describe('Messaging', () => { cy.get('#edit_textbox').should('be.visible'); // * Update the post message and type ENTER - cy.get('#edit_textbox', {timeout: TIMEOUTS.FIVE_SEC}).invoke('val', '').type(message2).type('{enter}').wait(TIMEOUTS.HALF_SEC); + cy.get('#edit_textbox', {timeout: TIMEOUTS.FIVE_SEC}).invoke('val', '').type(message2).wait(TIMEOUTS.HALF_SEC).type('{enter}').wait(TIMEOUTS.HALF_SEC); // * Post appears in RHS search results, displays Pinned badge cy.get(`#searchResult_${postId}`).findByText('Edited').should('exist'); diff --git a/e2e-tests/playwright/support/ui/components/channels/post_create.ts b/e2e-tests/playwright/support/ui/components/channels/post_create.ts index fa0fbba615..e74d669cb7 100644 --- a/e2e-tests/playwright/support/ui/components/channels/post_create.ts +++ b/e2e-tests/playwright/support/ui/components/channels/post_create.ts @@ -3,6 +3,7 @@ import {expect, Locator} from '@playwright/test'; import path from 'node:path'; +import {waitUntil} from '@e2e-support/test_action'; export default class ChannelsPostCreate { readonly container: Locator; @@ -106,6 +107,12 @@ export default class ChannelsPostCreate { }); await this.attachmentButton.click(); + + // wait for all files to be uploaded + await waitUntil(async () => { + const attachment = await this.container.locator('.file-preview').count(); + return attachment === files.length; + }); } await this.sendMessage(); 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 6f9e181c90..1aba3431d1 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 @@ -39,7 +39,7 @@ const AdvancedCreateComment = ({ { return ( ); diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx index d637d2ca94..eec928f3b3 100644 --- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx +++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx @@ -130,7 +130,7 @@ const baseProps = { uploadsProgressPercent: {}, currentChannel: initialState.entities.channels.channels.current_channel_id as Channel, channelId, - postId: '', + rootId: '', errorClass: null, serverError: null, postError: null, @@ -421,7 +421,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => { it('should show @mention warning when a mention exists in the message', () => { const props = { ...baseProps, - postId: 'post_id_1', + rootId: 'post_id_1', isInEditMode: true, }; @@ -457,7 +457,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => { ); expect(container.querySelector('#createPostFileDropOverlay')).toBeVisible(); - props.postId = 'post_id_1'; + props.rootId = 'post_id_1'; rerender(); expect(container.querySelector('#createCommentFileDropOverlay')).toBeVisible(); 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 79134183fd..abf348210c 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 @@ -9,11 +9,9 @@ import {useDispatch, useSelector} from 'react-redux'; import type {ServerError} from '@mattermost/types/errors'; import type {SchedulingInfo} from '@mattermost/types/schedule_post'; -import {FileTypes} from 'mattermost-redux/action_types'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {Permissions} from 'mattermost-redux/constants'; import {getChannel, makeGetChannel, getDirectChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getFilesIdsForPost} from 'mattermost-redux/selectors/entities/files'; import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general'; import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences'; import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; @@ -98,7 +96,8 @@ export type Props = { */ location: string; channelId: string; - postId: string; + rootId: string; + postId?: string; isThreadView?: boolean; placeholder?: string; isInEditMode?: boolean; @@ -118,6 +117,7 @@ export type Props = { const AdvancedTextEditor = ({ location, channelId, + rootId, postId, isThreadView = false, placeholder, @@ -146,7 +146,7 @@ const AdvancedTextEditor = ({ textboxId = AdvancedTextEditorTextboxIds.Default; } - const isRHS = Boolean(postId && !isThreadView); + const isRHS = isThreadView ? false : Boolean(rootId) || location === Locations.RHS_COMMENT; const getFormattingBarPreferenceName = () => { let name: string; @@ -164,7 +164,7 @@ const AdvancedTextEditor = ({ const channelDisplayName = channel?.display_name || ''; const channelType = channel?.type || ''; const isChannelShared = channel?.shared; - const draftFromStore = useSelector((state: GlobalState) => getDraftSelector(state, channelId, postId, storageKey)); + const draftFromStore = useSelector((state: GlobalState) => getDraftSelector(state, channelId, rootId, storageKey)); const badConnection = useSelector((state: GlobalState) => connectionErrorCount(state) > 1); const maxPostSize = useSelector((state: GlobalState) => parseInt(getConfig(state).MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT); const canUploadFiles = useSelector((state: GlobalState) => canUploadFilesAccordingToConfig(getConfig(state))); @@ -188,7 +188,7 @@ const AdvancedTextEditor = ({ }); const showSendTutorialTip = useSelector((state: GlobalState) => { // We don't show the tutorial tip neither on RHS nor Thread view - if (postId) { + if (rootId) { return false; } const config = getConfig(state); @@ -202,7 +202,6 @@ const AdvancedTextEditor = ({ return enableTutorial && (tutorialStep === tourStep); }); - const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId)); const editorActionsRef = useRef(null); const editorBodyRef = useRef(null); @@ -235,8 +234,8 @@ const AdvancedTextEditor = ({ }, []); const emitTypingEvent = useCallback(() => { - GlobalActions.emitLocalUserTypingEvent(channelId, postId); - }, [channelId, postId]); + GlobalActions.emitLocalUserTypingEvent(channelId, rootId); + }, [channelId, rootId]); const handleDraftChange = useCallback((draftToChange: PostDraft, options: {instant?: boolean; show?: boolean} = {instant: false, show: false}) => { if (saveDraftFrame.current) { @@ -307,12 +306,12 @@ const AdvancedTextEditor = ({ }])); }, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]); - useOrientationHandler(textboxRef, postId); + useOrientationHandler(textboxRef, rootId); const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange); const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost); const [attachmentPreview, fileUploadJSX] = useUploadFiles( draft, - postId, + rootId, channelId, isThreadView, storedDrafts, @@ -348,7 +347,7 @@ const AdvancedTextEditor = ({ draft, postError, channelId, - postId, + rootId, serverError, lastBlurAt, focusTextbox, @@ -360,6 +359,7 @@ const AdvancedTextEditor = ({ afterSubmit, undefined, isInEditMode, + postId, ); const handleSubmitWithErrorHandling = useCallback((submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => { @@ -383,30 +383,10 @@ const AdvancedTextEditor = ({ createAt: 0, updateAt: 0, channelId, - rootId: postId, + rootId, metadata: {}, }); - }, [handleDraftChange, channelId, postId]); - - const handleFileChangesOnSave = useCallback((draft: PostDraft) => { - // sets the updated data for file IDs by post ID part - dispatch({ - type: FileTypes.RECEIVED_FILES_FOR_POST, - data: draft.fileInfos, - postId, - }); - - // removes the data for the deleted files from store - const deletedFileIds = postFileIds.filter((id: string) => !draft.fileInfos.find((file) => file.id === id)); - if (deletedFileIds) { - dispatch({ - type: FileTypes.REMOVED_FILE, - data: { - fileIds: deletedFileIds, - }, - }); - } - }, [dispatch, postFileIds, postId]); + }, [handleDraftChange, channelId, rootId]); const handleSubmitWrapper = useCallback(() => { const isEmptyPost = isPostDraftEmpty(draft); @@ -425,17 +405,13 @@ const AdvancedTextEditor = ({ return; } - if (isInEditMode) { - handleFileChangesOnSave(draft); - } - handleSubmitWithErrorHandling(); - }, [dispatch, draft, handleFileChangesOnSave, handleSubmitWithErrorHandling, isInEditMode, isRHS]); + }, [dispatch, draft, handleSubmitWithErrorHandling, isInEditMode, isRHS]); const [handleKeyDown, postMsgKeyPress] = useKeyHandler( draft, channelId, - postId, + rootId, caretPosition, isValidPersistentNotifications, location, @@ -583,11 +559,11 @@ const AdvancedTextEditor = ({ useEffect(() => { setShowPreview(false); setServerError(null); - }, [channelId, postId]); + }, [channelId, rootId]); // Remove uploads in progress on mount useEffect(() => { - dispatch(actionOnGlobalItemsWithPrefix(postId ? StoragePrefixes.COMMENT_DRAFT : StoragePrefixes.DRAFT, (_key: string, draft: PostDraft) => { + dispatch(actionOnGlobalItemsWithPrefix(rootId ? StoragePrefixes.COMMENT_DRAFT : StoragePrefixes.DRAFT, (_key: string, draft: PostDraft) => { if (!draft || !draft.uploadsInProgress || draft.uploadsInProgress.length === 0) { return draft; } @@ -626,7 +602,7 @@ const AdvancedTextEditor = ({ handleDraftChange(draftRef.current, {instant: true, show: true}); } }; - }, [channelId, postId]); + }, [channelId, rootId]); const disableSendButton = Boolean(isDisabled || (!draft.message.trim().length && !draft.fileInfos.length)) || !isValidPersistentNotifications; const sendButton = readOnlyChannel || isInEditMode ? null : ( @@ -647,7 +623,7 @@ const AdvancedTextEditor = ({ let createMessage; if (placeholder) { createMessage = placeholder; - } else if (!postId && !isDisabled) { + } else if (!rootId && !isDisabled) { createMessage = formatMessage( { id: 'create_post.write', @@ -750,9 +726,9 @@ const AdvancedTextEditor = ({ return (
{canPost && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0) && ( @@ -764,7 +740,7 @@ const AdvancedTextEditor = ({ channelId={channelId} teammateDisplayName={teammateDisplayName} location={location} - postId={postId} + postId={rootId} /> )}
@@ -876,7 +852,7 @@ const AdvancedTextEditor = ({ errorClass={errorClass} serverError={serverError} channelId={channelId} - postId={postId} + rootId={rootId} noArgumentHandleSubmit={handleSubmitWrapper} isInEditMode={isInEditMode} /> diff --git a/webapp/channels/src/components/advanced_text_editor/footer.tsx b/webapp/channels/src/components/advanced_text_editor/footer.tsx index c39b59d3c6..0998c05eec 100644 --- a/webapp/channels/src/components/advanced_text_editor/footer.tsx +++ b/webapp/channels/src/components/advanced_text_editor/footer.tsx @@ -17,7 +17,7 @@ interface Props { errorClass: string | null; serverError: ServerError & {submittedMessage?: string} | null; channelId: Channel['id']; - postId: Post['id']; + rootId: Post['id']; noArgumentHandleSubmit: () => void; isInEditMode: boolean; } @@ -27,7 +27,7 @@ export default function Footer({ errorClass, serverError, channelId, - postId, + rootId, noArgumentHandleSubmit, isInEditMode, }: Props) { @@ -52,7 +52,7 @@ export default function Footer({ {!isInEditMode && ( )}
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 be3a22283a..e61baa7771 100644 --- a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx +++ b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx @@ -12,6 +12,7 @@ import {FileTypes} from 'mattermost-redux/action_types'; import {getChannelTimezones} from 'mattermost-redux/actions/channels'; import {Permissions} from 'mattermost-redux/constants'; import {getChannel, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels'; +import {getFilesIdsForPost} from 'mattermost-redux/selectors/entities/files'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; @@ -57,7 +58,7 @@ const useSubmit = ( draft: PostDraft, postError: React.ReactNode, channelId: string, - postId: string, + rootId: string, serverError: (ServerError & { submittedMessage?: string }) | null, lastBlurAt: React.MutableRefObject, focusTextbox: (forceFocust?: boolean) => void, @@ -69,6 +70,7 @@ const useSubmit = ( afterSubmit?: (response: SubmitPostReturnType) => void, skipCommands?: boolean, isInEditMode?: boolean, + postId?: string, ): [ (submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => void, string | null, @@ -77,6 +79,8 @@ const useSubmit = ( const dispatch = useDispatch(); + const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId || '')); + const isDraftSubmitting = useRef(false); const [errorClass, setErrorClass] = useState(null); const isDirectOrGroup = useSelector((state: GlobalState) => { @@ -92,10 +96,10 @@ const useSubmit = ( }); const isRootDeleted = useSelector((state: GlobalState) => { - if (!postId) { + if (!rootId) { return false; } - const post = getPost(state, postId); + const post = getPost(state, rootId); if (!post || post.delete_at || post.state === 'DELETED') { return true; } @@ -191,7 +195,7 @@ const useSubmit = ( createAt: 0, updateAt: 0, channelId, - rootId: postId, + rootId, }, {instant: true}); } catch (err: unknown) { if (isServerError(err)) { @@ -209,7 +213,7 @@ const useSubmit = ( return; } - if (!postId && !schedulingInfo) { + if (!rootId && !schedulingInfo) { dispatch(scrollPostListToBottom()); } @@ -230,7 +234,7 @@ const useSubmit = ( skipCommands, afterSubmit, afterOptimisticSubmit, - postId, + rootId, showPostDeletedModal, handleDraftChange, channelId, @@ -238,12 +242,24 @@ const useSubmit = ( ]); const handleFileChange = useCallback((submittingDraft: PostDraft) => { + // sets the updated data for file IDs by post ID part dispatch({ type: FileTypes.RECEIVED_FILES_FOR_POST, data: submittingDraft.fileInfos, postId, }); - }, [dispatch, postId]); + + // removes the data for the deleted files from store + const deletedFileIds = postFileIds.filter((id: string) => !submittingDraft.fileInfos.find((file) => file.id === id)); + if (deletedFileIds) { + dispatch({ + type: FileTypes.REMOVED_FILE, + data: { + fileIds: deletedFileIds, + }, + }); + } + }, [dispatch, postFileIds, postId]); const setUpdatedFileIds = useCallback((draft: PostDraft) => { // new object creation is needed here to support sending a draft with files. diff --git a/webapp/channels/src/components/edit_post/edit_post.tsx b/webapp/channels/src/components/edit_post/edit_post.tsx index 7b97ecddda..ce9b74a50a 100644 --- a/webapp/channels/src/components/edit_post/edit_post.tsx +++ b/webapp/channels/src/components/edit_post/edit_post.tsx @@ -24,7 +24,6 @@ export default function EditPost() { const channelId = editingPostDetailsAndPost.post.channel_id; const location = editingPostDetailsAndPost.isRHS ? Locations.RHS_COMMENT : Locations.CENTER; - const rootId = editingPostDetailsAndPost.post.root_id || editingPostDetailsAndPost.post.id || ''; const storageKey = `${StoragePrefixes.EDIT_DRAFT}${editingPostDetailsAndPost.post.id}`; return ( @@ -32,7 +31,8 @@ export default function EditPost() { { const baseProps = { typingUsers: [], channelId: 'test', - postId: '', + rootId: '', userStartedTyping: jest.fn(), userStoppedTyping: jest.fn(), }; diff --git a/webapp/channels/src/components/msg_typing/msg_typing.tsx b/webapp/channels/src/components/msg_typing/msg_typing.tsx index 45f7a3350c..5949030064 100644 --- a/webapp/channels/src/components/msg_typing/msg_typing.tsx +++ b/webapp/channels/src/components/msg_typing/msg_typing.tsx @@ -11,7 +11,7 @@ import {useWebSocket} from 'utils/use_websocket'; type Props = { channelId: string; - postId: string; + rootId: string; typingUsers: string[]; userStartedTyping: (userId: string, channelId: string, rootId: string, now: number) => void; userStoppedTyping: (userId: string, channelId: string, rootId: string, now: number) => void; @@ -26,7 +26,7 @@ export default function MsgTyping(props: Props) { const rootId = msg.data.parent_id; const userId = msg.data.user_id; - if (props.channelId === channelId && props.postId === rootId) { + if (props.channelId === channelId && props.rootId === rootId) { userStartedTyping(userId, channelId, rootId, Date.now()); } } else if (msg.event === SocketEvents.POSTED) { @@ -36,11 +36,11 @@ export default function MsgTyping(props: Props) { const rootId = post.root_id; const userId = post.user_id; - if (props.channelId === channelId && props.postId === rootId) { + if (props.channelId === channelId && props.rootId === rootId) { userStoppedTyping(userId, channelId, rootId, Date.now()); } } - }, [props.channelId, props.postId, userStartedTyping, userStoppedTyping]), + }, [props.channelId, props.rootId, userStartedTyping, userStoppedTyping]), }); const getTypingText = () => {