308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import {
|
|
MessageSquare,
|
|
Search,
|
|
Plus,
|
|
Send,
|
|
Paperclip,
|
|
Smile,
|
|
MoreHorizontal,
|
|
Users,
|
|
Circle,
|
|
CheckCheck,
|
|
Clock,
|
|
File,
|
|
Download,
|
|
Brain,
|
|
Shield,
|
|
TrendingUp,
|
|
CheckCircle,
|
|
FileText,
|
|
BarChart3,
|
|
Zap,
|
|
} from "lucide-react"
|
|
import { useSearchParams } from "next/navigation"
|
|
import { PairingProcess } from "@/lib/4nk/models/PairingProcess"
|
|
|
|
interface ChatProps {
|
|
heightClass?: string
|
|
processes?: any
|
|
myProcesses?: string[]
|
|
}
|
|
|
|
export default function Chat({ heightClass = "h-[calc(100vh-8rem)]", processes, myProcesses }: ChatProps) {
|
|
const [selectedConversation, setSelectedConversation] = useState("")
|
|
const [newMessage, setNewMessage] = useState("")
|
|
const [pairingProcesses, setPairingProcesses] = useState<PairingProcess[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
|
|
const searchParams = useSearchParams()
|
|
const userId = searchParams.get("user")
|
|
const messageType = searchParams.get("message")
|
|
const groupType = searchParams.get("type")
|
|
|
|
// Filter pairing processes when processes prop changes
|
|
useEffect(() => {
|
|
if (processes && Object.keys(processes).length > 0) {
|
|
setIsLoading(true)
|
|
|
|
// Filter pairing processes (those with memberPublicName in publicData)
|
|
const pairingList: PairingProcess[] = []
|
|
Object.entries(processes).forEach(([processId, process]) => {
|
|
// Get the latest state
|
|
const latestState = process.states?.[process.states.length - 2] // -2 because last state is usually empty
|
|
|
|
// Check if memberPublicName field exists (even if empty) - indicates pairing process
|
|
if (latestState?.public_data?.hasOwnProperty('memberPublicName')) {
|
|
const memberPublicName = latestState.public_data.memberPublicName || `Pairing ${processId.slice(0, 8)}`
|
|
pairingList.push({
|
|
id: processId,
|
|
memberPublicName: memberPublicName
|
|
})
|
|
}
|
|
})
|
|
|
|
setPairingProcesses(pairingList)
|
|
setIsLoading(false)
|
|
} else {
|
|
setIsLoading(true)
|
|
setPairingProcesses([])
|
|
}
|
|
}, [processes, myProcesses])
|
|
|
|
useEffect(() => {
|
|
if (messageType === "new") {
|
|
if (userId) {
|
|
const messageData = sessionStorage.getItem("newMessage")
|
|
if (messageData) {
|
|
const data = JSON.parse(messageData)
|
|
setSelectedConversation(userId)
|
|
setNewMessage(`${data.subject ? `[${data.subject}] ` : ""}${data.content}`)
|
|
sessionStorage.removeItem("newMessage")
|
|
}
|
|
} else if (groupType === "group") {
|
|
const groupData = sessionStorage.getItem("newGroupMessage")
|
|
if (groupData) {
|
|
const data = JSON.parse(groupData)
|
|
setSelectedConversation("group-new")
|
|
setNewMessage(`${data.subject ? `[${data.subject}] ` : ""}${data.content}`)
|
|
sessionStorage.removeItem("newGroupMessage")
|
|
}
|
|
}
|
|
}
|
|
}, [userId, messageType, groupType])
|
|
|
|
// Create conversations array with pairing processes only
|
|
const conversations = [
|
|
...pairingProcesses.map(process => {
|
|
// Generate avatar from memberPublicName or processId
|
|
let avatar = "P" // Default for Pairing
|
|
if (process.memberPublicName && typeof process.memberPublicName === 'string' && process.memberPublicName.trim().length > 0) {
|
|
// Use memberPublicName if not empty
|
|
avatar = process.memberPublicName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
|
} else {
|
|
// Use first 2 chars of processId if memberPublicName is empty
|
|
avatar = process.id.slice(0, 2).toUpperCase()
|
|
}
|
|
|
|
// Safe display name
|
|
const displayName = typeof process.memberPublicName === 'string' && process.memberPublicName.trim().length > 0
|
|
? process.memberPublicName
|
|
: `Membre ${process.id.slice(0, 8)}`
|
|
|
|
return {
|
|
id: process.id,
|
|
name: displayName,
|
|
avatar: avatar,
|
|
lastMessage: "",
|
|
lastMessageTime: "",
|
|
unreadCount: 0,
|
|
isOnline: true,
|
|
isTyping: false,
|
|
pairingId: process.id
|
|
}
|
|
})
|
|
]
|
|
|
|
const messages: any[] = []
|
|
|
|
// Filter conversations based on search query
|
|
const filteredConversations = conversations.filter(conversation => {
|
|
if (!searchQuery.trim()) return true
|
|
|
|
// Search by ID (process ID)
|
|
const matchesId = conversation.id.toLowerCase().includes(searchQuery.toLowerCase())
|
|
// Search by name
|
|
const matchesName = conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
|
|
return matchesId || matchesName
|
|
})
|
|
|
|
const currentConversation = conversations.find((conv) => conv.id === selectedConversation)
|
|
|
|
const handleSendMessage = () => {
|
|
if (newMessage.trim()) {
|
|
console.log("Sending message:", newMessage)
|
|
setNewMessage("")
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={`${heightClass} flex`}>
|
|
<div className="w-80 border-r bg-white dark:bg-gray-800 flex flex-col">
|
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Messages</h2>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
<Input
|
|
placeholder="Rechercher par ID ou nom..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center p-8">
|
|
<div className="text-gray-500 dark:text-gray-400">Chargement des processus de pairing...</div>
|
|
</div>
|
|
) : filteredConversations.length > 0 ? (
|
|
filteredConversations.map((conversation) => (
|
|
<div
|
|
key={conversation.id}
|
|
onClick={() => setSelectedConversation(conversation.id)}
|
|
className={`p-4 border-b cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
|
selectedConversation === conversation.id
|
|
? "bg-blue-50 dark:bg-blue-900 border-r-2 border-blue-500 dark:border-blue-400"
|
|
: ""
|
|
}`}
|
|
>
|
|
<div className="flex items-start space-x-3">
|
|
<div className="relative">
|
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
|
<span className="text-blue-600 dark:text-blue-400 font-medium">{conversation.avatar}</span>
|
|
</div>
|
|
{conversation.isOnline && (
|
|
<Circle className="absolute -bottom-1 -right-1 h-4 w-4 text-green-500 fill-current" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-medium text-gray-900 dark:text-gray-100 truncate">{conversation.name}</h3>
|
|
</div>
|
|
{'pairingId' in conversation && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 font-mono">
|
|
ID: {conversation.pairingId.slice(0, 8)}...{conversation.pairingId.slice(-4)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="flex items-center justify-center p-8">
|
|
<div className="text-center">
|
|
<Search className="h-8 w-8 mx-auto text-gray-400 dark:text-gray-500 mb-2" />
|
|
<p className="text-gray-500 dark:text-gray-400">Aucun membre trouvé</p>
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
Essayez de rechercher par ID ou nom
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 flex flex-col">
|
|
{currentConversation ? (
|
|
<>
|
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="relative">
|
|
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
|
<span className="text-blue-600 dark:text-blue-400 font-medium">{currentConversation.avatar}</span>
|
|
</div>
|
|
{currentConversation.isOnline && (
|
|
<Circle className="absolute -bottom-1 -right-1 h-3 w-3 text-green-500 fill-current" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium text-gray-900 dark:text-gray-100">{currentConversation.name}</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{currentConversation.isOnline ? "En ligne" : "Hors ligne"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button variant="outline" size="sm">
|
|
<MoreHorizontal className="h-4 w-4 text-gray-900 dark:text-gray-100" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 dark:bg-gray-900">
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<MessageSquare className="h-12 w-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">Aucun message</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">Commencez une conversation en envoyant un message</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
|
<div className="flex items-end space-x-2">
|
|
<Button variant="outline" size="sm">
|
|
<Paperclip className="h-4 w-4 text-gray-900 dark:text-gray-100" />
|
|
</Button>
|
|
<div className="flex-1">
|
|
<Textarea
|
|
placeholder="Tapez votre message..."
|
|
value={newMessage}
|
|
onChange={(e) => setNewMessage(e.target.value)}
|
|
onKeyPress={(e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSendMessage();
|
|
}
|
|
}}
|
|
rows={1}
|
|
className="resize-none bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-700"
|
|
/>
|
|
</div>
|
|
<Button variant="outline" size="sm">
|
|
<Smile className="h-4 w-4 text-gray-900 dark:text-gray-100" />
|
|
</Button>
|
|
<Button onClick={handleSendMessage} disabled={!newMessage.trim()}>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex-1 flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
<div className="text-center">
|
|
<MessageSquare className="h-12 w-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">Sélectionnez une conversation</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">Choisissez une conversation pour commencer à discuter</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|