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
*