diff --git a/apps/web/src/features/chat/components/ChatMessages.tsx b/apps/web/src/features/chat/components/ChatMessages.tsx
index 8c0f24eee..6f95eb8b3 100644
--- a/apps/web/src/features/chat/components/ChatMessages.tsx
+++ b/apps/web/src/features/chat/components/ChatMessages.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useMemo } from 'react';
+import React, { useEffect, useRef, useMemo } from 'react';
import { useChatStore } from '../store/chatStore';
import { useUser } from '@/features/auth/hooks/useUser';
import { Card, CardContent } from '@/components/ui/card';
@@ -13,6 +13,34 @@ import {
MessageSquare,
} from 'lucide-react';
+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 (
+
+ );
+}
+
export function ChatMessages() {
const { currentConversationId, conversations, messages, typingUsers } = useChatStore();
const { data: user } = useUser();
@@ -80,71 +108,81 @@ export function ChatMessages() {
Aucun message dans cette conversation
) : (
- conversationMessages.map((message) => {
+ conversationMessages.map((message, index) => {
const isOwn = message.sender_id === user?.id;
+ const prevMessage = index > 0 ? conversationMessages[index - 1] : null;
+ const showDateSeparator =
+ !prevMessage ||
+ new Date(message.created_at).toDateString() !==
+ new Date(prevMessage.created_at).toDateString();
+
return (
-
-
-
-
- {isOwn ? 'Vous' : `Utilisateur ${message.sender_id}`}
-
-
- {new Date(message.created_at).toLocaleTimeString([], {
- hour: '2-digit',
- minute: '2-digit',
- })}
-
-
-
-
-
+
+ {showDateSeparator && (
+
+ )}
+
+
+
+
+ {isOwn ? 'Vous' : `Utilisateur ${message.sender_id}`}
+
+
+ {new Date(message.created_at).toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+
+
- {/* Réactions */}
- {message.reactions && Object.keys(message.reactions).length > 0 && (
-
- {Object.entries(message.reactions).map(([emoji, userIds]) => (
-
- ))}
-
- )}
-
-
+ {/* Réactions */}
+ {message.reactions && Object.keys(message.reactions).length > 0 && (
+
+ {Object.entries(message.reactions).map(([emoji, userIds]) => (
+
+ ))}
+
+ )}
+
+
- {/* Actions sur le message */}
-
-
-
-
-
+ {/* Actions sur le message */}
+
+
+
+
+
+
-
+
);
})
)}
diff --git a/apps/web/src/features/settings/pages/SettingsPage.tsx b/apps/web/src/features/settings/pages/SettingsPage.tsx
index f2092edd4..70e908e31 100644
--- a/apps/web/src/features/settings/pages/SettingsPage.tsx
+++ b/apps/web/src/features/settings/pages/SettingsPage.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from 'react';
+import { useState, useEffect, useRef, useCallback } from 'react';
import { useUser } from '@/features/auth/hooks/useUser';
import { usersApi } from '@/services/api/users';
import { UserSettings } from '../types/settings';
@@ -11,6 +11,7 @@ import toast from '@/utils/toast';
import { settingsSchema } from '../schemas/settingsSchema';
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
import { Save, Shield, Sliders } from 'lucide-react';
+import { useUnsavedChanges, useFormDirtyState } from '@/hooks/useUnsavedChanges';
function SettingsPageSkeleton() {
return (
@@ -72,6 +73,13 @@ export function SettingsPage() {
const [mutationError, setMutationError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const lastMutationRef = useRef<(() => Promise) | null>(null);
+ const { isDirty, markDirty, markClean } = useFormDirtyState();
+ useUnsavedChanges(isDirty);
+
+ const handleSettingsChange = useCallback((newSettings: UserSettings) => {
+ setSettings(newSettings);
+ markDirty();
+ }, [markDirty]);
const loadSettings = async () => {
if (!user?.id) {
@@ -107,6 +115,7 @@ export function SettingsPage() {
setMutationError(null);
setRetryCount(0);
lastMutationRef.current = null;
+ markClean();
};
lastMutationRef.current = performMutation;
@@ -166,7 +175,7 @@ export function SettingsPage() {
Global Preferences
-
+
diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts
index caa50ed9d..d25cc2260 100644
--- a/apps/web/src/hooks/index.ts
+++ b/apps/web/src/hooks/index.ts
@@ -22,6 +22,7 @@ export { useKeyboardNavigation } from './useKeyboardNavigation';
export { useGlobalKeyboardShortcuts } from './useGlobalKeyboardShortcuts';
export { usePreload, usePreloadRoute } from './usePreload';
export { useCopyToClipboard } from './useCopyToClipboard';
+export { useUnsavedChanges, useFormDirtyState } from './useUnsavedChanges';
// Hook types
export type {
diff --git a/apps/web/src/hooks/useUnsavedChanges.ts b/apps/web/src/hooks/useUnsavedChanges.ts
new file mode 100644
index 000000000..60ca0406e
--- /dev/null
+++ b/apps/web/src/hooks/useUnsavedChanges.ts
@@ -0,0 +1,33 @@
+import { useEffect, useCallback, useState } from 'react';
+
+/**
+ * Hook that warns users when they try to navigate away with unsaved changes.
+ * Shows a browser confirmation dialog on page unload and can be used
+ * with React Router's navigation blocking.
+ */
+export function useUnsavedChanges(hasChanges: boolean) {
+ useEffect(() => {
+ if (!hasChanges) return;
+
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ e.preventDefault();
+ e.returnValue = '';
+ };
+
+ window.addEventListener('beforeunload', handleBeforeUnload);
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload);
+ }, [hasChanges]);
+}
+
+/**
+ * Hook to track form dirty state.
+ * Returns isDirty and methods to mark dirty/clean.
+ */
+export function useFormDirtyState() {
+ const [isDirty, setIsDirty] = useState(false);
+
+ const markDirty = useCallback(() => setIsDirty(true), []);
+ const markClean = useCallback(() => setIsDirty(false), []);
+
+ return { isDirty, markDirty, markClean };
+}