feat(web): add spaces, grouped channels and file chat

This commit is contained in:
justme committed 2026-10-13 01:39:23 +08:00
1 parent c7d8ef15bc
commit f6cfb12716
111 files changed
+4371 -506

No files matched your search

+70 -3
View File
@@ -1,9 +1,13 @@
import type {
ChannelInfo,
ChannelGroupInfo,
ConnectionState,
Device,
FolderInfo,
Item,
SenderInfo,
Snapshot,
ThreadInfo,
WorkspaceEvent,
} from '@/core-bridge/types';
@@ -12,7 +16,13 @@ export interface WorkspaceData {
connection: ConnectionState;
channel: ChannelInfo | null;
devices: Device[];
threads: ThreadInfo[];
groups: ChannelGroupInfo[];
threadGroups: Record<string, string | null>;
folders: FolderInfo[];
locations: Record<string, { threadId: string; folderId?: string }>;
byId: Record<string, Item>;
authors: Record<string, SenderInfo>;
/** 新的在前 */
order: string[];
snapshot: Snapshot;
@@ -24,7 +34,13 @@ export const initialData: WorkspaceData = {
connection: 'offline',
channel: null,
devices: [],
threads: [],
groups: [],
threadGroups: {},
folders: [],
locations: {},
byId: {},
authors: {},
order: [],
snapshot: { text: '', updatedBy: '', updatedAt: 0 },
error: null,
@@ -51,18 +67,69 @@ export function reduce(s: WorkspaceData, e: WorkspaceEvent): WorkspaceData {
return { ...s, channel: e.info };
case 'devices':
return { ...s, devices: e.devices };
case 'thread-created': {
const thread = { ...e.thread };
if (thread.id in s.threadGroups) {
const groupId = s.threadGroups[thread.id];
if (groupId) thread.groupId = groupId;
else delete thread.groupId;
}
return { ...s, threads: [thread, ...s.threads.filter((entry) => entry.id !== thread.id)] };
}
case 'thread-renamed':
return { ...s, threads: s.threads.map((thread) => thread.id === e.id ? { ...thread, name: e.name } : thread) };
case 'group-created':
return { ...s, groups: [...s.groups.filter((group) => group.id !== e.group.id), e.group] };
case 'group-renamed':
return { ...s, groups: s.groups.map((group) => group.id === e.id ? { ...group, name: e.name } : group) };
case 'thread-grouped':
return { ...s, threadGroups: { ...s.threadGroups, [e.id]: e.groupId ?? null },
threads: s.threads.map((thread) => thread.id === e.id ? { ...thread, groupId: e.groupId } : thread) };
case 'folder-created':
return { ...s, folders: [e.folder, ...s.folders.filter((folder) => folder.id !== e.folder.id)] };
case 'folder-renamed':
return { ...s, folders: s.folders.map((folder) => folder.id === e.id ? { ...folder, name: e.name } : folder) };
case 'items-moved': {
const byId = { ...s.byId };
const locations = { ...s.locations };
for (const id of e.itemIds) {
locations[id] = { threadId: e.threadId, ...(e.folderId ? { folderId: e.folderId } : {}) };
const item = byId[id];
if (!item) continue;
if (item.kind === 'file' && item.metadataHidden) continue;
const moved = { ...item, threadId: e.threadId };
if (e.folderId === undefined) delete moved.folderId;
else moved.folderId = e.folderId;
byId[id] = moved;
}
return { ...s, byId, locations };
}
case 'snapshot':
return { ...s, snapshot: e.snapshot };
case 'item-author': {
const item = s.byId[e.id];
return { ...s, authors: { ...s.authors, [e.id]: e.author },
byId: item ? { ...s.byId, [e.id]: { ...item, author: e.author } } : s.byId };
}
case 'file-catalog':
case 'item': {
const known = e.item.id in s.byId;
const byId = { ...s.byId, [e.item.id]: e.item };
return { ...s, byId, order: known ? s.order : insertByTime(s.order, byId, e.item) };
let item = e.item;
const current = s.byId[item.id];
const author = item.author ?? s.authors[item.id] ?? 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];
return { ...s, byId, order: s.order.filter((id) => id !== 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;