v0.1 64c03e0

chat.svelte.ts RawHistoryFolder

1import type { User, Message, EnrichedMessage } from '../types/chat';
2import type { 
3    SendMessageRequest, 
4    SendMessageResponse, 
5    GetMessagesResponse 
6} from '../types/payloads';
7import { invalidate } from '$app/navigation';
8
9
10class ChatState {
11    private users = $state<User[]>([]);
12    private messages = $state<Message[]>([]);
13    private currentUser = $state<User | null>(null);
14    private eventSource: EventSource | null = null;
15    private reconnectAttempts = 0;
16    private maxReconnectAttempts = 5;
17    private reconnectDelay = 1000; // Start with 1 second
18    private isInitializing = $state(false);
19    private currentRoomId = $state<string>('default');
20    private userCache = $state<Record<string, User>>({});
21
22    public async reinitialize() {
23        console.debug('Reinitializing chat state...');
24        if (this.eventSource) {
25            this.eventSource.close();
26            this.eventSource = null;
27        }
28        this.reconnectAttempts = 0;
29        this.reconnectDelay = 1000;
30        this.messages = [];
31        if (this.currentUser) {
32            await Promise.all([
33                this.initializeMessages(),
34                this.initializeRoomUsers()
35            ]);
36            this.setupSSE();
37        }
38    }
39
40    // User methods
41    getCurrentUser() {
42        return this.currentUser;
43    }
44
45    async setCurrentUser(user: User | null) {
46        console.debug('setCurrentUser called with', user);
47        this.currentUser = user;
48
49        if (user) {
50            await Promise.all([
51                this.initializeMessages(),
52                this.initializeRoomUsers()
53            ]);
54            this.setupSSE();
55        } else {
56            // If no global session, clear SSE and state
57            if (this.eventSource) {
58                this.eventSource.close();
59                this.eventSource = null;
60            }
61            this.messages = [];
62            this.users = [];
63        }
64
65        // Invalidate both session and messages to trigger reloads
66        await Promise.all([
67            invalidate('chat:session'),
68            invalidate('chat:messages')
69        ]);
70    }
71
72    private async initializeRoomUsers() {
73        console.debug('Initializing room users for room:', this.currentRoomId);
74        try {
75            const response = await fetch(`/api/rooms/${this.currentRoomId}`, {
76                credentials: 'include'
77            });
78            
79            if (!response.ok) {
80                throw new Error('Failed to fetch room users');
81            }
82
83            const data = await response.json();
84            if (!data.success) {
85                throw new Error(data.error);
86            }
87
88            console.debug('Fetched room buddy list:', data.buddyList);
89            this.users = data.buddyList;
90            // Update cache with new buddy list users
91            this.users.forEach(user => {
92                this.userCache[user.id] = user;
93            });
94        } catch (error) {
95            console.debug('Error fetching room users:', error);
96            this.users = [];
97        }
98    }
99
100    getOnlineUsers() {
101        return this.users.filter((user: User) => user.status !== 'offline');
102    }
103
104    getUserById(userId: string) {
105        // First check the cache
106        if (this.userCache[userId]) {
107            return this.userCache[userId];
108        }
109        
110        // If not in cache, look up in users array
111        const user = this.users.find((user: User) => user.id === userId);
112        if (user) {
113            // Update cache
114            this.userCache[userId] = user;
115            console.debug('getUserById: cached user data', { userId, user });
116        } else {
117            console.debug('getUserById: user not found', { userId });
118        }
119        return user;
120    }
121
122    updateUserStatus(userId: string, status: User['status']) {
123        this.users = this.users.map((user: User) => {
124            if (user.id === userId) {
125                const updatedUser = { ...user, status };
126                // Update cache
127                this.userCache[userId] = updatedUser;
128                return updatedUser;
129            }
130            return user;
131        });
132    }
133
134    // Message methods
135    getMessages() {
136        // Return messages with enriched user data
137        return this.enrichMessages(this.messages);
138    }
139
140    // Add method to update user cache for public access
141    updateUserCache(users: User[]) {
142        users.forEach(user => {
143            this.userCache[user.id] = user;
144        });
145        // Also update the users array
146        this.users = users;
147    }
148
149    // Add method to update messages for public access
150    updateMessages(messages: Message[]) {
151        this.messages = messages;
152    }
153
154    async initializeMessages() {
155        try {
156            const response = await fetch('/api/chat/messages', {
157                credentials: 'include'
158            });
159            if (!response.ok) throw new Error('Failed to fetch messages');
160            
161            const data = await response.json() as GetMessagesResponse;
162            if (!data.success) {
163                throw new Error(data.error);
164            }
165
166            // Get unique user IDs from messages that aren't in the cache
167            const uniqueUserIds = new Set(data.messages.map(msg => msg.senderId));
168            const missingUserIds = Array.from(uniqueUserIds).filter(id => !this.userCache[id]);
169
170            // Fetch missing user data in parallel
171            if (missingUserIds.length > 0) {
172                console.debug('Fetching missing user data for IDs:', missingUserIds);
173                await Promise.all(missingUserIds.map(async (userId) => {
174                    try {
175                        const userResponse = await fetch(`/api/users/${userId}`, {
176                            credentials: 'include'
177                        });
178                        if (userResponse.ok) {
179                            const userData = await userResponse.json();
180                            if (userData.success && userData.user) {
181                                this.userCache[userId] = userData.user;
182                                // Only add to users array if not already present
183                                if (!this.users.some(u => u.id === userId)) {
184                                    this.users = [...this.users, userData.user];
185                                }
186                            }
187                        }
188                    } catch (error) {
189                        console.debug('Error fetching user data:', { userId, error });
190                    }
191                }));
192            }
193
194            this.messages = data.messages;
195        } catch (error) {
196            console.debug('Error fetching initial messages:', error);
197            this.messages = [];
198        }
199    }
200
201    async sendMessage(content: string, type: Message['type'] = 'chat') {
202        const user = this.getCurrentUser();
203        if (!user) {
204            console.debug('Cannot send message: No current user');
205            return;
206        }
207        
208        try {
209            const payload: SendMessageRequest = {
210                content,
211                type,
212                userId: user.id,
213                chatRoomId: 'default'
214            };
215
216            console.debug('Sending message with payload:', payload);
217
218            const response = await fetch('/api/chat/messages', {
219                method: 'POST',
220                headers: {
221                    'Content-Type': 'application/json',
222                },
223                credentials: 'include',
224                body: JSON.stringify(payload)
225            });
226
227            if (!response.ok) {
228                const errorData = await response.json();
229                throw new Error(errorData.error || 'Failed to save message');
230            }
231
232            const data = await response.json() as SendMessageResponse;
233            if (!data.success) {
234                throw new Error(data.error);
235            }
236
237            console.debug('Message sent successfully:', data.message);
238            await invalidate('chat:messages');
239        } catch (error) {
240            console.debug('Error sending message:', error);
241            throw error;
242        }
243    }
244
245    private setupSSE() {
246        console.debug('Setting up SSE connection for chat messages');
247        this.connectSSE();
248    }
249
250    private connectSSE() {
251        if (this.eventSource) {
252            this.eventSource.close();
253        }
254
255        this.eventSource = new EventSource('/api/chat/messages', {
256            withCredentials: true
257        });
258        
259        this.eventSource.onmessage = async (event) => {
260            try {
261                if (event.data === 'Connected') {
262                    console.debug('SSE connection established');
263                    this.reconnectAttempts = 0;
264                    this.reconnectDelay = 1000;
265                    return;
266                }
267
268                const messageData = JSON.parse(event.data) as Message;
269                
270                // Ensure we have the sender's data before adding the message
271                if (!this.userCache[messageData.senderId]) {
272                    try {
273                        console.debug('Fetching user data for new message:', messageData.senderId);
274                        const response = await fetch(`/api/users/${messageData.senderId}`, {
275                            credentials: 'include'
276                        });
277                        if (response.ok) {
278                            const userData = await response.json();
279                            if (userData.success && userData.user) {
280                                this.userCache[userData.user.id] = userData.user;
281                                // Only add to users array if not already present
282                                if (!this.users.some(u => u.id === userData.user.id)) {
283                                    this.users = [...this.users, userData.user];
284                                }
285                            }
286                        }
287                    } catch (error) {
288                        console.debug('Error fetching user data for new message:', error);
289                    }
290                }
291
292                // Update messages state
293                this.messages = [...this.messages, messageData];
294                await invalidate('chat:messages');
295            } catch (error) {
296                console.debug('Error parsing SSE message data:', error);
297            }
298        };
299
300        this.eventSource.onerror = (event) => {
301            console.debug('SSE encountered an error:', event);
302            this.handleSSEError();
303        };
304    }
305
306    private handleSSEError() {
307        if (this.eventSource) {
308            this.eventSource.close();
309            this.eventSource = null;
310        }
311
312        if (this.reconnectAttempts < this.maxReconnectAttempts) {
313            console.debug(`Attempting to reconnect (${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
314            setTimeout(() => {
315                this.reconnectAttempts++;
316                this.reconnectDelay *= 2; // Exponential backoff
317                this.connectSSE();
318            }, this.reconnectDelay);
319        } else {
320            console.debug('Max reconnection attempts reached');
321        }
322    }
323
324    private reconnectSSE() {
325        this.reconnectAttempts = 0;
326        this.reconnectDelay = 1000;
327        this.connectSSE();
328    }
329
330    // Add new method to enrich multiple messages at once
331    enrichMessages(messages: Message[]): EnrichedMessage[] {
332        return messages.map(msg => this.enrichMessageWithUser(msg));
333    }
334
335    // Add new method to efficiently get user data for messages
336    enrichMessageWithUser(message: Message): EnrichedMessage {
337        const user = this.getUserById(message.senderId);
338        return {
339            ...message,
340            user: user || {
341                id: message.senderId,
342                nickname: 'Unknown User',
343                status: 'offline',
344                password: '', // This is a system user, no password needed
345                createdAt: Date.now(),
346                lastSeen: null,
347                avatarUrl: null
348            }
349        };
350    }
351}
352
353// Export a singleton instance
354export const chatState = new ChatState(); 

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