feat: Add chat management actions including clear and delete conversation, and enhance session token refresh logic.

This commit is contained in:
pradeepkumar
2026-03-09 23:47:28 +05:30
parent ff5562a573
commit 7d641eec12
6 changed files with 254 additions and 22 deletions

View File

@@ -0,0 +1,66 @@
'use client';
import { useRef, useEffect } from 'react';
export interface DropdownMenuItem {
label: string;
onClick: () => void;
danger?: boolean;
}
interface ChatDropdownMenuProps {
items: DropdownMenuItem[];
isOpen: boolean;
onClose: () => void;
}
export function ChatDropdownMenu({ items, isOpen, onClose }: ChatDropdownMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
ref={menuRef}
className="absolute right-0 top-full mt-1 bg-white border border-[#00293d]/10 rounded-[10px] shadow-lg z-50 min-w-[200px] py-1 overflow-hidden"
>
{items.map((item, index) => (
<button
key={index}
onClick={() => {
item.onClick();
onClose();
}}
className={`w-full text-left px-4 py-2.5 font-serif font-normal text-[14px] leading-[19px] transition-colors cursor-pointer ${
item.danger
? 'text-red-600 hover:bg-red-50'
: 'text-[#00293D] hover:bg-[#00293d]/5'
}`}
>
{item.label}
</button>
))}
</div>
);
}

View File

@@ -1,6 +1,8 @@
'use client'; 'use client';
import { useState, useCallback } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu';
interface ChatHeaderProps { interface ChatHeaderProps {
name: string; name: string;
@@ -12,6 +14,8 @@ interface ChatHeaderProps {
expertise?: string[]; expertise?: string[];
isTyping?: boolean; isTyping?: boolean;
typingText?: string; typingText?: string;
onClearChat?: () => void;
onDeleteConversation?: () => void;
} }
export function ChatHeader({ export function ChatHeader({
@@ -24,7 +28,42 @@ export function ChatHeader({
expertise = [], expertise = [],
isTyping = false, isTyping = false,
typingText, typingText,
onClearChat,
onDeleteConversation,
}: ChatHeaderProps) { }: ChatHeaderProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const menuItems: DropdownMenuItem[] = [
{
label: 'Clear Chat',
onClick: () => onClearChat?.(),
},
{
label: isMuted ? 'Unmute Notifications' : 'Mute Notifications',
onClick: () => setIsMuted((prev) => !prev),
},
{
label: 'Report User',
onClick: () => {
// Placeholder — no backend endpoint yet
},
},
{
label: 'Delete Conversation',
onClick: () => onDeleteConversation?.(),
danger: true,
},
];
const handleToggleMenu = useCallback(() => {
setIsMenuOpen((prev) => !prev);
}, []);
const handleCloseMenu = useCallback(() => {
setIsMenuOpen(false);
}, []);
return ( return (
<div className="border-b border-[#00293d]/10 pb-4"> <div className="border-b border-[#00293d]/10 pb-4">
{/* Top row - Name, status, actions */} {/* Top row - Name, status, actions */}
@@ -46,14 +85,24 @@ export function ChatHeader({
</span> </span>
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"> <div className="relative">
<Image <button
src="/assets/icons/three-dots-horizontal-icon.svg" onClick={handleToggleMenu}
alt="More options" className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
width={24} >
height={24} <Image
src="/assets/icons/three-dots-horizontal-icon.svg"
alt="More options"
width={24}
height={24}
/>
</button>
<ChatDropdownMenu
items={menuItems}
isOpen={isMenuOpen}
onClose={handleCloseMenu}
/> />
</button> </div>
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"> <button className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
<Image <Image
src="/assets/icons/star-outline-icon.svg" src="/assets/icons/star-outline-icon.svg"

View File

@@ -423,14 +423,6 @@ export function MessageInput({ onSend, onSendGif, onSendFile, onTypingStart, onT
> >
{isUploading ? 'Uploading...' : 'Send'} {isUploading ? 'Uploading...' : 'Send'}
</button> </button>
<button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
<Image
src="/assets/icons/three-dots-icon.svg"
alt="More options"
width={24}
height={24}
/>
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,6 +5,7 @@ import Image from 'next/image';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { ChatHeader } from './ChatHeader'; import { ChatHeader } from './ChatHeader';
import { MessageInput } from './MessageInput'; import { MessageInput } from './MessageInput';
import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu';
import { useMessaging } from '@/hooks/useMessaging'; import { useMessaging } from '@/hooks/useMessaging';
import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service'; import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service';
import { uploadService } from '@/services/upload.service'; import { uploadService } from '@/services/upload.service';
@@ -107,9 +108,15 @@ export function MessagingPage() {
loadMoreMessages, loadMoreMessages,
hasMoreMessages, hasMoreMessages,
startConversation, startConversation,
markAllAsRead,
deleteConversation,
clearMessages,
} = useMessaging(); } = useMessaging();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isPageMenuOpen, setIsPageMenuOpen] = useState(false);
const [isAllMuted, setIsAllMuted] = useState(false);
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false);
const [connectedAgents, setConnectedAgents] = useState<ConnectedAgent[]>([]); const [connectedAgents, setConnectedAgents] = useState<ConnectedAgent[]>([]);
@@ -339,6 +346,36 @@ export function MessagingPage() {
stopTyping(); stopTyping();
}, [stopTyping]); }, [stopTyping]);
// Handle delete conversation with confirmation
const handleDeleteConversation = useCallback(async () => {
if (!currentConversation) return;
setConfirmDeleteId(currentConversation.id);
}, [currentConversation]);
const confirmDelete = useCallback(async () => {
if (!confirmDeleteId) return;
try {
await deleteConversation(confirmDeleteId);
} catch (error) {
console.error('Failed to delete conversation:', error);
}
setConfirmDeleteId(null);
}, [confirmDeleteId, deleteConversation]);
// Page header menu items
const pageMenuItems: DropdownMenuItem[] = [
{
label: 'Mark All as Read',
onClick: () => {
markAllAsRead().catch(console.error);
},
},
{
label: isAllMuted ? 'Unmute All Notifications' : 'Mute All Notifications',
onClick: () => setIsAllMuted((prev) => !prev),
},
];
// Handle scroll events // Handle scroll events
const handleScroll = () => { const handleScroll = () => {
if (messagesContainerRef.current) { if (messagesContainerRef.current) {
@@ -420,6 +457,35 @@ export function MessagingPage() {
return ( return (
<> <>
<style>{customScrollbarStyles}</style> <style>{customScrollbarStyles}</style>
{/* Delete confirmation dialog */}
{confirmDeleteId && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-[15px] p-6 max-w-[400px] w-full mx-4 shadow-xl">
<h3 className="font-fractul font-bold text-[16px] text-[#00293D] mb-2">
Delete Conversation
</h3>
<p className="font-serif font-normal text-[14px] text-[#00293D]/70 mb-6">
Are you sure you want to delete this conversation? This action cannot be undone and all messages will be permanently removed.
</p>
<div className="flex items-center justify-end gap-3">
<button
onClick={() => setConfirmDeleteId(null)}
className="px-4 py-2 font-serif font-normal text-[14px] text-[#00293D] rounded-[10px] border border-[#00293d]/10 hover:bg-gray-50 transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={confirmDelete}
className="px-4 py-2 font-serif font-normal text-[14px] text-white bg-red-600 rounded-[10px] hover:bg-red-700 transition-colors cursor-pointer"
>
Delete
</button>
</div>
</div>
</div>
)}
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden"> <div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4 p-4 border-b border-[#00293d]/10"> <div className="flex items-center gap-4 p-4 border-b border-[#00293d]/10">
@@ -456,14 +522,24 @@ export function MessagingPage() {
{/* Header actions */} {/* Header actions */}
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"> <div className="relative">
<Image <button
src="/assets/icons/three-dots-horizontal-icon.svg" onClick={() => setIsPageMenuOpen((prev) => !prev)}
alt="More options" className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
width={24} >
height={24} <Image
src="/assets/icons/three-dots-horizontal-icon.svg"
alt="More options"
width={24}
height={24}
/>
</button>
<ChatDropdownMenu
items={pageMenuItems}
isOpen={isPageMenuOpen}
onClose={() => setIsPageMenuOpen(false)}
/> />
</button> </div>
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"> <button className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
<Image <Image
src="/assets/icons/compose-icon.svg" src="/assets/icons/compose-icon.svg"
@@ -634,6 +710,8 @@ export function MessagingPage() {
{...selectedUserInfo} {...selectedUserInfo}
isTyping={typingUserNames.length > 0} isTyping={typingUserNames.length > 0}
typingText={typingUserNames.length > 0 ? `${typingUserNames.join(', ')} is typing...` : undefined} typingText={typingUserNames.length > 0 ? `${typingUserNames.join(', ')} is typing...` : undefined}
onClearChat={clearMessages}
onDeleteConversation={handleDeleteConversation}
/> />
{/* Messages area wrapper */} {/* Messages area wrapper */}

View File

@@ -40,6 +40,9 @@ interface UseMessagingReturn {
startTyping: () => void; startTyping: () => void;
stopTyping: () => void; stopTyping: () => void;
markAsRead: () => Promise<void>; markAsRead: () => Promise<void>;
markAllAsRead: () => Promise<void>;
deleteConversation: (conversationId: string) => Promise<void>;
clearMessages: () => void;
updateUserStatus: (userId: string, isOnline: boolean) => void; updateUserStatus: (userId: string, isOnline: boolean) => void;
hasMoreMessages: boolean; hasMoreMessages: boolean;
} }
@@ -314,6 +317,40 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
} }
}, [isConnected]); }, [isConnected]);
// Mark all conversations as read
const markAllAsRead = useCallback(async () => {
const unreadConversations = conversations.filter((c) => c.unreadCount > 0);
await Promise.all(
unreadConversations.map((c) => messagesService.markAsRead(c.id))
);
setConversations((prev) =>
prev.map((c) => ({ ...c, unreadCount: 0 }))
);
}, [conversations]);
// Delete a conversation
const deleteConversation = useCallback(
async (conversationId: string) => {
await messagesService.deleteConversation(conversationId);
// Remove from list
setConversations((prev) => prev.filter((c) => c.id !== conversationId));
// Clear current if it was the deleted one
if (currentConversationIdRef.current === conversationId) {
currentConversationIdRef.current = null;
setCurrentConversation(null);
setMessages([]);
}
},
[]
);
// Clear messages from current conversation view (local only)
const clearMessages = useCallback(() => {
setMessages([]);
}, []);
// Update user online status (called from status change events) // Update user online status (called from status change events)
const updateUserStatus = useCallback((userId: string, isOnline: boolean) => { const updateUserStatus = useCallback((userId: string, isOnline: boolean) => {
setConversations((prev) => setConversations((prev) =>
@@ -478,6 +515,9 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
startTyping, startTyping,
stopTyping, stopTyping,
markAsRead, markAsRead,
markAllAsRead,
deleteConversation,
clearMessages,
updateUserStatus, updateUserStatus,
hasMoreMessages, hasMoreMessages,
}; };

View File

@@ -74,6 +74,13 @@ class MessagesService {
await api.patch(`/messages/conversations/${conversationId}/read`); await api.patch(`/messages/conversations/${conversationId}/read`);
} }
/**
* Delete a conversation
*/
async deleteConversation(conversationId: string): Promise<void> {
await api.delete(`/messages/conversations/${conversationId}`);
}
/** /**
* Get total unread message count * Get total unread message count
*/ */