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
@@ -1,10 +1,12 @@
|
||||
import { removeChannelItem } from './channelItems';
|
||||
import { queueFileCatalog, queueItemAuthor } from './channelThreads';
|
||||
import { senderSnapshot } from './senderSnapshot';
|
||||
import { errorMessage } from '../errors';
|
||||
|
||||
import { saveResource } from '../database';
|
||||
|
||||
import { previewKindOf } from '@/lib/fileKind';
|
||||
import type { FileItem, Item } from '../types';
|
||||
import type { FileItem, FileUploadOptions, Item } from '../types';
|
||||
|
||||
import { blobs, getSession } from './sessions';
|
||||
import { deviceIdentity } from './presentation';
|
||||
@@ -14,38 +16,53 @@ import { deviceIdentity } from './presentation';
|
||||
import { flushOutbox, MAX_FRAME_DATA, MOBILE_UPLOAD_FRAME_DATA_BYTES, persistPlan } from './transfer';
|
||||
import { publishWorkspace } from './presentation';
|
||||
|
||||
export async function sendText(resourceId: string, text: string): Promise<{ itemId: string; uploaded: boolean }> {
|
||||
export async function sendText(resourceId: string, text: string, threadId?: string): Promise<{ itemId: string; uploaded: boolean }> {
|
||||
if (!threadId) throw new Error('请先选择频道');
|
||||
const { session, resource } = await getSession(resourceId);
|
||||
if (resource.kind !== 'channel' || (!session.can('owner') && (!session.can('write') || !resource.membersCanUpload))) throw new Error('此链接没有上传权限');
|
||||
const device = deviceIdentity();
|
||||
const identity = await deviceIdentity(session.endpointSecret);
|
||||
const device = { ...identity, name: resource.deviceLabel ?? identity.name };
|
||||
session.deviceName = device.name;
|
||||
const ts = Date.now();
|
||||
const plan = session.encryptText(text, { dev: device.name, ts });
|
||||
const item: Item = { id: plan.itemId, kind: 'text', ts, deviceId: device.name, state: { kind: 'receiving', progress: 0 }, text };
|
||||
const author = await senderSnapshot();
|
||||
const plan = session.encryptText(text, { dev: device.name, ts, threadId });
|
||||
const item: Item = { id: plan.itemId, kind: 'text', ts, deviceId: device.name, ...(author ? { author } : {}), state: { kind: 'receiving', progress: 0 }, text, ...(threadId ? { threadId } : {}) };
|
||||
blobs.set(`${resourceId}:${item.id}`, new Blob([text], { type: 'text/plain' }));
|
||||
publishWorkspace(resourceId, { t: 'item', item });
|
||||
await persistPlan(resource, plan);
|
||||
if (author) await queueItemAuthor(resource, session, plan.itemId, author, threadId, ts);
|
||||
const uploaded = await flushOutbox(resource, session);
|
||||
publishWorkspace(resourceId, { t: 'item', item: { ...item, state: uploaded ? { kind: 'complete' } : { kind: 'incomplete' } } });
|
||||
if (!uploaded) publishWorkspace(resourceId, { t: 'connection', state: 'offline' });
|
||||
return { itemId: plan.itemId, uploaded };
|
||||
}
|
||||
|
||||
export async function sendFiles(resourceId: string, files: File[]): Promise<void> {
|
||||
export async function sendFiles(resourceId: string, files: File[], options: FileUploadOptions = { directPreview: true, maxDownloads: 10 }, threadId?: string, folderId?: string): Promise<void> {
|
||||
if (!threadId) throw new Error('请先选择频道');
|
||||
const { session, resource } = await getSession(resourceId);
|
||||
if (resource.kind !== 'channel' || (!session.can('owner') && (!session.can('write') || !resource.membersCanUpload))) throw new Error('此链接没有上传权限');
|
||||
const device = deviceIdentity();
|
||||
const identity = await deviceIdentity(session.endpointSecret);
|
||||
const device = { ...identity, name: resource.deviceLabel ?? identity.name };
|
||||
session.deviceName = device.name;
|
||||
const maxFrameData = device.kind === 'phone' ? MOBILE_UPLOAD_FRAME_DATA_BYTES : MAX_FRAME_DATA;
|
||||
const author = await senderSnapshot();
|
||||
for (const file of files) {
|
||||
const ts = Date.now();
|
||||
let item: FileItem | undefined;
|
||||
try {
|
||||
const plan = session.encryptFile(file, {}, { maxDownloads: resource.maxDownloads ?? 10, dev: device.name, ts, maxFrameData });
|
||||
const plan = session.encryptFile(file, {}, {
|
||||
...(options.directPreview ? { gated: 'auto' as const } : { gated: true, maxDownloads: options.maxDownloads }),
|
||||
dev: device.name, ts, maxFrameData, threadId, folderId,
|
||||
});
|
||||
item = {
|
||||
id: plan.itemId, kind: 'file', ts, deviceId: device.name, state: { kind: 'receiving', progress: 0, phase: 'encrypting' },
|
||||
...(author ? { author } : {}),
|
||||
...(threadId ? { threadId } : {}), ...(folderId ? { folderId } : {}),
|
||||
name: file.name, mime: file.type || 'application/octet-stream', size: file.size,
|
||||
remaining: resource.maxDownloads || null, preview: previewKindOf(file.type, file.name), gated: true,
|
||||
remaining: plan.gated
|
||||
? (options.directPreview ? resource.maxDownloads ?? 10 : options.maxDownloads) || null
|
||||
: null,
|
||||
preview: previewKindOf(file.type, file.name), gated: plan.gated,
|
||||
};
|
||||
resource.localItems = [...(resource.localItems ?? []).filter((entry) => entry.id !== item?.id), item];
|
||||
await saveResource(resource);
|
||||
@@ -69,6 +86,7 @@ export async function sendFiles(resourceId: string, files: File[]): Promise<void
|
||||
updateProgress(file.size === 0 ? 0.5 : Math.min(0.5, processed / file.size * 0.5), 'encrypting');
|
||||
});
|
||||
const uploadedFrames = new Set<number>();
|
||||
await queueFileCatalog(resource, session, item);
|
||||
let uploadedBytes = 0;
|
||||
const uploadStartedAt = performance.now();
|
||||
const uploaded = await flushOutbox(resource, session, (frame, loadedBytes, totalBytes, complete) => {
|
||||
@@ -85,7 +103,7 @@ export async function sendFiles(resourceId: string, files: File[]): Promise<void
|
||||
resource.localItems = resource.localItems.map((entry) => entry.id === item?.id ? item! : entry);
|
||||
await saveResource(resource);
|
||||
publishWorkspace(resourceId, { t: 'item', item });
|
||||
if (!uploaded) publishWorkspace(resourceId, { t: 'error', message: '上传暂停了。重新打开频道并连接网络后,可以继续发送已加密内容。' });
|
||||
if (!uploaded) publishWorkspace(resourceId, { t: 'error', message: '上传暂停了。重新打开空间并连接网络后,可以继续发送已加密内容。' });
|
||||
} catch (error) {
|
||||
if (item) {
|
||||
item = { ...item, state: { kind: 'incomplete' } };
|
||||
@@ -101,13 +119,14 @@ export async function sendFiles(resourceId: string, files: File[]): Promise<void
|
||||
|
||||
export async function updateSnapshot(resourceId: string, text: string): Promise<void> {
|
||||
const { session, resource } = await getSession(resourceId);
|
||||
const plan = session.encryptJson({ text, updatedBy: deviceIdentity().name, updatedAt: Date.now() }, { snapshot: true, dev: deviceIdentity().name });
|
||||
const name = resource.deviceLabel ?? (await deviceIdentity(session.endpointSecret)).name;
|
||||
const plan = session.encryptJson({ text, updatedBy: name, updatedAt: Date.now() }, { snapshot: true, dev: name });
|
||||
await persistPlan(resource, plan);
|
||||
await flushOutbox(resource, session);
|
||||
}
|
||||
|
||||
export async function editChannelText(resourceId: string, itemId: string, text: string): Promise<void> {
|
||||
const created = await sendText(resourceId, text);
|
||||
export async function editChannelText(resourceId: string, itemId: string, text: string, threadId?: string): Promise<void> {
|
||||
const created = await sendText(resourceId, text, threadId);
|
||||
if (!created.uploaded) throw new Error('新版本已排队,网络恢复前保留旧内容。');
|
||||
await removeChannelItem(resourceId, itemId);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user