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:
@@ -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...');
|
||||||
|
|||||||
@@ -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
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user