import { useOlmContext } from "@/contexts/olm-context"; import { useSocketContext } from "@/contexts/socket-context"; import { clearUnread, db, sendMessage } from "@/lib/db"; import { useLiveQuery } from "dexie-react-hooks"; import { KeyRound } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Avatar, AvatarFallback, AvatarImage } from "../avatar"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../dialog"; import { Input } from "../input"; interface DMChannelContentProps { userId: string channelId: string participantDetails: SiPher.ParticipantDetail[] } export default function DMChannelContent( { userId, channelId, participantDetails, }: DMChannelContentProps ) { const otherUser = useMemo(() => { return participantDetails.find((p) => p.id !== userId); }, [participantDetails, userId]); const [olmSession, setOlmSession] = useState(null); const [sessionError, setSessionError] = useState(null); const [messageInput, setMessageInput] = useState(""); const [messageLimit, setMessageLimit] = useState(50); const [isLoadingMore, setIsLoadingMore] = useState(false); const { sendMessage: sendMessageToServer } = useSocketContext(); const { olmAccount, password, isReady, getSession } = useOlmContext(); const messagesEndRef = React.useRef(null); const scrollContainerRef = React.useRef(null); const prevScrollHeightRef = React.useRef(0); // Get total message count const totalMessageCount = useLiveQuery( () => db.messages.where("channelId").equals(channelId).count(), [channelId] ) ?? 0; // Get messages from the local database with pagination const allMessages = useLiveQuery( () => db.messages.where("channelId").equals(channelId).sortBy("timestamp"), [channelId] ) ?? []; // Take only the most recent messages based on limit const messages = useMemo(() => { return allMessages.slice(-messageLimit); }, [allMessages, messageLimit]); const hasMoreMessages = messages.length < totalMessageCount; // Reset message limit when channel changes useEffect(() => { setMessageLimit(50); }, [channelId]); // Handle scroll to load more messages const handleScroll = React.useCallback(async (e: React.UIEvent) => { const target = e.currentTarget; const scrollTop = target.scrollTop; // If scrolled near the top (within 100px) and there are more messages if (scrollTop < 100 && hasMoreMessages && !isLoadingMore) { setIsLoadingMore(true); // Save current scroll height prevScrollHeightRef.current = target.scrollHeight; // Load 50 more messages await new Promise(resolve => setTimeout(resolve, 100)); // Small delay to avoid rapid firing setMessageLimit(prev => prev + 50); setIsLoadingMore(false); } }, [hasMoreMessages, isLoadingMore]); // Preserve scroll position after loading more messages useEffect(() => { if (prevScrollHeightRef.current > 0 && scrollContainerRef.current) { const newScrollHeight = scrollContainerRef.current.scrollHeight; const scrollDiff = newScrollHeight - prevScrollHeightRef.current; scrollContainerRef.current.scrollTop += scrollDiff; prevScrollHeightRef.current = 0; } }, [messages.length]); // Scroll to bottom on initial load / channel change (instant) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "auto" }); }, [channelId]); // Auto-scroll to bottom when new messages arrive (smooth) useEffect(() => { if (messages.length > 0 && scrollContainerRef.current) { const container = scrollContainerRef.current; const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 200; // Only auto-scroll if user is near the bottom if (isNearBottom) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } } }, [allMessages.length]); // Clear unread count when entering the channel useEffect(() => { clearUnread(channelId); console.debug("[DMChannelContent] Cleared unread count for channel", channelId); }, [channelId]); // Guard: Check if otherUser exists if (!otherUser) { return (

Loading participant information...

); } // Get or create session when OLM is ready and we have the other user's account useEffect(() => { const loadSession = async () => { if (!isReady || !olmAccount || !otherUser || !otherUser.olmAccount) { return; } setSessionError(null); try { const session = await getSession(otherUser.id, { identityKey: otherUser.olmAccount.identityKey, oneTimeKeys: otherUser.olmAccount.oneTimeKeys, }); if (session) { setOlmSession(session); } else { setSessionError("Failed to create encryption session"); } } catch (err) { console.error("[DMChannelContent] Failed to get session:", err); setSessionError(err instanceof Error ? err.message : "Unknown error"); } }; loadSession(); }, [isReady, olmAccount, otherUser, password, getSession]) // Check if OLM is ready if (!isReady || !olmAccount) { return
Loading encryption keys...
} // Get the other user's id key and OT keys from the server to be prepared for messaging if (!otherUser.olmAccount) { return ( { }}>
Encryption Setup Required

{otherUser.name} hasn't set up end-to-end encryption yet.

They need to log in and complete the encryption setup

Once complete, you'll be able to send encrypted messages

🔒 All messages are end-to-end encrypted for your privacy

) } // Show error if session creation failed if (sessionError) { return (

Failed to create encryption session

{sessionError}

); } // Wait for session to be established if (!olmSession) { return (

Establishing secure connection...

); } return (
{/* Load more indicator */} {hasMoreMessages && (
{isLoadingMore ? (
Loading older messages...
) : ( )}
)} {messages.map((msg, index) => { const sender = participantDetails.find((p) => p.id === msg.fromUserId); const selfDetail = participantDetails.find((p) => p.id === userId); const isSelf = msg.fromUserId === userId; const displayName = isSelf ? selfDetail?.displayUsername ?? selfDetail?.username ?? selfDetail?.name ?? "You" : (sender?.displayUsername ?? sender?.username ?? sender?.name ?? "Unknown"); const timeLabel = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""; // Check if this message is from the same user as the previous one within 5 minutes const prevMsg = index > 0 ? messages[index - 1] : null; const isGrouped = prevMsg && prevMsg.fromUserId === msg.fromUserId && msg.timestamp && prevMsg.timestamp && (msg.timestamp - prevMsg.timestamp) < 5 * 60 * 1000; return (
{!isGrouped ? ( // Full message with avatar and header
{displayName.slice(0, 2).toUpperCase()}
{displayName} {timeLabel}
{msg.content}
) : ( // Compact message without avatar (grouped)
{new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
{msg.content}
)}
); })} {/* Invisible element for auto-scrolling */}
{/* Message input */}
setMessageInput(e.target.value)} onKeyDown={async (e) => { if (e.key === 'Enter' && !e.shiftKey && messageInput.trim() && password) { e.preventDefault(); try { const messageId = await sendMessage({ channelId, content: messageInput, fromUserId: userId, to: otherUser.id, timestamp: Date.now(), status: "sent", }, olmSession, sendMessageToServer, { userId, recipientId: otherUser.id, password, }); if (messageId) { setMessageInput(""); } } catch (error) { console.error("[DMChannelContent] Failed to send message:", error); toast.error("Failed to send message: " + (error instanceof Error ? error.message : "Unknown error")); } } }} />
); }