diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue index 9741b5cbe1d..adf6bf561ad 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue @@ -13,8 +13,8 @@ @ended="handleEnded"> {{ t('spreed', 'Your browser does not support playing audio files') }} - - {{ name }} + + {{ fileNameWithoutExtension }}{{ fileExtension }} @@ -25,6 +25,7 @@ import { encodePath } from '@nextcloud/paths' import { generateRemoteUrl } from '@nextcloud/router' import { EventBus } from '../../../../../services/EventBus.ts' import { useActorStore } from '../../../../../stores/actor.ts' +import { getFileExtension, sanitizeFileName } from '../../../../../utils/fileUpload.ts' export default { name: 'AudioPlayer', @@ -86,6 +87,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 @@ -157,6 +170,10 @@ export default { font-weight: bold; } + &__basename { + unicode-bidi: isolate; + } + &__audio { display: block; } diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js index 38726385ada..321ecf8f047 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js @@ -127,16 +127,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', () => { @@ -403,8 +404,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 45771aa02b2..24c8ad0bb83 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -19,19 +19,19 @@ @click.exact="handleClick" @keydown.enter="handleClick">
- {{ fileDetail }} + {{ fileNameWithoutExtension }} + {{ fileExtension }}
@@ -89,6 +90,7 @@ import { getTalkConfig } from '../../../../../services/CapabilitiesManager.ts' import { useActorStore } from '../../../../../stores/actor.ts' import { useSharedItemsStore } from '../../../../../stores/sharedItems.ts' import { useUploadStore } from '../../../../../stores/upload.ts' +import { getFileExtension, sanitizeFileName } from '../../../../../utils/fileUpload.ts' import { canPlayAudio } from '../../../../../utils/sounds.js' const PREVIEW_TYPE = { @@ -143,14 +145,6 @@ export default { default: '', }, - /** - * Whether to render a small preview to embed in replies - */ - smallPreview: { - type: Boolean, - default: false, - }, - /** * Whether the container is the upload editor. * True if this component is used in the upload editor. @@ -218,8 +212,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() { @@ -267,7 +271,7 @@ export default { previewImageClass() { let classes = '' - if (this.smallPreview) { + if (this.rowLayout) { classes += 'preview-small ' } else if (this.mediumPreview) { classes += 'preview-medium ' @@ -295,16 +299,21 @@ export default { return {} } + // Row layout always shows a small fixed-size icon, never a medium/full preview + if (this.rowLayout) { + return { width: '24px', height: '24px' } + } + // Fallback for loading mimeicons (preview for audio files is not provided) if (this.file['preview-available'] !== 'yes' || this.file.mimetype.startsWith('audio/') || this.failed) { return { - width: this.smallPreview ? '24px' : '128px', - height: this.smallPreview ? '24px' : '128px', + width: '128px', + height: '128px', } } - const widthConstraint = this.smallPreview ? 24 : (this.mediumPreview ? 192 : 600) - const heightConstraint = this.smallPreview ? 24 : (this.mediumPreview ? 192 : 384) + const widthConstraint = this.mediumPreview ? 192 : 600 + const heightConstraint = this.mediumPreview ? 192 : 384 // Actual size when no metadata available if (!this.file.width || !this.file.height) { @@ -367,11 +376,7 @@ export default { } // use preview provider URL to render a smaller preview - let previewSize = 384 - if (this.smallPreview) { - previewSize = 24 - } - previewSize = Math.ceil(previewSize * window.devicePixelRatio) + const previewSize = Math.ceil(384 * window.devicePixelRatio) if (userId === null) { // guest mode: grab token from the link URL // FIXME: use a cleaner way... @@ -476,7 +481,7 @@ export default { }, removeAriaLabel() { - return t('spreed', 'Remove {fileName}', { fileName: this.file.name }) + return t('spreed', 'Remove {fileName}', { fileName: this.sanitizedFileName }) }, }, @@ -672,7 +677,19 @@ 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; + } } &:not(.file-preview--viewer-available) { diff --git a/src/components/RightSidebar/SharedItems/SharedItems.vue b/src/components/RightSidebar/SharedItems/SharedItems.vue index bca71493e93..0c0ba564c52 100644 --- a/src/components/RightSidebar/SharedItems/SharedItems.vue +++ b/src/components/RightSidebar/SharedItems/SharedItems.vue @@ -46,7 +46,6 @@ ({ 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 00448c2a117..0fdf64f4c5d 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 * diff --git a/src/utils/textParse.ts b/src/utils/textParse.ts index 328251decfe..765af4d2114 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.ts' /** * Parse message text to return proper formatting for mentions @@ -62,7 +63,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() }