Refactor chat page useEffect organization and improve compose intent handling

Reorganizes useEffect hooks in chat/page.tsx to follow a helper-functions-first structure, improving readability and maintainability. Adds error handling and user feedback for compose intent, preventing UI flash while processing. In send/route.ts, ensures remote user handles are fully qualified and adds debug logging. Temporarily disables key caching in keys/route.ts to address development key mismatches.
This commit is contained in:
Christopher
2026-01-28 07:37:27 -08:00
parent a21af87144
commit 93a587f185
3 changed files with 148 additions and 126 deletions
+7 -6
View File
@@ -46,12 +46,13 @@ export async function GET(request: NextRequest) {
), ),
}); });
if (cached) { // DEBUG: Force refresh for now to fix development key mismatches
console.log('[Chat Keys GET] Found cached remote key'); // if (cached) {
return NextResponse.json({ // console.log('[Chat Keys GET] Found cached remote key');
publicKey: cached.publicKey, // return NextResponse.json({
}); // publicKey: cached.publicKey,
} // });
// }
// If not in cache, try to resolve DID to a node // If not in cache, try to resolve DID to a node
console.log('[Chat Keys GET] resolving DID to node...'); console.log('[Chat Keys GET] resolving DID to node...');
+8 -1
View File
@@ -114,11 +114,18 @@ export async function POST(request: NextRequest) {
}); });
if (!registryEntry) { if (!registryEntry) {
console.error('Recipient DID not found in registry:', recipientDid);
return NextResponse.json({ error: 'Recipient not found in registry' }, { status: 404 }); return NextResponse.json({ error: 'Recipient not found in registry' }, { status: 404 });
} }
const targetDomain = registryEntry.nodeDomain; const targetDomain = registryEntry.nodeDomain;
const targetHandle = registryEntry.handle; // e.g. user@domain // Ensure handle is fully qualified for remote users
let targetHandle = registryEntry.handle;
if (!targetHandle.includes('@') && targetDomain) {
targetHandle = `${targetHandle}@${targetDomain}`;
}
console.log(`[Remote Send] Sending to ${targetHandle} at ${targetDomain}`);
// 2. Prepare Payload // 2. Prepare Payload
const messageData = { const messageData = {
+133 -119
View File
@@ -63,64 +63,6 @@ export default function ChatPage() {
// Track the current conversation ID to prevent race conditions // Track the current conversation ID to prevent race conditions
const currentConversationIdRef = useRef<string | null>(null); const currentConversationIdRef = useRef<string | null>(null);
// Load conversations
useEffect(() => {
if (user && isReady) {
loadConversations(true); // Initial load with spinner
// Poll for new conversations every 5 seconds (no spinner)
const pollInterval = setInterval(() => {
loadConversations(false);
}, 5000);
return () => clearInterval(pollInterval);
}
}, [user, isReady]);
// Handle Compose Intent
useEffect(() => {
if (composeHandle && isReady && !selectedConversation && conversations.length >= 0) {
// Check if we already have a conversation with this user
const existing = conversations.find(c =>
c.participant2.handle.toLowerCase() === composeHandle.toLowerCase()
);
if (existing) {
setSelectedConversation(existing);
// Clear the query param so refresh doesn't keep resetting state
router.replace('/chat', { scroll: false });
} else if (!loading) {
// Fetch user details to create a draft conversation
const fetchUserAndInitDraft = async () => {
try {
const res = await fetch(`/api/users/${encodeURIComponent(composeHandle)}`);
const data = await res.json();
if (data.user) {
const draftConv: Conversation = {
id: 'new',
participant2: {
handle: data.user.handle,
displayName: data.user.displayName || data.user.handle,
avatarUrl: data.user.avatarUrl,
did: data.user.did
},
lastMessageAt: new Date().toISOString(),
lastMessagePreview: 'New Conversation',
unreadCount: 0
};
setSelectedConversation(draftConv);
router.replace('/chat', { scroll: false });
}
} catch (e) {
console.error("Failed to load user for compose", e);
}
};
fetchUserAndInitDraft();
}
}
}, [composeHandle, isReady, selectedConversation, conversations, loading, router]);
// Legacy / V2 Hybrid State // Legacy / V2 Hybrid State
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [conversationToDelete, setConversationToDelete] = useState<Conversation | null>(null); const [conversationToDelete, setConversationToDelete] = useState<Conversation | null>(null);
@@ -130,6 +72,10 @@ export default function ChatPage() {
const messagesContainerRef = useRef<HTMLDivElement>(null); const messagesContainerRef = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true); const [isAtBottom, setIsAtBottom] = useState(true);
// ============================================
// HELPER FUNCTIONS (Defined before useEffects)
// ============================================
// Check if user is scrolled to bottom // Check if user is scrolled to bottom
const checkIfAtBottom = () => { const checkIfAtBottom = () => {
if (!messagesContainerRef.current) return true; if (!messagesContainerRef.current) return true;
@@ -150,56 +96,6 @@ export default function ChatPage() {
} }
}; };
// Redirect if not logged in
useEffect(() => {
if (user === null) {
router.push('/login');
}
}, [user, router]);
// Load messages when conversation is selected
useEffect(() => {
if (selectedConversation && isReady) {
// Update current conversation ref
currentConversationIdRef.current = selectedConversation.id;
// Clear messages immediately to prevent flash
setMessages([]);
if (selectedConversation.id === 'new') {
setLoadingMessages(false);
return; // Don't load messages for new/draft conversation
}
setLoadingMessages(true);
loadMessages(selectedConversation.id);
markAsRead(selectedConversation.id);
// Poll for new messages every 3 seconds
const pollInterval = setInterval(() => {
// Only load if still the same conversation
if (currentConversationIdRef.current === selectedConversation.id && selectedConversation.id !== 'new') {
loadMessages(selectedConversation.id);
}
}, 3000);
return () => clearInterval(pollInterval);
} else if (!selectedConversation) {
// Clear messages when no conversation selected
currentConversationIdRef.current = null;
setMessages([]);
setLoadingMessages(false);
}
}, [selectedConversation, isReady]);
// Auto-scroll to bottom of messages only if user was already at bottom
useEffect(() => {
if (messagesEndRef.current && isAtBottom) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, isAtBottom]);
const loadConversations = async (isInitialLoad = false) => { const loadConversations = async (isInitialLoad = false) => {
if (isInitialLoad) setLoading(true); if (isInitialLoad) setLoading(true);
try { try {
@@ -247,12 +143,6 @@ export default function ChatPage() {
// - We need the OTHER party's public key // - We need the OTHER party's public key
// - We use OUR private key // - We use OUR private key
// console.log('[Chat UI] Decrypting message:', {
// isSentByMe: msg.isSentByMe,
// recipientDid: envelope.recipientDid,
// senderPublicKey: envelope.senderPublicKey?.substring(0, 20) + '...'
// });
// If I sent this message, the "other party" is the recipient // If I sent this message, the "other party" is the recipient
// If I received this message, the "other party" is the sender // If I received this message, the "other party" is the sender
let otherPartyPublicKey = envelope.senderPublicKey; let otherPartyPublicKey = envelope.senderPublicKey;
@@ -264,13 +154,12 @@ export default function ChatPage() {
if (keyRes.ok) { if (keyRes.ok) {
const keyData = await keyRes.json(); const keyData = await keyRes.json();
otherPartyPublicKey = keyData.publicKey; otherPartyPublicKey = keyData.publicKey;
// console.log('[Chat UI] Fetched recipient public key:', otherPartyPublicKey?.substring(0, 20) + '...');
} }
} catch (e) { } catch (e) {
console.error('[Chat UI] Failed to fetch recipient key:', e); console.error('[Chat UI] Failed to fetch recipient key:', e);
} }
} else { } else {
// console.log('[Chat UI] Using sender public key from envelope'); // Using sender public key from envelope
} }
const plaintext = await decryptMessage( const plaintext = await decryptMessage(
@@ -390,9 +279,6 @@ export default function ChatPage() {
return; return;
} }
// Previously we auto-sent "👋" here.
// Now we just setup the draft conversation.
// Check if existing conversation // Check if existing conversation
const existing = conversations.find(c => const existing = conversations.find(c =>
c.participant2.handle.toLowerCase() === data.user.handle.toLowerCase() c.participant2.handle.toLowerCase() === data.user.handle.toLowerCase()
@@ -454,6 +340,125 @@ export default function ChatPage() {
} }
}; };
// ============================================
// EFFECTS (Now that functions are defined)
// ============================================
// Load conversations
useEffect(() => {
if (user && isReady) {
loadConversations(true); // Initial load with spinner
// Poll for new conversations every 5 seconds (no spinner)
const pollInterval = setInterval(() => {
loadConversations(false);
}, 5000);
return () => clearInterval(pollInterval);
}
}, [user, isReady]);
// Handle Compose Intent
useEffect(() => {
if (composeHandle && isReady && !selectedConversation && conversations.length >= 0) {
// Check if we already have a conversation with this user
const existing = conversations.find(c =>
c.participant2.handle.toLowerCase() === composeHandle.toLowerCase()
);
if (existing) {
setSelectedConversation(existing);
// Clear the query param so refresh doesn't keep resetting state
router.replace('/chat', { scroll: false });
} else if (!loading) {
// Fetch user details to create a draft conversation
const fetchUserAndInitDraft = async () => {
try {
const res = await fetch(`/api/users/${encodeURIComponent(composeHandle)}`);
const data = await res.json();
if (data.user) {
const draftConv: Conversation = {
id: 'new',
participant2: {
handle: data.user.handle,
displayName: data.user.displayName || data.user.handle,
avatarUrl: data.user.avatarUrl,
did: data.user.did
},
lastMessageAt: new Date().toISOString(),
lastMessagePreview: 'New Conversation',
unreadCount: 0
};
setSelectedConversation(draftConv);
router.replace('/chat', { scroll: false });
} else {
// User not found, clear compose param to show list
console.error('User not found for compose');
router.replace('/chat');
}
} catch (e) {
console.error("Failed to load user for compose", e);
router.replace('/chat');
}
};
fetchUserAndInitDraft();
}
}
}, [composeHandle, isReady, selectedConversation, conversations, loading, router]);
// Redirect if not logged in
useEffect(() => {
if (user === null) {
router.push('/login');
}
}, [user, router]);
// Load messages when conversation is selected
useEffect(() => {
if (selectedConversation && isReady) {
// Update current conversation ref
currentConversationIdRef.current = selectedConversation.id;
// Clear messages immediately to prevent flash
setMessages([]);
if (selectedConversation.id === 'new') {
setLoadingMessages(false);
return; // Don't load messages for new/draft conversation
}
setLoadingMessages(true);
loadMessages(selectedConversation.id);
markAsRead(selectedConversation.id);
// Poll for new messages every 3 seconds
const pollInterval = setInterval(() => {
// Only load if still the same conversation
if (currentConversationIdRef.current === selectedConversation.id && selectedConversation.id !== 'new') {
loadMessages(selectedConversation.id);
}
}, 3000);
return () => clearInterval(pollInterval);
} else if (!selectedConversation) {
// Clear messages when no conversation selected
currentConversationIdRef.current = null;
setMessages([]);
setLoadingMessages(false);
}
}, [selectedConversation, isReady]);
// Auto-scroll to bottom of messages only if user was already at bottom
useEffect(() => {
if (messagesEndRef.current && isAtBottom) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, isAtBottom]);
// ============================================
// RENDER LOGIC
// ============================================
const filteredConversations = conversations.filter((conv) => const filteredConversations = conversations.filter((conv) =>
conv.participant2.displayName?.toLowerCase().includes(searchQuery.toLowerCase()) || conv.participant2.displayName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -511,6 +516,15 @@ export default function ChatPage() {
); );
} }
// Prevent flash of list view while processing compose intent
if (composeHandle && !selectedConversation) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', alignItems: 'center', justifyContent: 'center' }}>
<Loader2 className="animate-spin" size={32} />
</div>
);
}
// Thread View // Thread View
if (selectedConversation) { if (selectedConversation) {
return ( return (