From 00579e5de2d0f5359bf98c4ecb50748c196ddc97 Mon Sep 17 00:00:00 2001 From: lemon Date: Sat, 20 Jun 2026 22:38:49 -0700 Subject: [PATCH] Request verification by message --- .../profile/ProfileVerifierSection.tsx | 18 +++++- src/hooks/useDirectMessages.ts | 12 +++- src/locales/ar.json | 1 + src/locales/en.json | 3 +- src/locales/es.json | 1 + src/locales/fa.json | 1 + src/locales/fr.json | 1 + src/locales/hi.json | 1 + src/locales/id.json | 1 + src/locales/km.json | 1 + src/locales/ps.json | 1 + src/locales/pt.json | 1 + src/locales/ru.json | 1 + src/locales/sn.json | 1 + src/locales/sw.json | 1 + src/locales/tr.json | 1 + src/locales/zh-Hant.json | 1 + src/locales/zh.json | 1 + src/pages/MessagesPage.tsx | 56 +++++++++++++++---- 19 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/components/profile/ProfileVerifierSection.tsx b/src/components/profile/ProfileVerifierSection.tsx index c2176d74..e24d70cd 100644 --- a/src/components/profile/ProfileVerifierSection.tsx +++ b/src/components/profile/ProfileVerifierSection.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Loader2, Trash2 } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { Loader2, MessageSquare, Trash2 } from 'lucide-react'; import { PolicyMarkdown } from '@/components/PolicyMarkdown'; import { @@ -81,7 +82,7 @@ export function ProfileVerifierSection({ pubkey, isOwnProfile = false, className

{t('verifier.howWeVerifyTitle')}

- {isOwnProfile && ( + {isOwnProfile ? ( + ) : ( + )} diff --git a/src/hooks/useDirectMessages.ts b/src/hooks/useDirectMessages.ts index 6257b641..77b9882e 100644 --- a/src/hooks/useDirectMessages.ts +++ b/src/hooks/useDirectMessages.ts @@ -34,6 +34,13 @@ export interface Conversation { latest: DirectMessage; } +/** Minimal thread target; used for existing conversations and blank outbound threads. */ +export interface DirectMessageThreadTarget { + peer: string; + events: NostrEvent[]; + messageCount: number; +} + interface DirectMessagesPage { conversations: Conversation[]; sentUntil: number | null; @@ -301,17 +308,18 @@ async function decryptMessage({ } /** Decrypts a selected thread on demand instead of blocking the inbox list. */ -export function useDirectMessageThread(conversation: Conversation | null) { +export function useDirectMessageThread(conversation: DirectMessageThreadTarget | null) { const { user } = useCurrentUser(); const self = user?.pubkey; const nip04 = user?.signer.nip04; + const latestId = conversation?.events.at(-1)?.id ?? ''; return useQuery({ queryKey: [ 'direct-message-thread', self, conversation?.peer, - conversation?.latest.id, + latestId, conversation?.messageCount, ], enabled: !!self && !!nip04 && !!conversation, diff --git a/src/locales/ar.json b/src/locales/ar.json index 8f80a685..274523e6 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "سجّل الدخول لتصبح مُحقِّقاً", "loginGateBody": "يجب أن تكون مسجلاً للدخول لنشر بيان تحقق.", "howWeVerifyTitle": "كيف نتحقّق", + "requestVerification": "طلب التحقق", "withdrawConfirmTitle": "هل تريد سحب بيان التوثيق الخاص بك؟", "withdrawConfirmBody": "سيُزال بيان «كيف نتحقّق» من ملفك الشخصي. يمكنك نشر بيان جديد في أي وقت." }, diff --git a/src/locales/en.json b/src/locales/en.json index c3e1bdde..dda09056 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1660,7 +1660,8 @@ "disclaimer": "Anyone can become a verifier. Your statement is published publicly to Nostr and shown prominently on your profile. It does not grant any special permissions — it simply tells donors how you vet campaigns.", "loginGateTitle": "Sign in to become a verifier", "loginGateBody": "You need to be signed in to publish a verifier statement.", - "howWeVerifyTitle": "How We Verify" + "howWeVerifyTitle": "How We Verify", + "requestVerification": "Request Verification" }, "mdEditor": { "toolbar": { diff --git a/src/locales/es.json b/src/locales/es.json index e0139e6f..caad45d9 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "Inicia sesión para convertirte en verificador", "loginGateBody": "Debes haber iniciado sesión para publicar una declaración de verificador.", "howWeVerifyTitle": "Cómo verificamos", + "requestVerification": "Solicitar verificación", "withdrawConfirmTitle": "¿Retirar tu declaración de verificador?", "withdrawConfirmBody": "Tu declaración «Cómo verificamos» se eliminará de tu perfil. Puedes publicar una nueva en cualquier momento." }, diff --git a/src/locales/fa.json b/src/locales/fa.json index a6655e6f..2bdd4f77 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "برای تأییدکننده‌شدن وارد شوید", "loginGateBody": "برای انتشار بیانیهٔ تأییدکننده باید وارد شده باشید.", "howWeVerifyTitle": "چگونه تأیید می‌کنیم", + "requestVerification": "درخواست تأیید", "withdrawConfirmTitle": "بیانیهٔ تأییدکنندهٔ خود را پس می‌گیرید؟", "withdrawConfirmBody": "بیانیهٔ «چگونه تأیید می‌کنیم» شما از نمایه‌تان حذف می‌شود. هر زمان می‌توانید بیانیهٔ جدیدی منتشر کنید." }, diff --git a/src/locales/fr.json b/src/locales/fr.json index 69fea06d..58dad024 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "Connectez-vous pour devenir vérificateur", "loginGateBody": "Vous devez être connecté pour publier une déclaration de vérificateur.", "howWeVerifyTitle": "Comment nous vérifions", + "requestVerification": "Demander une vérification", "withdrawConfirmTitle": "Retirer votre déclaration de vérificateur ?", "withdrawConfirmBody": "Votre déclaration « Comment nous vérifions » sera retirée de votre profil. Vous pourrez en publier une nouvelle à tout moment." }, diff --git a/src/locales/hi.json b/src/locales/hi.json index 29d17fb5..b8468e7d 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "सत्यापनकर्ता बनने के लिए साइन इन करें", "loginGateBody": "सत्यापनकर्ता वक्तव्य प्रकाशित करने के लिए आपको साइन इन होना आवश्यक है।", "howWeVerifyTitle": "हम कैसे सत्यापित करते हैं", + "requestVerification": "सत्यापन का अनुरोध करें", "withdrawConfirmTitle": "अपना सत्यापनकर्ता वक्तव्य वापस लें?", "withdrawConfirmBody": "आपका \"हम कैसे सत्यापित करते हैं\" वक्तव्य आपकी प्रोफ़ाइल से हटा दिया जाएगा। आप कभी भी नया प्रकाशित कर सकते हैं।" }, diff --git a/src/locales/id.json b/src/locales/id.json index 1d92725c..ffe844ef 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "Masuk untuk menjadi verifikator", "loginGateBody": "Anda harus masuk untuk memublikasikan pernyataan verifikator.", "howWeVerifyTitle": "Cara kami memverifikasi", + "requestVerification": "Minta Verifikasi", "withdrawConfirmTitle": "Tarik pernyataan verifikator Anda?", "withdrawConfirmBody": "Pernyataan \"Cara Kami Memverifikasi\" Anda akan dihapus dari profil Anda. Anda bisa memublikasikan yang baru kapan saja." }, diff --git a/src/locales/km.json b/src/locales/km.json index c3757309..8f1a0876 100644 --- a/src/locales/km.json +++ b/src/locales/km.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "ចូលគណនីដើម្បីក្លាយជាអ្នកផ្ទៀងផ្ទាត់", "loginGateBody": "អ្នកត្រូវចូលគណនីដើម្បីផ្សព្វផ្សាយសេចក្តីថ្លែងការណ៍អ្នកផ្ទៀងផ្ទាត់។", "howWeVerifyTitle": "របៀបដែលយើងផ្ទៀងផ្ទាត់", + "requestVerification": "ស្នើសុំការផ្ទៀងផ្ទាត់", "withdrawConfirmTitle": "ដកសេចក្តីថ្លែងការណ៍អ្នកផ្ទៀងផ្ទាត់របស់អ្នកវិញ?", "withdrawConfirmBody": "សេចក្តីថ្លែងការណ៍ «របៀបដែលយើងផ្ទៀងផ្ទាត់» របស់អ្នកនឹងត្រូវដកចេញពីប្រវត្តិរូបរបស់អ្នក។ អ្នកអាចផ្សព្វផ្សាយមួយថ្មីបាននៅពេលណាក៏បាន។" }, diff --git a/src/locales/ps.json b/src/locales/ps.json index fbdc7d1f..f5c1a3bb 100644 --- a/src/locales/ps.json +++ b/src/locales/ps.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "د تصدیق کوونکي کیدو لپاره ننوځئ", "loginGateBody": "د تصدیق کوونکي بیان د خپرولو لپاره باید ننوتلي اوسئ.", "howWeVerifyTitle": "موږ څنګه تصدیق کوو", + "requestVerification": "د تایید غوښتنه وکړئ", "withdrawConfirmTitle": "خپل د تصدیق کوونکي بیان بیرته اخلئ؟", "withdrawConfirmBody": "ستاسو د \"موږ څنګه تصدیق کوو\" بیان به ستاسو له پروفایل څخه لرې شي. تاسو کولی شئ هر وخت یو نوی خپور کړئ." }, diff --git a/src/locales/pt.json b/src/locales/pt.json index 232d68f8..2d9be498 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "Entre para se tornar um verificador", "loginGateBody": "Você precisa estar conectado para publicar uma declaração de verificador.", "howWeVerifyTitle": "Como verificamos", + "requestVerification": "Solicitar verificação", "withdrawConfirmTitle": "Retirar sua declaração de verificador?", "withdrawConfirmBody": "Sua declaração \"Como verificamos\" será removida do seu perfil. Você pode publicar uma nova a qualquer momento." }, diff --git a/src/locales/ru.json b/src/locales/ru.json index 36866510..7a384f33 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "Войдите, чтобы стать верификатором", "loginGateBody": "Вы должны войти, чтобы опубликовать заявление верификатора.", "howWeVerifyTitle": "Как мы проверяем", + "requestVerification": "Запросить проверку", "withdrawConfirmTitle": "Отозвать ваше заявление верификатора?", "withdrawConfirmBody": "Ваше заявление «Как мы проверяем» будет удалено из вашего профиля. Вы можете опубликовать новое в любой момент." }, diff --git a/src/locales/sn.json b/src/locales/sn.json index 3808751a..2b94edf3 100644 --- a/src/locales/sn.json +++ b/src/locales/sn.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "Pinda kuti uve musimbisi", "loginGateBody": "Unofanira kunge wakapinda kuti uburitse chirevo chemusimbisi.", "howWeVerifyTitle": "Matiro atinosimbisa nawo", + "requestVerification": "Kumbira kusimbiswa", "withdrawConfirmTitle": "Bvisa chirevo chako chemusimbisi?", "withdrawConfirmBody": "Chirevo chako che\"Matiro Atinosimbisa Nawo\" chichabviswa pa profile yako. Unogona kuburitsa chitsva chero nguva." }, diff --git a/src/locales/sw.json b/src/locales/sw.json index c3048fef..f9482f68 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "Ingia ili kuwa mthibitishaji", "loginGateBody": "Unahitaji kuwa umeingia ili kuchapisha taarifa ya mthibitishaji.", "howWeVerifyTitle": "Jinsi tunavyothibitisha", + "requestVerification": "Omba uthibitishaji", "withdrawConfirmTitle": "Ondoa taarifa yako ya mthibitishaji?", "withdrawConfirmBody": "Taarifa yako ya \"Jinsi Tunavyothibitisha\" itaondolewa kwenye wasifu wako. Unaweza kuchapisha mpya wakati wowote." }, diff --git a/src/locales/tr.json b/src/locales/tr.json index d57b41f0..2c70e1b2 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1570,6 +1570,7 @@ "loginGateTitle": "Doğrulayıcı olmak için giriş yapın", "loginGateBody": "Doğrulayıcı beyanı yayımlamak için giriş yapmış olmanız gerekir.", "howWeVerifyTitle": "Nasıl doğrularız", + "requestVerification": "Doğrulama iste", "withdrawConfirmTitle": "Doğrulayıcı beyanınız geri çekilsin mi?", "withdrawConfirmBody": "\"Nasıl Doğrularız\" beyanınız profilinizden kaldırılacak. İstediğiniz zaman yenisini yayımlayabilirsiniz." }, diff --git a/src/locales/zh-Hant.json b/src/locales/zh-Hant.json index 6d678aa2..0172d845 100644 --- a/src/locales/zh-Hant.json +++ b/src/locales/zh-Hant.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "登入以成為驗證者", "loginGateBody": "你需要登入才能發布驗證者聲明。", "howWeVerifyTitle": "我們如何驗證", + "requestVerification": "請求驗證", "withdrawConfirmTitle": "撤回你的驗證者聲明?", "withdrawConfirmBody": "你的「我們如何驗證」聲明將從你的個人資料中移除。你可以隨時發布新的聲明。" }, diff --git a/src/locales/zh.json b/src/locales/zh.json index 2982e68b..4370300c 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1128,6 +1128,7 @@ "loginGateTitle": "登录以成为验证者", "loginGateBody": "你需要登录才能发布验证者声明。", "howWeVerifyTitle": "我们如何验证", + "requestVerification": "请求验证", "withdrawConfirmTitle": "撤回你的验证者声明?", "withdrawConfirmBody": "你的「我们如何验证」声明将从你的个人资料中移除。你可以随时发布新的声明。" }, diff --git a/src/pages/MessagesPage.tsx b/src/pages/MessagesPage.tsx index fb3c50a2..6f1cb8e6 100644 --- a/src/pages/MessagesPage.tsx +++ b/src/pages/MessagesPage.tsx @@ -1,6 +1,6 @@ import { useSeoMeta } from '@unhead/react'; import { useEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEvent } from 'react'; -import { Link, Navigate } from 'react-router-dom'; +import { Link, Navigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useInView } from 'react-intersection-observer'; import { ArrowLeft, ArrowUp, BellOff, Inbox, Loader2, Lock, MessageSquare, Search, UserCheck, X } from 'lucide-react'; @@ -32,6 +32,7 @@ import { useSendDirectMessage, type Conversation, type DirectMessage, + type DirectMessageThreadTarget, } from '@/hooks/useDirectMessages'; import { useFollowList } from '@/hooks/useFollowActions'; import { useMuteList } from '@/hooks/useMuteList'; @@ -46,6 +47,10 @@ import { cn } from '@/lib/utils'; const DM_TRANSLATION_CONFIRM_KEY = 'agora.dmTranslationConfirmed'; const DISMISSED_VERIFIER_STATEMENTS_KEY = 'agora.dismissedVerifierStatementBanners'; +function getValidPubkey(value: string | null): string | null { + return value && /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : null; +} + function readDismissedVerifierStatements(): Set { if (typeof window === 'undefined') return new Set(); @@ -206,7 +211,7 @@ function MessageThread({ onBack, onMuted, }: { - conversation: Conversation; + conversation: DirectMessageThreadTarget; onBack: () => void; onMuted: (peer: string) => void; }) { @@ -520,6 +525,7 @@ function MessageThread({ export function MessagesPage() { const { t } = useTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); const { user } = useCurrentUser(); const { config } = useAppContext(); const { @@ -532,7 +538,8 @@ export function MessagesPage() { } = useDirectMessages(); const { data: followData } = useFollowList(); const { muteItems } = useMuteList(); - const [selectedPeer, setSelectedPeer] = useState(null); + const requestedPeer = getValidPubkey(searchParams.get('to')); + const [selectedPeer, setSelectedPeer] = useState(() => requestedPeer); const [search, setSearch] = useState(''); const [conversationFilter, setConversationFilter] = useState<'all' | 'friends'>('all'); const [hiddenMutedPeers, setHiddenMutedPeers] = useState>(() => new Set()); @@ -555,14 +562,41 @@ export function MessagesPage() { )), [conversationFilter, conversations, followedPubkeys, hiddenMutedPeers, mutedPubkeys], ); - const selected = useMemo( + const selectedConversation = useMemo( () => visibleConversations?.find((c) => c.peer === selectedPeer) ?? null, [visibleConversations, selectedPeer], ); + const selectedThread = useMemo(() => { + if (selectedConversation) return selectedConversation; + if (!selectedPeer) return null; + return { peer: selectedPeer, events: [], messageCount: 0 }; + }, [selectedConversation, selectedPeer]); + + useEffect(() => { + if (requestedPeer) setSelectedPeer(requestedPeer); + }, [requestedPeer]); + + const selectPeer = (peer: string) => { + setSelectedPeer(peer); + setSearchParams((current) => { + const next = new URLSearchParams(current); + next.delete('to'); + return next; + }, { replace: true }); + }; + + const clearSelectedPeer = () => { + setSelectedPeer(null); + setSearchParams((current) => { + const next = new URLSearchParams(current); + next.delete('to'); + return next; + }, { replace: true }); + }; const handleMuted = (peer: string) => { setHiddenMutedPeers((prev) => new Set(prev).add(peer)); - setSelectedPeer(null); + clearSelectedPeer(); }; useEffect(() => { @@ -597,8 +631,8 @@ export function MessagesPage() {
@@ -643,7 +677,7 @@ export function MessagesPage() { conversation={conversation} active={conversation.peer === selectedPeer} searchQuery={search} - onSelect={() => setSelectedPeer(conversation.peer)} + onSelect={() => selectPeer(conversation.peer)} /> )) ) : ( @@ -663,9 +697,9 @@ export function MessagesPage() {
{/* Thread */} -
- {selected ? ( - setSelectedPeer(null)} onMuted={handleMuted} /> +
+ {selectedThread ? ( + ) : (