From 9d02d159f2d8d4ce59610707f5c3f303d00122cc Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Tue, 1 Sep 2026 18:34:00 +0200 Subject: [PATCH 1/4] fix(FilePreview): drop unused smallPreview prop - only used in SharedItems.vue together with rowLayout, so redundant Assisted-by: ClaudeCode:claude-sonnet-4-6 Signed-off-by: Maksim Sukharev --- .../Message/MessagePart/FilePreview.spec.js | 13 ++++---- .../Message/MessagePart/FilePreview.vue | 33 ++++++++----------- .../RightSidebar/SharedItems/SharedItems.vue | 1 - 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js index 746e685dfbd..28b150bce99 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js @@ -136,16 +136,17 @@ describe('FilePreview.vue', () => { expect(imageUrl.searchParams.get('y')).toBe('576') }) - test('renders small previews when requested', async () => { - props.smallPreview = true + test('renders a mime icon instead of a scaled preview in row layout', async () => { + props.rowLayout = true + OC.MimeType.getIconUrl.mockReturnValueOnce(imagePath('core', 'image/jpeg')) const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') expect(wrapper.element.tagName).toBe('A') - const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src')) - expect(imageUrl.searchParams.get('y')).toBe('24') + const imageUrl = wrapper.find('img').attributes('src') + expect(imageUrl).toBe(imagePath('core', 'image/jpeg')) }) describe('uploading', () => { @@ -412,8 +413,8 @@ describe('FilePreview.vue', () => { await testPlayButtonVisible(true) }) - test('does not render play icon for small previews', async () => { - props.smallPreview = true + test('does not render play icon in row layout', async () => { + props.rowLayout = true await testPlayButtonVisible(false) }) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index 448c91b2c06..ca23c1ab33a 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -39,7 +39,7 @@ :alt="file.name" :src="defaultIconUrl"> - + @@ -52,7 +52,7 @@ @load="onLoad" @error="onError"> @@ -26,6 +26,7 @@ import { generateRemoteUrl } from '@nextcloud/router' import { inject } from 'vue' import { EventBus } from '../../../../../services/EventBus.ts' import { useActorStore } from '../../../../../stores/actor.ts' +import { getFileExtension, sanitizeFileName } from '../../../../../utils/fileUpload.ts' export default { name: 'AudioPlayer', @@ -84,6 +85,18 @@ export default { }, computed: { + sanitizedFileName() { + return sanitizeFileName(this.name) + }, + + fileNameWithoutExtension() { + return this.name.slice(0, this.name.length - getFileExtension(this.name).length) + }, + + fileExtension() { + return getFileExtension(this.name) + }, + internalAbsolutePath() { if (this.path.startsWith('/')) { return this.path @@ -155,6 +168,10 @@ export default { font-weight: bold; } + &__basename { + unicode-bidi: isolate; + } + &__audio { display: block; } diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index ca23c1ab33a..3dcf2ab66ef 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -19,7 +19,7 @@ @click.exact="handleClick" @keydown.enter="handleClick"> @@ -36,7 +36,7 @@ v-else class="file-preview__image" :class="previewImageClass" - :alt="file.name" + :alt="sanitizedFileName" :src="defaultIconUrl"> @@ -47,7 +47,7 @@ @@ -95,7 +95,7 @@
- {{ fileDetail }} + {{ fileNameWithoutExtension }}{{ fileExtension }}
@@ -122,6 +122,7 @@ import { useActorStore } from '../../../../../stores/actor.ts' import { useSharedItemsStore } from '../../../../../stores/sharedItems.ts' import { useUploadStore } from '../../../../../stores/upload.ts' import { isClassifiedConversation } from '../../../../../utils/conversation.ts' +import { getFileExtension, sanitizeFileName } from '../../../../../utils/fileUpload.ts' import { canPlayAudio } from '../../../../../utils/sounds.js' const PREVIEW_TYPE = { @@ -239,8 +240,18 @@ export default { ) }, - fileDetail() { - return this.file.name + // file.name with bidi control chars replaced by '_', for title/alt/aria-label text + sanitizedFileName() { + return sanitizeFileName(this.file.name) + }, + + fileNameWithoutExtension() { + return this.file.name.slice(0, this.file.name.length - getFileExtension(this.file.name).length) + }, + + // Dot included, original case + fileExtension() { + return getFileExtension(this.file.name) }, fileSizeLabel() { @@ -528,7 +539,7 @@ export default { }, removeAriaLabel() { - return t('spreed', 'Remove {fileName}', { fileName: this.file.name }) + return t('spreed', 'Remove {fileName}', { fileName: this.sanitizedFileName }) }, }, @@ -786,6 +797,10 @@ export default { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + + &__basename { + unicode-bidi: isolate; + } } &:not(.file-preview--viewer-available) { diff --git a/src/test-setup.js b/src/test-setup.js index 3be3c32f916..9a82d3035a7 100644 --- a/src/test-setup.js +++ b/src/test-setup.js @@ -28,6 +28,7 @@ vi.mock('@nextcloud/dialogs', () => ({ vi.mock('@nextcloud/files', () => ({ validateFileName: vi.fn(), + formatFileSize: vi.fn((size) => `${size} B`), })) vi.mock('@nextcloud/files/dav', () => ({ diff --git a/src/utils/fileUpload.ts b/src/utils/fileUpload.ts index daca1d99bfc..8e03b1921ce 100644 --- a/src/utils/fileUpload.ts +++ b/src/utils/fileUpload.ts @@ -8,6 +8,7 @@ import type { UploadEntry } from '../types/index.ts' const extensionRegex = /\.[0-9a-z]+$/i const suffixRegex = / \(\d+\)$/ +const bidiControlRegex = /[\u202A-\u202E\u2066-\u2069]/g /** * Returns the file extension for the given path @@ -19,6 +20,16 @@ export function getFileExtension(path: string): string { return path.match(extensionRegex)?.[0] ?? '' } +/** + * Returns name with bidi control characters replaced by '_' + * + * @param name file name + * @return sanitized file name + */ +export function sanitizeFileName(name: string): string { + return name.replace(bidiControlRegex, '_') +} + /** * Returns the file suffix for the given path * From 24ef0f2f1642976e720e9f92e9554a8c9efa5c34 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 2 Sep 2026 12:15:46 +0200 Subject: [PATCH 3/4] fix(FilePreview): ellipsize long filenames, highlight extensions Signed-off-by: Joas Schilling Signed-off-by: Maksim Sukharev --- .../Message/MessagePart/FilePreview.vue | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index 3dcf2ab66ef..edeaa5c52a8 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -95,7 +95,8 @@
- {{ fileNameWithoutExtension }}{{ fileExtension }} + {{ fileNameWithoutExtension }} + {{ fileExtension }}
@@ -796,10 +797,18 @@ export default { width: 100%; overflow: hidden; white-space: nowrap; - text-overflow: ellipsis; + display: inline-flex; &__basename { unicode-bidi: isolate; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &__extension { + color: var(--color-text-maxcontrast); + overflow: visible; } } From 956e42818745d942db1ba89ce24b34800a2d24f1 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 2 Sep 2026 12:28:04 +0200 Subject: [PATCH 4/4] fix(conversation): sanitize file names in last message Signed-off-by: Maksim Sukharev --- src/utils/textParse.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/textParse.ts b/src/utils/textParse.ts index 328251decfe..8bf6b75b902 100644 --- a/src/utils/textParse.ts +++ b/src/utils/textParse.ts @@ -8,6 +8,8 @@ import type { ChatMessage, Mention } from '../types/index.ts' import { getBaseUrl } from '@nextcloud/router' import { decodeHTML } from 'entities' import { MENTION } from '../constants.ts' +import { sanitizeFileName } from './fileUpload.ts' +import { getFileKeys } from './message.ts' /** * Parse message text to return proper formatting for mentions @@ -61,8 +63,10 @@ function parseToSimpleMessage(text: string, parameters: ChatMessage['messagePara return text.trim() } + const fileKeys = getFileKeys({ messageParameters: parameters }) Object.entries(parameters).forEach(([key, value]) => { - text = text.replaceAll('{' + key + '}', value.name) + const name = fileKeys.includes(key) ? sanitizeFileName(value.name) : value.name + text = text.replaceAll('{' + key + '}', name) }) return text.trim() }