feat(web): discord-style chat, members pane, invitations and optimistic send [CI]
- chat: left-aligned discord-style messages with grouped senders, hover actions, an auto-growing composer and file cards - send: render messages and files locally first and replace the placeholder when the worker publishes the real item; failures keep the text with a retry action - navigation: space rail plus a flatter channel list with the space name header and collapsible groups - members: right pane tabs (files / members) grouped by online, offline and pending users, with a prominent invite action and a top bar shortcut - notifications: toast host for invite results; invitees get a toast with an accept action, and the invitation payload now carries the space name and inviter inside the encrypted grant - users over devices: drop device names, the device list and device rename/release; senders show the account (guests show as guest) and the share panel only keeps links - follow contract 7.0.0 (max_members, members_used, members_full, /join) - docs: ui-design and frontend updated for the new layout and concepts
This commit is contained in:
1 parent
5fd4ddd411
commit
4b8d0b5a5f
83 files changed
+1291
-661
No files matched your search
@@ -8,16 +8,30 @@ interface ChannelGrant {
|
||||
mode: 'enc' | 'raw';
|
||||
role: 'owner' | 'member';
|
||||
keys: ParsedChannelLink['keys'];
|
||||
/** 仅供被邀请方在收件箱显示,在加密 payload 内,服务端不可见 */
|
||||
name?: string;
|
||||
invitedBy?: string;
|
||||
}
|
||||
|
||||
export async function sealChannelGrant(link: string, recipient: string, publicKey: JsonWebKey): Promise<string> {
|
||||
export interface InvitationMeta { name?: string; invitedBy?: string }
|
||||
|
||||
export async function sealChannelGrant(link: string, recipient: string, publicKey: JsonWebKey, meta: InvitationMeta = {}): Promise<string> {
|
||||
const parsed = parseChannelLink(link);
|
||||
if (!parsed.ok) throw new Error('空间凭据格式无效');
|
||||
const grant: ChannelGrant = { v: 1, channelId: parsed.link.id, mode: parsed.link.mode,
|
||||
role: parsed.link.isOwner ? 'owner' : 'member', keys: parsed.link.keys };
|
||||
role: parsed.link.isOwner ? 'owner' : 'member', keys: parsed.link.keys,
|
||||
...(meta.name ? { name: meta.name } : {}), ...(meta.invitedBy ? { invitedBy: meta.invitedBy } : {}) };
|
||||
return encryptInvitation(JSON.stringify(grant), recipient, grant.channelId, publicKey);
|
||||
}
|
||||
|
||||
/** 只读取显示用的元信息;解不开或没有这些字段时返回空,调用方回退到空间 ID */
|
||||
export async function peekChannelGrant(payload: string, recipient: string, channelId: string, privateKey: JsonWebKey): Promise<InvitationMeta> {
|
||||
try {
|
||||
const grant = JSON.parse(await decryptInvitation(payload, recipient, channelId, privateKey)) as Partial<ChannelGrant>;
|
||||
return { ...(typeof grant.name === 'string' ? { name: grant.name } : {}), ...(typeof grant.invitedBy === 'string' ? { invitedBy: grant.invitedBy } : {}) };
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
export async function openChannelGrant(payload: string, recipient: string, channelId: string, privateKey: JsonWebKey,
|
||||
role: 'owner' | 'member', origin: string): Promise<ParsedChannelLink> {
|
||||
const plain = await decryptInvitation(payload, recipient, channelId, privateKey);
|
||||
|
||||
Reference in new issue
Block a user