feat(web): space roles, upload queue with pipelined frames, background resume and faster sending

This commit is contained in:
justme committed 2026-10-16 19:42:33 +08:00
1 parent 96c4335cb5
commit a574583943
39 files changed
+1515 -194

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ const text = (id: string, ts: number, body = id): TextItem => ({
describe('reduce', () => {
it('renders server structure and preserves server author and location across late decrypted content', () => {
const stateEvent = { t: 'structure' as const, state: { revision: 1,
const stateEvent = { t: 'structure' as const, state: { revision: 1, roles: [], rolesSeeded: true,
channels: [{ id: 'channel-a', name: '常规', groupId: 'group-a', createdAt: 1, createdBy: 'alice', chatEpoch: 1, filesEpoch: 1, chatHistory: true, filesHistory: true, fileBytes: 0 }],
groups: [{ id: 'group-a', name: '项目', createdAt: 1 }],
folders: [{ id: 'folder-a', name: '目录', threadId: 'channel-a' }],
+4 -1
View File
@@ -1,6 +1,7 @@
import type {
ChannelInfo,
ChannelGroupInfo,
RoleInfo,
ConnectionState,
FolderInfo,
Item,
@@ -16,6 +17,7 @@ export interface WorkspaceData {
channel: ChannelInfo | null;
threads: ThreadInfo[];
groups: ChannelGroupInfo[];
roles: RoleInfo[];
folders: FolderInfo[];
locations: Record<string, { threadId: string; folderId?: string; authorAccount?: string }>;
byId: Record<string, Item>;
@@ -34,6 +36,7 @@ export const initialData: WorkspaceData = {
channel: null,
threads: [],
groups: [],
roles: [],
folders: [],
locations: {},
byId: {},
@@ -71,7 +74,7 @@ export function reduce(s: WorkspaceData, e: WorkspaceEvent): WorkspaceData {
...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,
return { ...s, people: profiles, threads: e.state.channels.map(c => ({ ...c, groupId: c.groupId ?? undefined })), groups: e.state.groups, roles: e.state.roles ?? [],
folders: e.state.folders.map(f => ({ ...f, parentId: f.parentId ?? undefined })), locations, authors, byId };
}
case 'reset':
+20 -3
View File
@@ -44,7 +44,8 @@ function settleLocal(id: string): void {
async function deliver(id: string): Promise<void> {
const job = pendingJobs.get(id);
if (!job) return;
useWorkspace.setState((s) => s.byId[id] ? { byId: { ...s.byId, [id]: { ...s.byId[id], state: { kind: 'receiving', progress: 0 } } } } : s);
const phase = job.kind === 'file' ? 'queued' as const : undefined;
useWorkspace.setState((s) => s.byId[id] ? { byId: { ...s.byId, [id]: { ...s.byId[id], state: { kind: 'receiving', progress: 0, ...(phase ? { phase } : {}) } } } } : s);
try {
if (job.kind === 'text') await api.sendText(job.text, job.threadId, id);
else await api.sendFiles([job.file], job.options, job.threadId, job.folderId, [id]);
@@ -61,12 +62,18 @@ interface Actions {
sendText: (text: string, threadId?: string) => Promise<void>;
/** 重试发送失败的本地占位项 */
retrySend: (localId: string) => Promise<void>;
/** 取消排队中或上传中的文件 */
cancelUpload: (localId: string) => Promise<void>;
updateChannelSettings: (patch: ChannelSettingsPatch) => Promise<void>;
sendFiles: (files: File[], options?: FileUploadOptions, threadId?: string, folderId?: string) => Promise<void>;
createThread: (name: string, groupId?: string) => Promise<string>;
createGroup: typeof api.createGroup;
renameGroup: typeof api.renameGroup;
deleteGroup: typeof api.deleteGroup;
createRole: typeof api.createRole;
updateRole: typeof api.updateRole;
deleteRole: typeof api.deleteRole;
setRoleMembers: typeof api.setRoleMembers;
groupThread: typeof api.groupThread;
renameThread: (threadId: string, name: string) => Promise<void>;
setChannelHistory: typeof api.setChannelHistory;
@@ -98,22 +105,32 @@ export const useWorkspace = create<WorkspaceData & Actions>()(() => ({
return deliver(id);
},
retrySend: (id) => deliver(id),
cancelUpload: async (localId) => {
pendingJobs.delete(localId);
useWorkspace.setState((s) => withoutItem(s, localId));
await api.cancelUpload(localId).catch(() => false);
},
updateChannelSettings: (patch) => api.updateChannelSettings(patch),
sendFiles: async (files, options, threadId, folderId) => {
const ids = files.map((file) => {
const id = newLocalId();
pendingJobs.set(id, { kind: 'file', file, ...(options ? { options } : {}), ...(threadId ? { threadId } : {}), ...(folderId ? { folderId } : {}) });
const item: FileItem = { id, kind: 'file', localId: id, name: file.name, mime: file.type || 'application/octet-stream', size: file.size, remaining: null,
preview: previewKindOf(file.type, file.name), ...localBase(threadId, folderId), state: { kind: 'receiving', progress: 0, phase: 'encrypting' } };
preview: previewKindOf(file.type, file.name), ...localBase(threadId, folderId), state: { kind: 'receiving', progress: 0, phase: 'queued' } };
putLocal(item);
return id;
});
for (const id of ids) await deliver(id);
// 全部交给 worker 的上传队列:一次传一个,其余显示「排队中」
await Promise.all(ids.map(deliver));
},
createThread: (name, groupId) => api.createThread(name, groupId),
createGroup: (name) => api.createGroup(name),
renameGroup: (id, name) => api.renameGroup(id, name),
deleteGroup: (id) => api.deleteGroup(id),
createRole: (role) => api.createRole(role),
updateRole: (id, role) => api.updateRole(id, role),
deleteRole: (id) => api.deleteRole(id),
setRoleMembers: (id, members) => api.setRoleMembers(id, members),
groupThread: (id, groupId) => api.groupThread(id, groupId),
renameThread: (threadId, name) => api.renameThread(threadId, name),
setChannelHistory: (threadId, chatHistory, filesHistory) => api.setChannelHistory(threadId, chatHistory, filesHistory),