feat(web): add spaces, grouped channels and file chat
This commit is contained in:
1 parent
c7d8ef15bc
commit
f6cfb12716
111 files changed
+4371
-506
No files matched your search
@@ -6,21 +6,27 @@ import { blobs, readers } from './sessions';
|
||||
import { showRejected, deviceIdentity, deviceInfo, publishWorkspace } from './presentation';
|
||||
import { type ChannelWire, toChannelInfo } from './wire';
|
||||
import { apiFetch, readJson } from './http';
|
||||
import { applyControl } from './channelThreads';
|
||||
import { clearFileCatalog, fileCatalogs, visibleFiles } from './fileCatalog';
|
||||
|
||||
export function mapReceived(item: ReceivedItem): Item {
|
||||
const timestamp = item.meta?.ts ?? Date.now();
|
||||
const deviceId = item.meta?.dev ?? '未知设备';
|
||||
const state = item.complete ? { kind: 'complete' as const } : { kind: 'incomplete' as const };
|
||||
if (item.kind === 'file' || item.kind === 'raw') {
|
||||
if (item.kind === 'file') {
|
||||
const name = item.meta?.name ?? '加密文件';
|
||||
return {
|
||||
id: item.itemId, kind: 'file', ts: timestamp, deviceId, state, name,
|
||||
...(item.meta?.threadId ? { threadId: item.meta.threadId } : {}),
|
||||
...(item.meta?.folderId ? { folderId: item.meta.folderId } : {}),
|
||||
mime: item.meta?.mime ?? 'application/octet-stream', size: item.meta?.size ?? item.content.byteLength,
|
||||
remaining: null, preview: previewKindOf(item.meta?.mime ?? '', name),
|
||||
gated: item.gated,
|
||||
};
|
||||
}
|
||||
return { id: item.itemId, kind: 'text', ts: timestamp, deviceId, state, text: new TextDecoder().decode(item.content) };
|
||||
return { id: item.itemId, kind: 'text', ts: timestamp, deviceId, state,
|
||||
...(item.meta?.threadId ? { threadId: item.meta.threadId } : {}),
|
||||
text: new TextDecoder().decode(item.content) };
|
||||
}
|
||||
|
||||
export async function applyChannelRecord(resource: SavedResource, session: ChannelSession, input: Uint8Array | LogRecord): Promise<void> {
|
||||
@@ -33,7 +39,19 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann
|
||||
for (const event of events) {
|
||||
switch (event.type) {
|
||||
case 'item': {
|
||||
const control = applyControl(event.item);
|
||||
if (control !== false) {
|
||||
if (control && control.t === 'file-catalog') {
|
||||
const key = `${resource.id}:${control.item.id}`;
|
||||
fileCatalogs.set(key, control.item);
|
||||
if (visibleFiles.has(key)) publishWorkspace(resource.id, control);
|
||||
break;
|
||||
}
|
||||
if (control) publishWorkspace(resource.id, control);
|
||||
break;
|
||||
}
|
||||
const item = mapReceived(event.item);
|
||||
if (item.kind === 'file') visibleFiles.add(`${resource.id}:${item.id}`);
|
||||
if (event.item.snapshot) {
|
||||
let snapshot: Snapshot = { text: '', updatedBy: item.deviceId, updatedAt: item.ts };
|
||||
if (item.kind === 'text') {
|
||||
@@ -52,16 +70,22 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann
|
||||
break;
|
||||
}
|
||||
case 'announce': {
|
||||
const key = `${resource.id}:${event.itemId}`;
|
||||
visibleFiles.add(key);
|
||||
const catalog = fileCatalogs.get(key);
|
||||
if (catalog) { publishWorkspace(resource.id, { t: 'file-catalog', item: catalog }); break; }
|
||||
const local = (await getResource(resource.id))?.localItems?.find((item) => item.id === event.itemId);
|
||||
const placeholder: FileItem = local?.kind === 'file' ? local : {
|
||||
id: event.itemId, kind: 'file', ts: event.createdAt, deviceId: '未知设备', state: { kind: 'incomplete' },
|
||||
name: '受限下载内容', mime: 'application/octet-stream', size: 0, remaining: null, preview: 'none',
|
||||
gated: true,
|
||||
gated: true, metadataHidden: true,
|
||||
};
|
||||
publishWorkspace(resource.id, { t: 'item', item: placeholder });
|
||||
break;
|
||||
}
|
||||
case 'removed':
|
||||
visibleFiles.delete(`${resource.id}:${event.itemId}`);
|
||||
fileCatalogs.delete(`${resource.id}:${event.itemId}`);
|
||||
blobs.delete(`${resource.id}:${event.itemId}`);
|
||||
publishWorkspace(resource.id, { t: 'removed', id: event.itemId });
|
||||
break;
|
||||
@@ -79,13 +103,15 @@ export async function applyChannelRecord(resource: SavedResource, session: Chann
|
||||
export async function channelMetadata(resource: SavedResource, session: ChannelSession): Promise<void> {
|
||||
const info = await readJson<ChannelWire>(`/channels/${resource.id}`, session.authHeaders('read'));
|
||||
session.verifyChannelInfo(info);
|
||||
resource.name = info.settings.name;
|
||||
resource.membersCanUpload = info.settings.members_can_upload;
|
||||
resource.expiresAt = info.expires_at;
|
||||
const latest = await getResource(resource.id);
|
||||
await saveResource({ ...latest, ...resource, localItems: latest?.localItems });
|
||||
publishWorkspace(resource.id, { t: 'channel', info: toChannelInfo(info, resource) });
|
||||
const ownId = toBase64Url(session.endpointHash);
|
||||
let devices = [deviceInfo(ownId, Date.now(), deviceIdentity().name, ownId)];
|
||||
const ownName = resource.deviceLabel ?? (await deviceIdentity(session.endpointSecret)).name;
|
||||
let devices = [deviceInfo(ownId, Date.now(), ownName, ownId)];
|
||||
if (session.can('owner')) {
|
||||
const seats = await readJson<Array<{ endpoint_hash: string; joined_at: number; label?: string }>>(
|
||||
`/channels/${resource.id}/seats`, session.authHeaders('owner'));
|
||||
@@ -95,16 +121,15 @@ export async function channelMetadata(resource: SavedResource, session: ChannelS
|
||||
try { name = await session.openLabel(seat.label, seat.endpoint_hash); }
|
||||
catch { name = '设备名校验失败'; }
|
||||
}
|
||||
return deviceInfo(seat.endpoint_hash, seat.joined_at, name, ownId);
|
||||
return deviceInfo(seat.endpoint_hash, seat.joined_at, name ?? (seat.endpoint_hash === ownId ? ownName : undefined), ownId);
|
||||
}));
|
||||
const own = devices.find((device) => device.self);
|
||||
if (own) own.name = deviceIdentity().name;
|
||||
}
|
||||
publishWorkspace(resource.id, { t: 'devices', devices });
|
||||
}
|
||||
|
||||
export async function syncChannel(resource: SavedResource, session: ChannelSession, signal: AbortSignal): Promise<number> {
|
||||
publishWorkspace(resource.id, { t: 'reset' });
|
||||
clearFileCatalog(resource.id);
|
||||
readers.set(resource.id, session.createItemReader());
|
||||
let from = 0;
|
||||
for (;;) {
|
||||
|
||||
Reference in new issue
Block a user