feat(web): add account workspaces and signed content flows
This commit is contained in:
1 parent
7b8d61d1ba
commit
b5cf3d5958
117 files changed
+2789
-1674
No files matched your search
@@ -23,11 +23,16 @@ export function formatTransferProgress(state: {
|
||||
}
|
||||
|
||||
const timeFmt = new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
const dateFmt = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
|
||||
export function formatTime(ts: number): string {
|
||||
return timeFmt.format(ts);
|
||||
}
|
||||
|
||||
export function formatDate(ts: number): string {
|
||||
return dateFmt.format(ts);
|
||||
}
|
||||
|
||||
export function formatAgo(ts: number, now = Date.now()): string {
|
||||
const m = Math.max(0, Math.round((now - ts) / 60000));
|
||||
if (m < 1) return '刚刚';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readSender } from './sender';
|
||||
|
||||
describe('sender display snapshots', () => {
|
||||
it('accepts account identity and safe local avatar data', () => {
|
||||
expect(readSender({ username: 'alice', displayName: '小白', avatar: 'data:image/webp;base64,AA==' })).toEqual({ username: 'alice', displayName: '小白', avatar: 'data:image/webp;base64,AA==' });
|
||||
expect(readSender({ username: 'alice', displayName: ' Alice ' })).toEqual({ username: 'alice', displayName: 'Alice' });
|
||||
});
|
||||
it.each([
|
||||
{ username: 'alice', displayName: 'Alice', avatar: 'https://tracker.test/avatar.png' },
|
||||
{ username: 'alice', displayName: 'Alice', avatar: 'data:image/svg+xml;base64,AA==' },
|
||||
{ username: 'alice', displayName: 'Alice', avatar: `data:image/png;base64,${'A'.repeat(8000)}` },
|
||||
{ username: 'alice', displayName: '\u0000Alice' },
|
||||
{ username: 'alice', displayName: '' }, { username: 'invalid user', displayName: 'Alice' }, null,
|
||||
])('rejects invalid or executable identity data %#', (value) => { expect(readSender(value)).toBeUndefined(); });
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { SenderInfo } from '@/core-bridge/types';
|
||||
|
||||
export function readSender(value: unknown): SenderInfo | undefined {
|
||||
if (!value || typeof value !== 'object') return undefined;
|
||||
const row = value as Record<string, unknown>;
|
||||
if (typeof row.username !== 'string' || !/^[a-z0-9_-]{3,32}$/.test(row.username)
|
||||
|| typeof row.displayName !== 'string' || !row.displayName.trim() || row.displayName.length > 48 || /[\u0000-\u001f]/.test(row.displayName)
|
||||
|| row.avatar !== undefined && (typeof row.avatar !== 'string' || row.avatar.length > 8000 || !/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(row.avatar))) return undefined;
|
||||
return { username: row.username, displayName: row.displayName.trim(), ...(row.avatar ? { avatar: row.avatar as string } : {}) };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ttlLabel, withCurrent } from './settingOptions';
|
||||
|
||||
describe('server setting presentation', () => {
|
||||
it('formats arbitrary deployment-provided retention times', () => {
|
||||
expect(ttlLabel(90)).toBe('90 秒'); expect(ttlLabel(120)).toBe('2 分钟'); expect(ttlLabel(3600)).toBe('1 小时');
|
||||
});
|
||||
it('keeps existing settings visible when deployment choices change', () => {
|
||||
expect(withCurrent([60, 120, 120], 90)).toEqual([60, 90, 120]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export function ttlLabel(seconds: number): string {
|
||||
if (seconds % 86400 === 0) return `${seconds / 86400} 天`;
|
||||
if (seconds % 3600 === 0) return `${seconds / 3600} 小时`;
|
||||
if (seconds % 60 === 0) return `${seconds / 60} 分钟`;
|
||||
return `${seconds} 秒`;
|
||||
}
|
||||
export function withCurrent(values: number[], current: number): number[] {
|
||||
return [...new Set([...values, current])].sort((a, b) => a - b);
|
||||
}
|
||||
Reference in new issue
Block a user