import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { nip19 } from 'nostr-tools'; import { Layout } from '@/components/Layout'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useConversations, formatPubkeyShort } from '@/hooks/useDirectMessages'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { formatDistanceToNow } from 'date-fns'; import { MessageCircle, Plus, Search, Lock, AlertCircle, ShieldCheck, X, } from 'lucide-react'; import { cn } from '@/lib/utils'; /** * Dismissible phishing-warning banner shown once at the top of the DM page. * Standard practice (Alby, Metamask, Muun) — protects users from social-engineering * attacks where a bad actor pretends to be "SkateSpots Support" via DM. */ function PhishingWarningBanner() { const [dismissed, setDismissed] = useLocalStorage('dm-phishing-warning-dismissed', false); if (dismissed) return null; return (

Security reminder

SkateSpots will never DM you asking to verify your account, share your keys, or send payments. If someone claims to be SkateSpots support via DM — it's a scam.

); } // Conversation preview component function ConversationPreview({ pubkey, lastMessage }: { pubkey: string; lastMessage?: { content: string; createdAt: number; pubkey: string }; }) { const author = useAuthor(pubkey); const { user } = useCurrentUser(); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(pubkey); const avatar = metadata?.picture; const isFromMe = lastMessage?.pubkey === user?.pubkey; const timeAgo = lastMessage ? formatDistanceToNow(new Date(lastMessage.createdAt * 1000), { addSuffix: true }) : ''; return (
{displayName.slice(0, 2).toUpperCase()}

{displayName}

{timeAgo && ( {timeAgo} )}
{lastMessage && (

{isFromMe && You: } {lastMessage.content}

)}
); } function ConversationSkeleton() { return (
); } export default function DirectMessagesPage() { const { user } = useCurrentUser(); const { data: conversations, isLoading, error } = useConversations(); const [showNewMessage, setShowNewMessage] = useState(false); const [newRecipient, setNewRecipient] = useState(''); const [recipientError, setRecipientError] = useState(''); useSeoMeta({ title: 'Messages - SkateSpots', description: 'Private encrypted messages with the skateboarding community', }); const handleStartConversation = () => { setRecipientError(''); // Try to parse the input as npub or hex pubkey let pubkey = newRecipient.trim(); try { if (pubkey.startsWith('npub1')) { const decoded = nip19.decode(pubkey); if (decoded.type === 'npub') { pubkey = decoded.data; } else { throw new Error('Invalid npub'); } } else if (!/^[a-f0-9]{64}$/i.test(pubkey)) { throw new Error('Invalid pubkey format'); } // Navigate to conversation window.location.href = `/messages/${nip19.npubEncode(pubkey)}`; } catch { setRecipientError('Please enter a valid npub or pubkey'); } }; // Not logged in if (!user) { return (

Log in to access messages

Private, encrypted messaging with skaters in the community

); } // Check if NIP-44 is available if (!user.signer?.nip44) { const isAndroid = /Android/i.test(navigator.userAgent); const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); const isMobile = isAndroid || isIOS; return (

Secure Login Required

Private messages require a secure signer app to encrypt your conversations.

{isMobile ? (
{isAndroid && (

On Android: Use the Amber app

Get Amber →
)} {isIOS && (

On iOS: Try logging in with a bunker connection (nsec.app) from Settings

)}
) : (

Try logging in with a browser extension like Alby or nos2x that supports encryption.

)}
); } return (
{/* Phishing warning — shown once, dismissible */} {/* Header */}

MESSAGES

End-to-end encrypted with NIP-17 Encrypted

{/* Conversations List */}
{isLoading ? ( <> ) : error ? (

Failed to load messages

) : conversations && conversations.length > 0 ? ( conversations.map((conv) => ( )) ) : (

No messages yet

Start a conversation with someone in the community

)}
{/* New Message Dialog */} New Message Enter the npub or pubkey of the person you want to message
{ setNewRecipient(e.target.value); setRecipientError(''); }} placeholder="npub1... or hex pubkey" className="bg-black border-gray-700 text-white font-mono text-sm h-11" /> {recipientError && (

{recipientError}

)}
); }