import { useEffect, useRef, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Icon } from '@/app/Icon'; import { createGroupQRCode, createMyQRCode, createUserQRCode, type GroupQRCode, type MyQRCode } from '@/lib/api/qrcodes'; import { avatarBgClass, avatarLetter } from '@/lib/avatar'; import { useEscapeKey } from '@/lib/hooks'; interface QRCodeModalProps { /** Omit both to show the CURRENT user's own code; pass a friend's user_id/account to show theirs * (README's "二维码名片" row on a friend's profile). */ identifier?: string; /** 03-group.dc.html #1a's `qr_code_2` icon on the group-info card (admin/owner-visible group-join * code) -- mutually exclusive with `identifier`, a group is never also a user. */ groupTopic?: string; onClose: () => void; } /** * README §5.4/§8.2's "我的二维码"/"二维码名片", plus the group-join code (03-group.dc.html #1a). The * QR image encodes `qr_payload` (`im://qr?token=...&purpose=add_friend|join_group`), never a raw * user/group id -- confirmed live for the personal-code path that this is a real, working backend * feature (`POST /me/qrcodes` / `POST /users/{id}/qrcodes`), not a documented-but-unbuilt one; the * group-code path (`POST /groups/{topic}/qrcodes`) is the same documented shape (docs/IM-前后端对接 * 文档.md:1590,1737) but not yet live-verified against a real deployment. Re-mints a fresh token on * every mount (POST semantics, not cached) since each one is short-lived (~24h) and single-purpose. */ export function QRCodeModal({ identifier, groupTopic, onClose }: QRCodeModalProps) { useEscapeKey(onClose); const canvasRef = useRef(null); const [drawError, setDrawError] = useState(''); const query = useQuery({ queryKey: ['qrcode', groupTopic ? `group:${groupTopic}` : identifier || 'me'], queryFn: (): Promise => (groupTopic ? createGroupQRCode(groupTopic) : identifier ? createUserQRCode(identifier) : createMyQRCode()), }); const draw = () => { if (!query.data || !canvasRef.current) return; const canvas = canvasRef.current; setDrawError(''); // Dynamically imported (like agora-rtc-sdk-ng in agora-client.ts) so a QR-generating library // isn't in every visitor's main bundle when most sessions never open this modal. import('qrcode') .then(({ default: QRCode }) => QRCode.toCanvas(canvas, query.data.qr_payload, { width: 200, margin: 1, color: { dark: '#22262d', light: '#ffffff' } })) .catch(() => setDrawError('二维码生成失败')); }; useEffect(draw, [query.data]); const retry = () => { if (query.isError) query.refetch(); else draw(); }; const profile = query.data && 'display_profile' in query.data ? query.data.display_profile : undefined; const group = query.data && 'group_snapshot' in query.data ? query.data.group_snapshot : undefined; const title = groupTopic ? '群二维码' : identifier ? '二维码名片' : '我的二维码'; const fileBaseName = groupTopic ? '群二维码' : identifier ? '二维码名片' : '我的二维码'; const caption = groupTopic ? '扫一扫上面的二维码,申请加入群聊' : '扫一扫上面的二维码,加我为好友'; const canvasToBlob = (): Promise => new Promise((resolve) => canvasRef.current?.toBlob((b) => resolve(b), 'image/png')); const saveToAlbum = async () => { const blob = await canvasToBlob(); if (!blob) return; const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${fileBaseName}.png`; link.click(); URL.revokeObjectURL(url); }; const shareToFriend = async () => { const blob = await canvasToBlob(); if (!blob) return; const file = new File([blob], 'qrcode.png', { type: 'image/png' }); // Real capability check, not just feature-detecting `navigator.share` itself -- some browsers // implement `share()` for URLs/text only and reject a `files` payload at call time, so // `canShare({ files })` is the one call that actually answers "can this share an image". if (navigator.canShare?.({ files: [file] })) { try { await navigator.share({ files: [file], title: fileBaseName }); } catch { // AbortError (user cancelled the native share sheet) or any other failure -- either way // there's nothing useful to surface, the share sheet's own UI already communicated the outcome. } } else { await saveToAlbum(); } }; return (
e.stopPropagation()}>
{title}
{profile ? ( <> {avatarLetter(profile.display_name)} {profile.display_name} {profile.public_account ? ID:{profile.public_account} : null} ) : group ? ( <> {avatarLetter(group.name)} {group.name} {group.member_count} 人 ) : null}
{query.isLoading ? ( 生成中… ) : query.isError || drawError ? (
{drawError || '二维码生成失败,请重试'}
) : ( )}
{caption} {/* No mock anywhere in this design handoff draws these two buttons (grepped every screens/*.dc.html) -- added on the strength of a near-universal QR-display convention (save/share), not a documented spec. Reuses the `share` icon the add-friend chip (04-contacts.dc.html:101) already uses for the same verb elsewhere in this app. */} {query.data && !drawError ? (
) : null}
); }