Two side-by-side zones (stacked on narrow screens) with an icon and a one-line positioning each: quick share is temporary, read-only and needs no account; spaces are long-lived, multi-user and need a login. Shorter copy, my shares listed under the share input, and an empty state for spaces.
71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
import { useState } from 'react';
|
|
import type { ChannelInfo, Share } from '@/core-bridge/types';
|
|
import { formatAgo, formatRemaining } from '@/lib/format';
|
|
import { useChannels } from '@/store/channels';
|
|
import { useShares } from '@/store/shares';
|
|
import { Button } from '@/ui/Button';
|
|
import { Icon } from '@/ui/Icon';
|
|
import styles from './ChannelList.module.css';
|
|
|
|
interface Props {
|
|
kind: 'channel' | 'share';
|
|
title: string;
|
|
onOpen: (c: ChannelInfo | Share) => void;
|
|
/** 标题只给屏幕阅读器(所在分区已有标题) */
|
|
quietTitle?: boolean;
|
|
}
|
|
|
|
export function ChannelList({ kind, title, onOpen, quietTitle = false }: Props) {
|
|
const channels = useChannels((s) => s.list);
|
|
const forgetChannel = useChannels((s) => s.forget);
|
|
const shares = useShares((s) => s.list);
|
|
const forgetShare = useShares((s) => s.forget);
|
|
const list = kind === 'share' ? shares : channels;
|
|
const [confirmId, setConfirmId] = useState<string>();
|
|
if (list.length === 0) return null;
|
|
|
|
return (
|
|
<section aria-label={title}>
|
|
<h2 className={`${styles.h} ${quietTitle ? styles.quiet : ''}`}>{title}</h2>
|
|
<ul className={styles.list}>
|
|
{list.map((c) => (
|
|
<li key={c.id} className={styles.row}>
|
|
<button type="button" className={styles.open} onClick={() => onOpen(c)}>
|
|
<span className={styles.id}>{'state' in c ? c.id.slice(0, 8) : c.name || c.id.slice(0, 8)}…</span>
|
|
<span className={styles.meta}>
|
|
{c.dataSource === 'cache' ? <span>离线缓存</span> : null}
|
|
{'state' in c ? (
|
|
<>
|
|
<span className={styles.enc}><Icon name="lock" size={12} /> 加密</span>
|
|
<span className={c.state === 'sealed' ? styles.enc : styles.raw}>{c.state === 'sealed' ? '已封存' : '草稿'}</span>
|
|
<span>{c.mine ? '我创建的' : '收到的'}</span>
|
|
<span>{formatRemaining(c.expiresAt)}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
{c.name ? <span className={styles.channelId}>{c.id.slice(0, 8)}…</span> : null}
|
|
<span className={c.mode === 'enc' ? styles.enc : styles.raw}>
|
|
{c.mode === 'enc' ? <><Icon name="lock" size={12} /> 加密</> : '明文'}
|
|
</span>
|
|
<span>{c.role === 'owner' ? '所有者' : '成员'}</span>
|
|
<span>最多 {c.maxMembers} 人</span>
|
|
<span>{formatAgo(c.createdAt)}创建</span>
|
|
</>
|
|
)}
|
|
</span>
|
|
</button>
|
|
{confirmId === c.id ? (
|
|
<span className={styles.confirm}>
|
|
<Button variant="ghost" onClick={() => { (kind === 'share' ? forgetShare : forgetChannel)(c.id); setConfirmId(undefined); }}>确认移除</Button>
|
|
<Button variant="ghost" onClick={() => setConfirmId(undefined)}>取消</Button>
|
|
</span>
|
|
) : (
|
|
<Button variant="ghost" icon="trash" label="从当前资源清单移除(不会删除服务端内容)" onClick={() => setConfirmId(c.id)} />
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|