Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
@ended="handleEnded">
{{ t('spreed', 'Your browser does not support playing audio files') }}
</audio>
<span v-if="showFileName" class="audio-player__name" :title="name">
{{ name }}
<span v-if="showFileName" class="audio-player__name" :title="sanitizedFileName">
<span class="audio-player__basename">{{ fileNameWithoutExtension }}</span><span v-if="fileExtension">{{ fileExtension }}</span>
</span>
</div>
</template>
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -157,6 +170,10 @@ export default {
font-weight: bold;
}

&__basename {
unicode-bidi: isolate;
}

&__audio {
display: block;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@
@click.exact="handleClick"
@keydown.enter="handleClick">
<span
:title="file.name"
:title="sanitizedFileName"
class="image-container"
:class="{ playable: isPlayable }"
:style="imageContainerStyle">
<img
class="file-preview__image"
:class="previewImageClass"
:alt="file.name"
:alt="sanitizedFileName"
:src="failed ? defaultIconUrl : previewUrl"
@load="onLoad"
@error="onError">
<template v-if="!isLoading || fallbackLocalUrl">
<span v-if="isPlayable && !smallPreview" class="play-video-button">
<span v-if="isPlayable && !rowLayout" class="play-video-button">
<IconPlayCircleOutline
:size="48"
fillColor="#ffffff" />
Expand Down Expand Up @@ -65,7 +65,8 @@
</template>
</NcButton>
<div v-if="shouldShowFileDetail" class="name-container">
{{ fileDetail }}
<span class="name-container__basename">{{ fileNameWithoutExtension }}</span>
<span v-if="fileExtension" class="name-container__extension">{{ fileExtension }}</span>
</div>
</component>
</template>
Expand All @@ -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 = {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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...
Expand Down Expand Up @@ -476,7 +481,7 @@ export default {
},

removeAriaLabel() {
return t('spreed', 'Remove {fileName}', { fileName: this.file.name })
return t('spreed', 'Remove {fileName}', { fileName: this.sanitizedFileName })
},
},

Expand Down Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion src/components/RightSidebar/SharedItems/SharedItems.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
<FilePreview
v-else
:token="token"
:smallPreview="!isMedia"
:rowLayout="!isMedia"
:itemType="type"
isSharedItems
Expand Down
1 change: 1 addition & 0 deletions src/test-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down
11 changes: 11 additions & 0 deletions src/utils/fileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
*
Expand Down
4 changes: 3 additions & 1 deletion src/utils/textParse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
Loading