From 51e2eee15ac5fca122f069e87f4f6378be448a97 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:54:50 +0200 Subject: [PATCH] fix(app): stop aborting shared uploads when cancelling one attachment (#1967) --- .../features/messaging/state/MessageQueue.tsx | 37 ++++------------- .../TextareaAttachmentUploadCancellation.ts | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 fluxer_app/src/features/messaging/state/TextareaAttachmentUploadCancellation.ts diff --git a/fluxer_app/src/features/messaging/state/MessageQueue.tsx b/fluxer_app/src/features/messaging/state/MessageQueue.tsx index 4681426ed..05c5aa43c 100644 --- a/fluxer_app/src/features/messaging/state/MessageQueue.tsx +++ b/fluxer_app/src/features/messaging/state/MessageQueue.tsx @@ -22,6 +22,7 @@ import { resolveMessageQueueRequestOutcomeDecision, resolveMessageQueueSendExecutionDecision, } from '@app/features/messaging/state/MessageQueueStateMachine'; +import {planTextareaAttachmentCancellation} from '@app/features/messaging/state/TextareaAttachmentUploadCancellation'; import { type ChunkedUploadPart, type ChunkedUploadPlan, @@ -50,7 +51,7 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; import {formatUserSettingsPath} from '@app/features/user/components/settings_utils/SettingsConstants'; import PrivacyPreferences from '@app/features/user/state/PrivacyPreferences'; -import {Queue, type QueueEntry} from '@app/lib/list/ListQueue'; +import {Queue} from '@app/lib/list/ListQueue'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import type { AllowedMentions, @@ -254,10 +255,6 @@ const isNetworkRequestError = (error: unknown): boolean => { return error instanceof Error && error.message === 'Network error during request'; }; -function isSendPayload(payload: MessageQueuePayload): payload is SendMessagePayload { - return payload.type === 'send'; -} - function isRateLimitError(error: HttpError): boolean { if (error.status !== 429) return false; return !isSlowmodeError(error); @@ -516,23 +513,6 @@ export class MessageQueue extends Queue { - const cancelled: Array = []; - const remaining: Array | undefined>> = []; - while (this.queue.length > 0) { - const entry = this.queue.shift()!; - if (isSendPayload(entry.message) && entry.message.channelId === channelId) { - cancelled.push(entry.message); - this.cancelRequest(entry.message.nonce); - } else { - remaining.push(entry); - } - } - this.queue.push(...remaining); - logger.info('Cancel pending send requests', cancelled.length); - return cancelled; - } - async sendImmediately(payload: SendMessagePayload): Promise | undefined> { while (true) { const completion = await this.drainSendImmediately(payload); @@ -713,16 +693,15 @@ export class MessageQueue extends Queue): void { - const channelIds = new Set(); - for (const attachmentId of attachmentIds) { - const upload = this.textareaAttachmentUploads.get(attachmentId); - if (!upload) continue; - channelIds.add(upload.channelId); - upload.requestAbortController.abort(); + const plan = planTextareaAttachmentCancellation(this.textareaAttachmentUploads, attachmentIds); + for (const {attachmentId, upload} of plan.cancelled) { upload.abortController.abort(); this.textareaAttachmentUploads.delete(attachmentId); } - for (const channelId of channelIds) { + for (const controller of plan.requestControllersToAbort) { + controller.abort(); + } + for (const channelId of plan.channelIds) { this.disposeTextareaAttachmentUploadPrunerIfIdle(channelId); } } diff --git a/fluxer_app/src/features/messaging/state/TextareaAttachmentUploadCancellation.ts b/fluxer_app/src/features/messaging/state/TextareaAttachmentUploadCancellation.ts new file mode 100644 index 000000000..a1e885a18 --- /dev/null +++ b/fluxer_app/src/features/messaging/state/TextareaAttachmentUploadCancellation.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +export interface CancellableTextareaAttachmentUpload { + channelId: string; + requestAbortController: AbortController; + abortController: AbortController; +} + +export interface TextareaAttachmentCancellationPlan { + cancelled: Array<{attachmentId: number; upload: TUpload}>; + requestControllersToAbort: Array; + channelIds: Array; +} + +export function planTextareaAttachmentCancellation( + uploads: ReadonlyMap, + attachmentIds: ReadonlyArray, +): TextareaAttachmentCancellationPlan { + const cancelled: Array<{attachmentId: number; upload: TUpload}> = []; + const cancelledIds = new Set(); + const channelIds = new Set(); + const releasedRequestControllers = new Set(); + for (const attachmentId of attachmentIds) { + if (cancelledIds.has(attachmentId)) continue; + const upload = uploads.get(attachmentId); + if (!upload) continue; + cancelledIds.add(attachmentId); + cancelled.push({attachmentId, upload}); + channelIds.add(upload.channelId); + releasedRequestControllers.add(upload.requestAbortController); + } + for (const [attachmentId, upload] of uploads) { + if (cancelledIds.has(attachmentId)) continue; + releasedRequestControllers.delete(upload.requestAbortController); + } + return { + cancelled, + requestControllersToAbort: Array.from(releasedRequestControllers), + channelIds: Array.from(channelIds), + }; +}