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 @@ -95,8 +95,11 @@
</template>
</NcButton>
<div v-if="shouldShowFileDetail" class="name-container">
<span class="name-container__basename">{{ fileNameWithoutExtension }}</span>
<span v-if="fileExtension" class="name-container__extension">{{ fileExtension }}</span>
<span class="name-container__name">
<span class="name-container__basename">{{ fileNameWithoutExtension }}</span>
<span v-if="fileExtension" class="name-container__extension">{{ fileExtension }}</span>
</span>
<span v-if="rowLayout && fileMetaLabel" class="name-container__meta">{{ fileMetaLabel }}</span>
</div>
</component>
</template>
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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
Expand All @@ -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 ''
Expand Down Expand Up @@ -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) {
Comment thread
Antreesy marked this conversation as resolved.
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)
},
},
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -850,14 +875,14 @@ 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);
}
}

&--row-layout {
display: flex;
align-items: center;
min-width: 0;
border-radius: var(--border-radius);
padding: 2px 4px;

Expand All @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', …)
*/
Expand All @@ -30,38 +65,102 @@ function getReferenceId(key: string): string {
</script>

<template>
<div
class="file-previews-wrapper"
:class="{ 'file-previews-wrapper--combined': fileKeys.length > 1 }">
<FilePreview
v-for="key in fileKeys"
:key="key"
:token="message.token"
:messageId="message.id"
:itemType="getItemTypeFromMessage(message, key)"
:referenceId="getReferenceId(key)"
:file="message.messageParameters[key]" />
<div class="file-previews-wrapper">
<div
v-if="imageKeys.length"
class="file-previews-wrapper__image-row"
:class="{
'file-previews-wrapper__image-row--has-more': hiddenImageCount > 0,
'file-previews-wrapper__image-row--combined': isImageRowCombined,
}">
<FilePreview
v-for="key in visibleImageKeys"
:key="key"
:token="message.token"
:messageId="message.id"
:itemType="getItemTypeFromMessage(message, key)"
:referenceId="getReferenceId(key)"
:viewerList="viewerList"
:file="message.messageParameters[key]" />
</div>
<div
v-if="otherKeys.length"
class="file-previews-wrapper__other-column">
<FilePreview
v-for="key in otherKeys"
:key="key"
rowLayout
Comment thread
Antreesy marked this conversation as resolved.
:token="message.token"
:messageId="message.id"
:itemType="getItemTypeFromMessage(message, key)"
:referenceId="getReferenceId(key)"
:file="message.messageParameters[key]" />
</div>
</div>
</template>

<style lang="scss" scoped>
.file-previews-wrapper {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
flex-direction: column;
align-items: stretch;
width: 100%;
min-width: 0;
gap: var(--default-grid-baseline);

&__image-row {
display: flex;
align-items: flex-start;
gap: var(--default-grid-baseline);
}

&__other-column {
display: flex;
flex-direction: column;
align-items: stretch;
gap: calc(var(--default-grid-baseline) / 2);
}

// Dim the last tile with a "+X more" overlay
&__image-row--has-more :deep(.file-preview:last-child) {
position: relative;

&::after {
content: v-bind(moreCountLabel);
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--border-radius);
background-color: rgba(0, 0, 0, 0.5);
color: #ffffff;
font-size: var(--font-size-large);
font-weight: bold;
pointer-events: none;
}
}
}

.file-previews-wrapper--combined {
.file-previews-wrapper__image-row--combined {
:deep(.file-preview) {
--preview-size: 80px;
--preview-name-height: 24px;
flex-shrink: 0;
flex: 1 1 var(--preview-size);
width: var(--preview-size);
height: calc(var(--preview-size) + var(--preview-name-height));
min-width: 0;
max-width: var(--preview-size);
height: var(--preview-size);
line-height: 0;

// Reserve space for the name, shown only when a preview fails to load
&:has(.name-container) {
height: calc(var(--preview-size) + var(--preview-name-height));
line-height: normal;
}

.image-container {
width: var(--preview-size) !important;
width: 100% !important;
height: var(--preview-size) !important;
}

Expand All @@ -79,7 +178,7 @@ function getReferenceId(key: string): string {

.name-container {
height: var(--preview-name-height);
font-weight: normal;
line-height: normal;
font-size: var(--font-size-small);
}
}
Expand Down
Loading