Files
im_webapp/web/src/features/messages/MentionPicker.tsx
T
Your NameandClaude Sonnet 5 5c3f3b7653 feat(web): complete groups/contacts/favorites/settings/calls/system domains + extensive review-round fixes
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>
2026-09-09 20:16:33 +08:00

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>
);
}