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 {