From 12d4606debfc8b00f6ab2b211979f7348d8610aa Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 2 Sep 2026 11:18:26 +0200 Subject: [PATCH 1/3] fix(FilePreview): sanitize file name - bidi-control unicode characters are not visible in browser, but causing confusion with inline rendering under certain conditions - for rendered DOM: align with Files app, render file extension in separate tag, isolate unicode-bidi - for titles and aria-labels: replace with underscore (same as downloaded file name) Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Maksim Sukharev --- .../Message/MessagePart/FilePreview.vue | 27 ++++++++++++++----- src/test-setup.js | 1 + src/utils/fileUpload.js | 11 ++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index 42d755c7b97..fc0dc354f95 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -19,14 +19,14 @@ @click.exact="handleClick" @keydown.enter="handleClick"> @@ -65,7 +65,7 @@
- {{ fileDetail }} + {{ fileNameWithoutExtension }}{{ fileExtension }}
@@ -88,6 +88,7 @@ import { SHARED_ITEM } from '../../../../../constants.ts' import { getTalkConfig } from '../../../../../services/CapabilitiesManager.ts' import { useActorStore } from '../../../../../stores/actor.ts' import { useSharedItemsStore } from '../../../../../stores/sharedItems.ts' +import { getFileExtension, sanitizeFileName } from '../../../../../utils/fileUpload.js' const PREVIEW_TYPE = { TEMPORARY: 0, @@ -215,8 +216,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) }, fallbackLocalUrl() { @@ -452,7 +463,7 @@ export default { }, removeAriaLabel() { - return t('spreed', 'Remove {fileName}', { fileName: this.file.name }) + return t('spreed', 'Remove {fileName}', { fileName: this.sanitizedFileName }) }, }, @@ -649,6 +660,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 2f23e02802a..7fc9d22cdc1 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.js b/src/utils/fileUpload.js index 585b1e94f9b..012b56916db 100644 --- a/src/utils/fileUpload.js +++ b/src/utils/fileUpload.js @@ -5,6 +5,7 @@ const extensionRegex = /\.[0-9a-z]+$/i const suffixRegex = / \(\d+\)$/ +const bidiControlRegex = /[\u202A-\u202E\u2066-\u2069]/g /** * Returns the file extension for the given path @@ -16,6 +17,16 @@ function getFileExtension(path) { return path.match(extensionRegex)?.[0] ?? '' } +/** + * Returns name with bidi control characters replaced by '_' + * + * @param {string} name file name + * @return {string} sanitized file name + */ +export function sanitizeFileName(name) { + return name.replace(bidiControlRegex, '_') +} + /** * Returns the file suffix for the given path * From fe7ace3b7725ebb3a70f73cf4622c6c7bdae3de7 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 2 Sep 2026 12:15:46 +0200 Subject: [PATCH 2/3] 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 fc0dc354f95..eea736b6804 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -65,7 +65,8 @@
- {{ fileNameWithoutExtension }}{{ fileExtension }} + {{ fileNameWithoutExtension }} + {{ fileExtension }}
@@ -659,10 +660,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 6b75dfcfd7d3279d0e054ecfc7a087337c0912e1 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 2 Sep 2026 12:28:04 +0200 Subject: [PATCH 3/3] fix(conversation): sanitize file names in last message Signed-off-by: Maksim Sukharev --- src/utils/textParse.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/textParse.ts b/src/utils/textParse.ts index 13a818a6e45..14736d390ba 100644 --- a/src/utils/textParse.ts +++ b/src/utils/textParse.ts @@ -8,6 +8,7 @@ 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.js' /** * Parse message text to return proper formatting for mentions @@ -63,7 +64,8 @@ function parseToSimpleMessage(text: string, parameters: ChatMessage['messagePara } Object.entries(parameters).forEach(([key, value]) => { - text = text.replaceAll('{' + key + '}', value.name) + const name = key === 'file' ? sanitizeFileName(value.name) : value.name + text = text.replaceAll('{' + key + '}', name) }) return text.trim() }