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 +151 -252
- package/package.json +1 -1
- package/src/chatListPane.js +104 -2
- package/src/longChatPane.js +4 -0
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
|
-
|
|
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
|
-
<
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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="
|
|
258
|
-
<aside class="sidebar" id="sidebar"></aside>
|
|
74
|
+
<div class="sidebar" id="sidebar"></div>
|
|
259
75
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
</
|
|
268
|
-
|
|
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
|
-
|
|
196
|
+
leftPanel.classList.toggle('open')
|
|
308
197
|
sidebarOverlay.classList.toggle('open')
|
|
309
198
|
})
|
|
310
199
|
|
|
311
200
|
sidebarOverlay.addEventListener('click', () => {
|
|
312
|
-
|
|
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 <
|
|
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"
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
1020
|
-
|
|
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
|
-
//
|
|
1028
|
-
|
|
1029
|
-
|
|
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
package/src/chatListPane.js
CHANGED
|
@@ -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
|
-
|
|
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 }
|
package/src/longChatPane.js
CHANGED
|
@@ -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)) {
|