import { computed, reactive, readonly, ref } from "vue"; export interface Person { id: string; name: string; handle: string; color: string; online: boolean; bio: string; } export interface Message { id: string; personId: string; body: string; sentAt: number; mine: boolean; status: "sent" | "delivered" | "failed"; } export interface Conversation { id: string; personId: string; unread: number; pinned?: boolean; messages: Message[]; } const people: Person[] = [ { id: "maya", name: "Maya Chen", handle: "@mayac", color: "#ff7a8a", online: true, bio: "Product designer · Sydney to everywhere.", }, { id: "noah", name: "Noah Williams", handle: "@noahw", color: "#4f9cff", online: true, bio: "Film, tiny cameras, and very long walks.", }, { id: "sofia", name: "Sofia Rossi", handle: "@sofiar", color: "#9d67ff", online: false, bio: "Making typefaces and better pasta.", }, { id: "liam", name: "Liam Park", handle: "@liamp", color: "#35c7a0", online: true, bio: "Engineer. Climber. Questionable DJ.", }, { id: "amara", name: "Amara Okafor", handle: "@amarao", color: "#f4ad42", online: false, bio: "Architecture and cities after dark.", }, ]; const timestamp = Date.now(); const seedConversations: Conversation[] = [ { id: "maya", personId: "maya", unread: 2, pinned: true, messages: [ { id: "m1", personId: "maya", body: "That transition feels ridiculously smooth ✨", sentAt: timestamp - 840_000, mine: false, status: "delivered", }, { id: "m2", personId: "maya", body: "Try holding it halfway, then let go slowly.", sentAt: timestamp - 780_000, mine: false, status: "delivered", }, ], }, { id: "noah", personId: "noah", unread: 0, messages: [ { id: "n1", personId: "noah", body: "Uploaded the photos from yesterday.", sentAt: timestamp - 4_200_000, mine: false, status: "delivered", }, { id: "n2", personId: "noah", body: "The grain is perfect.", sentAt: timestamp - 4_000_000, mine: true, status: "delivered", }, ], }, { id: "sofia", personId: "sofia", unread: 1, messages: [ { id: "s1", personId: "sofia", body: "Coffee at the new place tomorrow?", sentAt: timestamp - 18_000_000, mine: false, status: "delivered", }, ], }, { id: "liam", personId: "liam", unread: 0, messages: [ { id: "l1", personId: "liam", body: "The build is green. Ship it.", sentAt: timestamp - 86_000_000, mine: false, status: "delivered", }, ], }, { id: "amara", personId: "amara", unread: 0, messages: [ { id: "a1", personId: "amara", body: "This city never really goes quiet.", sentAt: timestamp - 172_000_000, mine: false, status: "delivered", }, ], }, ]; const conversations = ref(structuredClone(seedConversations)); const ready = ref(false); const offline = ref(!navigator.onLine); const settings = reactive({ simulatedLatency: 180, simulateFailures: false }); function openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open("native-vue-messenger", 1); request.onupgradeneeded = () => request.result.createObjectStore("state"); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function readStored() { const database = await openDatabase(); return await new Promise((resolve, reject) => { const transaction = database.transaction("state", "readonly"); const request = transaction.objectStore("state").get("conversations"); request.onsuccess = () => resolve(request.result as Conversation[] | undefined); request.onerror = () => reject(request.error); }).finally(() => database.close()); } async function persist() { try { const database = await openDatabase(); await new Promise((resolve, reject) => { const transaction = database.transaction("state", "readwrite"); transaction .objectStore("state") .put(structuredClone(conversations.value), "conversations"); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); database.close(); } catch { // Private browsing and locked-down webviews can reject IndexedDB. } } async function initialize() { try { const stored = await readStored(); if (stored?.length) conversations.value = stored; else await persist(); } finally { ready.value = true; } } window.addEventListener("online", () => { offline.value = false; }); window.addEventListener("offline", () => { offline.value = true; }); void initialize(); export function useDemoStore() { const conversationFor = (id: string) => computed(() => conversations.value.find((conversation) => conversation.id === id), ); const personFor = (id: string) => people.find((person) => person.id === id); const markRead = (id: string) => { const conversation = conversations.value.find((item) => item.id === id); if (conversation) conversation.unread = 0; void persist(); }; const sendMessage = async (id: string, body: string) => { const conversation = conversations.value.find((item) => item.id === id); if (!conversation || !body.trim()) return false; const message: Message = { id: crypto.randomUUID(), personId: id, body: body.trim(), sentAt: Date.now(), mine: true, status: "sent", }; conversation.messages.push(message); await persist(); await new Promise((resolve) => window.setTimeout(resolve, settings.simulatedLatency), ); message.status = settings.simulateFailures || offline.value ? "failed" : "delivered"; await persist(); return message.status === "delivered"; }; const reset = async () => { conversations.value = structuredClone(seedConversations); await persist(); }; return { people: readonly(ref(people)), conversations: readonly(conversations), ready: readonly(ready), offline: readonly(offline), settings, conversationFor, personFor, markRead, sendMessage, reset, }; } export function relativeTime(value: number) { const minutes = Math.floor((Date.now() - value) / 60_000); if (minutes < 1) return "now"; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; return `${Math.floor(hours / 24)}d`; }