shellwhisper-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
screens/chat_screen.py ADDED
@@ -0,0 +1,661 @@
1
+ from textual.widgets import Header, Footer, Input, Label, RichLog, Button
2
+ from textual.screen import Screen
3
+ from textual.containers import Vertical, Horizontal
4
+
5
+ from rich.markup import escape
6
+
7
+ from src.events import NewWhisperReceived
8
+ from src.components.sidebar import Sidebar
9
+ from src.screens.room_action_screen import RoomActionScreen
10
+ from src.screens.security_screen import SecurityScreen
11
+ from src.screens.private_whisper_screen import PrivateWhisperPromptScreen
12
+
13
+ import json
14
+ import base64
15
+ import os
16
+
17
+ from datetime import datetime
18
+
19
+ class ChatScreen(Screen):
20
+ TITLE = "ShellWhisper"
21
+ BINDINGS = [
22
+ ("ctrl+l", "logout", "Logout"),
23
+ ("ctrl+d", "toggle_dark", "Toggle dark mode"),
24
+ ("ctrl+q", "quit", "Quit"),
25
+ ]
26
+
27
+ def compose(self):
28
+ yield Header()
29
+
30
+ with Horizontal():
31
+ yield Sidebar(id="sidebar")
32
+
33
+ with Vertical(id="chat-view-container"):
34
+ yield Label("Select a room to start whispering...", id="empty-view")
35
+
36
+ with Vertical(id="chat-view"):
37
+ yield RichLog(id="chat_log", highlight=True, markup=True)
38
+ yield Input(
39
+ placeholder="Type a whisper or @help for commands...",
40
+ id="chat_input",
41
+ )
42
+
43
+ yield Footer()
44
+
45
+ async def on_mount(self) -> None:
46
+ self.sub_title = f"Logged in as {self.app.current_user}"
47
+ self._current_subscription_id = None
48
+ self.current_room_messages = []
49
+ self._pending_room_data = None
50
+ self.rooms = []
51
+
52
+ self.app.connect_websocket()
53
+ self.set_interval(5.0, self.refresh_rooms)
54
+ self.run_worker(self._load_rooms_worker, thread=True)
55
+
56
+ # --- Sidebar / room loading --- #
57
+
58
+ def _load_rooms_worker(self) -> None:
59
+ rooms = self.fetch_user_rooms()
60
+ self.rooms = rooms
61
+ self.app.call_from_thread(self.update_sidebar_data, rooms)
62
+
63
+ def refresh_rooms(self) -> None:
64
+ self.run_worker(self._load_rooms_worker, thread=True)
65
+
66
+ def update_sidebar_data(self, rooms) -> None:
67
+ sidebar = self.query_one("#sidebar")
68
+ self.app.run_worker(sidebar.update_rooms(rooms))
69
+
70
+ ### BUTTON HANDLING ###
71
+
72
+ def on_button_pressed(self, event: Button.Pressed) -> None:
73
+ if event.button.id == "btn_room_mgmt":
74
+ self.app.push_screen(RoomActionScreen(), self._on_room_action_dismissed)
75
+ elif event.button.id == "btn_private":
76
+ self.app.push_screen(PrivateWhisperPromptScreen(), self._on_private_chat_dismissed)
77
+ elif event.button.id == "logout_btn":
78
+ self.logout_process()
79
+ elif "room-link" in event.button.classes:
80
+ room_id = event.button.id.replace("room_", "")
81
+ self.switch_to_room(room_id)
82
+
83
+ # --- Private whisper flow --- #
84
+
85
+ def _on_private_chat_dismissed(self, target_username: str | None) -> None:
86
+ if not target_username:
87
+ return
88
+
89
+ self.run_worker(lambda: self._start_private_chat_worker(target_username), thread=True)
90
+
91
+ def _start_private_chat_worker(self, target_username: str) -> None:
92
+ try:
93
+ response = self.app.api.start_private_chat(target_username)
94
+
95
+ if response.status_code in (200, 201):
96
+ room_data = response.json()
97
+
98
+ self.app.call_from_thread(
99
+ self.app.notify,
100
+ f"Private channel open with {escape(target_username)}!",
101
+ severity="success",
102
+ )
103
+
104
+ rooms = self.fetch_user_rooms()
105
+ self.rooms = rooms
106
+ self.app.call_from_thread(self.update_sidebar_data, rooms)
107
+ self.app.call_from_thread(self._auto_switch_to_room, room_data.get("id"))
108
+
109
+ elif response.status_code == 404:
110
+ self.app.call_from_thread(
111
+ self.app.notify,
112
+ f"User '{escape(target_username)}' not found!",
113
+ severity="error"
114
+ )
115
+ else:
116
+ self.app.call_from_thread(
117
+ self.app.notify,
118
+ f"Action failed ({response.status_code}): {escape(response.text)}",
119
+ severity="error",
120
+ )
121
+ except Exception as e:
122
+ self.app.call_from_thread(
123
+ self.app.notify,
124
+ f"Network error: {escape(str(e))}",
125
+ severity="error",
126
+ )
127
+
128
+ ### RoomActionScreen ###
129
+
130
+ def _on_room_action_dismissed(self, data: dict | None) -> None:
131
+ if not data:
132
+ return
133
+
134
+ self._pending_room_data = data
135
+ self.app.push_screen(
136
+ SecurityScreen(action=data["action"], room_name=data["name"]),
137
+ self._on_security_action_dismissed,
138
+ )
139
+
140
+ def _on_security_action_dismissed(self, security_key: str | None) -> None:
141
+ if not security_key or not self._pending_room_data:
142
+ return
143
+
144
+ action = self._pending_room_data["action"]
145
+ room_name = self._pending_room_data["name"]
146
+ self._pending_room_data = None
147
+
148
+ if action == "create_btn":
149
+ self._do_create_room(room_name, security_key)
150
+ elif action == "join_btn":
151
+ self._do_join_room(room_name, security_key)
152
+
153
+ ### CREATE ROOM ###
154
+
155
+ def _do_create_room(self, room_name: str, security_key: str) -> None:
156
+ response = self.app.api.create_room(room_name, security_key)
157
+
158
+ if response.status_code == 201:
159
+ room_data = response.json()
160
+ self.app.notify(f"Room '{escape(room_name)}' created!", severity="success")
161
+ self.refresh_rooms()
162
+ self.call_after_refresh(self._auto_switch_to_room, room_data.get("id"))
163
+ elif response.status_code == 400:
164
+ self.app.notify(
165
+ escape(response.text) or "A room with that name already exists.",
166
+ severity="error",
167
+ )
168
+ elif response.status_code == 404:
169
+ self.app.notify("User account not found on server.", severity="error")
170
+ else:
171
+ self.app.notify(
172
+ f"Failed to create room ({response.status_code}): {escape(response.text)}",
173
+ severity="error",
174
+ )
175
+
176
+ ### JOIN ROOM ###
177
+
178
+ def _do_join_room(self, room_name: str, security_key: str) -> None:
179
+ response = self.app.api.join_room(room_name, security_key)
180
+
181
+ if response.status_code == 200:
182
+ room_data = response.json()
183
+ self.app.notify(f"Joined '{escape(room_name)}'!", severity="success")
184
+ self.refresh_rooms()
185
+ self.call_after_refresh(self._auto_switch_to_room, room_data.get("id"))
186
+ elif response.status_code == 401:
187
+ self.app.notify("Wrong security key - try again.", severity="error")
188
+ elif response.status_code == 404:
189
+ self.app.notify(
190
+ f"Room '{escape(room_name)}' not found. Check the name and try again.",
191
+ severity="error",
192
+ )
193
+ else:
194
+ self.app.notify(
195
+ f"Failed to join room ({response.status_code}): {escape(response.text)}",
196
+ severity="error",
197
+ )
198
+
199
+ ### DELETE ROOM ###
200
+
201
+ def _do_delete_room(self, room_id: str, security_str: str = "") -> None:
202
+ room = next((r for r in self.rooms if r["id"] == room_id), None)
203
+ room_name = room["roomName"] if room else room_id
204
+
205
+ try:
206
+ response = self.app.api.delete_room(room_id, security_str)
207
+
208
+ if response.status_code == 200:
209
+ # self.app.notify(f"Room '{escape(room_name)}' deleted successfully.", severity="success")
210
+ self._stomp_unsubscribe()
211
+ self.app.current_room_id = None
212
+ self.current_room_messages = []
213
+ self.app.file_cache.clear()
214
+ self._clear_to_empty_viewport()
215
+ self.refresh_rooms()
216
+ elif response.status_code == 403:
217
+ self.app.notify("You don't have permission to delete this room.", severity="error")
218
+ elif response.status_code == 404:
219
+ self.app.notify("Room not found.", severity="error")
220
+ else:
221
+ self.app.notify(
222
+ f"Failed to delete room ({response.status_code}): {escape(response.text)}",
223
+ severity="error"
224
+ )
225
+ except Exception as e:
226
+ self.app.notify(f"Network error: {escape(str(e))}", severity="error")
227
+
228
+ ### LEAVE ROOM ###
229
+
230
+ def _do_leave_room(self, room_id: str) -> None:
231
+ room = next((r for r in self.rooms if r["id"] == room_id), None)
232
+ room_name = room["roomName"] if room else room_id
233
+
234
+ try:
235
+ response = self.app.api.leave_room(room_id)
236
+
237
+ if response.status_code == 200:
238
+ self.app.notify(f"Left '{escape(room_name)}'.", severity="success")
239
+ self._stomp_unsubscribe()
240
+ self.app.current_room_id = None
241
+ self.current_room_messages = []
242
+ self.app.file_cache.clear()
243
+ self._clear_to_empty_viewport()
244
+ self.refresh_rooms()
245
+ elif response.status_code == 404:
246
+ self.app.notify("Room not found.", severity="error")
247
+ else:
248
+ self.app.notify(
249
+ f"Failed to leave room ({response.status_code}): "
250
+ )
251
+ except Exception as e:
252
+ self.app.notify(f"Network error: {escape(str(e))}", severity="error")
253
+
254
+ ### AUTO SWITCH ROOM ###
255
+
256
+ def _auto_switch_to_room(self, room_id: str | None) -> None:
257
+ if not room_id:
258
+ return
259
+
260
+ room = next((r for r in self.rooms if r["id"] == room_id), None)
261
+ if room:
262
+ self.switch_to_room(room_id)
263
+ else:
264
+ rooms = self.fetch_user_rooms()
265
+ self.rooms = rooms
266
+ self.call_after_refresh(self.switch_to_room, room_id)
267
+
268
+ ### ROOM SWITCHING ###
269
+
270
+ def switch_to_room(self, room_id: str) -> None:
271
+ self._stomp_unsubscribe()
272
+ self.app.file_cache.clear()
273
+ self.current_room_messages = []
274
+
275
+ self.app.current_room_id = room_id
276
+ room = next((r for r in self.rooms if r["id"] == room_id), None)
277
+
278
+ if not room:
279
+ self.app.notify("Room details not found", severity="error")
280
+ return
281
+
282
+ self._set_active_room_button(room_id)
283
+ self._stomp_subscribe(room_id)
284
+
285
+ self.query_one("#chat-view").styles.display = "block"
286
+ self.query_one("#empty-view").styles.display = "none"
287
+ self.query_one("#chat_log").clear()
288
+
289
+ self.run_worker(lambda: self._fetch_messages_worker(room_id), thread=True)
290
+
291
+ def _fetch_messages_worker(self, room_id: str) -> None:
292
+ try:
293
+ response = self.app.api.fetch_messages(room_id)
294
+
295
+ if response.status_code == 200:
296
+ messages = response.json()
297
+ self.current_room_messages = messages
298
+
299
+ self.app.call_from_thread(self._render_messages, messages)
300
+ else:
301
+ self.app.call_from_thread(
302
+ self.app.notify,
303
+ f"Failed to load message history",
304
+ severity="error"
305
+ )
306
+ except Exception as e:
307
+ self.app.call_from_thread(
308
+ self.app.notify,
309
+ f"Error fetching messages: {escape(str(e))}",
310
+ severity="error"
311
+ )
312
+
313
+ def _render_messages(self, messages: list) -> None:
314
+ chat_log = self.query_one("#chat_log", RichLog)
315
+ for msg in messages:
316
+ self._write_message(chat_log, msg)
317
+
318
+ # --- STOMP Subscribe / Unsubscribe --- #
319
+
320
+ def _stomp_subscribe(self, room_id: str) -> None:
321
+ if self.app.stomp_conn and self.app.stomp_conn.sock:
322
+ self._current_subscription_id = room_id
323
+ subscribe_frame = (
324
+ f"SUBSCRIBE\n"
325
+ f"id:sub-{room_id}\n"
326
+ f"destination:/topic/room/{room_id}\n"
327
+ f"ack:auto\n"
328
+ f"Authorization:Bearer {self.app.access_token}\n\n\x00"
329
+ )
330
+ try:
331
+ self.app.stomp_conn.send(subscribe_frame)
332
+ except Exception as e:
333
+ self.app.notify(
334
+ f"Subscribe failed: {escape(str(e))}",
335
+ severity="error",
336
+ )
337
+
338
+ def _stomp_unsubscribe(self) -> None:
339
+ if self._current_subscription_id and self.app.stomp_conn and self.app.stomp_conn.sock:
340
+ unsubscribe_frame = (
341
+ f"UNSUBSCRIBE\n"
342
+ f"id:sub-{self._current_subscription_id}\n\n\x00"
343
+ )
344
+ try:
345
+ self.app.stomp_conn.send(unsubscribe_frame)
346
+ except Exception:
347
+ pass
348
+ finally:
349
+ self._current_subscription_id = None
350
+
351
+ def _set_active_room_button(self, room_id: str) -> None:
352
+ try:
353
+ for btn in self.query(".room-link").results(Button):
354
+ btn.remove_class("active")
355
+ self.query_one(f"#room_{room_id}").add_class("active")
356
+ except Exception:
357
+ pass
358
+
359
+ ### MESSAGE DISPLAY ###
360
+
361
+ def _write_message(self, chat_log: RichLog, msg: dict) -> None:
362
+ sender = msg.get("sender", "System")
363
+ content = msg.get("content", "")
364
+
365
+ #Timestamp
366
+ ts_raw = msg.get("timeStamp") or msg.get("messageTime", "")
367
+ ts_str = ""
368
+
369
+ if ts_raw:
370
+ try:
371
+ dt = datetime.fromisoformat(str(ts_raw))
372
+
373
+ if dt.date() == datetime.now().date():
374
+ ts_str = f" [dim]{dt.strftime('%H:%M')}[/]"
375
+ else:
376
+ ts_str = f" [dim]{dt.strftime('%d %b, %H:%M')}[/]"
377
+ except Exception:
378
+ pass
379
+
380
+ # File message
381
+ if content.startswith("FILE:"):
382
+ try:
383
+ parts = content.split(":", 2)
384
+ if len(parts) >= 2:
385
+ filename = parts[1]
386
+ encoded_data = parts[2] if len(parts) > 2 else ""
387
+
388
+ if encoded_data:
389
+ self.app.file_cache[filename] = encoded_data
390
+
391
+ try:
392
+ size_kb = round(len(base64.b64decode(encoded_data)) / 1024, 1)
393
+ except Exception:
394
+ size_kb = 0.0
395
+
396
+ safe_filename = escape(filename)
397
+ display_content = (
398
+ f"📄 [bold]{safe_filename}[/] [dim]({size_kb} KB)[/] "
399
+ f"[@click=app.download_file('{safe_filename}')][underline cyan][ download ][/]"
400
+ )
401
+ else:
402
+ display_content = "📄 [italic][Malformed File Whisper][/]"
403
+ except Exception:
404
+ display_content = "📄 [italic][Error processing File Whisper][/]"
405
+ else:
406
+ display_content = escape(content)
407
+
408
+ safe_sender = escape(sender)
409
+ if sender == self.app.current_user:
410
+ chat_log.write(f"[bold cyan]You:[/]{ts_str} {display_content}")
411
+ else:
412
+ chat_log.write(f"[bold green]{safe_sender}:[/]{ts_str} {display_content}")
413
+
414
+ ### CLEAR VIEWPORT ###
415
+
416
+ def _clear_to_empty_viewport(self) -> None:
417
+ try:
418
+ self.query_one("#chat-view").styles.display = "none"
419
+ self.query_one("#empty-view").styles.display = "block"
420
+ self.query_one("#chat_log").clear()
421
+ self.current_room_messages = []
422
+ except Exception:
423
+ pass
424
+
425
+ ### DATA FETCHERS ###
426
+
427
+ def fetch_user_rooms(self) -> list:
428
+ try:
429
+ response = self.app.api.fetch_rooms()
430
+ if response.status_code == 200:
431
+ return response.json()
432
+ else:
433
+ self.app.notify(
434
+ f"Failed to load rooms ({response.status_code})",
435
+ severity="error",
436
+ )
437
+ except Exception as e:
438
+ self.app.notify(
439
+ f"Failed to load rooms {escape(str(e))}",
440
+ severity="error",
441
+ )
442
+
443
+ return []
444
+
445
+ ### INPUT / SEND ###
446
+
447
+ def on_input_submitted(self, event: Input.Submitted) -> None:
448
+ raw = event.value.strip()
449
+ self.query_one('#chat_input').value = ""
450
+
451
+ if not raw:
452
+ return
453
+
454
+ if raw.lower() == "@help":
455
+ self._show_help()
456
+ return
457
+
458
+ if not self.app.current_room_id:
459
+ self.app.notify("Select a room first.", severity="warning")
460
+ return
461
+
462
+ if raw.startswith("@"):
463
+ self._handle_command(raw)
464
+ else:
465
+ self._send_message(raw)
466
+
467
+ def _show_help(self) -> None:
468
+ try:
469
+ chat_log = self.query_one("#chat_log", RichLog)
470
+ if self.query_one("#chat_log").styles.display == "none":
471
+ raise Exception("chat not visible")
472
+ except Exception:
473
+ self.app.notify(
474
+ "@copy:/path send file | @get:name download | @delete delete room | @leave leave room",
475
+ severity="information"
476
+ )
477
+ return
478
+
479
+ chat_log.write("[bold yellow]-- ShellWhisper Commands --[/]")
480
+ chat_log.write(" [cyan]@copy:/path/to/file[/] send any file to this room")
481
+ chat_log.write(" [cyan]@get:filename[/] download a file from room history")
482
+ chat_log.write(" [cyan]@save:filename[/] alias for @get")
483
+ chat_log.write(" [cyan]@delete[/] delete the current room")
484
+ chat_log.write(" [cyan]@leave[/] leave the current room")
485
+ chat_log.write(" [cyan]@help[/] show this message")
486
+
487
+ def _handle_command(self, raw: str) -> None:
488
+ chat_log = self.query_one("#chat_log", RichLog)
489
+
490
+ if raw.lower().startswith("@copy:"):
491
+ file_path = os.path.expanduser(raw[6:].strip())
492
+
493
+ if not file_path:
494
+ self.app.notify("Usage: @copy:/path/to/file", severity="warning")
495
+ return
496
+ if not os.path.isfile(file_path):
497
+ self.app.notify(f"File not found: {escape(file_path)}", severity="error")
498
+ return
499
+
500
+ try:
501
+ with open(file_path, "rb") as f:
502
+ binary_data = f.read()
503
+
504
+ if len(binary_data) > 5 * 1024 * 1024:
505
+ self.app.notify("File too large - 5 MB maximum.", severity="error")
506
+ return
507
+
508
+ encoded = base64.b64encode(binary_data).decode("utf-8")
509
+ filename = os.path.basename(file_path)
510
+ size_kb = round(len(binary_data) / 1024, 1)
511
+
512
+ chat_log.write(f"[dim]Sending:[/] [bold]{escape(filename)}[/] [dim]({size_kb} KB)[/]")
513
+ self._send_message(f"FILE:{filename}:{encoded}")
514
+
515
+ except Exception as e:
516
+ self.app.notify(f"@copy failed: {escape(str(e))}", severity="error")
517
+
518
+ elif raw.lower().startswith("@get:") or raw.lower().startswith("@save:"):
519
+ prefix_len = 5 if raw.lower().startswith("@get:") else 6
520
+ target = raw[prefix_len:].strip()
521
+
522
+ if not target:
523
+ self.app.notify("Usage: @get:filename", severity="warning")
524
+ return
525
+
526
+ if target in self.app.file_cache:
527
+ self.app.action_download_file(target)
528
+ return
529
+
530
+ found = False
531
+ for msg in reversed(self.current_room_messages):
532
+ c = msg.get("content", "")
533
+
534
+ if c.startswith(f"FILE:{target}:"):
535
+ _, filename, data = c.split(":", 2)
536
+ self.app.file_cache[filename] = data
537
+ self.app.action_download_file(filename)
538
+ found = True
539
+ break
540
+
541
+ if not found:
542
+ self.app.notify(f"File '{escape(target)}' not found in whispers", severity="warning")
543
+
544
+ elif raw.lower() == "@delete":
545
+ room_id = self.app.current_room_id
546
+ if not room_id:
547
+ self.app.notify("Select an active room first.", severity="warning")
548
+ return
549
+
550
+ room = next((r for r in self.rooms if r["id"] == self.app.current_room_id), None)
551
+ if not room:
552
+ return
553
+
554
+ if room.get("type") == "PRIVATE":
555
+ self._do_delete_room(room_id, "")
556
+ else:
557
+ self._pending_room_data = {
558
+ "name": room["roomName"],
559
+ "action": "chat_command_delete",
560
+ "id": room_id
561
+ }
562
+ self.app.push_screen(
563
+ SecurityScreen(action="chat_command_delete", room_name=room["roomName"]),
564
+ self._on_chat_command_security_dismissed,
565
+ )
566
+
567
+ elif raw.lower() == "@leave":
568
+ room_id = self.app.current_room_id
569
+ if not room_id:
570
+ self.app.notify("No active room selected.", severity="warning")
571
+ return
572
+ self._do_leave_room(room_id)
573
+
574
+ elif raw.lower() == "@help":
575
+ self._show_help()
576
+
577
+ else:
578
+ self.app.notify(
579
+ f"Unknown command '{escape(raw)}'. Type @help for available commands.",
580
+ severity="warning",
581
+ )
582
+
583
+ def _on_delete_security_dismissed(self, security_key: str | None) -> None:
584
+ if not security_key or not self._pending_room_data:
585
+ return
586
+
587
+ room_id = self._pending_room_data.get("id")
588
+ self._pending_room_data = None
589
+
590
+ if room_id:
591
+ self._do_delete_room(room_id, security_key)
592
+
593
+ def _on_chat_command_security_dismissed(self, security_key: str | None) -> None:
594
+ if not security_key or not self._pending_room_data:
595
+ return
596
+
597
+ action = self._pending_room_data["action"]
598
+ room_id = self._pending_room_data.get("id")
599
+ self._pending_room_data = None
600
+
601
+ if action == "chat_command_delete" and room_id:
602
+ self._do_delete_room(room_id, security_key)
603
+
604
+ def _send_message(self, message_text: str) -> None:
605
+ payload = {
606
+ "sender": self.app.current_user,
607
+ "content": message_text,
608
+ "roomId": self.app.current_room_id,
609
+ "messageTime": datetime.now().isoformat(),
610
+ }
611
+
612
+ payload_str = json.dumps(payload)
613
+ byte_length = len(payload_str.encode('utf-8'))
614
+
615
+ frame = (
616
+ f"SEND\n"
617
+ f"destination:/app/sendMessage\n"
618
+ f"content-type:application/json\n"
619
+ f"content-length:{byte_length}\n\n"
620
+ f"{payload_str}\x00"
621
+ )
622
+
623
+ if self.app.stomp_conn and self.app.stomp_conn.sock:
624
+ try:
625
+ self.app.stomp_conn.send(frame)
626
+ except Exception as e:
627
+ self.app.notify(
628
+ f"Failed to send message: {escape(str(e))}",
629
+ severity="error"
630
+ )
631
+ else:
632
+ self.app.notify(
633
+ f"Not connected - message not sent.",
634
+ severity="error"
635
+ )
636
+
637
+ ### INCOMING REAL-TIME MESSAGES ###
638
+
639
+ def on_new_whisper_received(self, event: NewWhisperReceived) -> None:
640
+ data = event.data
641
+ self.current_room_messages.append(data)
642
+ chat_log = self.query_one("#chat_log", RichLog)
643
+ self._write_message(chat_log, data)
644
+
645
+ ### LOGOUT ###
646
+
647
+ def action_logout(self) -> None:
648
+ self.logout_process()
649
+
650
+ def logout_process(self) -> None:
651
+ from src.screens.login import LoginScreen
652
+
653
+ self._stomp_unsubscribe()
654
+ self.app._disconnect_websocket()
655
+ self.app.access_token = None
656
+ self.app.current_user = None
657
+ self.app.current_room_id = None
658
+ self.app.file_cache.clear()
659
+
660
+ self.app.notify("Logged out successfully", severity="information")
661
+ self.app.switch_screen(LoginScreen())