1<script lang="ts">
2 import '../app.css';;
3 import { chatState } from '$lib/states/chat.svelte';
4 import { onMount, onDestroy } from 'svelte';
5 import { browser } from '$app/environment';
6
7 let { children, data } = $props();
8 let isVisible = $state(true);
9 let statusInterval = $state<number | undefined>(undefined);
10 let lastUserUpdate = $state<string | null>(null);
11 let updateInProgress = $state(false);
12 let idleTimeout = $state<number | undefined>(undefined);
13
14 // New throttle state variables
15 let lastSentStatus = $state<string | null>(null);
16 let lastStatusSentAt = $state<number>(0);
17 let lastUserActivity = $state<number>(Date.now());
18 const STATUS_UPDATE_INTERVAL = 15000; // 15 seconds
19 const THROTTLE_TIME_MS = 10000; // 10 seconds
20 const FORCE_UPDATE_INTERVAL = 5 * 60000; // 5 minutes
21 const IDLE_TIMEOUT = 5 * 60000; // 5 minutes of inactivity = idle
22
23 type UserStatus = 'online' | 'offline' | 'busy' | 'idle';
24
25 async function updateUserStatus(status: UserStatus) {
26 if (!data.user?.id || updateInProgress) return;
27
28 const now = Date.now();
29 const timeSinceLastUpdate = now - lastStatusSentAt;
30
31 if (
32 status === lastSentStatus &&
33 timeSinceLastUpdate < FORCE_UPDATE_INTERVAL &&
34 timeSinceLastUpdate < THROTTLE_TIME_MS
35 ) {
36 console.debug('Skipping status update - no change and within throttle period:', {
37 status,
38 timeSinceLastUpdate,
39 lastUpdate: new Date(lastStatusSentAt).toISOString()
40 });
41 return;
42 }
43
44 updateInProgress = true;
45 const timestamp = new Date().toISOString();
46 console.debug('Updating user status:', {
47 userId: data.user.id,
48 status,
49 timestamp,
50 reason: status !== lastSentStatus ? 'status_changed' : 'force_update',
51 timeSinceLastUpdate,
52 trigger: new Error().stack?.split('\n')[2]?.trim(),
53 hasSession: true,
54 cookiePresent: browser ? document.cookie.includes('session=') : false
55 });
56
57 try {
58 if (status === 'online' && !document.cookie.includes('session=')) {
59 console.debug('Waiting for session cookie before status update...');
60 await new Promise(resolve => setTimeout(resolve, 500));
61 }
62
63 const response = await fetch('/api/status', {
64 method: 'POST',
65 headers: { 'Content-Type': 'application/json' },
66 body: JSON.stringify({
67 status,
68 lastSeen: timestamp
69 })
70 });
71
72 if (!response.ok) {
73 console.error('Failed to update user status:', await response.json());
74 } else {
75 lastSentStatus = status;
76 lastStatusSentAt = now;
77 console.debug('Status update successful:', {
78 userId: data.user.id,
79 status,
80 cookiePresent: document.cookie.includes('session=')
81 });
82 }
83 } catch (error) {
84 console.error('Error updating user status:', error);
85 } finally {
86 updateInProgress = false;
87 }
88 }
89
90 function resetIdleTimer() {
91 if (idleTimeout) {
92 window.clearTimeout(idleTimeout);
93 }
94
95 lastUserActivity = Date.now();
96
97 if (lastSentStatus === 'idle') {
98 updateUserStatus('online');
99 }
100
101 idleTimeout = window.setTimeout(() => {
102 if (isVisible && Date.now() - lastUserActivity >= IDLE_TIMEOUT) {
103 updateUserStatus('idle');
104 }
105 }, IDLE_TIMEOUT);
106 }
107
108 function handleUserActivity() {
109 resetIdleTimer();
110 }
111
112 function startStatusTicker() {
113 if (statusInterval) return;
114
115 console.debug('Starting status ticker');
116 const tickHandler = () => {
117 if (isVisible && lastSentStatus !== 'idle') {
118 updateUserStatus('online');
119 }
120 };
121
122 statusInterval = window.setInterval(tickHandler, STATUS_UPDATE_INTERVAL);
123 }
124
125 function stopStatusTicker() {
126 if (!statusInterval) return;
127 console.debug('Stopping status ticker');
128 window.clearInterval(statusInterval);
129 statusInterval = undefined;
130 }
131
132 function handleVisibilityChange() {
133 if (!browser) return;
134 isVisible = document.visibilityState === 'visible';
135
136 if (isVisible) {
137 resetIdleTimer();
138 updateUserStatus('online');
139 startStatusTicker();
140 } else {
141 updateUserStatus('busy');
142 stopStatusTicker();
143 }
144 }
145
146 function cleanup() {
147 if (!browser) return;
148 console.debug('Running cleanup - Setting user status to offline');
149 document.removeEventListener('visibilitychange', handleVisibilityChange);
150 window.removeEventListener('mousemove', handleUserActivity);
151 window.removeEventListener('keydown', handleUserActivity);
152 window.removeEventListener('click', handleUserActivity);
153 if (idleTimeout) {
154 window.clearTimeout(idleTimeout);
155 }
156 stopStatusTicker();
157 updateUserStatus('offline');
158 }
159
160 onMount(() => {
161 if (!browser) return;
162
163 // Set initial online status
164 updateUserStatus('online');
165
166 // Start monitoring for user activity
167 window.addEventListener('mousemove', handleUserActivity);
168 window.addEventListener('keydown', handleUserActivity);
169 window.addEventListener('click', handleUserActivity);
170
171 // Initialize idle timer
172 resetIdleTimer();
173
174 // Start the status ticker
175 startStatusTicker();
176
177 // Add visibility change listener
178 document.addEventListener('visibilitychange', handleVisibilityChange);
179
180 // Add beforeunload listener for cleanup
181 window.addEventListener('beforeunload', cleanup);
182
183 // Cleanup on component destroy
184 return () => {
185 cleanup();
186 window.removeEventListener('beforeunload', cleanup);
187 };
188 });
189
190 $effect(() => {
191 if (!browser) return;
192
193 const currentUserJson = data.user ? JSON.stringify(data.user) : null;
194 if (currentUserJson === lastUserUpdate) {
195 console.debug('Skipping duplicate user update');
196 return;
197 }
198
199 lastUserUpdate = currentUserJson;
200
201 console.debug('Layout reactive update - Session state:', {
202 timestamp: new Date().toISOString(),
203 hasUser: !!data.user,
204 userDetails: data.user ? {
205 nickname: data.user.nickname,
206 status: data.user.status,
207 id: data.user.id
208 } : null,
209 hasSession: !!data.session,
210 sessionDetails: data.session ? {
211 id: data.session.id,
212 expiresAt: new Date(data.session.expiresAt)
213 } : null,
214 cookiePresent: document.cookie.includes('session='),
215 isVisible
216 });
217
218 if (data.session?.id) {
219 console.debug('Updating global chatState with data.user:', data.user);
220 chatState.setCurrentUser(data.user);
221 }
222 });
223</script>
224
225<svelte:head>
226 <title>{data.meta.title}</title>
227 <meta name="description" content={data.meta.description} />
228 <meta name="keywords" content={data.meta.keywords} />
229 <meta name="language" content="fr" />
230
231 <!-- Open Graph / Facebook -->
232 <meta property="og:locale" content={data.meta.locale} />
233 <meta property="og:type" content={data.meta.type} />
234 <meta property="og:url" content={data.meta.url} />
235 <meta property="og:title" content={data.meta.title} />
236 <meta property="og:description" content={data.meta.description} />
237 <meta property="og:image" content={data.meta.ogImage} />
238
239 <!-- Twitter -->
240 <meta property="twitter:card" content="summary_large_image" />
241 <meta property="twitter:url" content={data.meta.url} />
242 <meta property="twitter:title" content={data.meta.title} />
243 <meta property="twitter:description" content={data.meta.description} />
244 <meta property="twitter:image" content={data.meta.ogImage} />
245
246 <!-- Additional Meta Tags -->
247 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
248 <meta name="theme-color" content="#2195f2" />
249 <link rel="canonical" href={data.meta.url} />
250 <link rel="alternate" hreflang="fr" href={data.meta.url} />
251</svelte:head>
252
253<div class="windows-xp-bg">
254 {@render children()}
255</div>
256
257<style>
258 .windows-xp-bg {
259 position: fixed;
260 top: 0;
261 left: 0;
262 width: 100vw;
263 height: 100vh;
264 margin: 0;
265 padding: 0;
266 overflow: hidden;
267 background: linear-gradient(
268 to bottom,
269 #2195f2 0%, /* Lighter blue at the top */
270 #4aa6e2 25%, /* Mid-light blue */
271 #7ab8e0 50%, /* Mid blue */
272 #86c55c 75%, /* Blue-green transition */
273 #90d047 100% /* Bright green at the bottom */
274 );
275 }
276</style>