v0.1.4 2490b00

+layout.svelte RawHistoryFolder

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>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close