Request verification by message
This commit is contained in:
@@ -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
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-primary">
|
||||
{t('verifier.howWeVerifyTitle')}
|
||||
</h2>
|
||||
{isOwnProfile && (
|
||||
{isOwnProfile ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -97,6 +98,19 @@ export function ProfileVerifierSection({ pubkey, isOwnProfile = false, className
|
||||
)}
|
||||
<span className="ml-1.5">{t('verifier.withdraw')}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="-mt-1 -mr-2 h-8 shrink-0 px-3 text-xs"
|
||||
asChild
|
||||
>
|
||||
<Link to={`/messages?to=${pubkey}`}>
|
||||
<MessageSquare className="mr-1.5 size-3.5" />
|
||||
{t('verifier.requestVerification')}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<PolicyMarkdown source={statement} />
|
||||
|
||||
@@ -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<DirectMessage[]>({
|
||||
queryKey: [
|
||||
'direct-message-thread',
|
||||
self,
|
||||
conversation?.peer,
|
||||
conversation?.latest.id,
|
||||
latestId,
|
||||
conversation?.messageCount,
|
||||
],
|
||||
enabled: !!self && !!nip04 && !!conversation,
|
||||
|
||||
@@ -1128,6 +1128,7 @@
|
||||
"loginGateTitle": "سجّل الدخول لتصبح مُحقِّقاً",
|
||||
"loginGateBody": "يجب أن تكون مسجلاً للدخول لنشر بيان تحقق.",
|
||||
"howWeVerifyTitle": "كيف نتحقّق",
|
||||
"requestVerification": "طلب التحقق",
|
||||
"withdrawConfirmTitle": "هل تريد سحب بيان التوثيق الخاص بك؟",
|
||||
"withdrawConfirmBody": "سيُزال بيان «كيف نتحقّق» من ملفك الشخصي. يمكنك نشر بيان جديد في أي وقت."
|
||||
},
|
||||
|
||||
+2
-1
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -1128,6 +1128,7 @@
|
||||
"loginGateTitle": "برای تأییدکنندهشدن وارد شوید",
|
||||
"loginGateBody": "برای انتشار بیانیهٔ تأییدکننده باید وارد شده باشید.",
|
||||
"howWeVerifyTitle": "چگونه تأیید میکنیم",
|
||||
"requestVerification": "درخواست تأیید",
|
||||
"withdrawConfirmTitle": "بیانیهٔ تأییدکنندهٔ خود را پس میگیرید؟",
|
||||
"withdrawConfirmBody": "بیانیهٔ «چگونه تأیید میکنیم» شما از نمایهتان حذف میشود. هر زمان میتوانید بیانیهٔ جدیدی منتشر کنید."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -1570,6 +1570,7 @@
|
||||
"loginGateTitle": "सत्यापनकर्ता बनने के लिए साइन इन करें",
|
||||
"loginGateBody": "सत्यापनकर्ता वक्तव्य प्रकाशित करने के लिए आपको साइन इन होना आवश्यक है।",
|
||||
"howWeVerifyTitle": "हम कैसे सत्यापित करते हैं",
|
||||
"requestVerification": "सत्यापन का अनुरोध करें",
|
||||
"withdrawConfirmTitle": "अपना सत्यापनकर्ता वक्तव्य वापस लें?",
|
||||
"withdrawConfirmBody": "आपका \"हम कैसे सत्यापित करते हैं\" वक्तव्य आपकी प्रोफ़ाइल से हटा दिया जाएगा। आप कभी भी नया प्रकाशित कर सकते हैं।"
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -1128,6 +1128,7 @@
|
||||
"loginGateTitle": "ចូលគណនីដើម្បីក្លាយជាអ្នកផ្ទៀងផ្ទាត់",
|
||||
"loginGateBody": "អ្នកត្រូវចូលគណនីដើម្បីផ្សព្វផ្សាយសេចក្តីថ្លែងការណ៍អ្នកផ្ទៀងផ្ទាត់។",
|
||||
"howWeVerifyTitle": "របៀបដែលយើងផ្ទៀងផ្ទាត់",
|
||||
"requestVerification": "ស្នើសុំការផ្ទៀងផ្ទាត់",
|
||||
"withdrawConfirmTitle": "ដកសេចក្តីថ្លែងការណ៍អ្នកផ្ទៀងផ្ទាត់របស់អ្នកវិញ?",
|
||||
"withdrawConfirmBody": "សេចក្តីថ្លែងការណ៍ «របៀបដែលយើងផ្ទៀងផ្ទាត់» របស់អ្នកនឹងត្រូវដកចេញពីប្រវត្តិរូបរបស់អ្នក។ អ្នកអាចផ្សព្វផ្សាយមួយថ្មីបាននៅពេលណាក៏បាន។"
|
||||
},
|
||||
|
||||
@@ -1128,6 +1128,7 @@
|
||||
"loginGateTitle": "د تصدیق کوونکي کیدو لپاره ننوځئ",
|
||||
"loginGateBody": "د تصدیق کوونکي بیان د خپرولو لپاره باید ننوتلي اوسئ.",
|
||||
"howWeVerifyTitle": "موږ څنګه تصدیق کوو",
|
||||
"requestVerification": "د تایید غوښتنه وکړئ",
|
||||
"withdrawConfirmTitle": "خپل د تصدیق کوونکي بیان بیرته اخلئ؟",
|
||||
"withdrawConfirmBody": "ستاسو د \"موږ څنګه تصدیق کوو\" بیان به ستاسو له پروفایل څخه لرې شي. تاسو کولی شئ هر وخت یو نوی خپور کړئ."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -1570,6 +1570,7 @@
|
||||
"loginGateTitle": "Войдите, чтобы стать верификатором",
|
||||
"loginGateBody": "Вы должны войти, чтобы опубликовать заявление верификатора.",
|
||||
"howWeVerifyTitle": "Как мы проверяем",
|
||||
"requestVerification": "Запросить проверку",
|
||||
"withdrawConfirmTitle": "Отозвать ваше заявление верификатора?",
|
||||
"withdrawConfirmBody": "Ваше заявление «Как мы проверяем» будет удалено из вашего профиля. Вы можете опубликовать новое в любой момент."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -1128,6 +1128,7 @@
|
||||
"loginGateTitle": "登入以成為驗證者",
|
||||
"loginGateBody": "你需要登入才能發布驗證者聲明。",
|
||||
"howWeVerifyTitle": "我們如何驗證",
|
||||
"requestVerification": "請求驗證",
|
||||
"withdrawConfirmTitle": "撤回你的驗證者聲明?",
|
||||
"withdrawConfirmBody": "你的「我們如何驗證」聲明將從你的個人資料中移除。你可以隨時發布新的聲明。"
|
||||
},
|
||||
|
||||
@@ -1128,6 +1128,7 @@
|
||||
"loginGateTitle": "登录以成为验证者",
|
||||
"loginGateBody": "你需要登录才能发布验证者声明。",
|
||||
"howWeVerifyTitle": "我们如何验证",
|
||||
"requestVerification": "请求验证",
|
||||
"withdrawConfirmTitle": "撤回你的验证者声明?",
|
||||
"withdrawConfirmBody": "你的「我们如何验证」声明将从你的个人资料中移除。你可以随时发布新的声明。"
|
||||
},
|
||||
|
||||
+45
-11
@@ -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<string> {
|
||||
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<string | null>(null);
|
||||
const requestedPeer = getValidPubkey(searchParams.get('to'));
|
||||
const [selectedPeer, setSelectedPeer] = useState<string | null>(() => requestedPeer);
|
||||
const [search, setSearch] = useState('');
|
||||
const [conversationFilter, setConversationFilter] = useState<'all' | 'friends'>('all');
|
||||
const [hiddenMutedPeers, setHiddenMutedPeers] = useState<Set<string>>(() => 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<DirectMessageThreadTarget | null>(() => {
|
||||
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() {
|
||||
<div
|
||||
className={cn(
|
||||
'h-full min-h-0 flex-col bg-muted/40',
|
||||
selected && 'hidden md:flex',
|
||||
!selected && 'flex',
|
||||
selectedThread && 'hidden md:flex',
|
||||
!selectedThread && 'flex',
|
||||
)}
|
||||
>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
@@ -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() {
|
||||
</div>
|
||||
|
||||
{/* Thread */}
|
||||
<div className={cn('h-full min-h-0 min-w-0', !selected && 'hidden md:block')}>
|
||||
{selected ? (
|
||||
<MessageThread conversation={selected} onBack={() => setSelectedPeer(null)} onMuted={handleMuted} />
|
||||
<div className={cn('h-full min-h-0 min-w-0', !selectedThread && 'hidden md:block')}>
|
||||
{selectedThread ? (
|
||||
<MessageThread conversation={selectedThread} onBack={clearSelectedPeer} onMuted={handleMuted} />
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-background via-muted/20 to-primary/5 p-8 text-center">
|
||||
<div className="max-w-sm space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user