solid-chat 0.0.6 → 0.0.17

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.
package/index.html CHANGED
@@ -28,244 +28,89 @@
28
28
  <link rel="manifest" href="manifest.json">
29
29
 
30
30
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
31
- <style>
32
- :root {
33
- --gradient-start: #667eea;
34
- --gradient-end: #9f7aea;
35
- --bg: #f7f8fc;
36
- --text: #2d3748;
37
- --text-muted: #a0aec0;
38
- --accent: #805ad5;
39
- }
40
31
 
32
+ <!-- Theme CSS - loaded dynamically -->
33
+ <link id="theme-css" rel="stylesheet" href="themes/wave.css">
34
+
35
+ <style>
36
+ /* Base styles that don't change between themes */
41
37
  * { box-sizing: border-box; margin: 0; padding: 0; }
42
38
 
43
39
  body {
44
40
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
45
41
  background: var(--bg);
46
42
  color: var(--text);
47
- min-height: 100vh;
48
- }
49
-
50
- .app-header {
51
- background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
52
- color: white;
53
- padding: 16px 24px;
54
- box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
55
- display: flex;
56
- align-items: center;
57
- gap: 16px;
58
- }
59
-
60
- .app-header h1 {
61
- font-size: 1.25rem;
62
- font-weight: 600;
63
- margin-bottom: 2px;
64
- }
65
-
66
- .app-header p {
67
- font-size: 0.8rem;
68
- opacity: 0.9;
69
- }
70
-
71
- .app-header button {
72
- padding: 6px 14px;
73
- background: rgba(255,255,255,0.2);
74
- color: white;
75
- border: 1px solid rgba(255,255,255,0.3);
76
- border-radius: 6px;
77
- font-size: 12px;
78
- font-weight: 500;
79
- cursor: pointer;
80
- transition: background 0.2s;
81
- }
82
-
83
- .app-header button:hover {
84
- background: rgba(255,255,255,0.3);
85
- }
86
-
87
- .app-header input {
88
- padding: 6px 10px;
89
- border: 1px solid rgba(255,255,255,0.3);
90
- border-radius: 6px;
91
- font-size: 12px;
92
- background: rgba(255,255,255,0.15);
93
- color: white;
94
- width: 140px;
95
- }
96
-
97
- .app-header input::placeholder {
98
- color: rgba(255,255,255,0.7);
99
- }
100
-
101
- #chatContainer {
102
- background: white;
103
- overflow: hidden;
104
- height: 100%;
105
- }
106
-
107
- .placeholder {
108
- display: flex;
109
- flex-direction: column;
110
- align-items: center;
111
- justify-content: center;
112
- height: 100%;
113
- color: var(--text-muted);
114
- text-align: center;
115
- }
116
-
117
- .placeholder-icon {
118
- font-size: 64px;
119
- margin-bottom: 16px;
120
- }
121
-
122
- .placeholder h2 {
123
- color: var(--text);
124
- margin-bottom: 8px;
125
- }
126
-
127
- .loading-spinner {
128
- width: 40px;
129
- height: 40px;
130
- border: 3px solid #e0e0e0;
131
- border-top-color: var(--primary);
132
- border-radius: 50%;
133
- animation: spin 0.8s linear infinite;
134
- margin-bottom: 16px;
135
- }
136
-
137
- @keyframes spin {
138
- to { transform: rotate(360deg); }
139
- }
140
-
141
- @keyframes fadeInUp {
142
- from {
143
- opacity: 0;
144
- transform: translate(-50%, 20px);
145
- }
146
- to {
147
- opacity: 1;
148
- transform: translate(-50%, 0);
149
- }
150
- }
151
-
152
- .error {
153
- background: #fff3f3;
154
- border: 1px solid #ffcdd2;
155
- color: #c62828;
156
- padding: 16px;
157
- border-radius: 8px;
158
- margin-top: 16px;
159
- }
160
-
161
- /* App layout with sidebar */
162
- .app-layout {
163
- display: flex;
164
- height: calc(100vh - 68px);
165
- }
166
-
167
- .sidebar {
168
- width: 320px;
169
- flex-shrink: 0;
170
- height: 100%;
43
+ height: 100vh;
171
44
  overflow: hidden;
172
45
  }
173
-
174
- .main-content {
175
- flex: 1;
176
- overflow: hidden;
177
- background: var(--bg);
178
- height: 100%;
179
- }
180
-
181
- /* Mobile hamburger */
182
- .mobile-menu-btn {
183
- display: none;
184
- width: 40px;
185
- height: 40px;
186
- background: rgba(255,255,255,0.2);
187
- border: none;
188
- border-radius: 8px;
189
- color: white;
190
- font-size: 24px;
191
- cursor: pointer;
192
- align-items: center;
193
- justify-content: center;
194
- }
195
-
196
- .sidebar-overlay {
197
- display: none;
198
- position: fixed;
199
- top: 0;
200
- left: 0;
201
- right: 0;
202
- bottom: 0;
203
- background: rgba(0,0,0,0.5);
204
- z-index: 99;
205
- }
206
-
207
- @media (max-width: 768px) {
208
- .mobile-menu-btn {
209
- display: flex;
210
- }
211
-
212
- .sidebar {
213
- position: fixed;
214
- top: 0;
215
- left: -320px;
216
- height: 100vh;
217
- z-index: 100;
218
- transition: left 0.3s ease;
219
- box-shadow: 4px 0 20px rgba(0,0,0,0.2);
220
- }
221
-
222
- .sidebar.open {
223
- left: 0;
224
- }
225
-
226
- .sidebar-overlay.open {
227
- display: block;
228
- }
229
-
230
- .app-layout {
231
- height: calc(100vh - 68px);
232
- }
233
-
234
- .main-content {
235
- width: 100%;
236
- }
237
- }
238
46
  </style>
239
47
  </head>
240
48
  <body>
241
49
 
242
- <header class="app-header">
243
- <button class="mobile-menu-btn" id="mobileMenuBtn">☰</button>
244
- <div>
245
- <h1>Solid Chat <span id="appVersion" style="font-size: 12px; font-weight: 400; opacity: 0.8;"></span></h1>
246
- <p>Decentralized messaging for the web</p>
247
- </div>
248
- <div id="headerLoginArea" style="margin-left: auto; display: flex; align-items: center; gap: 8px;">
249
- <button id="soundToggle" title="Toggle notification sound" style="background: none; border: none; font-size: 18px; cursor: pointer; padding: 4px;">🔔</button>
250
- <span id="userStatus" style="font-size: 13px; opacity: 0.9;">Loading...</span>
251
- <span id="loginArea"></span>
50
+ <div class="app-wrapper">
51
+ <div class="app-banner">
52
+ <div class="brand">
53
+ <h1>Solid Chat</h1>
54
+ <p>Decentralized messaging</p>
55
+ </div>
252
56
  </div>
253
- </header>
254
57
 
255
- <div class="sidebar-overlay" id="sidebarOverlay"></div>
58
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
59
+
60
+ <div class="app-container">
61
+ <!-- Left Panel - Chat List -->
62
+ <aside class="left-panel" id="leftPanel">
63
+ <div class="panel-header">
64
+ <div class="header-left">
65
+ <div class="user-avatar" id="userAvatar">S</div>
66
+ <div>
67
+ <div class="header-title">Solid Chat <span class="header-version" id="appVersion"></span></div>
68
+ </div>
69
+ </div>
70
+ <div class="header-actions">
71
+ </div>
72
+ </div>
256
73
 
257
- <div class="app-layout">
258
- <aside class="sidebar" id="sidebar"></aside>
74
+ <div class="sidebar" id="sidebar"></div>
259
75
 
260
- <main class="main-content">
261
- <div id="chatContainer">
262
- <div class="placeholder" id="placeholder">
263
- <div class="placeholder-icon">💬</div>
264
- <h2>Welcome to Solid Chat</h2>
265
- <p>Select a chat from the sidebar<br>or add a new one with the + button.</p>
76
+ <div class="sidebar-footer">
77
+ <select id="sidebarThemeSelect" class="sidebar-theme-select" title="Switch theme">
78
+ <option value="solid">Solid</option>
79
+ <option value="wave">Wave</option>
80
+ <option value="telegram">Telegram</option>
81
+ <option value="signal">Signal</option>
82
+ </select>
266
83
  </div>
267
- </div>
268
- </main>
84
+ </aside>
85
+
86
+ <!-- Right Panel - Chat -->
87
+ <main class="right-panel">
88
+ <div class="content-header">
89
+ <div class="header-left">
90
+ <button class="mobile-menu-btn" id="mobileMenuBtn">☰</button>
91
+ <span id="userStatus" class="user-status">Loading...</span>
92
+ </div>
93
+ <div class="header-right" id="headerLoginArea">
94
+ <span id="loginArea"></span>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="chat-area">
99
+ <div id="chatContainer">
100
+ <div class="placeholder" id="placeholder">
101
+ <div class="placeholder-icon">💬</div>
102
+ <h2>Welcome to Solid Chat</h2>
103
+ <p>Send and receive messages from your Solid POD.<br>Your data stays with you, always.<br><br>Select a chat from the sidebar or add a new one.</p>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </main>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="status-badge" id="statusBadge">
112
+ <div class="status-dot"></div>
113
+ <span>Connected</span>
269
114
  </div>
270
115
 
271
116
  <!-- Load rdflib and Solid auth -->
@@ -274,7 +119,49 @@
274
119
 
275
120
  <script type="module">
276
121
  import { longChatPane } from './src/longChatPane.js'
277
- import { chatListPane, addChat, updateChatPreview } from './src/chatListPane.js'
122
+ import { chatListPane, addChat, updateChatPreview, incrementUnread, resetUnread } from './src/chatListPane.js'
123
+
124
+ // Theme management
125
+ const THEMES = {
126
+ wave: { name: 'Wave', file: 'themes/wave.css' },
127
+ solid: { name: 'Solid', file: 'themes/solid.css' },
128
+ telegram: { name: 'Telegram', file: 'themes/telegram.css' },
129
+ signal: { name: 'Signal', file: 'themes/signal.css' }
130
+ }
131
+
132
+ function loadTheme(themeName) {
133
+ const theme = THEMES[themeName] || THEMES.wave
134
+ const themeLink = document.getElementById('theme-css')
135
+ themeLink.href = theme.file
136
+ localStorage.setItem('solidchat-theme', themeName)
137
+
138
+ // Update sidebar select
139
+ const sidebarSelect = document.getElementById('sidebarThemeSelect')
140
+ if (sidebarSelect) sidebarSelect.value = themeName
141
+
142
+ // Update title based on theme
143
+ const titles = { wave: 'Wave', solid: 'Solid Chat', telegram: 'Telegram', signal: 'Signal' }
144
+ document.title = titles[themeName] || 'Solid Chat'
145
+ document.querySelector('.header-title').innerHTML = `${titles[themeName]} <span class="header-version" id="appVersion"></span>`
146
+
147
+ // Reload version
148
+ fetch('./package.json')
149
+ .then(r => r.json())
150
+ .then(pkg => {
151
+ const el = document.getElementById('appVersion')
152
+ if (el) el.textContent = `v${pkg.version}`
153
+ })
154
+ .catch(() => {})
155
+ }
156
+
157
+ function initTheme() {
158
+ const saved = localStorage.getItem('solidchat-theme') || 'solid'
159
+ loadTheme(saved)
160
+
161
+ document.getElementById('sidebarThemeSelect').addEventListener('change', (e) => {
162
+ loadTheme(e.target.value)
163
+ })
164
+ }
278
165
 
279
166
  // Wait for libraries to be available
280
167
  async function waitForLibraries() {
@@ -299,17 +186,19 @@ const placeholder = document.getElementById('placeholder')
299
186
  const userStatus = document.getElementById('userStatus')
300
187
  const loginArea = document.getElementById('loginArea')
301
188
  const sidebar = document.getElementById('sidebar')
189
+ const leftPanel = document.getElementById('leftPanel')
302
190
  const mobileMenuBtn = document.getElementById('mobileMenuBtn')
303
191
  const sidebarOverlay = document.getElementById('sidebarOverlay')
192
+ const statusBadge = document.getElementById('statusBadge')
304
193
 
305
194
  // Mobile menu toggle
306
195
  mobileMenuBtn.addEventListener('click', () => {
307
- sidebar.classList.toggle('open')
196
+ leftPanel.classList.toggle('open')
308
197
  sidebarOverlay.classList.toggle('open')
309
198
  })
310
199
 
311
200
  sidebarOverlay.addEventListener('click', () => {
312
- sidebar.classList.remove('open')
201
+ leftPanel.classList.remove('open')
313
202
  sidebarOverlay.classList.remove('open')
314
203
  })
315
204
 
@@ -341,14 +230,18 @@ async function handleAuthRedirect() {
341
230
  function updateAuthUI(isLoggedIn) {
342
231
  if (isLoggedIn && currentWebId) {
343
232
  const shortId = currentWebId.split('//')[1]?.split('/')[0] || currentWebId
344
- userStatus.innerHTML = `Logged in as <strong><a href="${currentWebId}" target="_blank" style="color: white; text-decoration: underline; text-underline-offset: 2px;">${shortId}</a></strong>`
345
- loginArea.innerHTML = `<button id="logoutBtn">Logout</button>`
233
+ userStatus.innerHTML = `Logged in as <a href="${currentWebId}" target="_blank">${shortId}</a>`
234
+ loginArea.innerHTML = `<button class="btn btn-secondary" id="logoutBtn">Logout</button>`
346
235
  document.getElementById('logoutBtn').addEventListener('click', handleLogout)
236
+
237
+ // Update avatar with user initial
238
+ const initial = shortId.charAt(0).toUpperCase()
239
+ document.getElementById('userAvatar').textContent = initial
347
240
  } else {
348
241
  userStatus.textContent = 'Not logged in'
349
242
  loginArea.innerHTML = `
350
- <input type="text" id="idpInput" placeholder="e.g. solidweb.org" style="padding: 8px 12px; border: 2px solid #e2e8f0; border-radius: 8px; font-size: 13px; width: 160px;">
351
- <button id="loginBtn">Login</button>
243
+ <input type="text" class="input-field" id="idpInput" placeholder="e.g. solidweb.org">
244
+ <button class="btn btn-primary" id="loginBtn">Login</button>
352
245
  `
353
246
  document.getElementById('loginBtn').addEventListener('click', handleLogin)
354
247
  document.getElementById('idpInput').addEventListener('keydown', (e) => {
@@ -517,7 +410,11 @@ function toggleSound() {
517
410
 
518
411
  function updateSoundButton() {
519
412
  const btn = document.getElementById('soundToggle')
520
- if (btn) btn.textContent = soundEnabled ? '🔔' : '🔕'
413
+ if (btn) {
414
+ btn.innerHTML = soundEnabled
415
+ ? '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>'
416
+ : '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/><line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" stroke-width="2"/></svg>'
417
+ }
521
418
  }
522
419
 
523
420
  // Subscribe to real-time updates for a resource
@@ -592,6 +489,9 @@ async function subscribeWebSocketChannel2023(uri, authFetch) {
592
489
  console.log('WebSocketChannel2023 notification:', event.data)
593
490
  // Any message means the resource changed
594
491
  playNotificationSound()
492
+ if (document.hidden) {
493
+ incrementUnread(uri)
494
+ }
595
495
  setTimeout(() => refreshChat(), 500)
596
496
  }
597
497
 
@@ -629,6 +529,9 @@ function connectLegacyWebSocket(updatesVia, uri) {
629
529
  if (updatedUri === uri || uri.startsWith(updatedUri)) {
630
530
  console.log('Chat updated, refreshing...')
631
531
  playNotificationSound()
532
+ if (document.hidden) {
533
+ incrementUnread(uri)
534
+ }
632
535
  setTimeout(() => refreshChat(), 500)
633
536
  }
634
537
  }
@@ -706,7 +609,7 @@ async function loadChat(uri) {
706
609
  }
707
610
 
708
611
  // Close mobile sidebar if open
709
- sidebar.classList.remove('open')
612
+ leftPanel.classList.remove('open')
710
613
  sidebarOverlay.classList.remove('open')
711
614
 
712
615
  chatContainer.innerHTML = `
@@ -989,19 +892,6 @@ function showToast(message) {
989
892
  const toast = document.createElement('div')
990
893
  toast.className = 'toast'
991
894
  toast.textContent = message
992
- toast.style.cssText = `
993
- position: fixed;
994
- bottom: 24px;
995
- left: 50%;
996
- transform: translateX(-50%);
997
- background: #1e1e2e;
998
- color: white;
999
- padding: 12px 24px;
1000
- border-radius: 8px;
1001
- font-size: 14px;
1002
- z-index: 9999;
1003
- animation: fadeInUp 0.3s ease;
1004
- `
1005
895
  document.body.appendChild(toast)
1006
896
 
1007
897
  setTimeout(() => {
@@ -1013,20 +903,22 @@ function showToast(message) {
1013
903
 
1014
904
  // Make createChat and copyShareLink available globally for chatListPane
1015
905
  window.solidChat = { createChat, copyShareLink, getMyPodRoot }
906
+ window.toggleSound = toggleSound
1016
907
 
1017
908
  // Initialize: handle auth redirect first, then render sidebar
1018
909
  async function init() {
1019
- // Load and display version
1020
- fetch('./package.json')
1021
- .then(r => r.json())
1022
- .then(pkg => {
1023
- document.getElementById('appVersion').textContent = `v${pkg.version}`
1024
- })
1025
- .catch(() => {})
910
+ // Initialize theme
911
+ initTheme()
1026
912
 
1027
- // Set initial sound button state
1028
- updateSoundButton()
1029
- document.getElementById('soundToggle').addEventListener('click', toggleSound)
913
+ // Sound button is now created in chatListPane with onclick handler
914
+
915
+ // Clear unread count when user returns to tab
916
+ document.addEventListener('visibilitychange', () => {
917
+ if (!document.hidden && currentChatUri) {
918
+ resetUnread(currentChatUri)
919
+ chatListPane.setActiveChat(currentChatUri)
920
+ }
921
+ })
1030
922
 
1031
923
  // Handle auth redirect callback (if returning from IdP)
1032
924
  await handleAuthRedirect()
@@ -1047,6 +939,13 @@ async function init() {
1047
939
  })
1048
940
  sidebar.appendChild(sidebarElement)
1049
941
 
942
+ // Move theme selector above Discover Chats button
943
+ const discoverBtn = sidebar.querySelector('.discover-btn')
944
+ const themeSelect = document.getElementById('sidebarThemeSelect')
945
+ if (discoverBtn && themeSelect) {
946
+ discoverBtn.parentNode.insertBefore(themeSelect.parentNode, discoverBtn)
947
+ }
948
+
1050
949
  // Check for ?chat= deep link first, then ?uri= (legacy), then default
1051
950
  const deepLinkedChat = await handleDeepLink()
1052
951
  if (deepLinkedChat) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-chat",
3
- "version": "0.0.6",
3
+ "version": "0.0.17",
4
4
  "description": "Modern chat panes for Solid pods - longChatPane and chatListPane",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -19,6 +19,45 @@ const MEETING = {
19
19
 
20
20
  // Storage key for localStorage
21
21
  const STORAGE_KEY = 'solidchat-chats'
22
+ const UNREAD_KEY = 'solidchat-unread'
23
+
24
+ // Get unread count for a chat
25
+ function getUnreadCount(uri) {
26
+ try {
27
+ const data = JSON.parse(localStorage.getItem(UNREAD_KEY) || '{}')
28
+ return data[uri] || 0
29
+ } catch {
30
+ return 0
31
+ }
32
+ }
33
+
34
+ // Increment unread count for a chat (call when new message arrives)
35
+ function incrementUnread(uri) {
36
+ try {
37
+ const data = JSON.parse(localStorage.getItem(UNREAD_KEY) || '{}')
38
+ data[uri] = (data[uri] || 0) + 1
39
+ localStorage.setItem(UNREAD_KEY, JSON.stringify(data))
40
+ renderChatList()
41
+ } catch (e) {
42
+ console.warn('Failed to increment unread:', e)
43
+ }
44
+ }
45
+
46
+ // Reset unread count (call when chat is opened)
47
+ function resetUnread(uri) {
48
+ try {
49
+ const data = JSON.parse(localStorage.getItem(UNREAD_KEY) || '{}')
50
+ data[uri] = 0
51
+ localStorage.setItem(UNREAD_KEY, JSON.stringify(data))
52
+ } catch (e) {
53
+ console.warn('Failed to reset unread:', e)
54
+ }
55
+ }
56
+
57
+ // Legacy alias for compatibility
58
+ function setLastRead(uri) {
59
+ resetUnread(uri)
60
+ }
22
61
 
23
62
  // Default global chats
24
63
  const DEFAULT_CHATS = [
@@ -96,6 +135,24 @@ const styles = `
96
135
  background: rgba(255,255,255,0.3);
97
136
  }
98
137
 
138
+ .sound-toggle-btn {
139
+ background: none;
140
+ border: none;
141
+ color: rgba(255,255,255,0.7);
142
+ cursor: pointer;
143
+ padding: 6px;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ border-radius: 50%;
148
+ transition: all 0.2s;
149
+ }
150
+
151
+ .sound-toggle-btn:hover {
152
+ color: white;
153
+ background: rgba(255,255,255,0.15);
154
+ }
155
+
99
156
  .chat-list {
100
157
  flex: 1;
101
158
  overflow-y: auto;
@@ -164,6 +221,21 @@ const styles = `
164
221
  flex-shrink: 0;
165
222
  }
166
223
 
224
+ .chat-item-badge {
225
+ background: #e74c3c;
226
+ color: white;
227
+ font-size: 11px;
228
+ font-weight: 600;
229
+ min-width: 18px;
230
+ height: 18px;
231
+ border-radius: 9px;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ padding: 0 5px;
236
+ margin-left: 8px;
237
+ }
238
+
167
239
  .chat-item-preview {
168
240
  font-size: 13px;
169
241
  color: var(--text-muted);
@@ -555,6 +627,15 @@ function renderChatList() {
555
627
  header.appendChild(title)
556
628
  header.appendChild(time)
557
629
 
630
+ // Show unread count badge
631
+ const unreadCount = getUnreadCount(chat.uri)
632
+ if (unreadCount > 0) {
633
+ const badge = dom.createElement('div')
634
+ badge.className = 'chat-item-badge'
635
+ badge.textContent = unreadCount > 99 ? '99+' : unreadCount
636
+ header.appendChild(badge)
637
+ }
638
+
558
639
  const preview = dom.createElement('div')
559
640
  preview.className = 'chat-item-preview'
560
641
  preview.textContent = chat.lastMessage || 'No messages yet'
@@ -577,6 +658,7 @@ function renderChatList() {
577
658
 
578
659
  item.onclick = () => {
579
660
  activeUri = chat.uri
661
+ setLastRead(chat.uri)
580
662
  renderChatList()
581
663
  if (onSelectCallback) {
582
664
  onSelectCallback(chat.uri)
@@ -859,13 +941,33 @@ export const chatListPane = {
859
941
  title.className = 'sidebar-title'
860
942
  title.textContent = 'Chats'
861
943
 
944
+ // Sound toggle button (subtle white bell like Telegram)
945
+ const soundBtn = dom.createElement('button')
946
+ soundBtn.className = 'sound-toggle-btn'
947
+ soundBtn.id = 'soundToggle'
948
+ soundBtn.title = 'Toggle notification sound'
949
+ soundBtn.innerHTML = localStorage.getItem('solidchat-sound') !== 'false'
950
+ ? '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>'
951
+ : '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/><path d="M3.27 3L2 4.27l2.92 2.92C4.34 8.16 4 9.5 4 11v5l-2 2v1h14.73l2 2L20 19.73 3.27 3z"/></svg>'
952
+ soundBtn.onclick = () => {
953
+ if (window.toggleSound) {
954
+ window.toggleSound()
955
+ }
956
+ }
957
+
862
958
  const addBtn = dom.createElement('button')
863
959
  addBtn.className = 'add-chat-btn'
864
960
  addBtn.textContent = '+'
865
961
  addBtn.title = 'Add or create chat'
866
962
  addBtn.onclick = () => showAddModal(dom, options.webId)
867
963
 
868
- header.appendChild(title)
964
+ // Group title and sound button together (flush)
965
+ const titleGroup = dom.createElement('div')
966
+ titleGroup.style.cssText = 'display: flex; align-items: center; gap: 8px;'
967
+ titleGroup.appendChild(title)
968
+ titleGroup.appendChild(soundBtn)
969
+
970
+ header.appendChild(titleGroup)
869
971
  header.appendChild(addBtn)
870
972
 
871
973
  // Chat list
@@ -931,4 +1033,4 @@ export const chatListPane = {
931
1033
  }
932
1034
 
933
1035
  // Export for use in index.html
934
- export { addChat, removeChat, updateChatPreview }
1036
+ export { addChat, removeChat, updateChatPreview, setLastRead, incrementUnread, resetUnread }
@@ -695,6 +695,10 @@ function renderMessageContent(dom, content) {
695
695
  img.alt = 'Image'
696
696
  img.loading = 'lazy'
697
697
  img.onclick = () => window.open(part, '_blank')
698
+ img.onload = () => {
699
+ const mc = img.closest('.messages-container')
700
+ if (mc) mc.scrollTop = mc.scrollHeight
701
+ }
698
702
  wrapper.appendChild(img)
699
703
  container.appendChild(wrapper)
700
704
  } else if (VIDEO_EXT.test(part)) {