feat: integrate client and server sharing workflows
This commit is contained in:
1 parent
42b93ae2d8
commit
7406e2fe37
101 files changed
+3153
-253
No files matched your search
@@ -6,28 +6,32 @@ import type { ParsedChannelLink } from '@/lib/channelLink';
|
||||
interface ChannelsState {
|
||||
/** 我的频道(原型:只在内存里,刷新即清空;真实实现存 IndexedDB,architecture §6) */
|
||||
list: ChannelInfo[];
|
||||
hydrated: boolean;
|
||||
/** 从链接进来、还没确认加入的频道;含密钥,只放内存 */
|
||||
pendingLink: ParsedChannelLink | null;
|
||||
/** 刚创建的频道:进入时直接展示分享面板与二维码 */
|
||||
justCreatedId: string | null;
|
||||
|
||||
create: (opts: CreateOptions) => Promise<ChannelInfo>;
|
||||
create: (opts: CreateOptions, captchaToken?: string) => Promise<ChannelInfo>;
|
||||
join: (link: ParsedChannelLink) => Promise<ChannelInfo>;
|
||||
forget: (id: string) => void;
|
||||
hydrate: () => Promise<void>;
|
||||
forget: (id: string) => Promise<void>;
|
||||
loadDemo: () => void;
|
||||
setPendingLink: (link: ParsedChannelLink | null) => void;
|
||||
clearJustCreated: () => void;
|
||||
}
|
||||
|
||||
const upsert = (list: ChannelInfo[], c: ChannelInfo) => [c, ...list.filter((x) => x.id !== c.id)];
|
||||
let hydration: Promise<void> | undefined;
|
||||
|
||||
export const useChannels = create<ChannelsState>()((set) => ({
|
||||
list: [],
|
||||
hydrated: false,
|
||||
pendingLink: null,
|
||||
justCreatedId: null,
|
||||
|
||||
create: async (opts) => {
|
||||
const c = await api.createChannel(opts);
|
||||
create: async (opts, captchaToken) => {
|
||||
const c = await api.createChannel(opts, captchaToken);
|
||||
set((s) => ({ list: upsert(s.list, c), justCreatedId: c.id }));
|
||||
return c;
|
||||
},
|
||||
@@ -36,7 +40,14 @@ export const useChannels = create<ChannelsState>()((set) => ({
|
||||
set((s) => ({ list: upsert(s.list, c), pendingLink: null }));
|
||||
return c;
|
||||
},
|
||||
forget: (id) => set((s) => ({ list: s.list.filter((c) => c.id !== id) })),
|
||||
hydrate: () => {
|
||||
hydration ??= api.listSaved().then((list) => set({ list, hydrated: true }), () => set({ hydrated: true }));
|
||||
return hydration;
|
||||
},
|
||||
forget: async (id) => {
|
||||
await api.forget(id);
|
||||
set((s) => ({ list: s.list.filter((c) => c.id !== id) }));
|
||||
},
|
||||
loadDemo: () => {
|
||||
const demo = api.loadDemo?.() ?? [];
|
||||
set((s) => ({ list: demo.reduce(upsert, s.list) }));
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface WorkspaceData {
|
||||
/** 新的在前 */
|
||||
order: string[];
|
||||
snapshot: Snapshot;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const initialData: WorkspaceData = {
|
||||
@@ -26,6 +27,7 @@ export const initialData: WorkspaceData = {
|
||||
byId: {},
|
||||
order: [],
|
||||
snapshot: { text: '', updatedBy: '', updatedAt: 0 },
|
||||
error: null,
|
||||
};
|
||||
|
||||
function insertByTime(order: string[], byId: Record<string, Item>, item: Item): string[] {
|
||||
@@ -37,6 +39,10 @@ function insertByTime(order: string[], byId: Record<string, Item>, item: Item):
|
||||
|
||||
export function reduce(s: WorkspaceData, e: WorkspaceEvent): WorkspaceData {
|
||||
switch (e.t) {
|
||||
case 'reset':
|
||||
return { ...initialData, connection: s.connection };
|
||||
case 'error':
|
||||
return { ...s, error: e.message };
|
||||
case 'ready':
|
||||
return { ...s, ready: true };
|
||||
case 'connection':
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Share, TextItem } from '@/core-bridge/types';
|
||||
import { initialShareData, reduceShare } from './shareReduce';
|
||||
|
||||
const text = (id: string, ts: number): TextItem => ({ id, kind: 'text', ts, deviceId: 'x', state: { kind: 'complete' }, text: id });
|
||||
const share: Share = { id: 's', state: 'draft', mine: true, link: 'l', createdAt: 0, ttlSeconds: 1, seats: 2, maxDownloads: null };
|
||||
const share: Share = { id: 's', state: 'draft', mine: true, link: 'l', createdAt: 0, expiresAt: 86400000, ttlSeconds: 1, maxVisitors: 2, maxDownloads: null };
|
||||
|
||||
describe('reduceShare', () => {
|
||||
it('收到分享信息与就绪', () => {
|
||||
|
||||
@@ -6,9 +6,11 @@ export interface ShareData {
|
||||
byId: Record<string, Item>;
|
||||
/** 新的在前 */
|
||||
order: string[];
|
||||
integrityError: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const initialShareData: ShareData = { ready: false, share: null, byId: {}, order: [] };
|
||||
export const initialShareData: ShareData = { ready: false, share: null, byId: {}, order: [], integrityError: null, error: null };
|
||||
|
||||
function insertByTime(order: string[], byId: Record<string, Item>, item: Item): string[] {
|
||||
const at = order.findIndex((id) => (byId[id]?.ts ?? 0) < item.ts);
|
||||
@@ -34,6 +36,10 @@ export function reduceShare(s: ShareData, e: ShareEvent): ShareData {
|
||||
delete byId[e.id];
|
||||
return { ...s, byId, order: s.order.filter((id) => id !== e.id) };
|
||||
}
|
||||
case 'integrity':
|
||||
return { ...s, byId: {}, order: [], integrityError: e.message };
|
||||
case 'error':
|
||||
return { ...s, error: e.message };
|
||||
default: {
|
||||
const exhaustive: never = e;
|
||||
return exhaustive;
|
||||
|
||||
@@ -6,6 +6,7 @@ interface Actions {
|
||||
/** 打开(订阅)一个分享:先清空上一个的数据,返回取消订阅函数 */
|
||||
open: (shareId: string) => () => void;
|
||||
addText: (text: string) => Promise<void>;
|
||||
editText: (id: string, text: string) => Promise<void>;
|
||||
addFiles: (files: File[]) => Promise<void>;
|
||||
removeItem: (id: string) => Promise<void>;
|
||||
getBlob: (id: string) => Promise<Blob>;
|
||||
@@ -20,6 +21,7 @@ export const useShareSession = create<ShareData & Actions>()(() => ({
|
||||
return shareApi.connect(shareId, (e) => useShareSession.setState((s) => reduceShare(s, e)));
|
||||
},
|
||||
addText: (t) => shareApi.addText(t),
|
||||
editText: (id, text) => shareApi.editText(id, text),
|
||||
addFiles: (f) => shareApi.addFiles(f),
|
||||
removeItem: (id) => shareApi.removeItem(id),
|
||||
getBlob: (id) => shareApi.getBlob(id),
|
||||
|
||||
+17
-6
@@ -5,41 +5,49 @@ import type { ClipboardPayload } from '@/lib/clipboard';
|
||||
import type { ParsedShareLink } from '@/lib/shareLink';
|
||||
|
||||
/** 便捷分享的默认设置:最多 10 位访客、24 小时、每个文件最多下载 10 次 */
|
||||
export const SHARE_DEFAULTS: ShareSettings = { ttlSeconds: 86400, seats: 10, maxDownloads: 10 };
|
||||
export const SHARE_DEFAULTS: ShareSettings = { ttlSeconds: 86400, maxVisitors: 10, maxDownloads: 10 };
|
||||
|
||||
interface SharesState {
|
||||
/** 我的分享:自己创建的(草稿、已封存)和收到的。原型只在内存里 */
|
||||
list: Share[];
|
||||
hydrated: boolean;
|
||||
/** 从链接进来、还没加入的分享;含密钥,只放内存 */
|
||||
pendingLink: ParsedShareLink | null;
|
||||
/** 便捷分享:新建草稿时带来的内容,进入草稿页后放进去 */
|
||||
pendingPayload: ClipboardPayload | null;
|
||||
|
||||
/** 新建草稿(可带上要放进去的内容) */
|
||||
createDraft: (payload?: ClipboardPayload) => Promise<Share>;
|
||||
createDraft: (payload?: ClipboardPayload, captchaToken?: string) => Promise<Share>;
|
||||
hydrate: () => Promise<void>;
|
||||
updateSettings: (id: string, patch: Partial<ShareSettings>) => Promise<Share>;
|
||||
/** 完成分享:此后无人可改 */
|
||||
seal: (id: string) => Promise<Share>;
|
||||
/** 访客加入:直接进入,不预览 */
|
||||
join: (link: ParsedShareLink) => Promise<Share>;
|
||||
forget: (id: string) => void;
|
||||
forget: (id: string) => Promise<void>;
|
||||
loadDemo: () => void;
|
||||
setPendingLink: (link: ParsedShareLink | null) => void;
|
||||
consumePayload: () => ClipboardPayload | null;
|
||||
}
|
||||
|
||||
const upsert = (list: Share[], s: Share) => [s, ...list.filter((x) => x.id !== s.id)];
|
||||
let hydration: Promise<void> | undefined;
|
||||
|
||||
export const useShares = create<SharesState>()((set, get) => ({
|
||||
list: [],
|
||||
hydrated: false,
|
||||
pendingLink: null,
|
||||
pendingPayload: null,
|
||||
|
||||
createDraft: async (payload) => {
|
||||
const s = await shareApi.createDraft(SHARE_DEFAULTS);
|
||||
createDraft: async (payload, captchaToken) => {
|
||||
const s = await shareApi.createDraft(SHARE_DEFAULTS, captchaToken);
|
||||
set((st) => ({ list: upsert(st.list, s), pendingPayload: payload ?? null }));
|
||||
return s;
|
||||
},
|
||||
hydrate: () => {
|
||||
hydration ??= shareApi.listSaved().then((list) => set({ list, hydrated: true }), () => set({ hydrated: true }));
|
||||
return hydration;
|
||||
},
|
||||
updateSettings: async (id, patch) => {
|
||||
const s = await shareApi.updateSettings(id, patch);
|
||||
set((st) => ({ list: upsert(st.list, s) }));
|
||||
@@ -55,7 +63,10 @@ export const useShares = create<SharesState>()((set, get) => ({
|
||||
set((st) => ({ list: upsert(st.list, s), pendingLink: null }));
|
||||
return s;
|
||||
},
|
||||
forget: (id) => set((st) => ({ list: st.list.filter((x) => x.id !== id) })),
|
||||
forget: async (id) => {
|
||||
await shareApi.forget(id);
|
||||
set((st) => ({ list: st.list.filter((x) => x.id !== id) }));
|
||||
},
|
||||
loadDemo: () => {
|
||||
const demo = shareApi.loadDemo?.() ?? [];
|
||||
set((st) => ({ list: demo.reduce(upsert, st.list) }));
|
||||
|
||||
Reference in new issue
Block a user