solid-chat 0.0.10 → 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
@@ -68,7 +68,6 @@
68
68
  </div>
69
69
  </div>
70
70
  <div class="header-actions">
71
- <button class="icon-btn" id="soundToggle" title="Toggle notification sound">🔔</button>
72
71
  </div>
73
72
  </div>
74
73
 
@@ -76,8 +75,8 @@
76
75
 
77
76
  <div class="sidebar-footer">
78
77
  <select id="sidebarThemeSelect" class="sidebar-theme-select" title="Switch theme">
79
- <option value="wave">Wave</option>
80
78
  <option value="solid">Solid</option>
79
+ <option value="wave">Wave</option>
81
80
  <option value="telegram">Telegram</option>
82
81
  <option value="signal">Signal</option>
83
82
  </select>
@@ -92,14 +91,6 @@
92
91
  <span id="userStatus" class="user-status">Loading...</span>
93
92
  </div>
94
93
  <div class="header-right" id="headerLoginArea">
95
- <div class="theme-switcher">
96
- <select id="themeSelect" title="Switch theme">
97
- <option value="wave">Wave</option>
98
- <option value="solid">Solid</option>
99
- <option value="telegram">Telegram</option>
100
- <option value="signal">Signal</option>
101
- </select>
102
- </div>
103
94
  <span id="loginArea"></span>
104
95
  </div>
105
96
  </div>
@@ -128,7 +119,7 @@
128
119
 
129
120
  <script type="module">
130
121
  import { longChatPane } from './src/longChatPane.js'
131
- import { chatListPane, addChat, updateChatPreview } from './src/chatListPane.js'
122
+ import { chatListPane, addChat, updateChatPreview, incrementUnread, resetUnread } from './src/chatListPane.js'
132
123
 
133
124
  // Theme management
134
125
  const THEMES = {
@@ -144,9 +135,7 @@ function loadTheme(themeName) {
144
135
  themeLink.href = theme.file
145
136
  localStorage.setItem('solidchat-theme', themeName)
146
137
 
147
- // Update selects
148
- const select = document.getElementById('themeSelect')
149
- if (select) select.value = themeName
138
+ // Update sidebar select
150
139
  const sidebarSelect = document.getElementById('sidebarThemeSelect')
151
140
  if (sidebarSelect) sidebarSelect.value = themeName
152
141
 
@@ -169,10 +158,6 @@ function initTheme() {
169
158
  const saved = localStorage.getItem('solidchat-theme') || 'solid'
170
159
  loadTheme(saved)
171
160
 
172
- document.getElementById('themeSelect').addEventListener('change', (e) => {
173
- loadTheme(e.target.value)
174
- })
175
-
176
161
  document.getElementById('sidebarThemeSelect').addEventListener('change', (e) => {
177
162
  loadTheme(e.target.value)
178
163
  })
@@ -425,7 +410,11 @@ function toggleSound() {
425
410
 
426
411
  function updateSoundButton() {
427
412
  const btn = document.getElementById('soundToggle')
428
- 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
+ }
429
418
  }
430
419
 
431
420
  // Subscribe to real-time updates for a resource
@@ -500,6 +489,9 @@ async function subscribeWebSocketChannel2023(uri, authFetch) {
500
489
  console.log('WebSocketChannel2023 notification:', event.data)
501
490
  // Any message means the resource changed
502
491
  playNotificationSound()
492
+ if (document.hidden) {
493
+ incrementUnread(uri)
494
+ }
503
495
  setTimeout(() => refreshChat(), 500)
504
496
  }
505
497
 
@@ -537,6 +529,9 @@ function connectLegacyWebSocket(updatesVia, uri) {
537
529
  if (updatedUri === uri || uri.startsWith(updatedUri)) {
538
530
  console.log('Chat updated, refreshing...')
539
531
  playNotificationSound()
532
+ if (document.hidden) {
533
+ incrementUnread(uri)
534
+ }
540
535
  setTimeout(() => refreshChat(), 500)
541
536
  }
542
537
  }
@@ -908,15 +903,22 @@ function showToast(message) {
908
903
 
909
904
  // Make createChat and copyShareLink available globally for chatListPane
910
905
  window.solidChat = { createChat, copyShareLink, getMyPodRoot }
906
+ window.toggleSound = toggleSound
911
907
 
912
908
  // Initialize: handle auth redirect first, then render sidebar
913
909
  async function init() {
914
910
  // Initialize theme
915
911
  initTheme()
916
912
 
917
- // Set initial sound button state
918
- updateSoundButton()
919
- 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
+ })
920
922
 
921
923
  // Handle auth redirect callback (if returning from IdP)
922
924
  await handleAuthRedirect()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-chat",
3
- "version": "0.0.10",
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)) {