Rounds out the full rewrite with the remaining feature domains: group management (admin panel, mute, files, announcements, QR-based invite/join), contacts (friend profile, add-friend, blocklist, new-friends), favorites (real media rendering across text/image/video/voice/file/link), settings (profile/security/privacy/general/about), calls (Agora-backed audio/video signaling + call history), and system messages/file assistant. Also folds in a large batch of live-verified fixes surfaced by repeated fresh code-review passes and hands-on E2E testing against the real backend, including: WS reconnect not resubscribing any topic after a disconnect, missing x-im-client-mid on every send path (protocol compliance for retry dedup), group mute/member-settings/self-profile endpoint corrections, relationship-state type fixes across search/QR/ friend-profile, tenant-switch logout gap, password-policy now read from the real per-tenant config instead of guessed, NavRail highlight/ aria-current desync, several call-state-machine races, and responsive layout fixes across all four breakpoints (mobile/tablet/laptop/desktop) including a full mobile rebuild of favorites, call UI, and the settings home/sub-list split. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
|
|
import { Icon } from '@/app/Icon';
|
|
import { getGroupMembers, type GroupRole } from '@/lib/api/groups';
|
|
import { avatarBgClass, avatarLetter } from '@/lib/avatar';
|
|
import { useDebouncedValue } from '@/lib/hooks';
|
|
import { MENTION_ALL_SENTINEL, type PendingMention } from '@/lib/messages/mentions';
|
|
|
|
const ROLE_LABEL: Record<GroupRole, string> = { owner: '群主', admin: '管理员', member: '' };
|
|
|
|
interface MentionPickerProps {
|
|
topicId: string;
|
|
query: string;
|
|
onPick: (choice: PendingMention) => void;
|
|
}
|
|
|
|
/**
|
|
* 设计稿 4.3:输入 `@` 触发的 260px 浮层。候选来自 `GET .../members?query=&mentionable=true`
|
|
* (协议文档 1052/1735 行)-- 这一个响应同时带 `items`(已经是可提及成员,不用再拿全量列表自己过滤)
|
|
* 和 `permissions.can_mention_all`,比向 ConversationView 另外要这两样数据都便宜。"所有人"是虚拟项,
|
|
* 不对应真实成员,只在 `can_mention_all` 为真时出现在首位。
|
|
*/
|
|
export function MentionPicker({ topicId, query, onPick }: MentionPickerProps) {
|
|
const debouncedQuery = useDebouncedValue(query, 200);
|
|
const membersQuery = useQuery({
|
|
queryKey: ['group-mentionable-members', topicId, debouncedQuery],
|
|
queryFn: () => getGroupMembers(topicId, { query: debouncedQuery, mentionable: true }),
|
|
});
|
|
const items = membersQuery.data?.items || [];
|
|
const canMentionAll = !!membersQuery.data?.permissions?.can_mention_all;
|
|
const showAll = canMentionAll && (!query || '所有人'.includes(query));
|
|
|
|
if (!showAll && items.length === 0 && !membersQuery.isLoading) return null;
|
|
|
|
return (
|
|
<div className="w-[260px] overflow-hidden rounded-[10px] border border-border-panel bg-white py-1 shadow-menu">
|
|
{membersQuery.isLoading ? (
|
|
<div className="px-3 py-4 text-center text-xs text-text-tertiary-4">加载中…</div>
|
|
) : (
|
|
<>
|
|
{showAll ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => onPick({ userId: MENTION_ALL_SENTINEL, label: '所有人' })}
|
|
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-[#f2f6fd]"
|
|
>
|
|
<span className="grid h-7 w-7 flex-none place-items-center rounded-lg bg-accent text-white">
|
|
<Icon name="group" className="text-[16px]" />
|
|
</span>
|
|
<span className="text-sm text-text-body">所有人</span>
|
|
</button>
|
|
) : null}
|
|
{items.map((m) => (
|
|
<button
|
|
key={m.user_id}
|
|
type="button"
|
|
onClick={() => onPick({ userId: m.user_id, label: m.display_name || m.user_id })}
|
|
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-[#f2f6fd]"
|
|
>
|
|
<span className={`grid h-7 w-7 flex-none place-items-center rounded-lg text-xs font-medium text-white ${avatarBgClass(m.user_id)}`}>
|
|
{avatarLetter(m.display_name)}
|
|
</span>
|
|
<span className="min-w-0 flex-1 truncate text-sm text-text-body">{m.display_name || m.user_id}</span>
|
|
{m.role !== 'member' ? (
|
|
<span
|
|
className={`flex-none rounded-[3px] border px-1 text-[10px] leading-[14px] ${
|
|
m.role === 'owner' ? 'border-warn text-warn' : 'border-accent text-accent'
|
|
}`}
|
|
>
|
|
{ROLE_LABEL[m.role]}
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|