34dc636d13
Signed-off-by: Sebastian <krupinski01@gmail.com>
216 lines
6.3 KiB
Vue
216 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import RecipientDetails from '@/components/common/RecipientDetails.vue'
|
|
import { formatFileSize } from '@/utile/format'
|
|
import { mediaViewerCanDisplay, mediaViewerComponent } from '@/services/mediaViewer'
|
|
import { useMailUiStore } from '@/stores/mailUiStore'
|
|
import type { EntityObject } from '@MailManager/models';
|
|
import type { MessagePartInterface } from '@MailManager/types/message';
|
|
|
|
interface AttachmentItem {
|
|
id: string
|
|
title: string
|
|
mime: string
|
|
size?: number | null
|
|
meta: { index: number; attachment: MessagePartInterface }
|
|
}
|
|
|
|
interface Props {
|
|
entity: EntityObject | null
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emit = defineEmits<{
|
|
download: [index: number]
|
|
preview: [index: number]
|
|
}>()
|
|
|
|
const mailUiStore = useMailUiStore()
|
|
const previewPopover = computed(() => mediaViewerComponent('popover'))
|
|
|
|
const message = computed(() => {
|
|
return props.entity?.properties ?? null
|
|
})
|
|
|
|
const randomKey = computed(() => {
|
|
return Math.random().toString(36).substring(2, 15)
|
|
})
|
|
|
|
const attachmentItems = computed<AttachmentItem[]>(() =>
|
|
(message.value?.attachments ?? []).map((attachment, index) => ({
|
|
id: attachment.partId || attachment.blobId || attachment.cid || `att-${index}`,
|
|
title: attachment.name || 'Attachment',
|
|
mime: attachment.type || '',
|
|
size: attachment.size ?? null,
|
|
meta: { index, attachment },
|
|
})),
|
|
)
|
|
|
|
const isPreviewable = (attachment: MessagePartInterface): boolean => {
|
|
return mediaViewerCanDisplay(attachment.type)
|
|
}
|
|
|
|
const handleAttachmentClick = (attachment: MessagePartInterface, index: number): void => {
|
|
if (isPreviewable(attachment)) {
|
|
emit('preview', index)
|
|
} else {
|
|
emit('download', index)
|
|
}
|
|
}
|
|
|
|
const handleViewerRetrieve = (item: AttachmentItem): Promise<Blob> => {
|
|
if (!props.entity) return Promise.reject(new Error('No message selected'))
|
|
return mailUiStore.attachmentBlob(props.entity, item.meta.attachment)
|
|
}
|
|
|
|
// Format date for display
|
|
const formatDate = (date: Date | string | null | undefined): string => {
|
|
if (!date) return ''
|
|
|
|
const messageDate = new Date(date)
|
|
return messageDate.toLocaleString('en-US', {
|
|
weekday: 'short',
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="message-header pa-6">
|
|
<div class="text-h5 mb-4">{{ message?.subject || '(No subject)' }}</div>
|
|
|
|
<div class="d-flex align-center mb-3">
|
|
<v-avatar size="48" color="primary" class="mr-3">
|
|
<span class="text-white text-h6">
|
|
{{ (message?.from?.label || message?.from?.address || 'U')[0].toUpperCase() }}
|
|
</span>
|
|
</v-avatar>
|
|
|
|
<div class="flex-grow-1">
|
|
<div class="text-body-1 font-weight-medium">
|
|
<RecipientDetails :address="message?.from">
|
|
<template #default="{ label }">
|
|
<span class="contact-link">{{ label }}</span>
|
|
</template>
|
|
</RecipientDetails>
|
|
</div>
|
|
<div class="text-caption text-medium-emphasis">
|
|
{{ formatDate(message?.received || message?.sent) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recipients -->
|
|
<div v-if="message?.to && message?.to.length > 0" class="text-body-2 mb-1">
|
|
<span class="text-medium-emphasis">To:</span>
|
|
<template v-for="(recipient, index) in message.to" :key="randomKey">
|
|
<RecipientDetails :address="recipient">
|
|
<template #default="{ label }">
|
|
<span class="contact-link">{{ label }}</span>
|
|
</template>
|
|
</RecipientDetails>
|
|
<span v-if="index < message.to.length - 1">, </span>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="message?.cc && message?.cc.length > 0" class="text-body-2 mb-1">
|
|
<span class="text-medium-emphasis">Cc:</span>
|
|
<template v-for="(recipient, index) in message.cc" :key="randomKey">
|
|
<RecipientDetails :address="recipient">
|
|
<template #default="{ label }">
|
|
<span class="contact-link">{{ label }}</span>
|
|
</template>
|
|
</RecipientDetails>
|
|
<span v-if="index < message.cc.length - 1">, </span>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Attachments -->
|
|
<div v-if="message?.attachments && message?.attachments.length > 0" class="mt-4">
|
|
<div class="text-body-2 text-medium-emphasis mb-2">
|
|
Attachments ({{ message?.attachments.length }})
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<div
|
|
v-for="(attachment, index) in message?.attachments"
|
|
:key="attachment.partId || attachment.blobId || attachment.cid || index"
|
|
class="attachment-item"
|
|
>
|
|
<v-chip
|
|
:prepend-icon="isPreviewable(attachment) ? 'mdi-eye-outline' : 'mdi-paperclip'"
|
|
size="small"
|
|
variant="outlined"
|
|
class="attachment-chip"
|
|
:title="isPreviewable(attachment) ? 'Preview' : 'Download'"
|
|
@click="handleAttachmentClick(attachment, index)"
|
|
>
|
|
<span class="attachment-name">{{ attachment.name || 'Untitled' }}</span>
|
|
<span v-if="attachment.size != null" class="text-caption text-medium-emphasis ml-1">
|
|
({{ formatFileSize(attachment.size) }})
|
|
</span>
|
|
</v-chip>
|
|
|
|
<!-- Hover preview (provided by the media_viewer module) -->
|
|
<component
|
|
:is="previewPopover"
|
|
v-if="previewPopover && isPreviewable(attachment)"
|
|
:item="attachmentItems[index]"
|
|
:source="handleViewerRetrieve"
|
|
@expand="emit('preview', index)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.message-header {
|
|
background-color: rgba(var(--v-theme-surface));
|
|
}
|
|
|
|
.gap-2 {
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.attachment-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.attachment-chip {
|
|
max-width: 300px;
|
|
cursor: pointer;
|
|
|
|
.attachment-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
.attachment-error {
|
|
color: rgb(var(--v-theme-error));
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.contact-link {
|
|
display: inline-block;
|
|
border-radius: 4px;
|
|
padding: 1px 4px;
|
|
margin: -1px -4px;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.contact-link:hover {
|
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
|
}
|
|
</style>
|