diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
index eb1bf9b2300..565e4771b18 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
@@ -95,8 +95,11 @@
- {{ fileNameWithoutExtension }}
- {{ fileExtension }}
+
+ {{ fileNameWithoutExtension }}
+ {{ fileExtension }}
+
+ {{ fileMetaLabel }}
@@ -198,6 +201,12 @@ export default {
type: String,
default: '',
},
+
+ /** Sibling media (images/videos) from this message, to scope the Viewer's list to this group */
+ viewerList: {
+ type: Array,
+ default: undefined,
+ },
},
emits: ['removeFile'],
@@ -230,9 +239,9 @@ export default {
// display the file detail below the preview if the preview
// is not easily recognizable, when:
return (
- // the file is not an image
- !this.file.mimetype.startsWith('image/')
- // the image has no preview (ex: disabled on server)
+ // the file is not an image or video
+ !(this.file.mimetype.startsWith('image/') || this.file.mimetype.startsWith('video/'))
+ // the file has no preview (ex: disabled on server)
|| (this.file['preview-available'] !== 'yes' && !this.file.localUrl)
// the preview failed loading
|| this.failed
@@ -255,6 +264,12 @@ export default {
return getFileExtension(this.file.name)
},
+ fileMetaLabel() {
+ const size = parseInt(this.file.size, 10)
+ const sizeLabel = size ? formatFileSize(size, true) : ''
+ return [this.fileExtension.slice(1).toUpperCase(), sizeLabel].filter(Boolean).join(' · ')
+ },
+
fileSizeLabel() {
if (!this.isUploadEditor || !this.uploadFile) {
return ''
@@ -603,22 +618,25 @@ export default {
event.stopPropagation()
event.preventDefault()
- if (this.itemType === SHARED_ITEM.TYPES.MEDIA) {
+ let list = [this.file]
+ let loadMore = undefined
+
+ if (this.itemType === SHARED_ITEM.TYPES.MEDIA && this.isSharedItems) {
const getRevertedList = (items) => Object.values(items).reverse()
.map((item) => item.messageParameters.file)
// Get available media files from store and put them to the list to navigate through slides
- const mediaFiles = this.sharedItemsStore.sharedItems(this.token).media
- const list = getRevertedList(mediaFiles)
- const loadMore = async () => {
+ list = getRevertedList(this.sharedItemsStore.sharedItems(this.token).media)
+ loadMore = async () => {
const { messages } = await this.sharedItemsStore.fetchSharedItems(this.token, SHARED_ITEM.TYPES.MEDIA)
return getRevertedList(messages)
}
-
- this.openViewer(this.internalAbsolutePath, list, this.file, loadMore)
- } else {
- this.openViewer(this.internalAbsolutePath, [this.file], this.file)
+ } else if (this.itemType === SHARED_ITEM.TYPES.MEDIA && this.viewerList?.length) {
+ // Message context: only swipe through this message's images
+ list = this.viewerList
}
+
+ this.openViewer(this.internalAbsolutePath, list, this.file, loadMore)
},
},
}
@@ -794,20 +812,27 @@ export default {
}
.name-container {
- font-weight: bold;
+ font-weight: normal;
width: 100%;
overflow: hidden;
white-space: nowrap;
display: inline-flex;
+ &__name {
+ display: inline-flex;
+ min-width: 0;
+ }
+
&__basename {
unicode-bidi: isolate;
+ min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__extension {
+ flex-shrink: 0;
color: var(--color-text-maxcontrast);
overflow: visible;
}
@@ -850,7 +875,6 @@ export default {
// Fixed, so that the height of the tile stays the same for every font
.name-container {
height: var(--preview-name-height, 24px);
- font-weight: normal;
font-size: var(--font-size-small);
}
}
@@ -858,6 +882,7 @@ export default {
&--row-layout {
display: flex;
align-items: center;
+ min-width: 0;
border-radius: var(--border-radius);
padding: 2px 4px;
@@ -867,8 +892,21 @@ export default {
}
.name-container {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
padding: 0 4px;
- font-weight: normal;
+ font-size: var(--font-size-small);
+
+ &__name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__meta {
+ color: var(--color-text-maxcontrast);
+ }
}
}
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue
index 95e3ce07c4a..224516e7fcb 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreviewsWrapper.vue
@@ -15,11 +15,46 @@ const props = defineProps<{
message: ChatMessage
}>()
+// Image tiles shown before the rest collapse into a "+X more" tile
+const MAX_VISIBLE_IMAGES = 4
+
const fileKeys = computed(() => getFilePreviewKeys(props.message))
/**
- * Get referenceId of a file parameter to look up a local preview (if available).
- * Client-only workaround for combined file messages
+ * Whether a file parameter is an image or video with a server-side preview (grouped, no name shown)
+ *
+ * @param key key of the file parameter ('file', 'file-1', …)
+ */
+function isImageKey(key: string): boolean {
+ const file = props.message.messageParameters[key]
+ const isMedia = file.mimetype?.startsWith('image/') || file.mimetype?.startsWith('video/') || false
+ // @ts-expect-error: 'localUrl' does not exist in type RichObjectParameter (temporary upload)
+ return isMedia && (file['preview-available'] === 'yes' || !!file.localUrl)
+}
+
+const imageKeys = computed(() => fileKeys.value.filter(isImageKey))
+const otherKeys = computed(() => fileKeys.value.filter((key) => !isImageKey(key)))
+
+// Multiple media tiles shrink into a grid; a single one keeps its full size
+const isImageRowCombined = computed(() => imageKeys.value.length > 1)
+
+// For the Viewer to swipe through media in this message (FilePreview can't derive this itself, combined messages are client-only)
+const viewerList = computed(() => imageKeys.value.map((key) => props.message.messageParameters[key]))
+
+const hiddenImageCount = computed(() => {
+ return imageKeys.value.length > MAX_VISIBLE_IMAGES
+ ? imageKeys.value.length - (MAX_VISIBLE_IMAGES - 1)
+ : 0
+})
+
+// Last tile doubles as the "+X more" tile (dimmed via CSS) when some images are hidden
+const visibleImageKeys = computed(() => imageKeys.value.slice(0, MAX_VISIBLE_IMAGES))
+
+// Quoted, for the `content` CSS property via v-bind below
+const moreCountLabel = computed(() => `"+${hiddenImageCount.value}"`)
+
+/**
+ * referenceId of a file parameter, to look up a local preview (client-only workaround)
*
* @param key key of the file parameter ('file', 'file-1', …)
*/
@@ -30,38 +65,102 @@ function getReferenceId(key: string): string {
-