diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 458d35b8f1..efb68afb08 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -393,7 +393,7 @@ export const CompositionArea = memo(function CompositionArea({ const draftEditMessageBody = draftEditMessage?.body; const editedMessageId = draftEditMessage?.targetMessageId; - const canSend = + let canSend = // Text or link preview edited dirty || // Quote of edited message changed @@ -406,6 +406,11 @@ export const CompositionArea = memo(function CompositionArea({ // Not edit message, but has attachments (draftEditMessage == null && draftAttachments.length !== 0); + // Draft attachments should finish loading + if (draftAttachments.some(attachment => attachment.pending)) { + canSend = false; + } + const handleSubmit = useCallback( ( message: string, diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index ecfba47324..5aaf51d76a 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -232,6 +232,7 @@ export function Image({ className={classNames( 'module-image', className, + attachment.path ? 'module-image--loaded' : null, !noBackground ? 'module-image--with-background' : null, cropWidth || cropHeight ? 'module-image--cropped' : null )} diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index 7a57007b11..fc135bbf91 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -103,7 +103,7 @@ const log = createLogger('composer'); type ComposerStateByConversationType = { attachments: ReadonlyArray; focusCounter: number; - isDisabled: boolean; + disabledCounter: number; linkPreviewLoading: boolean; linkPreviewResult?: LinkPreviewForUIType; messageCompositionId: string; @@ -128,7 +128,7 @@ function getEmptyComposerState(): ComposerStateByConversationType { return { attachments: [], focusCounter: 0, - isDisabled: false, + disabledCounter: 0, linkPreviewLoading: false, messageCompositionId: generateUuid(), sendCounter: 0, @@ -151,7 +151,7 @@ const RESET_COMPOSER = 'composer/RESET_COMPOSER'; export const SET_FOCUS = 'composer/SET_FOCUS'; const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING'; const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE'; -const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED'; +const UPDATE_COMPOSER_DISABLED = 'composer/UPDATE_COMPOSER_DISABLED'; type AddPendingAttachmentActionType = ReadonlyDeep<{ type: typeof ADD_PENDING_ATTACHMENT; @@ -184,8 +184,8 @@ export type ResetComposerActionType = ReadonlyDeep<{ }; }>; -type SetComposerDisabledStateActionType = ReadonlyDeep<{ - type: typeof SET_COMPOSER_DISABLED; +type UpdateComposerDisabledActionType = ReadonlyDeep<{ + type: typeof UPDATE_COMPOSER_DISABLED; payload: { conversationId: string; value: boolean; @@ -226,7 +226,7 @@ type ComposerActionType = | ReplaceAttachmentsActionType | ResetComposerActionType | TargetedConversationChangedActionType - | SetComposerDisabledStateActionType + | UpdateComposerDisabledActionType | SetFocusActionType | SetHighQualitySettingActionType | SetQuotedMessageActionType; @@ -252,11 +252,11 @@ export const actions = { sendEditedMessage, sendMultiMediaMessage, sendStickerMessage, - setComposerDisabledState, setComposerFocus, setMediaQualitySetting, setQuoteByMessageId, setQuotedMessage, + updateComposerDisabled, }; function incrementSendCounter(conversationId: string): IncrementSendActionType { @@ -413,7 +413,7 @@ async function withPreSendChecks( dispatch: ThunkDispatch< RootStateType, unknown, - SetComposerDisabledStateActionType | ShowToastActionType + UpdateComposerDisabledActionType | ShowToastActionType >, body: () => Promise ): Promise { @@ -432,7 +432,7 @@ async function withPreSendChecks( options.draftAttachments ?? conversation.attributes.draftAttachments; try { - dispatch(setComposerDisabledState(conversationId, true)); + dispatch(updateComposerDisabled(conversationId, true)); try { const sendAnyway = await blockSendUntilConversationsAreVerified( @@ -440,7 +440,6 @@ async function withPreSendChecks( SafetyNumberChangeSource.MessageSend ); if (!sendAnyway) { - dispatch(setComposerDisabledState(conversationId, false)); return; } } catch (error) { @@ -475,7 +474,7 @@ async function withPreSendChecks( await body(); } finally { - dispatch(setComposerDisabledState(conversationId, false)); + dispatch(updateComposerDisabled(conversationId, false)); } conversation.clearTypingTimers(); @@ -493,7 +492,7 @@ function sendEditedMessage( void, RootStateType, unknown, - SetComposerDisabledStateActionType | ShowToastActionType + UpdateComposerDisabledActionType | ShowToastActionType > { return async dispatch => { const conversation = window.ConversationController.get(conversationId); @@ -548,7 +547,7 @@ function sendMultiMediaMessage( | IncrementSendActionType | NoopActionType | ResetComposerActionType - | SetComposerDisabledStateActionType + | UpdateComposerDisabledActionType | SetQuotedMessageActionType | ShowToastActionType > { @@ -622,7 +621,6 @@ function sendMultiMediaMessage( undefined ); dispatch(incrementSendCounter(conversationId)); - dispatch(setComposerDisabledState(conversationId, false)); if (state.items.audioMessage) { drop(new Sound({ soundType: SoundType.Whoosh }).play()); @@ -707,12 +705,7 @@ function getAttachmentsFromConversationModel( export function setQuoteByMessageId( conversationId: string, messageId: string | undefined -): ThunkAction< - void, - RootStateType, - unknown, - SetComposerDisabledStateActionType | SetQuotedMessageActionType -> { +): ThunkAction { return async (dispatch, getState) => { const conversation = window.ConversationController.get(conversationId); if (!conversation) { @@ -790,7 +783,6 @@ export function setQuoteByMessageId( ); dispatch(setComposerFocus(conversation.id)); - dispatch(setComposerDisabledState(conversationId, false)); } else { dispatch(setQuotedMessage(conversationId, undefined)); } @@ -1006,7 +998,7 @@ function processAttachments({ void, RootStateType, unknown, - NoopActionType | ShowToastActionType + NoopActionType | ShowToastActionType | UpdateComposerDisabledActionType > { return async (dispatch, getState) => { if (!files.length) { @@ -1062,40 +1054,46 @@ function processAttachments({ } } - await Promise.all( - filesToProcess.map(async file => { - try { - const attachment = await processAttachment(file, { - generateScreenshot: true, - flags, - }); - if (!attachment) { + dispatch(updateComposerDisabled(conversationId, true)); + + try { + await Promise.all( + filesToProcess.map(async file => { + try { + const attachment = await processAttachment(file, { + generateScreenshot: true, + flags, + }); + if (!attachment) { + removeAttachment(conversationId, webUtils.getPathForFile(file))( + dispatch, + getState, + undefined + ); + return; + } + addAttachment(conversationId, attachment)( + dispatch, + getState, + undefined + ); + } catch (err) { + log.error( + 'handleAttachmentsProcessing: failed to process attachment:', + err.stack + ); removeAttachment(conversationId, webUtils.getPathForFile(file))( dispatch, getState, undefined ); - return; + toastToShow = { toastType: ToastType.UnableToLoadAttachment }; } - addAttachment(conversationId, attachment)( - dispatch, - getState, - undefined - ); - } catch (err) { - log.error( - 'handleAttachmentsProcessing: failed to process attachment:', - err.stack - ); - removeAttachment(conversationId, webUtils.getPathForFile(file))( - dispatch, - getState, - undefined - ); - toastToShow = { toastType: ToastType.UnableToLoadAttachment }; - } - }) - ); + }) + ); + } finally { + dispatch(updateComposerDisabled(conversationId, false)); + } if (toastToShow) { dispatch({ @@ -1349,12 +1347,12 @@ function saveDraft( } } -function setComposerDisabledState( +function updateComposerDisabled( conversationId: string, value: boolean -): SetComposerDisabledStateActionType { +): UpdateComposerDisabledActionType { return { - type: SET_COMPOSER_DISABLED, + type: UPDATE_COMPOSER_DISABLED, payload: { conversationId, value, @@ -1525,9 +1523,10 @@ export function reducer( })); } - if (action.type === SET_COMPOSER_DISABLED) { - return updateComposerState(state, action, () => ({ - isDisabled: action.payload.value, + if (action.type === UPDATE_COMPOSER_DISABLED) { + return updateComposerState(state, action, oldState => ({ + disabledCounter: + oldState.disabledCounter + (action.payload.value ? 1 : -1), })); } diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 50442e4245..eece14e144 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -127,7 +127,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ const { attachments: draftAttachments, focusCounter, - isDisabled, + disabledCounter, linkPreviewLoading, linkPreviewResult, messageCompositionId, @@ -135,6 +135,8 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ shouldSendHighQualityAttachments, } = composerState; + const isDisabled = disabledCounter > 0; + const areSelectedMessagesForwardable = useMemo(() => { return selectedMessageIds?.every(messageId => { const message = messageLookup[messageId]; diff --git a/ts/test-mock/helpers.ts b/ts/test-mock/helpers.ts index aa5627701a..b38d8ee79b 100644 --- a/ts/test-mock/helpers.ts +++ b/ts/test-mock/helpers.ts @@ -335,16 +335,15 @@ export async function composerAttachImages( const AttachmentInput = page.getByTestId('attachfile-input'); const AttachmentsList = page.locator('.module-attachments'); - const AttachmentsListImage = AttachmentsList.locator('.module-image'); - const AttachmentsListImageLoaded = AttachmentsListImage.locator( - '.module-image__image' + const AttachmentsListImageLoaded = AttachmentsList.locator( + '.module-image--loaded' ); debug('setting input files'); await AttachmentInput.setInputFiles(filePaths); debug(`waiting for ${filePaths.length} items`); - await AttachmentsListImage.nth(filePaths.length - 1).waitFor(); + await AttachmentsListImageLoaded.nth(filePaths.length - 1).waitFor(); await Promise.all( filePaths.map(async (_, index) => { diff --git a/ts/test-mock/messaging/lightbox_test.ts b/ts/test-mock/messaging/lightbox_test.ts index db7ecd783d..db61791bbf 100644 --- a/ts/test-mock/messaging/lightbox_test.ts +++ b/ts/test-mock/messaging/lightbox_test.ts @@ -78,7 +78,7 @@ describe('lightbox', function (this: Mocha.Suite) { }); debug('wait for message to appear in timeline'); const Message = getMessageInTimelineByTimestamp(page, timestamp); - const MessageImageLoaded = Message.locator('img.module-image__image'); + const MessageImageLoaded = Message.locator('.module-image--loaded'); await Message.waitFor();