1363 lines
31 KiB
Vue
1363 lines
31 KiB
Vue
<template>
|
|
<webmailing-template>
|
|
<template #webmailing-sidebar>
|
|
<aside class="webmail-panel webmail-panel--sidebar">
|
|
<soft-button
|
|
color="primary"
|
|
variant="gradient"
|
|
size="sm"
|
|
class="webmail-compose"
|
|
@click="openCompose"
|
|
>
|
|
<i class="fas fa-pen me-2"></i>
|
|
<span>Compose</span>
|
|
</soft-button>
|
|
|
|
<nav class="webmail-sidebar-group">
|
|
<button
|
|
v-for="folder in folders"
|
|
:key="folder.id"
|
|
class="webmail-sidebar-item"
|
|
:class="{ 'is-active': selectedFolder === folder.id }"
|
|
type="button"
|
|
@click="selectedFolder = folder.id"
|
|
>
|
|
<span class="webmail-sidebar-item__content">
|
|
<span class="webmail-sidebar-item__icon">
|
|
<i :class="folder.icon"></i>
|
|
</span>
|
|
<span class="webmail-sidebar-item__label">{{
|
|
folder.label
|
|
}}</span>
|
|
</span>
|
|
<soft-badge
|
|
v-if="folder.count !== undefined"
|
|
:color="selectedFolder === folder.id ? 'primary' : 'secondary'"
|
|
:variant="selectedFolder === folder.id ? 'gradient' : 'fill'"
|
|
size="sm"
|
|
>
|
|
{{ folder.count }}
|
|
</soft-badge>
|
|
</button>
|
|
</nav>
|
|
|
|
<section class="webmail-sidebar-section">
|
|
<div class="webmail-sidebar-section__head">
|
|
<span>Labels</span>
|
|
</div>
|
|
|
|
<div class="webmail-sidebar-section__list">
|
|
<button
|
|
v-for="label in labels"
|
|
:key="label.id"
|
|
class="webmail-label-item"
|
|
type="button"
|
|
>
|
|
<span class="webmail-label-item__meta">
|
|
<span
|
|
class="webmail-label-item__dot"
|
|
:style="{ background: label.color }"
|
|
></span>
|
|
<span class="webmail-label-item__text">{{ label.label }}</span>
|
|
</span>
|
|
<soft-badge color="secondary" variant="fill" size="sm">
|
|
{{ label.count }}
|
|
</soft-badge>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="webmail-storage-card">
|
|
<div class="webmail-storage-card__top">
|
|
<div>
|
|
<h4>Mailbox</h4>
|
|
<p>{{ stats.total }} messages synchronised</p>
|
|
</div>
|
|
<soft-badge color="info" variant="gradient" size="sm">
|
|
{{
|
|
webmailStore.isSyncing ? "sync..." : `${stats.unread} unread`
|
|
}}
|
|
</soft-badge>
|
|
</div>
|
|
|
|
<soft-progress
|
|
:percentage="storagePercentage"
|
|
color="info"
|
|
variant="gradient"
|
|
/>
|
|
|
|
<button
|
|
class="webmail-storage-card__button"
|
|
type="button"
|
|
:disabled="webmailStore.isSyncing"
|
|
@click="refreshMailbox"
|
|
>
|
|
{{
|
|
webmailStore.isSyncing ? "Syncing mailbox..." : "Refresh inbox"
|
|
}}
|
|
</button>
|
|
</section>
|
|
</aside>
|
|
</template>
|
|
|
|
<template #webmailing-list>
|
|
<section class="webmail-panel webmail-panel--list">
|
|
<div class="webmail-list-tabs">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
class="webmail-tab-button"
|
|
:class="{ 'is-active': activeTab === tab.id }"
|
|
type="button"
|
|
@click="activeTab = tab.id"
|
|
>
|
|
<span class="webmail-tab-button__meta">
|
|
<i :class="tab.icon"></i>
|
|
<span>{{ tab.label }}</span>
|
|
</span>
|
|
<span class="webmail-tab-button__count">({{ tab.count }})</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="webmailStore.isLoading" class="webmail-empty-state">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="storeError"
|
|
class="webmail-empty-state webmail-empty-state--error"
|
|
>
|
|
<p>{{ storeError }}</p>
|
|
<soft-button color="primary" size="sm" @click="refreshMailbox">
|
|
Retry
|
|
</soft-button>
|
|
</div>
|
|
|
|
<div v-else-if="!filteredMessages.length" class="webmail-empty-state">
|
|
<p>No messages available in this folder.</p>
|
|
</div>
|
|
|
|
<div v-else class="webmail-list-scroll">
|
|
<button
|
|
v-for="message in filteredMessages"
|
|
:key="message.id"
|
|
class="webmail-message-item"
|
|
:class="{ 'is-active': selectedMessageId === message.id }"
|
|
type="button"
|
|
@click="selectMessage(message)"
|
|
>
|
|
<span
|
|
class="webmail-message-item__dot"
|
|
:class="{ 'is-read': message.is_read }"
|
|
></span>
|
|
<span class="webmail-message-item__content">
|
|
<span class="webmail-message-item__head">
|
|
<span class="webmail-message-item__sender">{{
|
|
getSenderLabel(message)
|
|
}}</span>
|
|
<span class="webmail-message-item__time">{{
|
|
getMessageTime(message)
|
|
}}</span>
|
|
</span>
|
|
<span class="webmail-message-item__subject">{{
|
|
message.subject || "(No subject)"
|
|
}}</span>
|
|
<span class="webmail-message-item__preview">{{
|
|
message.snippet || "No preview available."
|
|
}}</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<template #webmailing-detail>
|
|
<section class="webmail-panel webmail-panel--detail">
|
|
<div class="webmail-toolbar">
|
|
<div class="webmail-toolbar__group">
|
|
<soft-button
|
|
color="light"
|
|
size="sm"
|
|
class="btn-icon-only webmail-icon-button"
|
|
@click="openCompose"
|
|
>
|
|
<i class="fas fa-pen"></i>
|
|
</soft-button>
|
|
</div>
|
|
|
|
<div class="webmail-toolbar__group">
|
|
<soft-button
|
|
v-for="action in toolbarActions"
|
|
:key="action.id"
|
|
color="light"
|
|
size="sm"
|
|
class="btn-icon-only webmail-icon-button"
|
|
@click="handleToolbarAction(action.id)"
|
|
>
|
|
<i :class="action.icon"></i>
|
|
</soft-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isComposeMode" class="webmail-detail-scroll">
|
|
<div class="webmail-compose-panel">
|
|
<div class="webmail-compose-panel__head">
|
|
<div>
|
|
<h2>Compose message</h2>
|
|
<p>Send a message using the Laravel Webmail API.</p>
|
|
</div>
|
|
<soft-button color="light" size="sm" @click="cancelCompose">
|
|
Cancel
|
|
</soft-button>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<label class="webmail-field-label">To</label>
|
|
<soft-input
|
|
v-model="composeForm.to"
|
|
placeholder="client@example.com, second@example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="webmail-field-label">Cc</label>
|
|
<soft-input v-model="composeForm.cc" placeholder="Optional" />
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="webmail-field-label">Bcc</label>
|
|
<soft-input v-model="composeForm.bcc" placeholder="Optional" />
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="webmail-field-label">Subject</label>
|
|
<soft-input
|
|
v-model="composeForm.subject"
|
|
placeholder="Subject"
|
|
/>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="webmail-field-label">Message</label>
|
|
<textarea
|
|
v-model="composeForm.body"
|
|
class="form-control webmail-compose-textarea"
|
|
rows="12"
|
|
placeholder="Write your message here..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="webmail-message-actions mt-4">
|
|
<soft-button color="light" @click="cancelCompose">
|
|
Discard
|
|
</soft-button>
|
|
<soft-button
|
|
color="primary"
|
|
variant="gradient"
|
|
:disabled="webmailStore.isSending"
|
|
@click="submitCompose"
|
|
>
|
|
<i class="fas fa-paper-plane me-2"></i>
|
|
{{ webmailStore.isSending ? "Sending..." : "Send" }}
|
|
</soft-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="currentMessage" class="webmail-detail-scroll">
|
|
<div class="webmail-detail-head">
|
|
<div class="webmail-detail-head__main">
|
|
<div class="webmail-detail-head__title-row">
|
|
<h2>{{ currentMessage.subject || "(No subject)" }}</h2>
|
|
<soft-button
|
|
color="light"
|
|
class="btn-icon-only webmail-bookmark"
|
|
@click="toggleStarred"
|
|
>
|
|
<i
|
|
:class="
|
|
currentMessage.is_starred
|
|
? 'fas fa-star text-warning'
|
|
: 'far fa-star'
|
|
"
|
|
></i>
|
|
</soft-button>
|
|
</div>
|
|
|
|
<div class="webmail-sender-card">
|
|
<soft-avatar
|
|
:img="fallbackAvatar"
|
|
:alt="getSenderLabel(currentMessage)"
|
|
size="sm"
|
|
shadow="sm"
|
|
border-radius="lg"
|
|
/>
|
|
|
|
<div class="webmail-sender-card__copy">
|
|
<p>{{ getPrimaryEmail(currentMessage) }}</p>
|
|
<button class="webmail-sender-card__meta" type="button">
|
|
{{
|
|
currentMessage.direction === "outgoing"
|
|
? "to recipients"
|
|
: "to me"
|
|
}}
|
|
<i class="fas fa-chevron-down"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="webmail-detail-head__aside">
|
|
<div class="webmail-detail-head__actions">
|
|
<soft-button
|
|
v-for="action in messageActions"
|
|
:key="action.id"
|
|
color="light"
|
|
size="sm"
|
|
class="btn-icon-only webmail-icon-button"
|
|
@click="handleMessageAction(action.id)"
|
|
>
|
|
<i :class="action.icon"></i>
|
|
</soft-button>
|
|
</div>
|
|
<p>{{ getFullDate(currentMessage) }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<article class="webmail-message-body">
|
|
<p
|
|
v-for="paragraph in getBodyParagraphs(currentMessage)"
|
|
:key="paragraph"
|
|
>
|
|
{{ paragraph }}
|
|
</p>
|
|
</article>
|
|
|
|
<div class="webmail-message-actions">
|
|
<soft-button
|
|
color="light"
|
|
class="webmail-message-actions__button"
|
|
@click="replyToMessage"
|
|
>
|
|
<i class="fas fa-reply me-2"></i>
|
|
<span>Reply</span>
|
|
</soft-button>
|
|
<soft-button
|
|
color="light"
|
|
class="webmail-message-actions__button"
|
|
@click="forwardMessage"
|
|
>
|
|
<i class="fas fa-share me-2"></i>
|
|
<span>Forward</span>
|
|
</soft-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="webmail-empty-state">
|
|
<p>Select a message or start composing.</p>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</webmailing-template>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
|
import SoftBadge from "@/components/SoftBadge.vue";
|
|
import SoftButton from "@/components/SoftButton.vue";
|
|
import SoftInput from "@/components/SoftInput.vue";
|
|
import SoftProgress from "@/components/SoftProgress.vue";
|
|
import WebmailingTemplate from "@/components/templates/Webmailing/WebmailingTemplate.vue";
|
|
import { useNotificationStore } from "@/stores/notification";
|
|
import { useWebmailStore } from "@/stores/webmailStore";
|
|
|
|
const webmailStore = useWebmailStore();
|
|
const notificationStore = useNotificationStore();
|
|
|
|
const selectedFolder = ref("inbox");
|
|
const activeTab = ref("all");
|
|
const selectedMessageId = ref(null);
|
|
const isComposeMode = ref(false);
|
|
const fallbackAvatar = "https://via.placeholder.com/80x80.png?text=M";
|
|
|
|
const composeForm = ref({
|
|
to: "",
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "",
|
|
body: "",
|
|
});
|
|
|
|
const toolbarActions = [
|
|
{ id: "refresh", icon: "fas fa-rotate-right" },
|
|
{ id: "toggle-read", icon: "fas fa-envelope-open" },
|
|
{ id: "toggle-star", icon: "fas fa-star" },
|
|
{ id: "delete", icon: "fas fa-trash" },
|
|
];
|
|
|
|
const messageActions = [
|
|
{ id: "reply", icon: "fas fa-reply" },
|
|
{ id: "forward", icon: "fas fa-share" },
|
|
{ id: "delete", icon: "fas fa-trash" },
|
|
{ id: "toggle-star", icon: "fas fa-star" },
|
|
];
|
|
|
|
const stats = computed(() => webmailStore.getStats);
|
|
const storeError = computed(() => webmailStore.error);
|
|
|
|
const folders = computed(() => [
|
|
{
|
|
id: "inbox",
|
|
label: "Inbox",
|
|
icon: "fas fa-inbox",
|
|
count: stats.value.inbox,
|
|
},
|
|
{
|
|
id: "important",
|
|
label: "Important",
|
|
icon: "fas fa-bookmark",
|
|
count: stats.value.starred,
|
|
},
|
|
{
|
|
id: "drafts",
|
|
label: "Drafts",
|
|
icon: "fas fa-file-alt",
|
|
count: stats.value.drafts,
|
|
},
|
|
{
|
|
id: "sent",
|
|
label: "Sent",
|
|
icon: "fas fa-paper-plane",
|
|
count: stats.value.sent,
|
|
},
|
|
{
|
|
id: "trash",
|
|
label: "Trash",
|
|
icon: "fas fa-trash",
|
|
count: stats.value.trash,
|
|
},
|
|
{
|
|
id: "all-mail",
|
|
label: "All Mail",
|
|
icon: "fas fa-box-archive",
|
|
count: stats.value.total,
|
|
},
|
|
]);
|
|
|
|
const labels = computed(() => [
|
|
{
|
|
id: "unread",
|
|
label: "Unread",
|
|
color: "#5e72e4",
|
|
count: stats.value.unread,
|
|
},
|
|
{
|
|
id: "starred",
|
|
label: "Starred",
|
|
color: "#f53939",
|
|
count: stats.value.starred,
|
|
},
|
|
]);
|
|
|
|
const tabs = computed(() => [
|
|
{
|
|
id: "all",
|
|
label: "All",
|
|
icon: "fas fa-list",
|
|
count: webmailStore.allMessages.length,
|
|
},
|
|
{
|
|
id: "unread",
|
|
label: "Unread",
|
|
icon: "fas fa-envelope",
|
|
count: webmailStore.allMessages.filter((message) => !message.is_read)
|
|
.length,
|
|
},
|
|
]);
|
|
|
|
const filteredMessages = computed(() => {
|
|
if (activeTab.value === "unread") {
|
|
return webmailStore.allMessages.filter((message) => !message.is_read);
|
|
}
|
|
|
|
return webmailStore.allMessages;
|
|
});
|
|
|
|
const currentMessage = computed(() => {
|
|
if (selectedMessageId.value) {
|
|
const fromList = filteredMessages.value.find(
|
|
(message) => message.id === selectedMessageId.value
|
|
);
|
|
|
|
if (fromList) {
|
|
return fromList;
|
|
}
|
|
}
|
|
|
|
return webmailStore.selectedMessage;
|
|
});
|
|
|
|
const storagePercentage = computed(() => {
|
|
const total = Math.max(stats.value.total, 1);
|
|
return Math.min(100, Math.round((stats.value.unread / total) * 100));
|
|
});
|
|
|
|
const parseEmails = (value) =>
|
|
value
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
|
|
const buildFolderParams = () => {
|
|
if (selectedFolder.value === "all-mail") {
|
|
return {};
|
|
}
|
|
|
|
if (selectedFolder.value === "important") {
|
|
return { starred: true };
|
|
}
|
|
|
|
return { folder: selectedFolder.value };
|
|
};
|
|
|
|
const getSenderLabel = (message) => {
|
|
if (message.direction === "outgoing") {
|
|
return `To: ${message.to?.[0] || "Unknown"}`;
|
|
}
|
|
|
|
return message.from_name || message.from_email || "Unknown sender";
|
|
};
|
|
|
|
const getPrimaryEmail = (message) => {
|
|
if (message.direction === "outgoing") {
|
|
return message.to?.join(", ") || "No recipients";
|
|
}
|
|
|
|
return message.from_email || "Unknown sender";
|
|
};
|
|
|
|
const getMessageDateValue = (message) =>
|
|
message.received_at || message.sent_at || message.created_at;
|
|
|
|
const getMessageTime = (message) => {
|
|
const dateValue = getMessageDateValue(message);
|
|
if (!dateValue) {
|
|
return "-";
|
|
}
|
|
|
|
const date = new Date(dateValue);
|
|
return date.toLocaleDateString("en-GB", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
});
|
|
};
|
|
|
|
const getFullDate = (message) => {
|
|
const dateValue = getMessageDateValue(message);
|
|
if (!dateValue) {
|
|
return "Unknown date";
|
|
}
|
|
|
|
const date = new Date(dateValue);
|
|
return date.toLocaleString("en-GB", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
};
|
|
|
|
const getBodyParagraphs = (message) => {
|
|
const body = (message?.body || "").trim();
|
|
if (!body) {
|
|
return ["No content available."];
|
|
}
|
|
|
|
return body
|
|
.split(/\n{2,}/)
|
|
.map((paragraph) => paragraph.trim())
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const resetComposeForm = () => {
|
|
composeForm.value = {
|
|
to: "",
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "",
|
|
body: "",
|
|
};
|
|
};
|
|
|
|
const syncMailbox = async () => {
|
|
await Promise.all([
|
|
webmailStore.fetchMessages(buildFolderParams()),
|
|
webmailStore.fetchStats(),
|
|
webmailStore.fetchMailboxSettings(),
|
|
]);
|
|
|
|
selectedMessageId.value = webmailStore.selectedMessage?.id || null;
|
|
};
|
|
|
|
const refreshMailbox = async () => {
|
|
try {
|
|
const result = await webmailStore.syncMailbox(buildFolderParams());
|
|
|
|
if (result.imported > 0) {
|
|
notificationStore.success(
|
|
"Webmail",
|
|
`${result.imported} new message(s) imported from ${result.source}.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (result.skipped > 0) {
|
|
notificationStore.info(
|
|
"Webmail",
|
|
"No new messages found in the configured mailbox."
|
|
);
|
|
return;
|
|
}
|
|
|
|
await syncMailbox();
|
|
} catch {
|
|
notificationStore.error(
|
|
"Webmail",
|
|
"Unable to sync messages from the configured mailbox."
|
|
);
|
|
}
|
|
};
|
|
|
|
const selectMessage = async (message) => {
|
|
isComposeMode.value = false;
|
|
selectedMessageId.value = message.id;
|
|
|
|
try {
|
|
await webmailStore.fetchMessage(message.id);
|
|
|
|
if (!message.is_read) {
|
|
await webmailStore.updateMessage(message.id, { is_read: true });
|
|
}
|
|
} catch {
|
|
notificationStore.error("Webmail", "Unable to load the selected message.");
|
|
}
|
|
};
|
|
|
|
const openCompose = () => {
|
|
isComposeMode.value = true;
|
|
selectedMessageId.value = null;
|
|
};
|
|
|
|
const cancelCompose = () => {
|
|
isComposeMode.value = false;
|
|
resetComposeForm();
|
|
selectedMessageId.value = webmailStore.selectedMessage?.id || null;
|
|
};
|
|
|
|
const submitCompose = async () => {
|
|
const to = parseEmails(composeForm.value.to);
|
|
const cc = parseEmails(composeForm.value.cc);
|
|
const bcc = parseEmails(composeForm.value.bcc);
|
|
|
|
if (!to.length) {
|
|
notificationStore.warning(
|
|
"Webmail",
|
|
"Please provide at least one recipient."
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!composeForm.value.body.trim()) {
|
|
notificationStore.warning("Webmail", "Message content is required.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await webmailStore.sendMessage({
|
|
to,
|
|
cc,
|
|
bcc,
|
|
subject: composeForm.value.subject,
|
|
body: composeForm.value.body,
|
|
folder: "sent",
|
|
});
|
|
|
|
notificationStore.success("Webmail", "Message sent successfully.");
|
|
resetComposeForm();
|
|
isComposeMode.value = false;
|
|
selectedFolder.value = "sent";
|
|
await syncMailbox();
|
|
} catch {
|
|
notificationStore.error("Webmail", "Failed to send the message.");
|
|
}
|
|
};
|
|
|
|
const toggleStarred = async () => {
|
|
if (!currentMessage.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await webmailStore.updateMessage(currentMessage.value.id, {
|
|
is_starred: !currentMessage.value.is_starred,
|
|
});
|
|
} catch {
|
|
notificationStore.error("Webmail", "Unable to update the starred state.");
|
|
}
|
|
};
|
|
|
|
const replyToMessage = () => {
|
|
if (!currentMessage.value) {
|
|
return;
|
|
}
|
|
|
|
openCompose();
|
|
composeForm.value.to = currentMessage.value.from_email || "";
|
|
composeForm.value.subject = currentMessage.value.subject
|
|
? `Re: ${currentMessage.value.subject}`
|
|
: "Re:";
|
|
};
|
|
|
|
const forwardMessage = () => {
|
|
if (!currentMessage.value) {
|
|
return;
|
|
}
|
|
|
|
openCompose();
|
|
composeForm.value.subject = currentMessage.value.subject
|
|
? `Fwd: ${currentMessage.value.subject}`
|
|
: "Fwd:";
|
|
composeForm.value.body = getBodyParagraphs(currentMessage.value).join("\n\n");
|
|
};
|
|
|
|
const handleToolbarAction = async (actionId) => {
|
|
if (actionId === "refresh") {
|
|
await refreshMailbox();
|
|
return;
|
|
}
|
|
|
|
if (!currentMessage.value) {
|
|
return;
|
|
}
|
|
|
|
if (actionId === "toggle-read") {
|
|
try {
|
|
await webmailStore.updateMessage(currentMessage.value.id, {
|
|
is_read: !currentMessage.value.is_read,
|
|
});
|
|
} catch {
|
|
notificationStore.error("Webmail", "Unable to update read status.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (actionId === "toggle-star") {
|
|
await toggleStarred();
|
|
return;
|
|
}
|
|
|
|
if (actionId === "delete") {
|
|
try {
|
|
await webmailStore.deleteMessage(currentMessage.value.id);
|
|
notificationStore.success("Webmail", "Message deleted.");
|
|
selectedMessageId.value = webmailStore.selectedMessage?.id || null;
|
|
} catch {
|
|
notificationStore.error("Webmail", "Unable to delete this message.");
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleMessageAction = async (actionId) => {
|
|
if (actionId === "reply") {
|
|
replyToMessage();
|
|
return;
|
|
}
|
|
|
|
if (actionId === "forward") {
|
|
forwardMessage();
|
|
return;
|
|
}
|
|
|
|
if (actionId === "toggle-star") {
|
|
await toggleStarred();
|
|
return;
|
|
}
|
|
|
|
if (actionId === "delete") {
|
|
await handleToolbarAction("delete");
|
|
}
|
|
};
|
|
|
|
watch(selectedFolder, async () => {
|
|
activeTab.value = "all";
|
|
isComposeMode.value = false;
|
|
try {
|
|
await syncMailbox();
|
|
} catch {
|
|
notificationStore.error("Webmail", "Unable to load messages.");
|
|
}
|
|
});
|
|
|
|
watch(filteredMessages, (messages) => {
|
|
if (!messages.length) {
|
|
selectedMessageId.value = null;
|
|
return;
|
|
}
|
|
|
|
const exists = messages.some(
|
|
(message) => message.id === selectedMessageId.value
|
|
);
|
|
if (!exists && !isComposeMode.value) {
|
|
selectedMessageId.value = messages[0].id;
|
|
}
|
|
});
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
await syncMailbox();
|
|
} catch {
|
|
notificationStore.error("Webmail", "Unable to load messages.");
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.webmail-panel {
|
|
min-width: 0;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.webmail-panel--sidebar,
|
|
.webmail-panel--list {
|
|
border-right: 1px solid #f0f2f5;
|
|
}
|
|
|
|
.webmail-panel--sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.1rem;
|
|
padding: 1rem;
|
|
background: #fbfcfd;
|
|
}
|
|
|
|
.webmail-compose {
|
|
border-radius: 0.75rem !important;
|
|
padding: 0.7rem 0.9rem !important;
|
|
display: inline-flex !important;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.webmail-sidebar-item {
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
color: #67748e;
|
|
border-radius: 0.75rem;
|
|
padding: 0.7rem 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.55rem;
|
|
font-size: 0.75rem;
|
|
transition: background-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
|
|
.webmail-sidebar-item:hover {
|
|
background: #ffffff;
|
|
color: #344767;
|
|
}
|
|
|
|
.webmail-sidebar-item.is-active {
|
|
background: rgba(94, 114, 228, 0.12);
|
|
color: #344767;
|
|
}
|
|
|
|
.webmail-sidebar-item__content {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.8rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.webmail-sidebar-item__icon {
|
|
display: inline-flex;
|
|
width: 0.95rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.webmail-sidebar-item__label {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.webmail-sidebar-group,
|
|
.webmail-sidebar-section__list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
.webmail-sidebar-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
.webmail-sidebar-section__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 0.35rem;
|
|
color: #8392ab;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
font-size: 0.65rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.webmail-label-item {
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
border-radius: 0.75rem;
|
|
padding: 0.6rem 0.7rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
color: #67748e;
|
|
transition: background-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
|
|
.webmail-label-item:hover {
|
|
background: #ffffff;
|
|
color: #344767;
|
|
}
|
|
|
|
.webmail-label-item__meta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.7rem;
|
|
}
|
|
|
|
.webmail-label-item__dot {
|
|
width: 0.7rem;
|
|
height: 0.7rem;
|
|
border-radius: 0.25rem;
|
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
.webmail-label-item__text {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.webmail-storage-card {
|
|
margin-top: auto;
|
|
padding: 0.9rem;
|
|
border-radius: 0.9rem;
|
|
background: #ffffff;
|
|
border: 1px solid #f0f2f5;
|
|
}
|
|
|
|
.webmail-storage-card__top {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.webmail-storage-card__top h4 {
|
|
margin: 0 0 0.25rem;
|
|
color: #344767;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.webmail-storage-card__top p {
|
|
margin: 0;
|
|
color: #8392ab;
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.webmail-storage-card :deep(.progress) {
|
|
height: 0.4rem;
|
|
margin: 1rem 0 0.85rem;
|
|
background: #eaecf4;
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.webmail-storage-card__button {
|
|
border: 0;
|
|
padding: 0;
|
|
background: transparent;
|
|
color: #5e72e4;
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.webmail-panel--list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.webmail-list-tabs {
|
|
display: flex;
|
|
gap: 1rem;
|
|
padding: 0 1rem;
|
|
border-bottom: 1px solid #f0f2f5;
|
|
}
|
|
|
|
.webmail-tab-button {
|
|
position: relative;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0.75rem 0;
|
|
color: #8392ab;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.webmail-tab-button::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: -1px;
|
|
height: 3px;
|
|
border-radius: 999px;
|
|
background: transparent;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.webmail-tab-button.is-active {
|
|
color: #344767;
|
|
}
|
|
|
|
.webmail-tab-button.is-active::after {
|
|
background: #5e72e4;
|
|
}
|
|
|
|
.webmail-tab-button__meta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
.webmail-tab-button__count {
|
|
color: inherit;
|
|
}
|
|
|
|
.webmail-list-scroll,
|
|
.webmail-detail-scroll {
|
|
overflow-y: auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
.webmail-empty-state {
|
|
min-height: 16rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.75rem;
|
|
color: #67748e;
|
|
font-size: 0.82rem;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.webmail-empty-state--error {
|
|
color: #f53939;
|
|
}
|
|
|
|
.webmail-message-item {
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 0.75rem;
|
|
padding: 0.85rem 1rem;
|
|
border-bottom: 1px solid #f0f2f5;
|
|
text-align: left;
|
|
transition: background-color 0.2s ease, transform 0.2s ease;
|
|
}
|
|
|
|
.webmail-message-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.webmail-message-item.is-active {
|
|
background: rgba(94, 114, 228, 0.08);
|
|
}
|
|
|
|
.webmail-message-item__dot {
|
|
width: 0.65rem;
|
|
height: 0.65rem;
|
|
margin-top: 0.4rem;
|
|
border-radius: 999px;
|
|
background: #5e72e4;
|
|
box-shadow: 0 0 0 4px rgba(94, 114, 228, 0.12);
|
|
}
|
|
|
|
.webmail-message-item__dot.is-read {
|
|
background: #cbd5e1;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.webmail-message-item__content {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2rem;
|
|
}
|
|
|
|
.webmail-message-item__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.webmail-message-item__sender {
|
|
color: #344767;
|
|
font-size: 0.76rem;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.webmail-message-item__time {
|
|
color: #8392ab;
|
|
font-size: 0.7rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.webmail-message-item__subject {
|
|
color: #344767;
|
|
font-size: 0.84rem;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.webmail-message-item__preview {
|
|
color: #67748e;
|
|
font-size: 0.74rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.webmail-panel--detail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.webmail-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #f0f2f5;
|
|
}
|
|
|
|
.webmail-toolbar__group,
|
|
.webmail-detail-head__actions,
|
|
.webmail-message-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.65rem;
|
|
}
|
|
|
|
.webmail-icon-button,
|
|
.webmail-bookmark {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
padding: 0 !important;
|
|
display: inline-flex !important;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 0.5rem !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.webmail-detail-scroll {
|
|
padding: 1.25rem;
|
|
}
|
|
|
|
.webmail-detail-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.webmail-detail-head__main {
|
|
min-width: 0;
|
|
}
|
|
|
|
.webmail-detail-head__title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.webmail-detail-head__title-row h2 {
|
|
margin: 0;
|
|
color: #344767;
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.webmail-bookmark {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.webmail-sender-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.9rem;
|
|
}
|
|
|
|
.webmail-sender-card__copy p {
|
|
margin: 0 0 0.22rem;
|
|
color: #344767;
|
|
font-size: 0.82rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.webmail-sender-card__meta {
|
|
border: 0;
|
|
padding: 0;
|
|
background: transparent;
|
|
color: #67748e;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
.webmail-detail-head__aside {
|
|
text-align: right;
|
|
}
|
|
|
|
.webmail-detail-head__aside p {
|
|
margin: 0.75rem 0 0;
|
|
color: #8392ab;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.webmail-message-body {
|
|
max-width: 48rem;
|
|
margin-bottom: 1.75rem;
|
|
color: #67748e;
|
|
font-size: 0.84rem;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.webmail-message-body p {
|
|
margin-bottom: 1rem;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.webmail-compose-panel {
|
|
max-width: 48rem;
|
|
}
|
|
|
|
.webmail-compose-panel__head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.webmail-compose-panel__head h2 {
|
|
margin: 0 0 0.3rem;
|
|
color: #344767;
|
|
font-size: 1.05rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.webmail-compose-panel__head p {
|
|
margin: 0;
|
|
color: #8392ab;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.webmail-field-label {
|
|
display: block;
|
|
margin-bottom: 0.35rem;
|
|
color: #344767;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.webmail-compose-textarea {
|
|
min-height: 15rem;
|
|
font-size: 0.85rem;
|
|
color: #344767;
|
|
border-color: #d2d6da;
|
|
}
|
|
|
|
.webmail-message-actions {
|
|
padding-top: 1.1rem;
|
|
border-top: 1px solid #f0f2f5;
|
|
}
|
|
|
|
.webmail-message-actions__button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.55rem;
|
|
border-radius: 0.65rem !important;
|
|
padding: 0.65rem 0.9rem !important;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
@media (max-width: 992px) {
|
|
.webmail-panel--sidebar,
|
|
.webmail-panel--list {
|
|
border-right: 0;
|
|
border-bottom: 1px solid #f0f2f5;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.webmail-detail-head,
|
|
.webmail-compose-panel__head {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.webmail-detail-head__aside {
|
|
width: 100%;
|
|
text-align: left;
|
|
}
|
|
|
|
.webmail-toolbar,
|
|
.webmail-detail-scroll {
|
|
padding-inline: 1rem;
|
|
}
|
|
}
|
|
</style>
|