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
+11
-1
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { Home } from '@/features/home/Home';
|
||||
import { JoinScreen, MissingAccess } from '@/features/scan/JoinScreen';
|
||||
import { useChannels } from '@/store/channels';
|
||||
import { useShares } from '@/store/shares';
|
||||
import { useWorkspace } from '@/store/workspace';
|
||||
import { ChannelScreen } from './ChannelScreen';
|
||||
import { ShareScreen } from './ShareScreen';
|
||||
@@ -21,14 +22,21 @@ function splashSeen(): boolean {
|
||||
export function App() {
|
||||
const route = useRoute();
|
||||
const list = useChannels((s) => s.list);
|
||||
const channelsHydrated = useChannels((s) => s.hydrated);
|
||||
const pendingLink = useChannels((s) => s.pendingLink);
|
||||
const setPendingLink = useChannels((s) => s.setPendingLink);
|
||||
const sharesHydrated = useShares((s) => s.hydrated);
|
||||
const pendingShareLink = useShares((s) => s.pendingLink);
|
||||
const wsReady = useWorkspace((s) => s.ready);
|
||||
const [splash, setSplash] = useState(() => !splashSeen());
|
||||
|
||||
const saved = route.name === 'channel' && list.some((c) => c.id === route.id);
|
||||
// 只有打开已保存的频道才需要等数据;首页与加入页没有加载过程
|
||||
const ready = !saved || wsReady;
|
||||
const waitingOnSavedChannel = route.name === 'channel' && !pendingLink && !channelsHydrated;
|
||||
const waitingOnSavedShare = route.name === 'share' && pendingShareLink?.id !== route.id && !sharesHydrated;
|
||||
const ready = route.name === 'home' || (route.name === 'channel'
|
||||
? pendingLink?.id === route.id || (channelsHydrated && (!saved || wsReady))
|
||||
: pendingShareLink?.id === route.id || sharesHydrated);
|
||||
|
||||
function onSplashDone() {
|
||||
try {
|
||||
@@ -41,7 +49,9 @@ export function App() {
|
||||
|
||||
let screen;
|
||||
if (route.name === 'home') screen = <Home />;
|
||||
else if (route.name === 'share' && waitingOnSavedShare) screen = null;
|
||||
else if (route.name === 'share') screen = <ShareScreen id={route.id} />;
|
||||
else if (route.name === 'channel' && waitingOnSavedChannel) screen = null;
|
||||
else if (saved) screen = <ChannelScreen id={route.id} />;
|
||||
else if (pendingLink?.id === route.id) {
|
||||
screen = (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { takeLinkFromLocation } from './entry';
|
||||
|
||||
const ID = 'k3Fq9xWm2LpA7sTdQ8vZ1n';
|
||||
const KEY = 'Zm9vYmFyYmF6cXV4Zm9vYmFyYmF6cXV4Zm9vYmFyYmF';
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe('takeLinkFromLocation', () => {
|
||||
it('在解析 fragment 密钥前同步清除地址栏 fragment', () => {
|
||||
const location = new URL(`https://webdrop.example/c/${ID}#k=${KEY}`);
|
||||
const replaceState = vi.fn();
|
||||
vi.stubGlobal('location', location);
|
||||
vi.stubGlobal('history', { replaceState });
|
||||
|
||||
expect(takeLinkFromLocation()).toMatchObject({ kind: 'channel', link: { id: ID, keys: { k: KEY } } });
|
||||
expect(replaceState).toHaveBeenCalledWith(null, '', `/c/${ID}`);
|
||||
});
|
||||
|
||||
it('拒绝 query 中的密钥并同步移除,不再继续使用这个链接', () => {
|
||||
const location = new URL(`https://webdrop.example/s/${ID}?keep=1&k=${KEY}`);
|
||||
const replaceState = vi.fn();
|
||||
vi.stubGlobal('location', location);
|
||||
vi.stubGlobal('history', { replaceState });
|
||||
|
||||
expect(takeLinkFromLocation()).toBeNull();
|
||||
expect(replaceState).toHaveBeenCalledWith(null, '', `/s/${ID}?keep=1`);
|
||||
});
|
||||
});
|
||||
@@ -10,9 +10,15 @@ export type EntryLink =
|
||||
* 必须在任何异步代码和渲染之前同步调用。
|
||||
*/
|
||||
export function takeLinkFromLocation(): EntryLink | null {
|
||||
if (!location.hash) return null;
|
||||
const query = new URLSearchParams(location.search);
|
||||
const queryHasSecret = ['k', 'a', 'w'].some((name) => query.has(name));
|
||||
if (!location.hash && !queryHasSecret) return null;
|
||||
const href = location.href;
|
||||
history.replaceState(null, '', location.pathname + location.search);
|
||||
for (const name of ['k', 'a', 'w']) query.delete(name);
|
||||
const search = query.toString();
|
||||
history.replaceState(null, '', `${location.pathname}${search ? `?${search}` : ''}`);
|
||||
// query 在浏览器导航时已发给托管端;拒绝继续使用,避免它进入后续链接或请求。
|
||||
if (queryHasSecret) return null;
|
||||
|
||||
const share = parseShareLink(href);
|
||||
if (share.ok) return { kind: 'share', link: share.link };
|
||||
|
||||
Reference in new issue
Block a user