import UserAvatar from '@/components/UserAvatar' import { formatTimestamp } from '@/lib/timestamp' import { cn } from '@/lib/utils' import { useDM } from '@/providers/DMProvider' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import indexedDb from '@/services/indexed-db.service' import { TDirectMessage, TProfile } from '@/types' import { ChevronDown, ChevronUp, Loader2, MoreVertical, RefreshCw, Settings, Trash2, Undo2, Users, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Button } from '../ui/button' import { ScrollArea } from '../ui/scroll-area' import { Checkbox } from '../ui/checkbox' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu' import MessageComposer from './MessageComposer' import MessageContent from './MessageContent' import MessageInfoModal from './MessageInfoModal' import ConversationSettingsModal from './ConversationSettingsModal' import { useFollowList } from '@/providers/FollowListProvider' interface MessageViewProps { onBack?: () => void hideHeader?: boolean } export default function MessageView({ onBack, hideHeader }: MessageViewProps) { const { t } = useTranslation() const { pubkey } = useNostr() const { currentConversation, messages, isLoadingConversation, isNewConversation, clearNewConversationFlag, reloadConversation, // Selection mode selectedMessages, isSelectionMode, toggleMessageSelection, clearSelection, deleteSelectedMessages, deleteAllInConversation, undeleteAllInConversation } = useDM() const { followingSet } = useFollowList() const [profile, setProfile] = useState(null) const scrollRef = useRef(null) const [selectedMessage, setSelectedMessage] = useState(null) const [messageInfoOpen, setMessageInfoOpen] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false) const [selectedRelays, setSelectedRelays] = useState([]) const [showPulse, setShowPulse] = useState(false) const [showJumpButton, setShowJumpButton] = useState(false) const [newMessageCount, setNewMessageCount] = useState(0) const lastMessageCountRef = useRef(0) const isAtBottomRef = useRef(true) // Progressive loading: start with 20 messages, load more on demand const [visibleLimit, setVisibleLimit] = useState(20) const LOAD_MORE_INCREMENT = 20 const isFollowing = currentConversation ? followingSet.has(currentConversation) : false // Calculate visible messages (show most recent, allow loading older) const hasMoreMessages = messages.length > visibleLimit const visibleMessages = hasMoreMessages ? messages.slice(-visibleLimit) // Show last N messages (most recent) : messages // Load more older messages const loadMoreMessages = () => { setVisibleLimit((prev) => prev + LOAD_MORE_INCREMENT) } // Reset visible limit when conversation changes useEffect(() => { setVisibleLimit(20) }, [currentConversation]) // Handle pulsing animation for new conversations useEffect(() => { if (isNewConversation) { setShowPulse(true) const timer = setTimeout(() => { setShowPulse(false) clearNewConversationFlag() }, 10000) return () => clearTimeout(timer) } }, [isNewConversation, clearNewConversationFlag]) useEffect(() => { if (!currentConversation) return const fetchProfileData = async () => { try { const profileData = await client.fetchProfile(currentConversation) if (profileData) { setProfile(profileData) } } catch (error) { console.error('Failed to fetch profile:', error) } } fetchProfileData() }, [currentConversation]) // Load saved relay settings when conversation changes useEffect(() => { if (!currentConversation || !pubkey) return const loadRelaySettings = async () => { const saved = await indexedDb.getConversationRelaySettings(pubkey, currentConversation) setSelectedRelays(saved || []) } loadRelaySettings() }, [currentConversation, pubkey]) // Save relay settings when they change const handleRelaysChange = async (relays: string[]) => { setSelectedRelays(relays) if (pubkey && currentConversation) { await indexedDb.putConversationRelaySettings(pubkey, currentConversation, relays) } } // Handle scroll position tracking const handleScroll = () => { if (!scrollRef.current) return const { scrollTop, scrollHeight, clientHeight } = scrollRef.current const distanceFromBottom = scrollHeight - scrollTop - clientHeight const atBottom = distanceFromBottom < 100 // 100px threshold isAtBottomRef.current = atBottom setShowJumpButton(!atBottom) // Reset new message count when user scrolls to bottom if (atBottom) { setNewMessageCount(0) lastMessageCountRef.current = messages.length } } // Track new messages when scrolled up useEffect(() => { if (!isAtBottomRef.current && messages.length > lastMessageCountRef.current) { setNewMessageCount(messages.length - lastMessageCountRef.current) } else if (isAtBottomRef.current) { lastMessageCountRef.current = messages.length } }, [messages.length]) // Scroll to bottom when messages change (only if already at bottom) useEffect(() => { if (scrollRef.current && isAtBottomRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight lastMessageCountRef.current = messages.length } }, [messages]) // Scroll to bottom function const scrollToBottom = () => { if (scrollRef.current) { scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }) setNewMessageCount(0) lastMessageCountRef.current = messages.length isAtBottomRef.current = true setShowJumpButton(false) } } // Reset scroll state when conversation changes useEffect(() => { isAtBottomRef.current = true setShowJumpButton(false) setNewMessageCount(0) lastMessageCountRef.current = 0 }, [currentConversation]) // Scroll to bottom when conversation opens and messages are loaded const hasMessages = messages.length > 0 useEffect(() => { if (currentConversation && hasMessages && scrollRef.current) { // Use requestAnimationFrame to ensure DOM is ready requestAnimationFrame(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight lastMessageCountRef.current = messages.length } }) } }, [currentConversation, hasMessages]) if (!currentConversation || !pubkey) { return null } const displayName = profile?.username || currentConversation.slice(0, 8) + '...' return (
{/* Header - show when not hidden, or when in selection mode */} {(!hideHeader || isSelectionMode) && (
{isSelectionMode ? ( // Selection mode header <>
{t('Delete')}
) : ( // Normal header <>
{displayName} {isFollowing && ( )}
{profile?.nip05 && ( {profile.nip05} )}
{t('Delete All')} {t('Undelete All')} {onBack && ( )} )}
)} {/* Messages */}
{isLoadingConversation && messages.length === 0 ? (
) : messages.length === 0 ? (

{t('No messages yet. Send one to start the conversation!')}

) : (
{/* Load more button at top */} {hasMoreMessages && (
)} {isLoadingConversation && (
)} {visibleMessages.map((message) => { const isOwn = message.senderPubkey === pubkey const isSelected = selectedMessages.has(message.id) return (
{/* Checkbox - shows on hover or when in selection mode */}
toggleMessageSelection(message.id)} className="mt-2" />
{formatTimestamp(message.createdAt)}
) })}
)}
{/* Jump to newest button */} {showJumpButton && ( )}
{/* Composer */}
{/* Message Info Modal */} {/* Conversation Settings Modal */}
) }