first commit
This commit is contained in:
171
apps/demo/src/data.ts
Normal file
171
apps/demo/src/data.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
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<Conversation[]>(structuredClone(seedConversations))
|
||||
const ready = ref(false)
|
||||
const offline = ref(!navigator.onLine)
|
||||
const settings = reactive({ simulatedLatency: 180, simulateFailures: false })
|
||||
|
||||
function openDatabase() {
|
||||
return new Promise<IDBDatabase>((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<Conversation[] | undefined>((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<void>((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`
|
||||
}
|
||||
Reference in New Issue
Block a user