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
@@ -0,0 +1,44 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { errorMessage } from '@/core-bridge/errors';
|
||||
import { useToast } from '@/store/toast';
|
||||
import { useWorkspace } from '@/store/workspace';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Sheet } from '@/ui/Sheet';
|
||||
import styles from './MembersPane.module.css';
|
||||
|
||||
export function InviteSheet({ onClose, onInvited }: { onClose: () => void; onInvited: () => void }) {
|
||||
const invite = useWorkspace((s) => s.inviteUser);
|
||||
const [username, setUsername] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
const target = username.trim();
|
||||
if (!target || busy) return;
|
||||
setBusy(true); setError('');
|
||||
try {
|
||||
await invite(target);
|
||||
useToast.getState().push({ kind: 'ok', text: `已向 ${target} 发送邀请` });
|
||||
onInvited(); onClose();
|
||||
} catch (cause) {
|
||||
const message = errorMessage(cause);
|
||||
setError(message);
|
||||
useToast.getState().push({ kind: 'error', text: `邀请失败:${message}` });
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
|
||||
return <Sheet title="邀请成员" onClose={() => { if (!busy) onClose(); }} placement="top">
|
||||
<form className={styles.invite} onSubmit={(event) => { void submit(event); }}>
|
||||
<label>用户名
|
||||
<input autoFocus maxLength={32} value={username} disabled={busy} placeholder="已注册用户的用户名" onChange={(event) => setUsername(event.target.value)} />
|
||||
</label>
|
||||
<p className={styles.note}>对方登录后会收到通知,接受邀请后加入空间。能否读取加入前的历史,取决于各频道的历史开关。</p>
|
||||
{error ? <p className={styles.error} role="alert">{error}</p> : null}
|
||||
<div className={styles.inviteActions}>
|
||||
<Button variant="ghost" disabled={busy} onClick={onClose}>取消</Button>
|
||||
<Button type="submit" variant="primary" disabled={busy || !username.trim()}>{busy ? '发送中…' : '发送邀请'}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Sheet>;
|
||||
}
|
||||
Reference in new issue
Block a user