Menu
Public repository / ebbbadc91746d843b58df8c2619808a58602876c
AkurAI-Mail / src/lib/webmail/MessageList.svelte
Latest change aa05f301181c865428b6faf9c750f0631324c73a - feat: compute reply-needed, follow-up and priority signals for conversations (AKURAI-MAIL-7) by Ólafur Búi Ólafsson
<script lang="ts">
import { ChevronLeft, ChevronRight, CircleHelp, FileText, Inbox, MoreHorizontal, Paperclip, ShieldAlert, Star } from "lucide-svelte";
import SignalBadge from "$lib/webmail/SignalBadge.svelte";
let { folderList, folder, unreadCount, folderIcon, onFolder, messages, totalMessages, query, senderLabel, shortDate, selected, onRead, page, pageCount, nextCursor, busy, onPrevious, onNext, signalForMessage }: any = $props();
</script>
<aside class="folder-column" aria-label="Mail folders">
<div class="folder-summary"><span>Folders</span><strong>{unreadCount} unread</strong></div>
<div class="folder-group">
{#each folderList as item}
{@const Icon = folderIcon(item.name)}
<button class:active={item.name === folder} aria-current={item.name === folder ? "page" : undefined} onclick={() => onFolder(item.name)}>
<span class="folder-name"><Icon size={17} strokeWidth={1.8} /> {item.name}</span>
{#if item.unread}<em>{item.unread}</em>{:else if item.total}<small>{item.total}</small>{/if}
</button>
{/each}
</div>
<div class="folder-hint"><CircleHelp size={15} strokeWidth={1.8} /> Press <kbd>/</kbd> to search</div>
</aside>
<section class="message-column" aria-label="Message list">
<div class="column-bar"><div><span class="eyebrow">Current folder</span><strong>{folder}</strong><span class="message-count">{totalMessages} message{totalMessages === 1 ? "" : "s"}</span></div><button class="icon-button" aria-label="Message list options" title="Message list options"><MoreHorizontal size={19} /></button></div>
<div class="message-list" aria-live="polite">
{#each messages as message}
{@const convSig = signalForMessage?.(message.id)}
<button class:unread={message.unread} class:selected={selected?.id === message.id} onclick={() => onRead(message.id)}>
<span class="message-dot" aria-hidden="true"></span><span class="message-main"><span class="row-top"><b>{senderLabel(message)}</b><time>{shortDate(message.date)}</time></span><span class="subject-line">{#if message.flagged}<Star size={14} fill="currentColor" aria-label="Starred" />{/if}<strong>{message.subject || "(no subject)"}</strong></span><span class="preview">{message.preview}</span>
{#if message.attachments?.length || message.spamScore || convSig?.replyNeeded?.value || convSig?.priority?.value === "high"}<span class="message-tags">{#if message.attachments?.length}<em><Paperclip size={12} /> {message.attachments.length}</em>{/if}{#if message.spamScore}<em class="spam-tag"><ShieldAlert size={12} /> Review</em>{/if}{#if convSig?.replyNeeded?.value && !convSig.replyNeeded.dismissed}<SignalBadge signalType="replyNeeded" signal={convSig.replyNeeded} convId={convSig.conversationId} compact />{/if}{#if convSig?.priority?.value === "high" && !convSig.priority.dismissed}<SignalBadge signalType="priority" signal={convSig.priority} convId={convSig.conversationId} compact />{/if}</span>{/if}</span>
</button>
{:else}<div class="empty-state"><div class="empty-icon"><Inbox size={24} strokeWidth={1.6} /></div><strong>{query ? "No matching messages" : "Nothing in this folder"}</strong><p>{query ? "Try a different sender, subject, or phrase." : "New mail will appear here."}</p></div>{/each}
</div>
<div class="pager" aria-label="Message pagination"><button class="icon-button" aria-label="Previous page" title="Previous page" disabled={page === 0 || busy} onclick={onPrevious}><ChevronLeft size={18} /></button><span>Page {page + 1} of {pageCount}</span><button class="icon-button" aria-label="Next page" title="Next page" disabled={!nextCursor || busy} onclick={onNext}><ChevronRight size={18} /></button></div>
</section>
<style>
.mail-canvas { min-height: 0; display: grid; grid-template-columns: 205px minmax(310px, 400px) minmax(0, 1fr); overflow: hidden; background: var(--surface); }
.folder-column { min-width: 0; padding: 14px 12px; display: grid; align-content: start; gap: 14px; background: var(--surface-soft); border-right: 1px solid var(--line); }
.folder-summary,.column-bar,.pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.folder-group { display: grid; gap: 3px; }.folder-group button { min-height: 39px; display: flex; align-items: center; justify-content: space-between; gap: 8px; border: 0; border-radius: 9px; padding: 0 9px; background: transparent; color: var(--ink); text-align: left; }.folder-group button.active,.folder-group button:hover { background: var(--surface-selected); color: var(--accent); }.folder-name { display: inline-flex; align-items: center; gap: 10px; }.message-column { min-width: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; border-right: 1px solid var(--line); }.column-bar { min-height: 78px; padding: 12px 14px; border-bottom: 1px solid var(--line); }.column-bar div { display: grid; gap: 2px; }.message-list { min-height: 0; overflow: auto; }.message-list > button { width: 100%; min-height: 106px; display: flex; gap: 10px; border: 0; border-bottom: 1px solid var(--line-soft); padding: 13px 14px 12px 11px; background: var(--surface); color: var(--ink); text-align: left; }.message-list > button:hover,.message-list > button.selected { background: var(--surface-soft); }.message-main { min-width: 0; flex: 1; display: grid; gap: 4px; }.row-top,.subject-line,.message-tags { display: flex; align-items: center; gap: 7px; min-width: 0; }.row-top { justify-content: space-between; }.preview,.message-count,.pager,.folder-hint { color: var(--muted); font-size: 12px; }.preview { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }.pager { min-height: 50px; padding: 6px 12px; border-top: 1px solid var(--line); }.empty-state { min-height: 270px; display: grid; place-content: center; justify-items: center; gap: 8px; padding: 28px; color: var(--muted); text-align: center; }.empty-icon { inline-size: 48px; block-size: 48px; display: grid; place-items: center; border-radius: 14px; background: var(--accent-soft); color: var(--accent); }
@media (max-width: 640px) { .mail-canvas { grid-template-columns: minmax(0, 1fr); }.folder-column { display: block; overflow-x: auto; padding: 8px; border-right: 0; border-bottom: 1px solid var(--line); }.folder-summary,.folder-hint { display: none; }.folder-group { display: flex; gap: 5px; }.folder-group button { flex: 0 0 auto; min-width: max-content; min-height: 35px; }.message-column { border-right: 0; } }
</style>