From 616d7f76d7efce4c332558879a4d555557e0b7cf Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:20:58 +0100 Subject: [PATCH] feat: add support for online status and read receipt preferences - Added `showOnlineStatus` and `showReadReceipts` fields to settings form. - Introduced real-time synchronization for read receipts in message threads. - Enhanced avatars to display online status indicators. - Automatically mark messages as read when viewing active conversations. --- .../src/app/(dashboard)/messages/page.tsx | 43 +++++++++-- .../src/app/(dashboard)/settings/page.tsx | 75 +++++++++++++++++++ frontend/src/components/ui/avatar.tsx | 22 ++++-- 3 files changed, 127 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/(dashboard)/messages/page.tsx b/frontend/src/app/(dashboard)/messages/page.tsx index 6ec2d2c..ab7b985 100644 --- a/frontend/src/app/(dashboard)/messages/page.tsx +++ b/frontend/src/app/(dashboard)/messages/page.tsx @@ -2,7 +2,15 @@ import { formatDistanceToNow } from "date-fns"; import { fr } from "date-fns/locale"; -import { ArrowLeft, Search, Send, UserPlus, X } from "lucide-react"; +import { + ArrowLeft, + Check, + CheckCheck, + Search, + Send, + UserPlus, + X, +} from "lucide-react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import * as React from "react"; @@ -142,6 +150,8 @@ export default function MessagesPage() { if (activeConv?.id === data.conversationId) { setMessages((prev) => [...prev, data.message]); setIsOtherTyping(false); // S'il a envoyé un message, il ne tape plus + // Marquer comme lu immédiatement si on est sur la conversation + MessageService.markAsRead(data.conversationId).catch(console.error); } // Mettre à jour la liste des conversations setConversations((prev) => { @@ -184,10 +194,26 @@ export default function MessagesPage() { } }); + socket.on( + "messages_read", + (data: { conversationId: string; readerId: string }) => { + if (activeConv?.id === data.conversationId) { + setMessages((prev) => + prev.map((msg) => + msg.senderId !== data.readerId && !msg.readAt + ? { ...msg, readAt: new Date().toISOString() } + : msg, + ), + ); + } + }, + ); + return () => { socket.off("new_message"); socket.off("user_status"); socket.off("user_typing"); + socket.off("messages_read"); }; } }, [socket, activeConv]); @@ -351,7 +377,7 @@ export default function MessagesPage() { : "hover:bg-zinc-100 dark:hover:bg-zinc-900" }`} > - + {conv.recipient.username[0].toUpperCase()} @@ -403,7 +429,10 @@ export default function MessagesPage() { href={`/user/${activeConv.recipient.username}`} className="flex-1 flex items-center gap-3 hover:opacity-80 transition-opacity" > - + {activeConv.recipient.username[0].toUpperCase()} @@ -465,8 +494,12 @@ export default function MessagesPage() { })} {msg.senderId === user?.uuid && ( - - {msg.readAt ? "• Lu" : "• Envoyé"} + + {msg.readAt ? ( + + ) : ( + + )} )} diff --git a/frontend/src/app/(dashboard)/settings/page.tsx b/frontend/src/app/(dashboard)/settings/page.tsx index 85e1203..d2e27e3 100644 --- a/frontend/src/app/(dashboard)/settings/page.tsx +++ b/frontend/src/app/(dashboard)/settings/page.tsx @@ -10,6 +10,7 @@ import { Palette, Save, Settings, + Shield, Sun, Trash2, User as UserIcon, @@ -53,6 +54,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Spinner } from "@/components/ui/spinner"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { useAuth } from "@/providers/auth-provider"; import { UserService } from "@/services/user.service"; @@ -60,6 +62,8 @@ import { UserService } from "@/services/user.service"; const settingsSchema = z.object({ displayName: z.string().max(32, "Le nom d'affichage est trop long").optional(), bio: z.string().max(255, "La bio est trop longue").optional(), + showOnlineStatus: z.boolean(), + showReadReceipts: z.boolean(), }); type SettingsFormValues = z.infer; @@ -82,6 +86,8 @@ export default function SettingsPage() { defaultValues: { displayName: "", bio: "", + showOnlineStatus: true, + showReadReceipts: true, }, }); @@ -90,6 +96,8 @@ export default function SettingsPage() { form.reset({ displayName: user.displayName || "", bio: user.bio || "", + showOnlineStatus: user.showOnlineStatus ?? true, + showReadReceipts: user.showReadReceipts ?? true, }); } }, [user, form]); @@ -265,6 +273,73 @@ export default function SettingsPage() { + {/* Confidentialité */} + + +
+ +
+ Confidentialité + Gérez la visibilité de vos activités. +
+
+
+ +
+ +
+ ( + +
+ Statut en ligne + + Affiche quand vous êtes actif sur le site. + +
+ + + +
+ )} + /> + ( + +
+ + Confirmations de lecture + + + Permet aux autres de voir quand vous avez lu leurs messages. + +
+ + + +
+ )} + /> +
+
+ +
+
+ +
+
+ diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx index a03b6cf..55d9ce8 100644 --- a/frontend/src/components/ui/avatar.tsx +++ b/frontend/src/components/ui/avatar.tsx @@ -7,17 +7,23 @@ import { cn } from "@/lib/utils"; function Avatar({ className, + isOnline, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { isOnline?: boolean }) { return ( - + + {isOnline && ( + )} - {...props} - /> + ); }