feat(web): add formatDmPreview utility with attachment-aware previews

Supports text-only, attachment-only (with type-specific icons), and
mixed messages. PreviewAttachment accepts both type/filename (ready
payload) and mimetype/originalName (Attachment type) field shapes.
This commit is contained in:
Jannis Braun
2026-04-02 17:48:28 +02:00
parent 68fe04ef58
commit b82733868b
2 changed files with 211 additions and 0 deletions
+89
View File
@@ -1,3 +1,92 @@
// ─── DM Preview Formatting ────────────────────────────────────────────────────
/**
* A loose attachment shape that covers both the ready-payload preview
* (uses `type` + `filename`) and the full Attachment type from shared types
* (uses `mimetype` + `originalName`). At least one of each pair must be present.
*/
interface PreviewAttachment {
type?: string;
mimetype?: string;
filename?: string;
originalName?: string;
}
interface PreviewMessage {
content: string | null | undefined;
attachments?: PreviewAttachment[];
}
function resolveAttachmentType(a: PreviewAttachment): string {
return a.type ?? a.mimetype ?? '';
}
function resolveAttachmentName(a: PreviewAttachment): string {
return a.filename ?? a.originalName ?? '';
}
function getAttachmentIcon(mimeType: string): string {
if (mimeType.startsWith('image/')) return '📷';
if (mimeType.startsWith('video/')) return '🎬';
if (mimeType.startsWith('audio/')) return '🎵';
return '📎';
}
function getAttachmentLabel(mimeType: string, name: string): string {
if (mimeType.startsWith('image/')) return '📷 Image';
if (mimeType.startsWith('video/')) return '🎬 Video';
if (mimeType.startsWith('audio/')) return '🎵 Audio';
return `📎 ${name}`;
}
/**
* Determines a single icon representing a set of attachments.
* If all attachments share the same category icon, returns that icon.
* If mixed, falls back to 📎.
*/
function getUnifiedAttachmentIcon(attachments: PreviewAttachment[]): string {
const icons = new Set(attachments.map(a => getAttachmentIcon(resolveAttachmentType(a))));
return icons.size === 1 ? [...icons][0] : '📎';
}
/**
* Format a DM lastMessage into a sidebar preview string.
* Returns null if there is nothing displayable.
*
* Handles:
* - Text only → returns text content
* - Attachment only (single) → "📷 Image", "🎬 Video", "🎵 Audio", "📎 filename.ext"
* - Attachment only (multiple) → "📎 N files"
* - Text + attachments → "text 📷" (appends unified icon)
*/
export function formatDmPreview(lastMessage: PreviewMessage | null | undefined): string | null {
if (!lastMessage) return null;
const { content, attachments } = lastMessage;
const hasText = content != null && content.length > 0;
const hasAttachments = attachments != null && attachments.length > 0;
if (!hasText && !hasAttachments) return null;
if (hasText && !hasAttachments) {
return content!;
}
if (!hasText && hasAttachments) {
if (attachments!.length === 1) {
const a = attachments![0];
return getAttachmentLabel(resolveAttachmentType(a), resolveAttachmentName(a));
}
return `📎 ${attachments!.length} files`;
}
// Both text and attachments present
const icon = getUnifiedAttachmentIcon(attachments!);
return `${content} ${icon}`;
}
// ─── DM Timestamp Formatting ─────────────────────────────────────────────────
/**
* Smart timestamp for DM sidebar items.
* Today → time ("4:32 PM"), Yesterday → "Yesterday",