2026-02-09 23:07:19 +00:00
|
|
|
import React, { useEffect, useRef, useMemo } from 'react';
|
2026-01-15 18:31:40 +00:00
|
|
|
import { useChatStore } from '../store/chatStore';
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { sanitizeChatMessage } from '@/utils/sanitize';
|
|
|
|
|
import {
|
|
|
|
|
MoreVertical,
|
|
|
|
|
Reply,
|
|
|
|
|
Smile,
|
|
|
|
|
ThumbsUp,
|
|
|
|
|
ThumbsDown,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
2026-02-09 23:07:19 +00:00
|
|
|
function formatMessageDate(dateStr: string): string {
|
|
|
|
|
const date = new Date(dateStr);
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const diffDays = Math.floor(
|
|
|
|
|
(now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (diffDays === 0) return 'Today';
|
|
|
|
|
if (diffDays === 1) return 'Yesterday';
|
|
|
|
|
|
|
|
|
|
return date.toLocaleDateString('en-US', {
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DateSeparator({ date }: { date: string }) {
|
|
|
|
|
const formatted = formatMessageDate(date);
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-3 py-3">
|
|
|
|
|
<div className="flex-1 h-px bg-border" />
|
|
|
|
|
<span className="text-caption shrink-0 px-2">{formatted}</span>
|
|
|
|
|
<div className="flex-1 h-px bg-border" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
export function ChatMessages() {
|
2026-01-15 18:31:40 +00:00
|
|
|
const { currentConversationId, conversations, messages, typingUsers } = useChatStore();
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
const { data: user } = useUser();
|
2025-12-03 21:56:50 +00:00
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
2026-01-15 18:31:40 +00:00
|
|
|
// Action 4.5.1.5: Get current conversation from conversations array using ID
|
|
|
|
|
const currentConversation = useMemo(() => {
|
|
|
|
|
if (!currentConversationId) return null;
|
|
|
|
|
return conversations.find((c) => c.id === currentConversationId) || null;
|
|
|
|
|
}, [currentConversationId, conversations]);
|
|
|
|
|
|
|
|
|
|
const conversationMessages = currentConversationId
|
|
|
|
|
? messages[currentConversationId] || []
|
2025-12-03 21:56:50 +00:00
|
|
|
: [];
|
|
|
|
|
|
2026-01-15 18:31:40 +00:00
|
|
|
const typingUserIds = currentConversationId
|
|
|
|
|
? typingUsers[currentConversationId] || []
|
2025-12-03 21:56:50 +00:00
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
}, [conversationMessages]);
|
|
|
|
|
|
|
|
|
|
if (!currentConversation) {
|
|
|
|
|
return (
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="flex-1 flex items-center justify-center bg-muted/50">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<MessageSquare className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
|
|
|
|
<h3 className="text-lg font-medium text-muted-foreground">
|
2025-12-03 21:56:50 +00:00
|
|
|
Sélectionnez une conversation
|
|
|
|
|
</h3>
|
2025-12-13 02:34:34 +00:00
|
|
|
<p className="text-sm text-muted-foreground">
|
2025-12-03 21:56:50 +00:00
|
|
|
Choisissez une conversation pour commencer à discuter
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="flex-1 flex flex-col">
|
2025-12-03 21:56:50 +00:00
|
|
|
{/* En-tête de la conversation */}
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="p-4 border-b bg-background">
|
|
|
|
|
<div className="flex items-center justify-between">
|
2025-12-03 21:56:50 +00:00
|
|
|
<div>
|
2025-12-13 02:34:34 +00:00
|
|
|
<h3 className="font-semibold">
|
2025-12-03 21:56:50 +00:00
|
|
|
{currentConversation.name ||
|
|
|
|
|
`Conversation ${currentConversation.id.slice(0, 8)}`}
|
|
|
|
|
</h3>
|
2025-12-13 02:34:34 +00:00
|
|
|
<p className="text-sm text-muted-foreground">
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **True Light/Dark Mode**
- Implemented proper light mode with inverted color scheme
- Smooth theme transitions (0.3s ease)
- Light mode colors: white backgrounds, dark text, vibrant accents
- System theme detection with proper class application
🌈 **Enhanced Theme System**
- 4 color themes work in both light and dark modes
- Cyber (cyan/magenta), Ocean (blue/teal), Forest (green/lime), Sunset (orange/purple)
- Theme-specific glassmorphism effects
- Proper contrast in light mode
✨ **Premium Animations**
- Float, glow-pulse, slide-in, scale-in, rotate-in animations
- Smooth page transitions
- Hover effects with depth (lift, glow, scale)
- Micro-interactions on all interactive elements
🎯 **Visual Polish**
- Enhanced glassmorphism for light/dark modes
- Custom scrollbar with theme colors
- Beautiful text selection
- Focus indicators for accessibility
- Premium utility classes
🔧 **Technical Improvements**
- Updated UIStore to properly apply light/dark classes
- Added data-theme attribute for CSS targeting
- Smooth scroll behavior
- Optimized transitions
The app is now a visual masterpiece with perfect light/dark mode support!
2026-01-11 01:32:21 +00:00
|
|
|
{currentConversation.participants?.length || 0} participant
|
|
|
|
|
{(currentConversation.participants?.length || 0) > 1 ? 's' : ''}
|
2025-12-03 21:56:50 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-12-13 02:34:34 +00:00
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<MoreVertical className="h-4 w-4" />
|
2025-12-03 21:56:50 +00:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Messages */}
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
2025-12-03 21:56:50 +00:00
|
|
|
{conversationMessages.length === 0 ? (
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="text-center text-muted-foreground">
|
2025-12-03 21:56:50 +00:00
|
|
|
<p>Aucun message dans cette conversation</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-02-09 23:07:19 +00:00
|
|
|
conversationMessages.map((message, index) => {
|
2025-12-03 21:56:50 +00:00
|
|
|
const isOwn = message.sender_id === user?.id;
|
2026-02-09 23:07:19 +00:00
|
|
|
const prevMessage = index > 0 ? conversationMessages[index - 1] : null;
|
|
|
|
|
const showDateSeparator =
|
|
|
|
|
!prevMessage ||
|
|
|
|
|
new Date(message.created_at).toDateString() !==
|
|
|
|
|
new Date(prevMessage.created_at).toDateString();
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
return (
|
2026-02-09 23:07:19 +00:00
|
|
|
<React.Fragment key={message.id}>
|
|
|
|
|
{showDateSeparator && (
|
|
|
|
|
<DateSeparator date={message.created_at} />
|
|
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}
|
|
|
|
|
>
|
|
|
|
|
<div className={`max-w-[70%] ${isOwn ? 'order-2' : 'order-1'}`}>
|
|
|
|
|
<div className="flex items-center space-x-2 mb-1">
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{isOwn ? 'Vous' : `Utilisateur ${message.sender_id}`}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{new Date(message.created_at).toLocaleTimeString([], {
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Card
|
|
|
|
|
className={`${isOwn ? 'bg-primary text-primary-foreground' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
<p
|
|
|
|
|
className="text-sm"
|
|
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
|
__html: sanitizeChatMessage(message.content),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Réactions */}
|
|
|
|
|
{message.reactions && Object.keys(message.reactions).length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
|
|
|
{Object.entries(message.reactions).map(([emoji, userIds]) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={emoji}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-6 px-2 text-xs"
|
|
|
|
|
>
|
|
|
|
|
{emoji} {userIds.length}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Actions sur le message */}
|
|
|
|
|
<div className="flex items-center space-x-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
<Button variant="ghost" size="sm" className="h-6 px-2">
|
|
|
|
|
<ThumbsUp className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="sm" className="h-6 px-2">
|
|
|
|
|
<ThumbsDown className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="sm" className="h-6 px-2">
|
|
|
|
|
<Reply className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="sm" className="h-6 px-2">
|
|
|
|
|
<Smile className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-09 23:07:19 +00:00
|
|
|
</React.Fragment>
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Indicateur de frappe */}
|
|
|
|
|
{typingUserIds.length > 0 && (
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
|
|
|
<div className="flex space-x-1">
|
|
|
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"></div>
|
2025-12-03 21:56:50 +00:00
|
|
|
<div
|
2025-12-13 02:34:34 +00:00
|
|
|
className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"
|
2025-12-03 21:56:50 +00:00
|
|
|
style={{ animationDelay: '0.1s' }}
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
2025-12-13 02:34:34 +00:00
|
|
|
className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"
|
2025-12-03 21:56:50 +00:00
|
|
|
style={{ animationDelay: '0.2s' }}
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span>
|
|
|
|
|
{typingUserIds.length === 1
|
|
|
|
|
? "Quelqu'un"
|
|
|
|
|
: `${typingUserIds.length} personnes`}{' '}
|
|
|
|
|
est en train d'écrire...
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div ref={messagesEndRef} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|