+layout.svelte RawHistoryFolder

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