diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js
index 859bcd7b67d..eef8c145e44 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageItem.spec.js
@@ -18,9 +18,11 @@ import MessageQuote from '../../../MessageQuote.vue'
import CallButton from '../../../TopBar/CallButton.vue'
import MessageButtonsBar from './MessageButtonsBar/MessageButtonsBar.vue'
import MessageItem from './MessageItem.vue'
+import ContactCard from './MessagePart/ContactCard.vue'
import DeckCard from './MessagePart/DeckCard.vue'
import DefaultParameter from './MessagePart/DefaultParameter.vue'
import FilePreview from './MessagePart/FilePreview.vue'
+import FilePreviewsWrapper from './MessagePart/FilePreviewsWrapper.vue'
import LocationCard from './MessagePart/LocationCard.vue'
import MentionChip from './MessagePart/MentionChip.vue'
import router from '../../../../__mocks__/router.js'
@@ -329,20 +331,18 @@ describe('MessageItem.vue', () => {
mimetype: 'txt/plain',
},
}
- renderRichObject(
- '{file}',
- params,
- {
- actor: {
- component: MentionChip,
- props: params.actor,
- },
- file: {
- component: FilePreview,
- props: { file: params.file },
- },
- },
- )
+ messageProps.message.message = '{file}'
+ messageProps.message.messageParameters = params
+ store.dispatch('processMessage', { token: TOKEN, message: messageProps.message })
+ const wrapper = mountMessage(messageProps)
+
+ // File previews are rendered as a block, on top of NcRichText
+ const filePreviewsWrapper = wrapper.findComponent(FilePreviewsWrapper)
+ expect(filePreviewsWrapper.exists()).toBeTruthy()
+ expect(filePreviewsWrapper.props('message').messageParameters.file).toStrictEqual(params.file)
+
+ // No caption: NcRichText is not rendered at all
+ expect(wrapper.findComponent(NcRichText).exists()).toBe(false)
})
test('renders single file preview with caption', () => {
@@ -361,22 +361,72 @@ describe('MessageItem.vue', () => {
mimetype: 'txt/plain',
},
}
- const messageEl = renderRichObject(
- caption,
- params,
- {
- actor: {
- component: MentionChip,
- props: params.actor,
- },
- file: {
- component: FilePreview,
- props: { file: params.file },
- },
+ messageProps.message.message = caption
+ messageProps.message.messageParameters = params
+ store.dispatch('processMessage', { token: TOKEN, message: messageProps.message })
+ const wrapper = mountMessage(messageProps)
+
+ // Caption is rendered as-is, the file placeholder is not part of it anymore
+ // (no 'file' argument either: previews are rendered by FilePreviewsWrapper, not NcRichText)
+ const messageEl = wrapper.findComponent(NcRichText)
+ expect(messageEl.props('text')).toBe(caption)
+ expect(Object.keys(messageEl.props('arguments'))).toMatchObject(['actor'])
+
+ // File previews are still rendered as a block, on top of the caption
+ const filePreviewsWrapper = wrapper.findComponent(FilePreviewsWrapper)
+ expect(filePreviewsWrapper.exists()).toBeTruthy()
+ expect(filePreviewsWrapper.props('message').messageParameters.file).toStrictEqual(params.file)
+ })
+
+ test('renders combined file previews as a single block', () => {
+ const params = {
+ 'file-1': {
+ id: '123',
+ path: 'Talk/first.txt',
+ name: 'first.txt',
+ type: 'file',
+ mimetype: 'txt/plain',
},
- )
+ 'file-2': {
+ id: '456',
+ path: 'Talk/second.txt',
+ name: 'second.txt',
+ type: 'file',
+ mimetype: 'txt/plain',
+ },
+ }
+ messageProps.message.message = '{file-1} {file-2}'
+ messageProps.message.messageParameters = params
+ store.dispatch('processMessage', { token: TOKEN, message: messageProps.message })
+ const wrapper = mountMessage(messageProps)
+
+ // Both files are rendered together, as a single FilePreviewsWrapper
+ const filePreviewsWrapper = wrapper.findComponent(FilePreviewsWrapper)
+ expect(filePreviewsWrapper.exists()).toBeTruthy()
+ expect(filePreviewsWrapper.findAllComponents(FilePreview)).toHaveLength(2)
+
+ // No caption: NcRichText is not rendered at all
+ expect(wrapper.findComponent(NcRichText).exists()).toBe(false)
+ })
+
+ test('renders contact card (text/vcard) via NcRichText', () => {
+ const params = {
+ file: {
+ id: '123',
+ type: 'file',
+ mimetype: 'text/vcard',
+ name: 'John Doe.vcf',
+ link: 'https://example.com/John%20Doe.vcf',
+ },
+ }
+ messageProps.message.message = '{file}'
+ messageProps.message.messageParameters = params
+ store.dispatch('processMessage', { token: TOKEN, message: messageProps.message })
+ const wrapper = mountMessage(messageProps)
- expect(messageEl.props('text')).toBe('{file}\n\n' + caption)
+ expect(wrapper.findComponent(NcRichText).exists()).toBe(true)
+ expect(wrapper.findComponent(ContactCard).exists()).toBe(true)
+ expect(wrapper.findComponent(FilePreviewsWrapper).exists()).toBe(false)
})
test('renders deck cards', () => {
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue b/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue
index 2a34266acb5..141998fb415 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageItem.vue
@@ -103,7 +103,6 @@ import ScheduledMessageActions from './MessageButtonsBar/ScheduledMessageActions
import ContactCard from './MessagePart/ContactCard.vue'
import DeckCard from './MessagePart/DeckCard.vue'
import DefaultParameter from './MessagePart/DefaultParameter.vue'
-import FilePreview from './MessagePart/FilePreview.vue'
import MentionChip from './MessagePart/MentionChip.vue'
import MessageBody from './MessagePart/MessageBody.vue'
import PollCard from './MessagePart/PollCard.vue'
@@ -114,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 { getItemTypeFromMessage } from '../../../../utils/getItemTypeFromMessage.ts'
+import { isFilePreviewParameter } from '../../../../utils/message.ts'
const LocationCard = defineAsyncComponent(() => import('./MessagePart/LocationCard.vue'))
@@ -231,17 +230,9 @@ export default {
token: this.message.token,
},
}
- } else if (type === 'file' && mimetype !== 'text/vcard') {
- richParameters[p] = {
- component: FilePreview,
- props: {
- token: this.message.token,
- messageId: this.message.id,
- itemType: getItemTypeFromMessage(this.message, p),
- referenceId: this.message.messageParameters[p].referenceId ?? this.message.referenceId,
- file: this.message.messageParameters[p],
- },
- }
+ } else if (isFilePreviewParameter(p, this.message.messageParameters[p])) {
+ // File previews are rendered by FilePreviewsWrapper, skip from richParameters
+ return
} else if (type === SHARED_ITEM.OBJECT_TYPE.DECK_CARD) {
richParameters[p] = {
component: DeckCard,
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
index cd88d296205..0ef6a60f6fe 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
@@ -757,13 +757,15 @@ export default {
}
}
- // Both the upload editor and a message with combined file shares show a grid
- // of tiles of the same size. --preview-size and --preview-name-height define sizes.
+ // The upload editor shows a grid of tiles of the same size.
+ // --preview-size and --preview-name-height define sizes.
// The file name is not shown for every file, but its space is always reserved.
- &--upload-editor,
- .message-main--combined-files & {
+ // (FilePreviewsWrapper applies the same sizing to combined file shares in messages)
+ &--upload-editor {
width: var(--preview-size, 80px);
height: calc(var(--preview-size, 80px) + var(--preview-name-height, 24px));
+ padding: var(--preview-padding, 8px);
+ margin: var(--default-grid-baseline);
// A file without a preview gets an inline 128px size on the container
// and a min-height on the icon. Both are overridden, so that every
@@ -771,6 +773,7 @@ export default {
.image-container {
width: var(--preview-size, 80px) !important;
height: var(--preview-size, 80px) !important;
+ outline: 1px solid var(--color-border);
}
// The size class of the image varies with the preview type, so it is
@@ -790,15 +793,6 @@ export default {
}
}
- &--upload-editor {
- padding: var(--preview-padding, 8px);
- margin: var(--default-grid-baseline);
-
- .image-container {
- outline: 1px solid var(--color-border);
- }
- }
-
&--row-layout {
display: flex;
align-items: center;
@@ -816,22 +810,6 @@ export default {
}
}
- .message-main--combined-files & {
- // Same thumbnail size as in the upload editor
- --preview-size: 80px;
- --preview-name-height: 24px;
- // Align inline elements to the top, so looks even next to files with filename
- vertical-align: top;
-
- .file-preview__image {
- border-radius: var(--border-radius);
-
- &.mimeicon {
- object-fit: contain;
- }
- }
- }
-
&--shared-items-grid {
aspect-ratio: 1;
line-height: 0;
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue
new file mode 100644
index 00000000000..95e3ce07c4a
--- /dev/null
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+