117 lines
4.6 KiB
TypeScript
117 lines
4.6 KiB
TypeScript
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<string, { threadId: string; folderId?: string; authorAccount?: string }>;
|
|
byId: Record<string, Item>;
|
|
authors: Record<string, SenderInfo>;
|
|
/** 空间成员的资料(昵称、头像),按账号 ID 索引 */
|
|
people: Record<string, SenderInfo>;
|
|
/** 新的在前 */
|
|
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<string, Item>, 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;
|
|
}
|
|
}
|
|
}
|