Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
277 lines
7.7 KiB
TypeScript
277 lines
7.7 KiB
TypeScript
import {
|
|
DEMO_EMAILS,
|
|
DEMO_USER,
|
|
type DemoEmail,
|
|
} from "@/components/demo/demo-mail-data"
|
|
import type {
|
|
ApiMailAccount,
|
|
ApiMessageFull,
|
|
ApiMessageSummary,
|
|
MessageSearchFilter,
|
|
PaginatedResponse,
|
|
Recipient,
|
|
} from "@/lib/api/types"
|
|
import { apiMessageToEmail } from "@/lib/demo/demo-mail-email-map"
|
|
import { emailMatchesFolder } from "@/lib/mail-folder-filter"
|
|
import type { Email } from "@/lib/email-data"
|
|
import { normalizeListPageSize } from "@/lib/mail-list-page-size"
|
|
|
|
export const DEMO_MAIL_ACCOUNT_ID = "demo-mail-account"
|
|
|
|
export const DEMO_MAIL_ACCOUNT: ApiMailAccount = {
|
|
id: DEMO_MAIL_ACCOUNT_ID,
|
|
name: DEMO_USER.name,
|
|
email: DEMO_USER.email,
|
|
provider: "demo",
|
|
imap_host: "demo.local",
|
|
smtp_host: "demo.local",
|
|
is_active: true,
|
|
created_at: "2026-06-01T08:00:00+02:00",
|
|
}
|
|
|
|
function escapeHtml(text: string): string {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
}
|
|
|
|
function demoTimeToIso(time: string): string {
|
|
const now = new Date()
|
|
const base = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
if (time.includes(":")) {
|
|
const [h, m] = time.split(":").map(Number)
|
|
base.setHours(h, m ?? 0, 0, 0)
|
|
return base.toISOString()
|
|
}
|
|
if (time === "Hier") {
|
|
base.setDate(base.getDate() - 1)
|
|
base.setHours(14, 20, 0, 0)
|
|
return base.toISOString()
|
|
}
|
|
if (time === "Dim.") {
|
|
const day = base.getDay()
|
|
const diff = day === 0 ? 0 : day
|
|
base.setDate(base.getDate() - diff)
|
|
base.setHours(3, 0, 0, 0)
|
|
return base.toISOString()
|
|
}
|
|
if (time.startsWith("Lun")) {
|
|
const day = base.getDay()
|
|
const diff = day === 0 ? 6 : day - 1
|
|
base.setDate(base.getDate() - diff)
|
|
base.setHours(11, 0, 0, 0)
|
|
return base.toISOString()
|
|
}
|
|
const weekdayOffsets: Record<string, number> = {
|
|
"Mar.": 2,
|
|
"Mer.": 3,
|
|
"Jeu.": 4,
|
|
"Ven.": 5,
|
|
"Sam.": 6,
|
|
}
|
|
for (const [prefix, weekday] of Object.entries(weekdayOffsets)) {
|
|
if (time.startsWith(prefix)) {
|
|
const day = base.getDay()
|
|
const diff = day >= weekday ? day - weekday : day + (7 - weekday)
|
|
base.setDate(base.getDate() - diff)
|
|
base.setHours(10, 30, 0, 0)
|
|
return base.toISOString()
|
|
}
|
|
}
|
|
base.setHours(12, 0, 0, 0)
|
|
return base.toISOString()
|
|
}
|
|
|
|
function dedupeLabels(labels: string[]): string[] {
|
|
const seen = new Set<string>()
|
|
const out: string[] = []
|
|
for (const label of labels) {
|
|
const key = label.toLowerCase()
|
|
if (seen.has(key)) continue
|
|
seen.add(key)
|
|
out.push(label)
|
|
}
|
|
return out
|
|
}
|
|
|
|
function demoTagLabels(email: DemoEmail): string[] {
|
|
const tags = [...(email.tags ?? [])]
|
|
if (
|
|
email.label &&
|
|
!tags.some((tag) => tag.toLowerCase() === email.label!.text.toLowerCase())
|
|
) {
|
|
tags.push(email.label.text)
|
|
}
|
|
return tags
|
|
}
|
|
|
|
function demoFolderToLabels(email: DemoEmail): string[] {
|
|
const tags = demoTagLabels(email)
|
|
if (email.folder === "trash") return ["trash"]
|
|
if (email.folder === "sent") return ["sent"]
|
|
if (email.folder === "drafts") return ["drafts"]
|
|
if (email.folder === "archive") return dedupeLabels(tags)
|
|
return dedupeLabels(["inbox", ...tags])
|
|
}
|
|
|
|
function demoBodyToHtml(body: string[]): string {
|
|
return body
|
|
.map(
|
|
(paragraph) =>
|
|
`<p style="margin:0 0 1em;line-height:1.6;">${escapeHtml(paragraph).replace(/\n/g, "<br>")}</p>`
|
|
)
|
|
.join("")
|
|
}
|
|
|
|
export function demoEmailToApiMessage(email: DemoEmail): ApiMessageFull {
|
|
const flags: string[] = []
|
|
if (!email.unread) flags.push("\\Seen")
|
|
if (email.starred) flags.push("\\Flagged")
|
|
if (email.important) flags.push("important")
|
|
|
|
return {
|
|
id: email.id,
|
|
message_id: email.id,
|
|
thread_id: email.id,
|
|
account_id: DEMO_MAIL_ACCOUNT_ID,
|
|
subject: email.subject,
|
|
from: [{ name: email.fromName, address: email.fromEmail }],
|
|
to: [{ name: DEMO_USER.name, address: DEMO_USER.email }],
|
|
date: demoTimeToIso(email.time),
|
|
snippet: email.preview,
|
|
flags,
|
|
labels: demoFolderToLabels(email),
|
|
has_attachments: email.hasAttachment ?? false,
|
|
body_html: demoBodyToHtml(email.body),
|
|
body_text: email.body.join("\n\n"),
|
|
}
|
|
}
|
|
|
|
export function createInitialDemoMessages(): ApiMessageFull[] {
|
|
return DEMO_EMAILS.map(demoEmailToApiMessage)
|
|
}
|
|
|
|
function toSummary(message: ApiMessageFull): ApiMessageSummary {
|
|
const { body_html: _bodyHtml, body_text: _bodyText, cc: _cc, reply_to: _replyTo, auth_info: _auth, in_reply_to: _inReply, references: _refs, ...summary } = message
|
|
return summary
|
|
}
|
|
|
|
function messageToEmail(message: ApiMessageSummary | ApiMessageFull): Email {
|
|
return apiMessageToEmail(message)
|
|
}
|
|
|
|
function matchesSearch(message: ApiMessageSummary, filter: MessageSearchFilter): boolean {
|
|
const email = messageToEmail(message)
|
|
const q = filter.q?.trim().toLowerCase()
|
|
if (q) {
|
|
const haystack = [
|
|
email.sender,
|
|
email.senderEmail ?? "",
|
|
email.subject,
|
|
email.preview,
|
|
message.snippet,
|
|
]
|
|
.join(" ")
|
|
.toLowerCase()
|
|
if (!haystack.includes(q)) return false
|
|
}
|
|
if (filter.from) {
|
|
const from = filter.from.toLowerCase()
|
|
const sender = message.from[0]?.address?.toLowerCase() ?? ""
|
|
const name = message.from[0]?.name?.toLowerCase() ?? ""
|
|
if (!sender.includes(from) && !name.includes(from)) return false
|
|
}
|
|
if (filter.label) {
|
|
const needle = filter.label.toLowerCase()
|
|
if (!message.labels.some((l) => l.toLowerCase() === needle)) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
export function listDemoMessages(
|
|
messages: ApiMessageFull[],
|
|
folder: string,
|
|
page: number,
|
|
pageSize: number
|
|
): PaginatedResponse<ApiMessageSummary> {
|
|
const safePageSize = normalizeListPageSize(pageSize)
|
|
const ctx = { starredEmailIds: [], importantEmailIds: [] }
|
|
const filtered = messages
|
|
.map(toSummary)
|
|
.filter((message) =>
|
|
emailMatchesFolder(messageToEmail(message), folder, ctx)
|
|
)
|
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
|
|
const start = (page - 1) * safePageSize
|
|
const slice = filtered.slice(start, start + safePageSize)
|
|
|
|
return {
|
|
data: slice,
|
|
pagination: {
|
|
page,
|
|
page_size: safePageSize,
|
|
total: filtered.length,
|
|
},
|
|
}
|
|
}
|
|
|
|
export function searchDemoMessages(
|
|
messages: ApiMessageFull[],
|
|
filter: MessageSearchFilter | null,
|
|
page = 1,
|
|
pageSize = 50
|
|
): PaginatedResponse<ApiMessageSummary> {
|
|
const safePageSize = normalizeListPageSize(pageSize)
|
|
const filtered = messages
|
|
.map(toSummary)
|
|
.filter((message) => (filter ? matchesSearch(message, filter) : true))
|
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
|
|
const start = (page - 1) * safePageSize
|
|
return {
|
|
data: filtered.slice(start, start + safePageSize),
|
|
pagination: {
|
|
page,
|
|
page_size: safePageSize,
|
|
total: filtered.length,
|
|
},
|
|
}
|
|
}
|
|
|
|
export function demoSentMessageFromPayload(payload: {
|
|
account_id: string
|
|
to: Recipient[]
|
|
subject: string
|
|
body_html: string
|
|
}): ApiMessageFull {
|
|
const id = `sent-${Date.now()}`
|
|
const text = payload.body_html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
|
|
return {
|
|
id,
|
|
message_id: id,
|
|
thread_id: id,
|
|
account_id: payload.account_id,
|
|
subject: payload.subject || "(Sans objet)",
|
|
from: [{ name: DEMO_USER.name, address: DEMO_USER.email }],
|
|
to: payload.to,
|
|
date: new Date().toISOString(),
|
|
snippet: text.slice(0, 160) || "(message vide)",
|
|
flags: ["\\Seen"],
|
|
labels: ["sent"],
|
|
has_attachments: false,
|
|
body_html: payload.body_html,
|
|
body_text: text,
|
|
}
|
|
}
|
|
|
|
export function moveDemoMessageToTrash(message: ApiMessageFull): ApiMessageFull {
|
|
const labels = message.labels.filter((l) => l.toLowerCase() !== "inbox")
|
|
if (!labels.some((l) => l.toLowerCase() === "trash")) {
|
|
labels.push("trash")
|
|
}
|
|
return { ...message, labels }
|
|
}
|