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 ? (
+
) : (