Initial commit: thbd-portal, duplicated from elmi-portal
Rebranded for Thunderbird Portal (THBD): numbering prefix EL->TH, all "ELMI"/"ЭЛМИ" UI strings genericized, ELMI's real contract templates and company legal/bank details removed (not applicable to this company), fresh VAPID keypair and encryption key, fresh single-migration schema history. Same architecture as elmi-portal: Web Push notifications, unread-chat app badge, cookie-session auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
commit
579528edf7
197 files changed
+25022
No files matched your search
@@ -0,0 +1,97 @@
|
||||
import type { ChatMessage, ConversationSummary } from "./types";
|
||||
|
||||
// The chat API returns Drizzle relational shapes (nested `user`/`author`
|
||||
// objects, ISO date strings after JSON serialization) — these adapt that
|
||||
// wire shape into the flat, timestamp-as-number types the UI works with.
|
||||
|
||||
interface RawUser {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarPath: string | null;
|
||||
lastSeenAt: string | null;
|
||||
}
|
||||
|
||||
interface RawParticipant {
|
||||
userId: string;
|
||||
user: RawUser;
|
||||
}
|
||||
|
||||
interface RawAttachment {
|
||||
id: string;
|
||||
kind: "file" | "image" | "voice";
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
durationSeconds: number | null;
|
||||
}
|
||||
|
||||
interface RawReaction {
|
||||
userId: string;
|
||||
emoji: string;
|
||||
user: RawUser;
|
||||
}
|
||||
|
||||
interface RawMessage {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
authorId: string;
|
||||
author: RawUser;
|
||||
body: string | null;
|
||||
createdAt: string;
|
||||
replyTo: { id: string; body: string | null; author: RawUser } | null;
|
||||
attachments: RawAttachment[];
|
||||
reactions: RawReaction[];
|
||||
}
|
||||
|
||||
interface RawConversation {
|
||||
id: string;
|
||||
isGroup: boolean;
|
||||
name: string | null;
|
||||
unreadCount: number;
|
||||
participants: RawParticipant[];
|
||||
lastMessage: { id: string; body: string | null; authorId: string; createdAt: string; attachments: unknown[] } | null;
|
||||
}
|
||||
|
||||
export function mapMessage(raw: RawMessage): ChatMessage {
|
||||
return {
|
||||
id: raw.id,
|
||||
conversationId: raw.conversationId,
|
||||
authorId: raw.authorId,
|
||||
authorName: raw.author?.name ?? "?",
|
||||
authorHasAvatar: Boolean(raw.author?.avatarPath),
|
||||
body: raw.body,
|
||||
createdAt: new Date(raw.createdAt).getTime(),
|
||||
replyTo: raw.replyTo ? { id: raw.replyTo.id, body: raw.replyTo.body, authorName: raw.replyTo.author?.name ?? "?" } : null,
|
||||
attachments: (raw.attachments ?? []).map((a) => ({
|
||||
id: a.id,
|
||||
kind: a.kind,
|
||||
fileName: a.fileName,
|
||||
mimeType: a.mimeType,
|
||||
durationSeconds: a.durationSeconds,
|
||||
})),
|
||||
reactions: (raw.reactions ?? []).map((r) => ({ emoji: r.emoji, userId: r.userId, userName: r.user?.name ?? "?" })),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapConversation(raw: RawConversation): ConversationSummary {
|
||||
return {
|
||||
id: raw.id,
|
||||
isGroup: raw.isGroup,
|
||||
name: raw.name,
|
||||
unreadCount: raw.unreadCount,
|
||||
participants: raw.participants.map((p) => ({
|
||||
userId: p.userId,
|
||||
name: p.user?.name ?? "?",
|
||||
hasAvatar: Boolean(p.user?.avatarPath),
|
||||
lastSeenAt: p.user?.lastSeenAt ? new Date(p.user.lastSeenAt).getTime() : null,
|
||||
})),
|
||||
lastMessage: raw.lastMessage
|
||||
? {
|
||||
id: raw.lastMessage.id,
|
||||
body: raw.lastMessage.body,
|
||||
authorId: raw.lastMessage.authorId,
|
||||
createdAt: new Date(raw.lastMessage.createdAt).getTime(),
|
||||
hasAttachment: (raw.lastMessage.attachments ?? []).length > 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user