import type { ChannelInfo, ChannelGroupInfo, ConnectionState, FolderInfo, Item, SenderInfo, Snapshot, ThreadInfo, WorkspaceEvent, } from '@/core-bridge/types'; export interface WorkspaceData { ready: boolean; connection: ConnectionState; channel: ChannelInfo | null; threads: ThreadInfo[]; groups: ChannelGroupInfo[]; folders: FolderInfo[]; locations: Record; byId: Record; authors: Record; /** 空间成员的资料(昵称、头像),按账号 ID 索引 */ people: Record; /** 新的在前 */ order: string[]; snapshot: Snapshot; error: string | null; } export const initialData: WorkspaceData = { ready: false, connection: 'offline', channel: null, threads: [], groups: [], folders: [], locations: {}, byId: {}, authors: {}, people: {}, order: [], snapshot: { text: '', updatedBy: '', updatedAt: 0 }, error: null, }; function insertByTime(order: string[], byId: Record, item: Item): string[] { const at = order.findIndex((id) => (byId[id]?.ts ?? 0) < item.ts); const next = order.slice(); next.splice(at === -1 ? next.length : at, 0, item.id); return next; } export function withoutItem(s: WorkspaceData, id: string): WorkspaceData { if (!(id in s.byId)) return s; const byId = { ...s.byId }; delete byId[id]; return { ...s, byId, order: s.order.filter((entry) => entry !== id) }; } export function reduce(s: WorkspaceData, e: WorkspaceEvent): WorkspaceData { switch (e.t) { case 'structure': { const people = new Map(e.people.map(person => [person.username, person])); const locations = Object.fromEntries(e.state.locations.map(location => [location.itemId, { threadId: location.threadId, folderId: location.folderId ?? undefined, authorAccount: location.authorAccount ?? undefined }])); const authors = { ...s.authors }; for (const location of e.state.locations) { const person = location.authorAccount ? people.get(location.authorAccount) : undefined; if (person) authors[location.itemId] = { username: person.username, displayName: person.displayName || person.username, avatar: person.avatar ?? undefined }; } const byId = Object.fromEntries(Object.entries(s.byId).map(([id, item]) => [id, { ...item, ...(locations[id] ?? {}), ...(authors[id] ? { author: authors[id] } : {}), }])); const profiles = Object.fromEntries(e.people.map(person => [person.username, { username: person.username, displayName: person.displayName || person.username, ...(person.avatar ? { avatar: person.avatar } : {}) }])); return { ...s, people: profiles, threads: e.state.channels.map(c => ({ ...c, groupId: c.groupId ?? undefined })), groups: e.state.groups, folders: e.state.folders.map(f => ({ ...f, parentId: f.parentId ?? undefined })), locations, authors, byId }; } case 'reset': return { ...initialData, connection: s.connection }; case 'error': return { ...s, error: e.message }; case 'ready': return { ...s, ready: true }; case 'connection': return { ...s, connection: e.state }; case 'channel': return { ...s, channel: e.info }; case 'snapshot': return { ...s, snapshot: e.snapshot }; case 'file-catalog': case 'item': { // 乐观渲染:真实条目到达时,同一次更新里替换本地占位项,列表不闪、不重复 if (e.item.localId && e.item.localId !== e.item.id) s = withoutItem(s, e.item.localId); const known = e.item.id in s.byId; let item = e.item; const current = s.byId[item.id]; const author = s.authors[item.id] ?? item.author ?? current?.author; if (author) item = { ...item, author }; if (e.t === 'item' && item.kind === 'file' && item.metadataHidden && current?.kind === 'file' && !current.metadataHidden) return s; const location = s.locations[item.id]; if (location && !(item.kind === 'file' && item.metadataHidden)) item = { ...item, threadId: location.threadId, folderId: location.folderId }; const byId = { ...s.byId, [item.id]: item }; return { ...s, byId, ...(author ? { authors: { ...s.authors, [item.id]: author } } : {}), order: known ? s.order : insertByTime(s.order, byId, item) }; } case 'removed': { if (!(e.id in s.byId)) return s; const byId = { ...s.byId }; delete byId[e.id]; const authors = { ...s.authors }; delete authors[e.id]; return { ...s, byId, authors, order: s.order.filter((id) => id !== e.id) }; } default: { const exhaustive: never = e; return exhaustive; } } }