diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue b/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue index a0e7e904c07..9af87edeacd 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue @@ -113,7 +113,7 @@ import { hasTalkFeature } from '../../../../services/CapabilitiesManager.ts' import { EventBus } from '../../../../services/EventBus.ts' import { useActorStore } from '../../../../stores/actor.ts' import { useChatExtrasStore } from '../../../../stores/chatExtras.ts' -import { isFilePreviewParameter, isTemporaryId } from '../../../../utils/message.ts' +import { isFilePreviewParameter, isTemporaryId, isTemporaryMessage } from '../../../../utils/message.ts' const LocationCard = defineAsyncComponent(() => import('./MessagePart/LocationCard.vue')) @@ -187,7 +187,7 @@ export default { }, isTemporary() { - return !this.isScheduledMessage && isTemporaryId(this.message.id) + return !this.isScheduledMessage && isTemporaryMessage(this.message) }, isDeletedMessage() { @@ -490,10 +490,6 @@ export default { background-color: var(--color-primary-element-light-hover); border: 1px solid var(--color-border-dark); } - - :deep(.file-preview__progress) { - inset-inline-start: calc((var(--progress-bar-height) + var(--default-grid-baseline) * 3) * -1) - } } &.incoming { diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index 99d702cd4c3..448c91b2c06 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -60,8 +60,9 @@ + :value="uploadProgress || 30" /> { // Tag previously indexed files and add temporary messages to the MessagesList // If caption is provided, attach to the last temporary message - const lastIndex = getInitialisedUploads(uploadId).at(-1)![0] - for (const [index, uploadedFile] of getInitialisedUploads(uploadId)) { - // Compress before building the temporary message, so that the caption, - // the parent and the final file parameters are applied in a single pass - // and the backend receives the final file names (e.g. .webp instead of .png) - const compressed = compressImages - ? await compressUploadedImage(uploadId, index) - : null + const initialisedUploads = getInitialisedUploads(uploadId) + const lastIndex = initialisedUploads.at(-1)![0] + + // Compress all images in parallel first, then build each temporary message in a + // single pass over the precompressed results, so that the caption, the parent and + // the final file parameters are applied consistently and the backend receives the + // final file names (e.g. .webp instead of .png). + const compressedFiles = compressImages + ? await Promise.all(initialisedUploads.map(([index]) => compressUploadedImage(uploadId, index))) + : [] + + for (const [key, [index, uploadedFile]] of initialisedUploads.entries()) { + const compressed = compressedFiles[key] ?? null dismissCompressImage(uploadedFile.temporaryMessage.referenceId) // Store the previously created temporary message @@ -525,9 +530,9 @@ export const useUploadStore = defineStore('upload', () => { uploads[uploadId].files[index].temporaryMessage = message // Add temporary messages (files) to the messages list vuexStore.dispatch('addTemporaryMessage', { token, message }) - // Scroll the message list - EventBus.emit('scroll-chat-to-bottom', { smooth: true, force: true }) } + // Scroll the message list + EventBus.emit('scroll-chat-to-bottom', { smooth: true, force: true }) // With the conversation-subfolders feature enabled, // stage uploads inside the backend-provided Draft folder and diff --git a/src/utils/combineFileMessages.ts b/src/utils/combineFileMessages.ts index bbe2ed48c88..1d47331a42d 100644 --- a/src/utils/combineFileMessages.ts +++ b/src/utils/combineFileMessages.ts @@ -6,7 +6,7 @@ import type { ChatMessage } from '../types/index.ts' import { MESSAGE } from '../constants.ts' -import { getFileKeys, hasOnlyFilePlaceholders, isTemporaryId } from './message.ts' +import { getFileKeys, hasOnlyFilePlaceholders } from './message.ts' export type CombinedFileMessage = ChatMessage & { /** Ids of all messages combined into this one, in chronological order */ @@ -25,9 +25,9 @@ function isCombinableFileMessage(message: ChatMessage): boolean { } // TODO threads? - // TODO unified uploader? - // Temporary messages, which are not sent yet (or failed to be sent), are shown separately - if (isTemporaryId(message.id) || (message as ChatMessage & { sendingFailure?: string }).sendingFailure) { + // Messages that failed to send are shown separately, not combined into a group + // (sendingFailure is only ever set on temporary messages). + if ((message as ChatMessage & { sendingFailure?: string }).sendingFailure) { return false } @@ -82,7 +82,10 @@ function canBeCombinedWith(message1: ChatMessage, message2: ChatMessage): boolea * Create a single message out of several file shares. * The combined message is based on the last message of the group, so that the timestamp, * the reading state, the reactions and the message actions refer to an existing message - * (reactions of the other messages of the group are not shown) + * (reactions of the other messages of the group are not shown). + * If any of the combined ids is still temporary, the combined message is treated as + * temporary too (see isTemporaryMessage) and its actions stay disabled until every + * file in the group has been sent. * * @param messages array of grouped file share messages, in chronological order */ diff --git a/src/utils/message.ts b/src/utils/message.ts index 9dff3db5915..664676bad07 100644 --- a/src/utils/message.ts +++ b/src/utils/message.ts @@ -245,6 +245,22 @@ export function isTemporaryId(id: ChatMessage['id'] | string): boolean { return id.toString().startsWith('temp-') } +/** + * Returns whether the given message is not sent yet. A combined file share's own id is + * taken from whichever message is currently last in the group, so it can already be a + * confirmed (non-temporary) id even while other files of the same group are still in + * flight. combinedMessageIds is checked instead, so any still-temporary file in the + * group is caught. + * + * @param message Chat message + */ +export function isTemporaryMessage(message: ChatMessage): boolean { + const combinedMessageIds = (message as ChatMessage & { combinedMessageIds?: ChatMessage['id'][] }).combinedMessageIds + return combinedMessageIds + ? combinedMessageIds.some(isTemporaryId) + : isTemporaryId(message.id) +} + /** * Returns whether the given system message should be hidden in the UI *