detached 3275a96

feat: Enhance chat room loading and session management

- Implement session token validation and cookie management in server hooks
- Add initial loading state and loading dots for chat room messages
- Improve message loading with scroll position preservation
- Update chat state to handle public and authenticated message retrieval
- Modify API routes to support public and authenticated message access
commit
3275a9629d36752ab01640dcbd177bfc5161d0d4
author
Kit · · 2025-02-15 18:07 UTC
parent
840793d
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
9 files · +460 -38
A .cursor/rules/global.mdc +7 -0
@@ -0,0 +1,7 @@
1+---
2+description:
3+globs:
4+---
5+# Debugging
6+- Always add console.debug for client sided code
7+- Always add either console.log or follow the logger implementation logic is there is one for server/backend code⏎̸
@@ -0,0 +1,7 @@
1+---
2+description:
3+globs:
4+---
5+# Debugging
6+- Always add console.debug for client sided code
7+- Always add either console.log or follow the logger implementation logic is there is one for server/backend code⏎̸
A .cursor/rules/svelte5.mdc +228 -0
@@ -0,0 +1,228 @@
1+---
2+description:
3+globs:
4+---
5+cursor.directory
6+Generate rule
7+MCP Servers
8+Learn
9+
10+You are an expert in Svelte 5, SvelteKit, TypeScript, and modern web development.
11+
12+Key Principles
13+- Write concise, technical code with accurate Svelte 5 and SvelteKit examples.
14+- Leverage SvelteKit's server-side rendering (SSR) and static site generation (SSG) capabilities.
15+- Prioritize performance optimization and minimal JavaScript for optimal user experience.
16+- Use descriptive variable names and follow Svelte and SvelteKit conventions.
17+- Organize files using SvelteKit's file-based routing system.
18+
19+Code Style and Structure
20+- Write concise, technical TypeScript or JavaScript code with accurate examples.
21+- Use functional and declarative programming patterns; avoid unnecessary classes except for state machines.
22+- Prefer iteration and modularization over code duplication.
23+- Structure files: component logic, markup, styles, helpers, types.
24+- Follow Svelte's official documentation for setup and configuration: https://svelte.dev/docs
25+
26+Naming Conventions
27+- Use lowercase with hyphens for component files (e.g., `components/auth-form.svelte`).
28+- Use PascalCase for component names in imports and usage.
29+- Use camelCase for variables, functions, and props.
30+
31+TypeScript Usage
32+- Use TypeScript for all code; prefer interfaces over types.
33+- Avoid enums; use const objects instead.
34+- Use functional components with TypeScript interfaces for props.
35+- Enable strict mode in TypeScript for better type safety.
36+
37+Svelte Runes
38+- `$state`: Declare reactive state
39+ ```typescript
40+ let count = $state(0);
41+ ```
42+- `$derived`: Compute derived values
43+ ```typescript
44+ let doubled = $derived(count * 2);
45+ ```
46+- `$effect`: Manage side effects and lifecycle
47+ ```typescript
48+ $effect(() => {
49+ console.log(`Count is now ${count}`);
50+ });
51+ ```
52+- `$props`: Declare component props
53+ ```typescript
54+ let { optionalProp = 42, requiredProp } = $props();
55+ ```
56+- `$bindable`: Create two-way bindable props
57+ ```typescript
58+ let { bindableProp = $bindable() } = $props();
59+ ```
60+- `$inspect`: Debug reactive state (development only)
61+ ```typescript
62+ $inspect(count);
63+ ```
64+
65+UI and Styling
66+- Use Tailwind CSS for utility-first styling approach.
67+- Leverage Shadcn components for pre-built, customizable UI elements.
68+- Import Shadcn components from `$lib/components/ui`.
69+- Organize Tailwind classes using the `cn()` utility from `$lib/utils`.
70+- Use Svelte's built-in transition and animation features.
71+
72+Shadcn Color Conventions
73+- Use `background` and `foreground` convention for colors.
74+- Define CSS variables without color space function:
75+ ```css
76+ --primary: 222.2 47.4% 11.2%;
77+ --primary-foreground: 210 40% 98%;
78+ ```
79+- Usage example:
80+ ```svelte
81+ <div class="bg-primary text-primary-foreground">Hello</div>
82+ ```
83+- Key color variables:
84+ - `--background`, `--foreground`: Default body colors
85+ - `--muted`, `--muted-foreground`: Muted backgrounds
86+ - `--card`, `--card-foreground`: Card backgrounds
87+ - `--popover`, `--popover-foreground`: Popover backgrounds
88+ - `--border`: Default border color
89+ - `--input`: Input border color
90+ - `--primary`, `--primary-foreground`: Primary button colors
91+ - `--secondary`, `--secondary-foreground`: Secondary button colors
92+ - `--accent`, `--accent-foreground`: Accent colors
93+ - `--destructive`, `--destructive-foreground`: Destructive action colors
94+ - `--ring`: Focus ring color
95+ - `--radius`: Border radius for components
96+
97+SvelteKit Project Structure
98+- Use the recommended SvelteKit project structure:
99+ ```
100+ - src/
101+ - lib/
102+ - routes/
103+ - app.html
104+ - static/
105+ - svelte.config.js
106+ - vite.config.js
107+ ```
108+
109+Component Development
110+- Create .svelte files for Svelte components.
111+- Use .svelte.ts files for component logic and state machines.
112+- Implement proper component composition and reusability.
113+- Use Svelte's props for data passing.
114+- Leverage Svelte's reactive declarations for local state management.
115+
116+State Management
117+- Use classes for complex state management (state machines):
118+ ```typescript
119+ // counter.svelte.ts
120+ class Counter {
121+ count = $state(0);
122+ incrementor = $state(1);
123+
124+ increment() {
125+ this.count += this.incrementor;
126+ }
127+
128+ resetCount() {
129+ this.count = 0;
130+ }
131+
132+ resetIncrementor() {
133+ this.incrementor = 1;
134+ }
135+ }
136+
137+ export const counter = new Counter();
138+ ```
139+- Use in components:
140+ ```svelte
141+ <script lang="ts">
142+ import { counter } from './counter.svelte.ts';
143+ </script>
144+
145+ <button on:click={() => counter.increment()}>
146+ Count: {counter.count}
147+ </button>
148+ ```
149+
150+Routing and Pages
151+- Utilize SvelteKit's file-based routing system in the src/routes/ directory.
152+- Implement dynamic routes using [slug] syntax.
153+- Use load functions for server-side data fetching and pre-rendering.
154+- Implement proper error handling with +error.svelte pages.
155+
156+Server-Side Rendering (SSR) and Static Site Generation (SSG)
157+- Leverage SvelteKit's SSR capabilities for dynamic content.
158+- Implement SSG for static pages using prerender option.
159+- Use the adapter-auto for automatic deployment configuration.
160+
161+Performance Optimization
162+- Leverage Svelte's compile-time optimizations.
163+- Use `{#key}` blocks to force re-rendering of components when needed.
164+- Implement code splitting using dynamic imports for large applications.
165+- Profile and monitor performance using browser developer tools.
166+- Use `$effect.tracking()` to optimize effect dependencies.
167+- Minimize use of client-side JavaScript; leverage SvelteKit's SSR and SSG.
168+- Implement proper lazy loading for images and other assets.
169+
170+Data Fetching and API Routes
171+- Use load functions for server-side data fetching.
172+- Implement proper error handling for data fetching operations.
173+- Create API routes in the src/routes/api/ directory.
174+- Implement proper request handling and response formatting in API routes.
175+- Use SvelteKit's hooks for global API middleware.
176+
177+SEO and Meta Tags
178+- Use Svelte:head component for adding meta information.
179+- Implement canonical URLs for proper SEO.
180+- Create reusable SEO components for consistent meta tag management.
181+
182+Forms and Actions
183+- Utilize SvelteKit's form actions for server-side form handling.
184+- Implement proper client-side form validation using Svelte's reactive declarations.
185+- Use progressive enhancement for JavaScript-optional form submissions.
186+
187+Internationalization (i18n) with Paraglide.js
188+- Use Paraglide.js for internationalization: https://inlang.com/m/gerre34r/library-inlang-paraglideJs
189+- Install Paraglide.js: `npm install @inlang/paraglide-js`
190+- Set up language files in the `languages` directory.
191+- Use the `t` function to translate strings:
192+ ```svelte
193+ <script>
194+ import { t } from '@inlang/paraglide-js';
195+ </script>
196+
197+ <h1>{t('welcome_message')}</h1>
198+ ```
199+- Support multiple languages and RTL layouts.
200+- Ensure text scaling and font adjustments for accessibility.
201+
202+Accessibility
203+- Ensure proper semantic HTML structure in Svelte components.
204+- Implement ARIA attributes where necessary.
205+- Ensure keyboard navigation support for interactive elements.
206+- Use Svelte's bind:this for managing focus programmatically.
207+
208+Key Conventions
209+1. Embrace Svelte's simplicity and avoid over-engineering solutions.
210+2. Use SvelteKit for full-stack applications with SSR and API routes.
211+3. Prioritize Web Vitals (LCP, FID, CLS) for performance optimization.
212+4. Use environment variables for configuration management.
213+5. Follow Svelte's best practices for component composition and state management.
214+6. Ensure cross-browser compatibility by testing on multiple platforms.
215+7. Keep your Svelte and SvelteKit versions up to date.
216+
217+Documentation
218+- Svelte 5 Runes: https://svelte-5-preview.vercel.app/docs/runes
219+- Svelte Documentation: https://svelte.dev/docs
220+- SvelteKit Documentation: https://kit.svelte.dev/docs
221+- Paraglide.js Documentation: https://inlang.com/m/gerre34r/library-inlang-paraglideJs/usage
222+
223+Refer to Svelte, SvelteKit, and Paraglide.js documentation for detailed information on components, internationalization, and best practices.
224+MMBytes
225+MMBytes
226+Made by Midday
227+
228+An all-in-one tool for freelancers to monitor financial health, time-track, and send invoices. ↗
@@ -0,0 +1,228 @@
1+---
2+description:
3+globs:
4+---
5+cursor.directory
6+Generate rule
7+MCP Servers
8+Learn
9+
10+You are an expert in Svelte 5, SvelteKit, TypeScript, and modern web development.
11+
12+Key Principles
13+- Write concise, technical code with accurate Svelte 5 and SvelteKit examples.
14+- Leverage SvelteKit's server-side rendering (SSR) and static site generation (SSG) capabilities.
15+- Prioritize performance optimization and minimal JavaScript for optimal user experience.
16+- Use descriptive variable names and follow Svelte and SvelteKit conventions.
17+- Organize files using SvelteKit's file-based routing system.
18+
19+Code Style and Structure
20+- Write concise, technical TypeScript or JavaScript code with accurate examples.
21+- Use functional and declarative programming patterns; avoid unnecessary classes except for state machines.
22+- Prefer iteration and modularization over code duplication.
23+- Structure files: component logic, markup, styles, helpers, types.
24+- Follow Svelte's official documentation for setup and configuration: https://svelte.dev/docs
25+
26+Naming Conventions
27+- Use lowercase with hyphens for component files (e.g., `components/auth-form.svelte`).
28+- Use PascalCase for component names in imports and usage.
29+- Use camelCase for variables, functions, and props.
30+
31+TypeScript Usage
32+- Use TypeScript for all code; prefer interfaces over types.
33+- Avoid enums; use const objects instead.
34+- Use functional components with TypeScript interfaces for props.
35+- Enable strict mode in TypeScript for better type safety.
36+
37+Svelte Runes
38+- `$state`: Declare reactive state
39+ ```typescript
40+ let count = $state(0);
41+ ```
42+- `$derived`: Compute derived values
43+ ```typescript
44+ let doubled = $derived(count * 2);
45+ ```
46+- `$effect`: Manage side effects and lifecycle
47+ ```typescript
48+ $effect(() => {
49+ console.log(`Count is now ${count}`);
50+ });
51+ ```
52+- `$props`: Declare component props
53+ ```typescript
54+ let { optionalProp = 42, requiredProp } = $props();
55+ ```
56+- `$bindable`: Create two-way bindable props
57+ ```typescript
58+ let { bindableProp = $bindable() } = $props();
59+ ```
60+- `$inspect`: Debug reactive state (development only)
61+ ```typescript
62+ $inspect(count);
63+ ```
64+
65+UI and Styling
66+- Use Tailwind CSS for utility-first styling approach.
67+- Leverage Shadcn components for pre-built, customizable UI elements.
68+- Import Shadcn components from `$lib/components/ui`.
69+- Organize Tailwind classes using the `cn()` utility from `$lib/utils`.
70+- Use Svelte's built-in transition and animation features.
71+
72+Shadcn Color Conventions
73+- Use `background` and `foreground` convention for colors.
74+- Define CSS variables without color space function:
75+ ```css
76+ --primary: 222.2 47.4% 11.2%;
77+ --primary-foreground: 210 40% 98%;
78+ ```
79+- Usage example:
80+ ```svelte
81+ <div class="bg-primary text-primary-foreground">Hello</div>
82+ ```
83+- Key color variables:
84+ - `--background`, `--foreground`: Default body colors
85+ - `--muted`, `--muted-foreground`: Muted backgrounds
86+ - `--card`, `--card-foreground`: Card backgrounds
87+ - `--popover`, `--popover-foreground`: Popover backgrounds
88+ - `--border`: Default border color
89+ - `--input`: Input border color
90+ - `--primary`, `--primary-foreground`: Primary button colors
91+ - `--secondary`, `--secondary-foreground`: Secondary button colors
92+ - `--accent`, `--accent-foreground`: Accent colors
93+ - `--destructive`, `--destructive-foreground`: Destructive action colors
94+ - `--ring`: Focus ring color
95+ - `--radius`: Border radius for components
96+
97+SvelteKit Project Structure
98+- Use the recommended SvelteKit project structure:
99+ ```
100+ - src/
101+ - lib/
102+ - routes/
103+ - app.html
104+ - static/
105+ - svelte.config.js
106+ - vite.config.js
107+ ```
108+
109+Component Development
110+- Create .svelte files for Svelte components.
111+- Use .svelte.ts files for component logic and state machines.
112+- Implement proper component composition and reusability.
113+- Use Svelte's props for data passing.
114+- Leverage Svelte's reactive declarations for local state management.
115+
116+State Management
117+- Use classes for complex state management (state machines):
118+ ```typescript
119+ // counter.svelte.ts
120+ class Counter {
121+ count = $state(0);
122+ incrementor = $state(1);
123+
124+ increment() {
125+ this.count += this.incrementor;
126+ }
127+
128+ resetCount() {
129+ this.count = 0;
130+ }
131+
132+ resetIncrementor() {
133+ this.incrementor = 1;
134+ }
135+ }
136+
137+ export const counter = new Counter();
138+ ```
139+- Use in components:
140+ ```svelte
141+ <script lang="ts">
142+ import { counter } from './counter.svelte.ts';
143+ </script>
144+
145+ <button on:click={() => counter.increment()}>
146+ Count: {counter.count}
147+ </button>
148+ ```
149+
150+Routing and Pages
151+- Utilize SvelteKit's file-based routing system in the src/routes/ directory.
152+- Implement dynamic routes using [slug] syntax.
153+- Use load functions for server-side data fetching and pre-rendering.
154+- Implement proper error handling with +error.svelte pages.
155+
156+Server-Side Rendering (SSR) and Static Site Generation (SSG)
157+- Leverage SvelteKit's SSR capabilities for dynamic content.
158+- Implement SSG for static pages using prerender option.
159+- Use the adapter-auto for automatic deployment configuration.
160+
161+Performance Optimization
162+- Leverage Svelte's compile-time optimizations.
163+- Use `{#key}` blocks to force re-rendering of components when needed.
164+- Implement code splitting using dynamic imports for large applications.
165+- Profile and monitor performance using browser developer tools.
166+- Use `$effect.tracking()` to optimize effect dependencies.
167+- Minimize use of client-side JavaScript; leverage SvelteKit's SSR and SSG.
168+- Implement proper lazy loading for images and other assets.
169+
170+Data Fetching and API Routes
171+- Use load functions for server-side data fetching.
172+- Implement proper error handling for data fetching operations.
173+- Create API routes in the src/routes/api/ directory.
174+- Implement proper request handling and response formatting in API routes.
175+- Use SvelteKit's hooks for global API middleware.
176+
177+SEO and Meta Tags
178+- Use Svelte:head component for adding meta information.
179+- Implement canonical URLs for proper SEO.
180+- Create reusable SEO components for consistent meta tag management.
181+
182+Forms and Actions
183+- Utilize SvelteKit's form actions for server-side form handling.
184+- Implement proper client-side form validation using Svelte's reactive declarations.
185+- Use progressive enhancement for JavaScript-optional form submissions.
186+
187+Internationalization (i18n) with Paraglide.js
188+- Use Paraglide.js for internationalization: https://inlang.com/m/gerre34r/library-inlang-paraglideJs
189+- Install Paraglide.js: `npm install @inlang/paraglide-js`
190+- Set up language files in the `languages` directory.
191+- Use the `t` function to translate strings:
192+ ```svelte
193+ <script>
194+ import { t } from '@inlang/paraglide-js';
195+ </script>
196+
197+ <h1>{t('welcome_message')}</h1>
198+ ```
199+- Support multiple languages and RTL layouts.
200+- Ensure text scaling and font adjustments for accessibility.
201+
202+Accessibility
203+- Ensure proper semantic HTML structure in Svelte components.
204+- Implement ARIA attributes where necessary.
205+- Ensure keyboard navigation support for interactive elements.
206+- Use Svelte's bind:this for managing focus programmatically.
207+
208+Key Conventions
209+1. Embrace Svelte's simplicity and avoid over-engineering solutions.
210+2. Use SvelteKit for full-stack applications with SSR and API routes.
211+3. Prioritize Web Vitals (LCP, FID, CLS) for performance optimization.
212+4. Use environment variables for configuration management.
213+5. Follow Svelte's best practices for component composition and state management.
214+6. Ensure cross-browser compatibility by testing on multiple platforms.
215+7. Keep your Svelte and SvelteKit versions up to date.
216+
217+Documentation
218+- Svelte 5 Runes: https://svelte-5-preview.vercel.app/docs/runes
219+- Svelte Documentation: https://svelte.dev/docs
220+- SvelteKit Documentation: https://kit.svelte.dev/docs
221+- Paraglide.js Documentation: https://inlang.com/m/gerre34r/library-inlang-paraglideJs/usage
222+
223+Refer to Svelte, SvelteKit, and Paraglide.js documentation for detailed information on components, internationalization, and best practices.
224+MMBytes
225+MMBytes
226+Made by Midday
227+
228+An all-in-one tool for freelancers to monitor financial health, time-track, and send invoices. ↗
M src/hooks.server.ts +52 -3
@@ -4,6 +4,8 @@ import { handleRateLimit } from "$lib/api/rate-limiter";
44 import { users } from "$lib/db/schema";
55 import { createLogger } from "$lib/utils/logger.server";
66 import { ensureDefaultChatRoom } from "$lib/utils/chat.server";
7+import { validateSessionToken } from "$lib/api/session.server";
8+import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie";
79
810 const log = createLogger('hooks-server');
911
@@ -35,11 +37,58 @@ Promise.resolve()
3537 export const handle: Handle = async ({ event, resolve }) => {
3638 log.debug("Handling request", { path: event.url.pathname });
3739
38- // Identify public chat and room requests
40+ // Get session token from cookies
41+ const token = event.cookies.get("session") ?? null;
42+
43+ // Validate session token if token exists
44+ if (token) {
45+ const { session, user } = await validateSessionToken(token);
46+ if (session) {
47+ log.debug("Valid session found", { user: user?.nickname });
48+ setSessionTokenCookie(event, token, session.expiresAt);
49+ event.locals.session = session;
50+ event.locals.user = user;
51+ } else {
52+ log.debug("Invalid session token");
53+ deleteSessionTokenCookie(event);
54+ event.locals.session = null;
55+ event.locals.user = null;
56+ }
57+ }
58+
59+ // Identify public chat and room requests if there is no valid session
3960 const isPublicChatRequest = event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';
40- const isPublicRoomRequest = event.url.pathname.startsWith('/api/rooms') && event.request.method === 'GET';
61+ const isPublicRoomRequest = event.url.pathname.startsWith('/api/rooms/') && event.request.method === 'GET';
62+
63+ if ((isPublicChatRequest || isPublicRoomRequest) && !event.locals.session) {
64+ log.debug("Public request accessed", { path: event.url.pathname });
65+ const response = await resolve(event);
66+ return response;
67+ }
4168
42- // Skip rate limiting for public endpoints
69+ // Public routes that don't require authentication
70+ const publicRoutes = [
71+ "/",
72+ "/api/session/login",
73+ "/api/session/validate",
74+ "/api/register",
75+ "/login",
76+ "/register"
77+ ];
78+
79+ // Handle public routes
80+ if (publicRoutes.includes(event.url.pathname)) {
81+ log.debug("Public route accessed", { path: event.url.pathname });
82+ const response = await resolve(event);
83+ return response;
84+ }
85+
86+ // Skip rate limiting for SSE endpoint when authenticated
87+ if (event.url.pathname.startsWith('/api/sse')) {
88+ return await resolve(event);
89+ }
90+
91+ // Apply rate limiting for non-public endpoints
4392 if (!isPublicChatRequest && !isPublicRoomRequest) {
4493 const rateLimitResult = await handleRateLimit(event);
4594 if (rateLimitResult?.status === 429) {
@@ -4,6 +4,8 @@ import { handleRateLimit } from "$lib/api/rate-limiter";
4 import { users } from "$lib/db/schema";4 import { users } from "$lib/db/schema";
5 import { createLogger } from "$lib/utils/logger.server";5 import { createLogger } from "$lib/utils/logger.server";
6 import { ensureDefaultChatRoom } from "$lib/utils/chat.server";6 import { ensureDefaultChatRoom } from "$lib/utils/chat.server";
7+import { validateSessionToken } from "$lib/api/session.server";
8+import { setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/api/session.cookie";
7 9
8 const log = createLogger('hooks-server');10 const log = createLogger('hooks-server');
9 11
@@ -35,11 +37,58 @@ Promise.resolve()
35 export const handle: Handle = async ({ event, resolve }) => {37 export const handle: Handle = async ({ event, resolve }) => {
36 log.debug("Handling request", { path: event.url.pathname });38 log.debug("Handling request", { path: event.url.pathname });
37 39
38- // Identify public chat and room requests40+ // Get session token from cookies
41+ const token = event.cookies.get("session") ?? null;
42+
43+ // Validate session token if token exists
44+ if (token) {
45+ const { session, user } = await validateSessionToken(token);
46+ if (session) {
47+ log.debug("Valid session found", { user: user?.nickname });
48+ setSessionTokenCookie(event, token, session.expiresAt);
49+ event.locals.session = session;
50+ event.locals.user = user;
51+ } else {
52+ log.debug("Invalid session token");
53+ deleteSessionTokenCookie(event);
54+ event.locals.session = null;
55+ event.locals.user = null;
56+ }
57+ }
58+
59+ // Identify public chat and room requests if there is no valid session
39 const isPublicChatRequest = event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';60 const isPublicChatRequest = event.url.pathname.startsWith('/api/chat/messages') && event.request.method === 'GET';
40- const isPublicRoomRequest = event.url.pathname.startsWith('/api/rooms') && event.request.method === 'GET';61+ const isPublicRoomRequest = event.url.pathname.startsWith('/api/rooms/') && event.request.method === 'GET';
62+
63+ if ((isPublicChatRequest || isPublicRoomRequest) && !event.locals.session) {
64+ log.debug("Public request accessed", { path: event.url.pathname });
65+ const response = await resolve(event);
66+ return response;
67+ }
41 68
42- // Skip rate limiting for public endpoints69+ // Public routes that don't require authentication
70+ const publicRoutes = [
71+ "/",
72+ "/api/session/login",
73+ "/api/session/validate",
74+ "/api/register",
75+ "/login",
76+ "/register"
77+ ];
78+
79+ // Handle public routes
80+ if (publicRoutes.includes(event.url.pathname)) {
81+ log.debug("Public route accessed", { path: event.url.pathname });
82+ const response = await resolve(event);
83+ return response;
84+ }
85+
86+ // Skip rate limiting for SSE endpoint when authenticated
87+ if (event.url.pathname.startsWith('/api/sse')) {
88+ return await resolve(event);
89+ }
90+
91+ // Apply rate limiting for non-public endpoints
43 if (!isPublicChatRequest && !isPublicRoomRequest) {92 if (!isPublicChatRequest && !isPublicRoomRequest) {
44 const rateLimitResult = await handleRateLimit(event);93 const rateLimitResult = await handleRateLimit(event);
45 if (rateLimitResult?.status === 429) {94 if (rateLimitResult?.status === 429) {
M src/lib/components/chat-room.svelte +58 -15
@@ -8,6 +8,7 @@ import { resizable } from '$lib/actions/resizable';
88 import { maximizable } from '$lib/actions/maximizable';
99 import LoadingButton from './ui/button-loading.svelte';
1010 import Tooltip from './ui/tooltip.svelte';
11+import LoadingDots from './ui/loading-dots.svelte';
1112 import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format';
1213
1314 // Initialize with default values for SSR
@@ -19,6 +20,7 @@ let isMobile = $state(false);
1920 let currentMessage = $state('');
2021 let showUserList = $state(false);
2122 let isMaximized = $state(false);
23+let isInitialLoading = $state(true);
2224
2325 // Rate limiting state
2426 let cooldownEndTime = $state<number | null>(null);
@@ -299,7 +301,16 @@ async function handleScroll(event: Event) {
299301 // Check if we've scrolled near the top (within 100px) and not already loading
300302 if (scrollTop < 100 && !isLoadingMore && hasMoreMessages) {
301303 isLoadingMore = true;
302- console.debug('Loading more messages...', { oldestMessageTimestamp });
304+
305+ // Store the scroll height and position before loading
306+ const scrollHeight = chatArea.scrollHeight;
307+ const currentScrollTop = chatArea.scrollTop;
308+
309+ console.debug('Loading more messages...', {
310+ oldestMessageTimestamp,
311+ scrollHeight,
312+ currentScrollTop
313+ });
303314
304315 try {
305316 const response = await fetch(`/api/chat/messages?${new URLSearchParams({
@@ -314,31 +325,50 @@ async function handleScroll(event: Event) {
314325 if (data.messages.length === 0) {
315326 hasMoreMessages = false;
316327 } else {
317- // Preserve scroll position
318- const oldHeight = chatArea.scrollHeight;
328+ // Update messages through chat state
329+ chatState.prependMessages(data.messages, data.hasMore);
319330
320- // Update messages
321- chatState.prependMessages(data.messages);
322-
323- // After the DOM updates, adjust scroll position
324- setTimeout(() => {
325- const newHeight = chatArea.scrollHeight;
326- chatArea.scrollTop = newHeight - oldHeight;
327- }, 0);
331+ // Use requestAnimationFrame to ensure DOM has updated
332+ requestAnimationFrame(() => {
333+ // Calculate how much the content height has increased
334+ const newScrollHeight = chatArea.scrollHeight;
335+ const heightDifference = newScrollHeight - scrollHeight;
336+
337+ // Adjust scroll position to maintain relative position
338+ chatArea.scrollTop = currentScrollTop + heightDifference;
339+
340+ console.debug('Scroll position adjusted:', {
341+ heightDifference,
342+ newScrollTop: chatArea.scrollTop,
343+ newScrollHeight
344+ });
345+ });
328346
329347 // Update oldest timestamp
330348 const newOldest = Math.min(...data.messages.map((m: Message) => m.timestamp));
331349 oldestMessageTimestamp = newOldest;
350+
351+ // Update hasMoreMessages from response
352+ hasMoreMessages = data.hasMore;
332353 }
333354 }
334355 }
335356 } catch (error) {
336357 console.error('Error loading more messages:', error);
358+ hasMoreMessages = false;
337359 } finally {
338360 isLoadingMore = false;
339361 }
340362 }
341363 }
364+
365+// Add effect to track initial loading state
366+$effect(() => {
367+ const stateMessages = chatState.getMessages();
368+ if (stateMessages.length > 0 && isInitialLoading) {
369+ isInitialLoading = false;
370+ }
371+});
342372 </script>
343373
344374 {#if showChatRoom}
@@ -415,10 +445,14 @@ async function handleScroll(event: Event) {
415445 style="flex: 1; margin-bottom: 0.5rem; padding: 0.5rem; overflow-y: auto;"
416446 onscroll={(e) => handleScroll(e)}
417447 >
418- {#if isLoadingMore}
419- <div class="loading-messages">
420- Loading more messages...
421- </div>
448+ {#if isInitialLoading}
449+ <div class="initial-loading">
450+ <LoadingDots text="Chargement des messages" />
451+ </div>
452+ {:else if isLoadingMore}
453+ <div class="loading-messages">
454+ <LoadingDots text="Chargement" />
455+ </div>
422456 {/if}
423457 {#each visibleMessages as message (message.id)}
424458 <div class="message {message.type} text">
@@ -760,6 +794,15 @@ async function handleScroll(event: Event) {
760794 transition: width 0.1s linear;
761795 }
762796
797+ .initial-loading {
798+ display: flex;
799+ justify-content: center;
800+ align-items: center;
801+ height: 100%;
802+ font-size: 1.1rem;
803+ color: #666;
804+ }
805+
763806 .loading-messages {
764807 text-align: center;
765808 padding: 1rem;
@@ -8,6 +8,7 @@ import { resizable } from '$lib/actions/resizable';
8 import { maximizable } from '$lib/actions/maximizable';8 import { maximizable } from '$lib/actions/maximizable';
9 import LoadingButton from './ui/button-loading.svelte';9 import LoadingButton from './ui/button-loading.svelte';
10 import Tooltip from './ui/tooltip.svelte';10 import Tooltip from './ui/tooltip.svelte';
11+import LoadingDots from './ui/loading-dots.svelte';
11 import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format';12 import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format';
12 13
13 // Initialize with default values for SSR14 // Initialize with default values for SSR
@@ -19,6 +20,7 @@ let isMobile = $state(false);
19 let currentMessage = $state('');20 let currentMessage = $state('');
20 let showUserList = $state(false);21 let showUserList = $state(false);
21 let isMaximized = $state(false);22 let isMaximized = $state(false);
23+let isInitialLoading = $state(true);
22 24
23 // Rate limiting state25 // Rate limiting state
24 let cooldownEndTime = $state<number | null>(null);26 let cooldownEndTime = $state<number | null>(null);
@@ -299,7 +301,16 @@ async function handleScroll(event: Event) {
299 // Check if we've scrolled near the top (within 100px) and not already loading301 // Check if we've scrolled near the top (within 100px) and not already loading
300 if (scrollTop < 100 && !isLoadingMore && hasMoreMessages) {302 if (scrollTop < 100 && !isLoadingMore && hasMoreMessages) {
301 isLoadingMore = true;303 isLoadingMore = true;
302- console.debug('Loading more messages...', { oldestMessageTimestamp });304+
305+ // Store the scroll height and position before loading
306+ const scrollHeight = chatArea.scrollHeight;
307+ const currentScrollTop = chatArea.scrollTop;
308+
309+ console.debug('Loading more messages...', {
310+ oldestMessageTimestamp,
311+ scrollHeight,
312+ currentScrollTop
313+ });
303 314
304 try {315 try {
305 const response = await fetch(`/api/chat/messages?${new URLSearchParams({316 const response = await fetch(`/api/chat/messages?${new URLSearchParams({
@@ -314,31 +325,50 @@ async function handleScroll(event: Event) {
314 if (data.messages.length === 0) {325 if (data.messages.length === 0) {
315 hasMoreMessages = false;326 hasMoreMessages = false;
316 } else {327 } else {
317- // Preserve scroll position328+ // Update messages through chat state
318- const oldHeight = chatArea.scrollHeight;329+ chatState.prependMessages(data.messages, data.hasMore);
319 330
320- // Update messages331+ // Use requestAnimationFrame to ensure DOM has updated
321- chatState.prependMessages(data.messages);332+ requestAnimationFrame(() => {
322- 333+ // Calculate how much the content height has increased
323- // After the DOM updates, adjust scroll position334+ const newScrollHeight = chatArea.scrollHeight;
324- setTimeout(() => {335+ const heightDifference = newScrollHeight - scrollHeight;
325- const newHeight = chatArea.scrollHeight;336+
326- chatArea.scrollTop = newHeight - oldHeight;337+ // Adjust scroll position to maintain relative position
327- }, 0);338+ chatArea.scrollTop = currentScrollTop + heightDifference;
339+
340+ console.debug('Scroll position adjusted:', {
341+ heightDifference,
342+ newScrollTop: chatArea.scrollTop,
343+ newScrollHeight
344+ });
345+ });
328 346
329 // Update oldest timestamp347 // Update oldest timestamp
330 const newOldest = Math.min(...data.messages.map((m: Message) => m.timestamp));348 const newOldest = Math.min(...data.messages.map((m: Message) => m.timestamp));
331 oldestMessageTimestamp = newOldest;349 oldestMessageTimestamp = newOldest;
350+
351+ // Update hasMoreMessages from response
352+ hasMoreMessages = data.hasMore;
332 }353 }
333 }354 }
334 }355 }
335 } catch (error) {356 } catch (error) {
336 console.error('Error loading more messages:', error);357 console.error('Error loading more messages:', error);
358+ hasMoreMessages = false;
337 } finally {359 } finally {
338 isLoadingMore = false;360 isLoadingMore = false;
339 }361 }
340 }362 }
341 }363 }
364+
365+// Add effect to track initial loading state
366+$effect(() => {
367+ const stateMessages = chatState.getMessages();
368+ if (stateMessages.length > 0 && isInitialLoading) {
369+ isInitialLoading = false;
370+ }
371+});
342 </script>372 </script>
343 373
344 {#if showChatRoom}374 {#if showChatRoom}
@@ -415,10 +445,14 @@ async function handleScroll(event: Event) {
415 style="flex: 1; margin-bottom: 0.5rem; padding: 0.5rem; overflow-y: auto;"445 style="flex: 1; margin-bottom: 0.5rem; padding: 0.5rem; overflow-y: auto;"
416 onscroll={(e) => handleScroll(e)}446 onscroll={(e) => handleScroll(e)}
417 >447 >
418- {#if isLoadingMore}448+ {#if isInitialLoading}
419- <div class="loading-messages">449+ <div class="initial-loading">
420- Loading more messages...450+ <LoadingDots text="Chargement des messages" />
421- </div>451+ </div>
452+ {:else if isLoadingMore}
453+ <div class="loading-messages">
454+ <LoadingDots text="Chargement" />
455+ </div>
422 {/if}456 {/if}
423 {#each visibleMessages as message (message.id)}457 {#each visibleMessages as message (message.id)}
424 <div class="message {message.type} text">458 <div class="message {message.type} text">
@@ -760,6 +794,15 @@ async function handleScroll(event: Event) {
760 transition: width 0.1s linear;794 transition: width 0.1s linear;
761 }795 }
762 796
797+ .initial-loading {
798+ display: flex;
799+ justify-content: center;
800+ align-items: center;
801+ height: 100%;
802+ font-size: 1.1rem;
803+ color: #666;
804+ }
805+
763 .loading-messages {806 .loading-messages {
764 text-align: center;807 text-align: center;
765 padding: 1rem;808 padding: 1rem;
A src/lib/components/ui/loading-dots.svelte +50 -0
@@ -0,0 +1,50 @@
1+<!-- A reusable loading dots component with animated dots -->
2+<script lang="ts">
3+ let { text = 'Loading', color = 'inherit' } = $props();
4+</script>
5+
6+<div class="loading-text" style="color: {color}">
7+ {text}<span class="dots"><span>.</span><span>.</span><span>.</span></span>
8+</div>
9+
10+<style>
11+ .loading-text {
12+ display: inline-flex;
13+ align-items: center;
14+ font-size: inherit;
15+ }
16+
17+ .dots {
18+ display: inline-flex;
19+ margin-left: 2px;
20+ }
21+
22+ .dots span {
23+ animation: dots 1.5s infinite;
24+ opacity: 0;
25+ }
26+
27+ .dots span:nth-child(1) {
28+ animation-delay: 0s;
29+ }
30+
31+ .dots span:nth-child(2) {
32+ animation-delay: 0.5s;
33+ }
34+
35+ .dots span:nth-child(3) {
36+ animation-delay: 1s;
37+ }
38+
39+ @keyframes dots {
40+ 0% {
41+ opacity: 0;
42+ }
43+ 50% {
44+ opacity: 1;
45+ }
46+ 100% {
47+ opacity: 0;
48+ }
49+ }
50+</style> ⏎̸
@@ -0,0 +1,50 @@
1+<!-- A reusable loading dots component with animated dots -->
2+<script lang="ts">
3+ let { text = 'Loading', color = 'inherit' } = $props();
4+</script>
5+
6+<div class="loading-text" style="color: {color}">
7+ {text}<span class="dots"><span>.</span><span>.</span><span>.</span></span>
8+</div>
9+
10+<style>
11+ .loading-text {
12+ display: inline-flex;
13+ align-items: center;
14+ font-size: inherit;
15+ }
16+
17+ .dots {
18+ display: inline-flex;
19+ margin-left: 2px;
20+ }
21+
22+ .dots span {
23+ animation: dots 1.5s infinite;
24+ opacity: 0;
25+ }
26+
27+ .dots span:nth-child(1) {
28+ animation-delay: 0s;
29+ }
30+
31+ .dots span:nth-child(2) {
32+ animation-delay: 0.5s;
33+ }
34+
35+ .dots span:nth-child(3) {
36+ animation-delay: 1s;
37+ }
38+
39+ @keyframes dots {
40+ 0% {
41+ opacity: 0;
42+ }
43+ 50% {
44+ opacity: 1;
45+ }
46+ 100% {
47+ opacity: 0;
48+ }
49+ }
50+</style> ⏎̸
M src/lib/states/chat.svelte.ts +49 -9
@@ -7,7 +7,8 @@ import type {
77 import { invalidate } from '$app/navigation';
88 import { env } from '$env/dynamic/public';
99
10-const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID;
10+// Use the public environment variable with a fallback
11+const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001';
1112
1213 class ChatState {
1314 private users = $state<User[]>([]);
@@ -18,7 +19,7 @@ class ChatState {
1819 private maxReconnectAttempts = 5;
1920 private reconnectDelay = 1000; // Start with 1 second
2021 private isInitializing = $state(false);
21- private currentRoomId = $state<string>();
22+ private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID);
2223 private userCache = $state<Record<string, User>>({});
2324 private sseError = $state<string | null>(null);
2425 private sseRetryAfter = $state<number | null>(null);
@@ -33,6 +34,7 @@ class ChatState {
3334 private sseHandlersRegistered = false;
3435 private lastBuddyListUpdate = 0;
3536 private lastBuddyListHash = '';
37+ private hasMoreMessages = $state(false);
3638
3739 public async reinitialize() {
3840 if (this.isInitializing) {
@@ -48,6 +50,9 @@ class ChatState {
4850 this.reconnectAttempts = 0;
4951 this.reconnectDelay = 1000;
5052 this.messages = [];
53+ // Ensure currentRoomId is set
54+ this.currentRoomId = DEFAULT_CHAT_ROOM_ID;
55+
5156 if (this.currentUser) {
5257 console.debug('Waiting for session cookie update before setting up SSE...');
5358 await new Promise(resolve => setTimeout(resolve, 1000));
@@ -102,6 +107,8 @@ class ChatState {
102107 }
103108 }
104109
110+ // Clear existing messages before setting new user
111+ this.messages = [];
105112 this.currentUser = user;
106113
107114 if (user) {
@@ -128,7 +135,11 @@ class ChatState {
128135 private async initializeRoomUsers() {
129136 console.debug('Initializing room users for room:', this.currentRoomId);
130137 try {
131- const response = await fetch(`/api/rooms/${this.currentRoomId}`, {
138+ const isPublic = !this.currentUser;
139+ const url = `/api/rooms/${this.currentRoomId}${isPublic ? '?public=true' : ''}`;
140+ console.debug('Fetching room users from:', url);
141+
142+ const response = await fetch(url, {
132143 credentials: 'include'
133144 });
134145
@@ -305,7 +316,7 @@ class ChatState {
305316 return this._memoizedEnrichedMessages;
306317 }
307318
308- public prependMessages(newMessages: Message[]) {
319+ public prependMessages(newMessages: Message[], hasMore?: boolean) {
309320 console.debug('Prepending messages:', { count: newMessages.length });
310321 // Get unique user IDs from new messages that aren't in the cache
311322 const uniqueUserIds = new Set(newMessages.map(msg => msg.senderId));
@@ -335,9 +346,15 @@ class ChatState {
335346 }));
336347 }
337348
338- // Prepend new messages while maintaining uniqueness
339- const uniqueNewMessages = newMessages.filter(msg => !this.messages.some(m => m.id === msg.id));
349+ // Ensure incoming messages are sorted in ascending order before prepending
350+ const sortedNewMessages = newMessages.slice().sort((a, b) => a.timestamp - b.timestamp);
351+ const uniqueNewMessages = sortedNewMessages.filter(msg => !this.messages.some(m => m.id === msg.id));
340352 this.messages = [...uniqueNewMessages, ...this.messages];
353+
354+ // Update hasMore if provided
355+ if (hasMore !== undefined) {
356+ this.hasMoreMessages = hasMore;
357+ }
341358 }
342359
343360 // Add method to update user cache for public access
@@ -395,13 +412,21 @@ class ChatState {
395412 updateMessages(messages: Message[]) {
396413 // Deduplicate the messages by their id in case duplicates exist
397414 const uniqueMessages = Array.from(new Map(messages.map(msg => [msg.id, msg])).values());
398- this.messages = uniqueMessages;
415+ // Sort messages in ascending order (oldest first, newest at bottom)
416+ this.messages = uniqueMessages.sort((a, b) => a.timestamp - b.timestamp);
399417 }
400418
401419 // Note: For authenticated users, we no longer use the message cache. Messages are fetched directly from the database, and real-time updates are handled via SSE.
402420 async initializeMessages() {
403421 try {
404- const response = await fetch('/api/chat/messages', {
422+ // Build the URL with appropriate parameters
423+ const params = new URLSearchParams();
424+ if (!this.currentUser) {
425+ params.append('public', 'true');
426+ }
427+ params.append('roomId', this.currentRoomId);
428+
429+ const response = await fetch(`/api/chat/messages?${params}`, {
405430 credentials: 'include'
406431 });
407432 if (!response.ok) throw new Error('Failed to fetch messages');
@@ -439,10 +464,23 @@ class ChatState {
439464 }));
440465 }
441466
442- this.messages = data.messages;
467+ // Sort messages in ascending order (oldest first, most recent at the bottom)
468+ this.messages = data.messages.slice().sort((a, b) => a.timestamp - b.timestamp);
469+
470+ // Update hasMore based on response
471+ if ('hasMore' in data) {
472+ this.hasMoreMessages = data.hasMore;
473+ }
474+
475+ console.debug('Messages initialized:', {
476+ count: data.messages.length,
477+ isAuthenticated: !!this.currentUser,
478+ hasMore: this.hasMoreMessages
479+ });
443480 } catch (error) {
444481 console.debug('Error fetching initial messages:', error);
445482 this.messages = [];
483+ this.hasMoreMessages = false;
446484 }
447485 }
448486
@@ -592,6 +630,8 @@ class ChatState {
592630 console.debug('Received chat message via SSE:', messageData);
593631 // Deduplicate and update messages array with the new message
594632 this.messages = Array.from(new Map([...this.messages, messageData].map(m => [m.id, m])).values());
633+ // Sort messages so the newest are at the bottom
634+ this.messages.sort((a, b) => a.timestamp - b.timestamp);
595635 // Ensure the sender's data is available
596636 await this.ensureUserData(messageData.senderId);
597637 } catch (error) {
@@ -7,7 +7,8 @@ import type {
7 import { invalidate } from '$app/navigation';7 import { invalidate } from '$app/navigation';
8 import { env } from '$env/dynamic/public';8 import { env } from '$env/dynamic/public';
9 9
10-const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID;10+// Use the public environment variable with a fallback
11+const DEFAULT_CHAT_ROOM_ID = env.PUBLIC_DEFAULT_CHAT_ROOM_ID || '00000000-0000-0000-0000-000000000001';
11 12
12 class ChatState {13 class ChatState {
13 private users = $state<User[]>([]);14 private users = $state<User[]>([]);
@@ -18,7 +19,7 @@ class ChatState {
18 private maxReconnectAttempts = 5;19 private maxReconnectAttempts = 5;
19 private reconnectDelay = 1000; // Start with 1 second20 private reconnectDelay = 1000; // Start with 1 second
20 private isInitializing = $state(false);21 private isInitializing = $state(false);
21- private currentRoomId = $state<string>();22+ private currentRoomId = $state<string>(DEFAULT_CHAT_ROOM_ID);
22 private userCache = $state<Record<string, User>>({});23 private userCache = $state<Record<string, User>>({});
23 private sseError = $state<string | null>(null);24 private sseError = $state<string | null>(null);
24 private sseRetryAfter = $state<number | null>(null);25 private sseRetryAfter = $state<number | null>(null);
@@ -33,6 +34,7 @@ class ChatState {
33 private sseHandlersRegistered = false;34 private sseHandlersRegistered = false;
34 private lastBuddyListUpdate = 0;35 private lastBuddyListUpdate = 0;
35 private lastBuddyListHash = '';36 private lastBuddyListHash = '';
37+ private hasMoreMessages = $state(false);
36 38
37 public async reinitialize() {39 public async reinitialize() {
38 if (this.isInitializing) {40 if (this.isInitializing) {
@@ -48,6 +50,9 @@ class ChatState {
48 this.reconnectAttempts = 0;50 this.reconnectAttempts = 0;
49 this.reconnectDelay = 1000;51 this.reconnectDelay = 1000;
50 this.messages = [];52 this.messages = [];
53+ // Ensure currentRoomId is set
54+ this.currentRoomId = DEFAULT_CHAT_ROOM_ID;
55+
51 if (this.currentUser) {56 if (this.currentUser) {
52 console.debug('Waiting for session cookie update before setting up SSE...');57 console.debug('Waiting for session cookie update before setting up SSE...');
53 await new Promise(resolve => setTimeout(resolve, 1000));58 await new Promise(resolve => setTimeout(resolve, 1000));
@@ -102,6 +107,8 @@ class ChatState {
102 }107 }
103 }108 }
104 109
110+ // Clear existing messages before setting new user
111+ this.messages = [];
105 this.currentUser = user;112 this.currentUser = user;
106 113
107 if (user) {114 if (user) {
@@ -128,7 +135,11 @@ class ChatState {
128 private async initializeRoomUsers() {135 private async initializeRoomUsers() {
129 console.debug('Initializing room users for room:', this.currentRoomId);136 console.debug('Initializing room users for room:', this.currentRoomId);
130 try {137 try {
131- const response = await fetch(`/api/rooms/${this.currentRoomId}`, {138+ const isPublic = !this.currentUser;
139+ const url = `/api/rooms/${this.currentRoomId}${isPublic ? '?public=true' : ''}`;
140+ console.debug('Fetching room users from:', url);
141+
142+ const response = await fetch(url, {
132 credentials: 'include'143 credentials: 'include'
133 });144 });
134 145
@@ -305,7 +316,7 @@ class ChatState {
305 return this._memoizedEnrichedMessages;316 return this._memoizedEnrichedMessages;
306 }317 }
307 318
308- public prependMessages(newMessages: Message[]) {319+ public prependMessages(newMessages: Message[], hasMore?: boolean) {
309 console.debug('Prepending messages:', { count: newMessages.length });320 console.debug('Prepending messages:', { count: newMessages.length });
310 // Get unique user IDs from new messages that aren't in the cache321 // Get unique user IDs from new messages that aren't in the cache
311 const uniqueUserIds = new Set(newMessages.map(msg => msg.senderId));322 const uniqueUserIds = new Set(newMessages.map(msg => msg.senderId));
@@ -335,9 +346,15 @@ class ChatState {
335 }));346 }));
336 }347 }
337 348
338- // Prepend new messages while maintaining uniqueness349+ // Ensure incoming messages are sorted in ascending order before prepending
339- const uniqueNewMessages = newMessages.filter(msg => !this.messages.some(m => m.id === msg.id));350+ const sortedNewMessages = newMessages.slice().sort((a, b) => a.timestamp - b.timestamp);
351+ const uniqueNewMessages = sortedNewMessages.filter(msg => !this.messages.some(m => m.id === msg.id));
340 this.messages = [...uniqueNewMessages, ...this.messages];352 this.messages = [...uniqueNewMessages, ...this.messages];
353+
354+ // Update hasMore if provided
355+ if (hasMore !== undefined) {
356+ this.hasMoreMessages = hasMore;
357+ }
341 }358 }
342 359
343 // Add method to update user cache for public access360 // Add method to update user cache for public access
@@ -395,13 +412,21 @@ class ChatState {
395 updateMessages(messages: Message[]) {412 updateMessages(messages: Message[]) {
396 // Deduplicate the messages by their id in case duplicates exist413 // Deduplicate the messages by their id in case duplicates exist
397 const uniqueMessages = Array.from(new Map(messages.map(msg => [msg.id, msg])).values());414 const uniqueMessages = Array.from(new Map(messages.map(msg => [msg.id, msg])).values());
398- this.messages = uniqueMessages;415+ // Sort messages in ascending order (oldest first, newest at bottom)
416+ this.messages = uniqueMessages.sort((a, b) => a.timestamp - b.timestamp);
399 }417 }
400 418
401 // Note: For authenticated users, we no longer use the message cache. Messages are fetched directly from the database, and real-time updates are handled via SSE.419 // Note: For authenticated users, we no longer use the message cache. Messages are fetched directly from the database, and real-time updates are handled via SSE.
402 async initializeMessages() {420 async initializeMessages() {
403 try {421 try {
404- const response = await fetch('/api/chat/messages', {422+ // Build the URL with appropriate parameters
423+ const params = new URLSearchParams();
424+ if (!this.currentUser) {
425+ params.append('public', 'true');
426+ }
427+ params.append('roomId', this.currentRoomId);
428+
429+ const response = await fetch(`/api/chat/messages?${params}`, {
405 credentials: 'include'430 credentials: 'include'
406 });431 });
407 if (!response.ok) throw new Error('Failed to fetch messages');432 if (!response.ok) throw new Error('Failed to fetch messages');
@@ -439,10 +464,23 @@ class ChatState {
439 }));464 }));
440 }465 }
441 466
442- this.messages = data.messages;467+ // Sort messages in ascending order (oldest first, most recent at the bottom)
468+ this.messages = data.messages.slice().sort((a, b) => a.timestamp - b.timestamp);
469+
470+ // Update hasMore based on response
471+ if ('hasMore' in data) {
472+ this.hasMoreMessages = data.hasMore;
473+ }
474+
475+ console.debug('Messages initialized:', {
476+ count: data.messages.length,
477+ isAuthenticated: !!this.currentUser,
478+ hasMore: this.hasMoreMessages
479+ });
443 } catch (error) {480 } catch (error) {
444 console.debug('Error fetching initial messages:', error);481 console.debug('Error fetching initial messages:', error);
445 this.messages = [];482 this.messages = [];
483+ this.hasMoreMessages = false;
446 }484 }
447 }485 }
448 486
@@ -592,6 +630,8 @@ class ChatState {
592 console.debug('Received chat message via SSE:', messageData);630 console.debug('Received chat message via SSE:', messageData);
593 // Deduplicate and update messages array with the new message631 // Deduplicate and update messages array with the new message
594 this.messages = Array.from(new Map([...this.messages, messageData].map(m => [m.id, m])).values());632 this.messages = Array.from(new Map([...this.messages, messageData].map(m => [m.id, m])).values());
633+ // Sort messages so the newest are at the bottom
634+ this.messages.sort((a, b) => a.timestamp - b.timestamp);
595 // Ensure the sender's data is available635 // Ensure the sender's data is available
596 await this.ensureUserData(messageData.senderId);636 await this.ensureUserData(messageData.senderId);
597 } catch (error) {637 } catch (error) {
M src/lib/types/payloads.ts +1 -0
@@ -63,6 +63,7 @@ export type SendMessageResponse = SendMessageResponseSuccess | SendMessageRespon
6363 export interface GetMessagesResponseSuccess {
6464 success: true;
6565 messages: Message[];
66+ hasMore: boolean;
6667 }
6768
6869 export interface GetMessagesResponseError {
@@ -63,6 +63,7 @@ export type SendMessageResponse = SendMessageResponseSuccess | SendMessageRespon
63 export interface GetMessagesResponseSuccess {63 export interface GetMessagesResponseSuccess {
64 success: true;64 success: true;
65 messages: Message[];65 messages: Message[];
66+ hasMore: boolean;
66 }67 }
67 68
68 export interface GetMessagesResponseError {69 export interface GetMessagesResponseError {
M src/routes/api/chat/messages/+server.ts +14 -3
@@ -55,9 +55,16 @@ export async function GET({ request, locals }) {
5555 const url = new URL(request.url);
5656 const beforeTimestamp = url.searchParams.get('before');
5757 const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID;
58+ const isPublic = url.searchParams.get('public') === 'true';
5859
5960 try {
60- log.debug('Fetching messages from database', { beforeTimestamp, roomId, isAuthenticated: !!locals.session });
61+ log.debug('Fetching messages from database', {
62+ beforeTimestamp,
63+ roomId,
64+ isAuthenticated: !!locals.session,
65+ isPublic
66+ });
67+
6168 let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>;
6269
6370 if (beforeTimestamp) {
@@ -68,7 +75,10 @@ export async function GET({ request, locals }) {
6875 }
6976
7077 // Fetch messages with a limit based on authentication status
71- const fetchLimit = locals.session ? 100 : 10;
78+ // Public requests are always limited to 10 messages
79+ // Authenticated users get 100 messages per request
80+ const fetchLimit = (isPublic || !locals.session) ? 10 : 100;
81+
7282 const fetchedMessages = await db.select()
7383 .from(messages)
7484 .where(conditions)
@@ -77,7 +87,8 @@ export async function GET({ request, locals }) {
7787
7888 const response: GetMessagesResponse = {
7989 success: true,
80- messages: fetchedMessages
90+ messages: fetchedMessages,
91+ hasMore: fetchedMessages.length === fetchLimit // Indicate if there might be more messages
8192 };
8293
8394 return new Response(JSON.stringify(response), {
@@ -55,9 +55,16 @@ export async function GET({ request, locals }) {
55 const url = new URL(request.url);55 const url = new URL(request.url);
56 const beforeTimestamp = url.searchParams.get('before');56 const beforeTimestamp = url.searchParams.get('before');
57 const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID;57 const roomId = url.searchParams.get('roomId') || DEFAULT_CHAT_ROOM_ID;
58+ const isPublic = url.searchParams.get('public') === 'true';
58 59
59 try {60 try {
60- log.debug('Fetching messages from database', { beforeTimestamp, roomId, isAuthenticated: !!locals.session });61+ log.debug('Fetching messages from database', {
62+ beforeTimestamp,
63+ roomId,
64+ isAuthenticated: !!locals.session,
65+ isPublic
66+ });
67+
61 let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>;68 let conditions = eq(messages.chatRoomId, roomId) as SQL<unknown>;
62 69
63 if (beforeTimestamp) {70 if (beforeTimestamp) {
@@ -68,7 +75,10 @@ export async function GET({ request, locals }) {
68 }75 }
69 76
70 // Fetch messages with a limit based on authentication status77 // Fetch messages with a limit based on authentication status
71- const fetchLimit = locals.session ? 100 : 10;78+ // Public requests are always limited to 10 messages
79+ // Authenticated users get 100 messages per request
80+ const fetchLimit = (isPublic || !locals.session) ? 10 : 100;
81+
72 const fetchedMessages = await db.select()82 const fetchedMessages = await db.select()
73 .from(messages)83 .from(messages)
74 .where(conditions)84 .where(conditions)
@@ -77,7 +87,8 @@ export async function GET({ request, locals }) {
77 87
78 const response: GetMessagesResponse = {88 const response: GetMessagesResponse = {
79 success: true,89 success: true,
80- messages: fetchedMessages90+ messages: fetchedMessages,
91+ hasMore: fetchedMessages.length === fetchLimit // Indicate if there might be more messages
81 };92 };
82 93
83 return new Response(JSON.stringify(response), {94 return new Response(JSON.stringify(response), {
M src/routes/api/rooms/[roomId]/+server.ts +1 -8
@@ -2,7 +2,6 @@ import { desc, eq } from 'drizzle-orm';
22 import db from '$lib/db/db.server';
33 import { messages, users } from '$lib/db/schema';
44 import type { PublicRoomResponse } from '$lib/types/payloads';
5-import { error } from '@sveltejs/kit';
65 import { createSafeUser } from '$lib/types/chat';
76 import { createLogger } from '$lib/utils/logger.server';
87
@@ -10,14 +9,8 @@ const log = createLogger('rooms-server');
109
1110 export async function GET({ params, url, locals }): Promise<Response> {
1211 const { roomId } = params;
13- const isPublic = url.searchParams.get('public') === 'true';
12+ const isPublic = !locals.user || url.searchParams.get('public') === 'true';
1413
15- // Check authentication for non-public requests
16- if (!isPublic && !locals.user) {
17- log.warn('Authentication required');
18- throw error(401, 'Authentication required');
19- }
20-
2114 try {
2215 log.debug('Fetching room data', {
2316 roomId,
@@ -2,7 +2,6 @@ import { desc, eq } from 'drizzle-orm';
2 import db from '$lib/db/db.server';2 import db from '$lib/db/db.server';
3 import { messages, users } from '$lib/db/schema';3 import { messages, users } from '$lib/db/schema';
4 import type { PublicRoomResponse } from '$lib/types/payloads';4 import type { PublicRoomResponse } from '$lib/types/payloads';
5-import { error } from '@sveltejs/kit';
6 import { createSafeUser } from '$lib/types/chat';5 import { createSafeUser } from '$lib/types/chat';
7 import { createLogger } from '$lib/utils/logger.server';6 import { createLogger } from '$lib/utils/logger.server';
8 7
@@ -10,14 +9,8 @@ const log = createLogger('rooms-server');
10 9
11 export async function GET({ params, url, locals }): Promise<Response> {10 export async function GET({ params, url, locals }): Promise<Response> {
12 const { roomId } = params;11 const { roomId } = params;
13- const isPublic = url.searchParams.get('public') === 'true';12+ const isPublic = !locals.user || url.searchParams.get('public') === 'true';
14 13
15- // Check authentication for non-public requests
16- if (!isPublic && !locals.user) {
17- log.warn('Authentication required');
18- throw error(401, 'Authentication required');
19- }
20-
21 try {14 try {
22 log.debug('Fetching room data', { 15 log.debug('Fetching room data', {
23 roomId,16 roomId,

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