solid-chat 0.0.1
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/LICENSE +17 -0
- package/README.md +74 -0
- package/package.json +42 -0
- package/src/chatListPane.js +742 -0
- package/src/index.js +5 -0
- package/src/longChatPane.js +837 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat List Pane - Sidebar showing user's chats
|
|
3
|
+
*
|
|
4
|
+
* Follows same pattern as longChatPane.js.
|
|
5
|
+
* Uses vanilla JS DOM manipulation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Namespaces for Type Index discovery
|
|
9
|
+
const SOLID = {
|
|
10
|
+
publicTypeIndex: 'http://www.w3.org/ns/solid/terms#publicTypeIndex',
|
|
11
|
+
privateTypeIndex: 'http://www.w3.org/ns/solid/terms#privateTypeIndex',
|
|
12
|
+
forClass: 'http://www.w3.org/ns/solid/terms#forClass',
|
|
13
|
+
instance: 'http://www.w3.org/ns/solid/terms#instance'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MEETING = {
|
|
17
|
+
LongChat: 'http://www.w3.org/ns/pim/meeting#LongChat'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Storage key for localStorage
|
|
21
|
+
const STORAGE_KEY = 'solidchat-chats'
|
|
22
|
+
|
|
23
|
+
// Default global chat
|
|
24
|
+
const DEFAULT_CHAT = {
|
|
25
|
+
uri: 'https://solid-chat.solidcommunity.net/public/global/chat.ttl',
|
|
26
|
+
title: 'Solid Chat Global',
|
|
27
|
+
lastMessage: 'Welcome to the global chat!',
|
|
28
|
+
timestamp: '2025-12-31T09:00:00Z'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// CSS styles
|
|
32
|
+
const styles = `
|
|
33
|
+
.chat-list-pane {
|
|
34
|
+
--gradient-start: #667eea;
|
|
35
|
+
--gradient-end: #9f7aea;
|
|
36
|
+
--bg: #ffffff;
|
|
37
|
+
--bg-hover: #f7f8fc;
|
|
38
|
+
--bg-active: #ede9fe;
|
|
39
|
+
--text: #2d3748;
|
|
40
|
+
--text-secondary: #4a5568;
|
|
41
|
+
--text-muted: #a0aec0;
|
|
42
|
+
--border: #e2e8f0;
|
|
43
|
+
|
|
44
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
height: 100%;
|
|
48
|
+
background: var(--bg);
|
|
49
|
+
border-right: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.chat-list-pane * {
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.sidebar-header {
|
|
57
|
+
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
58
|
+
color: white;
|
|
59
|
+
padding: 16px 16px;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
gap: 12px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.sidebar-title {
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
font-size: 18px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.add-chat-btn {
|
|
72
|
+
width: 36px;
|
|
73
|
+
height: 36px;
|
|
74
|
+
border-radius: 50%;
|
|
75
|
+
border: none;
|
|
76
|
+
background: rgba(255,255,255,0.2);
|
|
77
|
+
color: white;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
transition: background 0.2s;
|
|
83
|
+
font-size: 20px;
|
|
84
|
+
font-weight: 300;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.add-chat-btn:hover {
|
|
88
|
+
background: rgba(255,255,255,0.3);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.chat-list {
|
|
92
|
+
flex: 1;
|
|
93
|
+
overflow-y: auto;
|
|
94
|
+
padding: 8px 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.chat-item {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 12px;
|
|
101
|
+
padding: 12px 16px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
transition: background 0.15s;
|
|
104
|
+
border-left: 3px solid transparent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.chat-item:hover {
|
|
108
|
+
background: var(--bg-hover);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.chat-item.active {
|
|
112
|
+
background: var(--bg-active);
|
|
113
|
+
border-left-color: var(--gradient-start);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.chat-item-avatar {
|
|
117
|
+
width: 48px;
|
|
118
|
+
height: 48px;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
121
|
+
color: white;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
font-size: 18px;
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.chat-item-content {
|
|
131
|
+
flex: 1;
|
|
132
|
+
min-width: 0;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.chat-item-header {
|
|
137
|
+
display: flex;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
align-items: baseline;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
margin-bottom: 4px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.chat-item-title {
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
font-size: 15px;
|
|
147
|
+
color: var(--text);
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
text-overflow: ellipsis;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.chat-item-time {
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
color: var(--text-muted);
|
|
156
|
+
flex-shrink: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.chat-item-preview {
|
|
160
|
+
font-size: 13px;
|
|
161
|
+
color: var(--text-muted);
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
text-overflow: ellipsis;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.sidebar-footer {
|
|
168
|
+
padding: 12px 16px;
|
|
169
|
+
border-top: 1px solid var(--border);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.discover-btn {
|
|
173
|
+
width: 100%;
|
|
174
|
+
padding: 10px 16px;
|
|
175
|
+
border: 1px solid var(--border);
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
background: var(--bg);
|
|
178
|
+
color: var(--text-secondary);
|
|
179
|
+
font-size: 14px;
|
|
180
|
+
font-weight: 500;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
transition: all 0.2s;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.discover-btn:hover {
|
|
190
|
+
background: var(--bg-hover);
|
|
191
|
+
border-color: var(--gradient-start);
|
|
192
|
+
color: var(--gradient-start);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.discover-btn:disabled {
|
|
196
|
+
opacity: 0.5;
|
|
197
|
+
cursor: not-allowed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.empty-list {
|
|
201
|
+
flex: 1;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
padding: 32px 16px;
|
|
207
|
+
text-align: center;
|
|
208
|
+
color: var(--text-muted);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.empty-list-icon {
|
|
212
|
+
font-size: 48px;
|
|
213
|
+
margin-bottom: 12px;
|
|
214
|
+
opacity: 0.5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.empty-list-text {
|
|
218
|
+
font-size: 14px;
|
|
219
|
+
line-height: 1.5;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Add Chat Modal */
|
|
223
|
+
.modal-overlay {
|
|
224
|
+
position: fixed;
|
|
225
|
+
top: 0;
|
|
226
|
+
left: 0;
|
|
227
|
+
right: 0;
|
|
228
|
+
bottom: 0;
|
|
229
|
+
background: rgba(0,0,0,0.5);
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
z-index: 1000;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.modal {
|
|
237
|
+
background: white;
|
|
238
|
+
border-radius: 16px;
|
|
239
|
+
padding: 24px;
|
|
240
|
+
width: 90%;
|
|
241
|
+
max-width: 400px;
|
|
242
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.modal-title {
|
|
246
|
+
font-size: 18px;
|
|
247
|
+
font-weight: 600;
|
|
248
|
+
margin-bottom: 16px;
|
|
249
|
+
color: var(--text);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.modal-input {
|
|
253
|
+
width: 100%;
|
|
254
|
+
padding: 12px 16px;
|
|
255
|
+
border: 1px solid var(--border);
|
|
256
|
+
border-radius: 8px;
|
|
257
|
+
font-size: 14px;
|
|
258
|
+
font-family: inherit;
|
|
259
|
+
margin-bottom: 16px;
|
|
260
|
+
transition: border-color 0.2s;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.modal-input:focus {
|
|
264
|
+
outline: none;
|
|
265
|
+
border-color: var(--gradient-start);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.modal-buttons {
|
|
269
|
+
display: flex;
|
|
270
|
+
gap: 12px;
|
|
271
|
+
justify-content: flex-end;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.modal-btn {
|
|
275
|
+
padding: 10px 20px;
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
font-size: 14px;
|
|
278
|
+
font-weight: 500;
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
transition: all 0.2s;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.modal-btn-cancel {
|
|
284
|
+
background: var(--bg-hover);
|
|
285
|
+
border: 1px solid var(--border);
|
|
286
|
+
color: var(--text-secondary);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.modal-btn-cancel:hover {
|
|
290
|
+
background: var(--border);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.modal-btn-add {
|
|
294
|
+
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
295
|
+
border: none;
|
|
296
|
+
color: white;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.modal-btn-add:hover {
|
|
300
|
+
transform: translateY(-1px);
|
|
301
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.modal-btn-add:disabled {
|
|
305
|
+
opacity: 0.5;
|
|
306
|
+
cursor: not-allowed;
|
|
307
|
+
transform: none;
|
|
308
|
+
box-shadow: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Delete button on chat item */
|
|
312
|
+
.chat-item-delete {
|
|
313
|
+
width: 24px;
|
|
314
|
+
height: 24px;
|
|
315
|
+
border-radius: 50%;
|
|
316
|
+
border: none;
|
|
317
|
+
background: transparent;
|
|
318
|
+
color: var(--text-muted);
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
display: none;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
font-size: 16px;
|
|
324
|
+
flex-shrink: 0;
|
|
325
|
+
transition: all 0.2s;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.chat-item:hover .chat-item-delete {
|
|
329
|
+
display: flex;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.chat-item-delete:hover {
|
|
333
|
+
background: #fee2e2;
|
|
334
|
+
color: #dc2626;
|
|
335
|
+
}
|
|
336
|
+
`
|
|
337
|
+
|
|
338
|
+
// Inject styles once
|
|
339
|
+
let stylesInjected = false
|
|
340
|
+
function injectStyles(dom) {
|
|
341
|
+
if (stylesInjected) return
|
|
342
|
+
const styleEl = dom.createElement('style')
|
|
343
|
+
styleEl.textContent = styles
|
|
344
|
+
dom.head.appendChild(styleEl)
|
|
345
|
+
stylesInjected = true
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// State
|
|
349
|
+
let chatList = []
|
|
350
|
+
let activeUri = null
|
|
351
|
+
let listContainer = null
|
|
352
|
+
let onSelectCallback = null
|
|
353
|
+
let contextRef = null
|
|
354
|
+
|
|
355
|
+
// Load chat list from localStorage
|
|
356
|
+
function loadChatList() {
|
|
357
|
+
try {
|
|
358
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
359
|
+
if (stored) {
|
|
360
|
+
chatList = JSON.parse(stored)
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
console.warn('Failed to load chat list:', e)
|
|
364
|
+
chatList = []
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Add default global chat if list is empty
|
|
368
|
+
if (chatList.length === 0) {
|
|
369
|
+
chatList.push({ ...DEFAULT_CHAT })
|
|
370
|
+
saveChatList()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Ensure default chat exists in list
|
|
374
|
+
const hasDefault = chatList.some(c => c.uri === DEFAULT_CHAT.uri)
|
|
375
|
+
if (!hasDefault) {
|
|
376
|
+
chatList.unshift({ ...DEFAULT_CHAT })
|
|
377
|
+
saveChatList()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return chatList
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Save chat list to localStorage
|
|
384
|
+
function saveChatList() {
|
|
385
|
+
try {
|
|
386
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(chatList))
|
|
387
|
+
} catch (e) {
|
|
388
|
+
console.warn('Failed to save chat list:', e)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Add a chat to the list
|
|
393
|
+
function addChat(uri, title, lastMessage = '', timestamp = null) {
|
|
394
|
+
// Check if already exists
|
|
395
|
+
const existing = chatList.find(c => c.uri === uri)
|
|
396
|
+
if (existing) {
|
|
397
|
+
// Update existing
|
|
398
|
+
if (title) existing.title = title
|
|
399
|
+
if (lastMessage) existing.lastMessage = lastMessage
|
|
400
|
+
if (timestamp) existing.timestamp = timestamp
|
|
401
|
+
} else {
|
|
402
|
+
chatList.unshift({
|
|
403
|
+
uri,
|
|
404
|
+
title: title || getTitleFromUri(uri),
|
|
405
|
+
lastMessage,
|
|
406
|
+
timestamp: timestamp || new Date().toISOString()
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
saveChatList()
|
|
410
|
+
renderChatList()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Remove a chat from the list
|
|
414
|
+
function removeChat(uri) {
|
|
415
|
+
chatList = chatList.filter(c => c.uri !== uri)
|
|
416
|
+
saveChatList()
|
|
417
|
+
renderChatList()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Update chat preview (called after loading messages)
|
|
421
|
+
function updateChatPreview(uri, lastMessage, timestamp) {
|
|
422
|
+
const chat = chatList.find(c => c.uri === uri)
|
|
423
|
+
if (chat) {
|
|
424
|
+
chat.lastMessage = lastMessage
|
|
425
|
+
chat.timestamp = timestamp
|
|
426
|
+
saveChatList()
|
|
427
|
+
renderChatList()
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Get title from URI
|
|
432
|
+
function getTitleFromUri(uri) {
|
|
433
|
+
try {
|
|
434
|
+
const url = new URL(uri)
|
|
435
|
+
const path = url.pathname
|
|
436
|
+
const parts = path.split('/').filter(Boolean)
|
|
437
|
+
const name = parts[parts.length - 1] || parts[parts.length - 2] || 'Chat'
|
|
438
|
+
return decodeURIComponent(name.replace(/\.ttl$/, '').replace(/[-_]/g, ' '))
|
|
439
|
+
} catch {
|
|
440
|
+
return 'Chat'
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Format timestamp for display
|
|
445
|
+
function formatTimestamp(timestamp) {
|
|
446
|
+
if (!timestamp) return ''
|
|
447
|
+
const date = new Date(timestamp)
|
|
448
|
+
const now = new Date()
|
|
449
|
+
const isToday = date.toDateString() === now.toDateString()
|
|
450
|
+
|
|
451
|
+
if (isToday) {
|
|
452
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
|
|
453
|
+
}
|
|
454
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Get initials from title
|
|
458
|
+
function getInitials(title) {
|
|
459
|
+
if (!title) return '?'
|
|
460
|
+
return title.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Render the chat list
|
|
464
|
+
function renderChatList() {
|
|
465
|
+
if (!listContainer || !contextRef) return
|
|
466
|
+
|
|
467
|
+
const dom = contextRef.dom
|
|
468
|
+
listContainer.innerHTML = ''
|
|
469
|
+
|
|
470
|
+
if (chatList.length === 0) {
|
|
471
|
+
const empty = dom.createElement('div')
|
|
472
|
+
empty.className = 'empty-list'
|
|
473
|
+
empty.innerHTML = `
|
|
474
|
+
<div class="empty-list-icon">💬</div>
|
|
475
|
+
<div class="empty-list-text">No chats yet.<br>Add one with the + button above.</div>
|
|
476
|
+
`
|
|
477
|
+
listContainer.appendChild(empty)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
chatList.forEach(chat => {
|
|
482
|
+
const item = dom.createElement('div')
|
|
483
|
+
item.className = 'chat-item' + (chat.uri === activeUri ? ' active' : '')
|
|
484
|
+
|
|
485
|
+
const avatar = dom.createElement('div')
|
|
486
|
+
avatar.className = 'chat-item-avatar'
|
|
487
|
+
avatar.textContent = getInitials(chat.title)
|
|
488
|
+
|
|
489
|
+
const content = dom.createElement('div')
|
|
490
|
+
content.className = 'chat-item-content'
|
|
491
|
+
|
|
492
|
+
const header = dom.createElement('div')
|
|
493
|
+
header.className = 'chat-item-header'
|
|
494
|
+
|
|
495
|
+
const title = dom.createElement('div')
|
|
496
|
+
title.className = 'chat-item-title'
|
|
497
|
+
title.textContent = chat.title
|
|
498
|
+
|
|
499
|
+
const time = dom.createElement('div')
|
|
500
|
+
time.className = 'chat-item-time'
|
|
501
|
+
time.textContent = formatTimestamp(chat.timestamp)
|
|
502
|
+
|
|
503
|
+
header.appendChild(title)
|
|
504
|
+
header.appendChild(time)
|
|
505
|
+
|
|
506
|
+
const preview = dom.createElement('div')
|
|
507
|
+
preview.className = 'chat-item-preview'
|
|
508
|
+
preview.textContent = chat.lastMessage || 'No messages yet'
|
|
509
|
+
|
|
510
|
+
content.appendChild(header)
|
|
511
|
+
content.appendChild(preview)
|
|
512
|
+
|
|
513
|
+
const deleteBtn = dom.createElement('button')
|
|
514
|
+
deleteBtn.className = 'chat-item-delete'
|
|
515
|
+
deleteBtn.textContent = '×'
|
|
516
|
+
deleteBtn.title = 'Remove from list'
|
|
517
|
+
deleteBtn.onclick = (e) => {
|
|
518
|
+
e.stopPropagation()
|
|
519
|
+
removeChat(chat.uri)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
item.appendChild(avatar)
|
|
523
|
+
item.appendChild(content)
|
|
524
|
+
item.appendChild(deleteBtn)
|
|
525
|
+
|
|
526
|
+
item.onclick = () => {
|
|
527
|
+
activeUri = chat.uri
|
|
528
|
+
renderChatList()
|
|
529
|
+
if (onSelectCallback) {
|
|
530
|
+
onSelectCallback(chat.uri)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
listContainer.appendChild(item)
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Show add chat modal
|
|
539
|
+
function showAddModal(dom) {
|
|
540
|
+
const overlay = dom.createElement('div')
|
|
541
|
+
overlay.className = 'modal-overlay'
|
|
542
|
+
|
|
543
|
+
const modal = dom.createElement('div')
|
|
544
|
+
modal.className = 'modal'
|
|
545
|
+
|
|
546
|
+
modal.innerHTML = `
|
|
547
|
+
<div class="modal-title">Add Chat</div>
|
|
548
|
+
<input type="url" class="modal-input" placeholder="Enter chat URL..." />
|
|
549
|
+
<div class="modal-buttons">
|
|
550
|
+
<button class="modal-btn modal-btn-cancel">Cancel</button>
|
|
551
|
+
<button class="modal-btn modal-btn-add">Add</button>
|
|
552
|
+
</div>
|
|
553
|
+
`
|
|
554
|
+
|
|
555
|
+
overlay.appendChild(modal)
|
|
556
|
+
dom.body.appendChild(overlay)
|
|
557
|
+
|
|
558
|
+
const input = modal.querySelector('.modal-input')
|
|
559
|
+
const cancelBtn = modal.querySelector('.modal-btn-cancel')
|
|
560
|
+
const addBtn = modal.querySelector('.modal-btn-add')
|
|
561
|
+
|
|
562
|
+
input.focus()
|
|
563
|
+
|
|
564
|
+
const close = () => {
|
|
565
|
+
overlay.remove()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
overlay.onclick = (e) => {
|
|
569
|
+
if (e.target === overlay) close()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
cancelBtn.onclick = close
|
|
573
|
+
|
|
574
|
+
addBtn.onclick = () => {
|
|
575
|
+
const uri = input.value.trim()
|
|
576
|
+
if (uri) {
|
|
577
|
+
addChat(uri)
|
|
578
|
+
activeUri = uri
|
|
579
|
+
renderChatList()
|
|
580
|
+
if (onSelectCallback) {
|
|
581
|
+
onSelectCallback(uri)
|
|
582
|
+
}
|
|
583
|
+
close()
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
input.onkeydown = (e) => {
|
|
588
|
+
if (e.key === 'Enter') {
|
|
589
|
+
addBtn.click()
|
|
590
|
+
} else if (e.key === 'Escape') {
|
|
591
|
+
close()
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Discover chats from Type Index
|
|
597
|
+
async function discoverChats(webId, context) {
|
|
598
|
+
const store = context.session.store
|
|
599
|
+
const $rdf = store.rdflib || globalThis.$rdf
|
|
600
|
+
if (!$rdf || !webId) return []
|
|
601
|
+
|
|
602
|
+
const discovered = []
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
// Load profile
|
|
606
|
+
const profile = $rdf.sym(webId)
|
|
607
|
+
await store.fetcher.load(profile.doc())
|
|
608
|
+
|
|
609
|
+
const ns = $rdf.Namespace
|
|
610
|
+
const SOLID = ns('http://www.w3.org/ns/solid/terms#')
|
|
611
|
+
const MEETING = ns('http://www.w3.org/ns/pim/meeting#')
|
|
612
|
+
|
|
613
|
+
// Get type indexes
|
|
614
|
+
const publicIndex = store.any(profile, SOLID('publicTypeIndex'))
|
|
615
|
+
const privateIndex = store.any(profile, SOLID('privateTypeIndex'))
|
|
616
|
+
|
|
617
|
+
const indexes = [publicIndex, privateIndex].filter(Boolean)
|
|
618
|
+
|
|
619
|
+
for (const indexUri of indexes) {
|
|
620
|
+
try {
|
|
621
|
+
await store.fetcher.load(indexUri)
|
|
622
|
+
|
|
623
|
+
// Find LongChat registrations
|
|
624
|
+
const registrations = store.statementsMatching(null, SOLID('forClass'), MEETING('LongChat'), indexUri.doc())
|
|
625
|
+
|
|
626
|
+
for (const reg of registrations) {
|
|
627
|
+
const instance = store.any(reg.subject, SOLID('instance'))
|
|
628
|
+
if (instance) {
|
|
629
|
+
discovered.push(instance.value)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.warn('Failed to load type index:', indexUri?.value, e)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch (e) {
|
|
637
|
+
console.warn('Failed to discover chats:', e)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return discovered
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Main pane definition
|
|
644
|
+
export const chatListPane = {
|
|
645
|
+
name: 'chat-list',
|
|
646
|
+
|
|
647
|
+
render: function(context, options = {}) {
|
|
648
|
+
const dom = context.dom
|
|
649
|
+
contextRef = context
|
|
650
|
+
onSelectCallback = options.onSelectChat
|
|
651
|
+
|
|
652
|
+
// Load saved chats
|
|
653
|
+
loadChatList()
|
|
654
|
+
|
|
655
|
+
// Inject styles
|
|
656
|
+
injectStyles(dom)
|
|
657
|
+
|
|
658
|
+
// Container
|
|
659
|
+
const container = dom.createElement('div')
|
|
660
|
+
container.className = 'chat-list-pane'
|
|
661
|
+
|
|
662
|
+
// Header
|
|
663
|
+
const header = dom.createElement('div')
|
|
664
|
+
header.className = 'sidebar-header'
|
|
665
|
+
|
|
666
|
+
const title = dom.createElement('div')
|
|
667
|
+
title.className = 'sidebar-title'
|
|
668
|
+
title.textContent = 'Chats'
|
|
669
|
+
|
|
670
|
+
const addBtn = dom.createElement('button')
|
|
671
|
+
addBtn.className = 'add-chat-btn'
|
|
672
|
+
addBtn.textContent = '+'
|
|
673
|
+
addBtn.title = 'Add chat'
|
|
674
|
+
addBtn.onclick = () => showAddModal(dom)
|
|
675
|
+
|
|
676
|
+
header.appendChild(title)
|
|
677
|
+
header.appendChild(addBtn)
|
|
678
|
+
|
|
679
|
+
// Chat list
|
|
680
|
+
listContainer = dom.createElement('div')
|
|
681
|
+
listContainer.className = 'chat-list'
|
|
682
|
+
|
|
683
|
+
// Footer with discover button
|
|
684
|
+
const footer = dom.createElement('div')
|
|
685
|
+
footer.className = 'sidebar-footer'
|
|
686
|
+
|
|
687
|
+
const discoverBtn = dom.createElement('button')
|
|
688
|
+
discoverBtn.className = 'discover-btn'
|
|
689
|
+
discoverBtn.innerHTML = '🔍 Discover Chats'
|
|
690
|
+
|
|
691
|
+
discoverBtn.onclick = async () => {
|
|
692
|
+
const webId = context.session?.webId || options.webId
|
|
693
|
+
if (!webId) {
|
|
694
|
+
alert('Please login first to discover chats from your pod.')
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
discoverBtn.disabled = true
|
|
699
|
+
discoverBtn.textContent = 'Discovering...'
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
const discovered = await discoverChats(webId, context)
|
|
703
|
+
if (discovered.length > 0) {
|
|
704
|
+
discovered.forEach(uri => addChat(uri))
|
|
705
|
+
alert(`Found ${discovered.length} chat(s)!`)
|
|
706
|
+
} else {
|
|
707
|
+
alert('No chats found in your Type Index.')
|
|
708
|
+
}
|
|
709
|
+
} catch (e) {
|
|
710
|
+
console.error('Discovery failed:', e)
|
|
711
|
+
alert('Discovery failed. Check console for details.')
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
discoverBtn.disabled = false
|
|
715
|
+
discoverBtn.innerHTML = '🔍 Discover Chats'
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
footer.appendChild(discoverBtn)
|
|
719
|
+
|
|
720
|
+
// Assemble
|
|
721
|
+
container.appendChild(header)
|
|
722
|
+
container.appendChild(listContainer)
|
|
723
|
+
container.appendChild(footer)
|
|
724
|
+
|
|
725
|
+
// Initial render
|
|
726
|
+
renderChatList()
|
|
727
|
+
|
|
728
|
+
return container
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
// Public API
|
|
732
|
+
addChat,
|
|
733
|
+
removeChat,
|
|
734
|
+
updateChatPreview,
|
|
735
|
+
setActiveChat: (uri) => {
|
|
736
|
+
activeUri = uri
|
|
737
|
+
renderChatList()
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Export for use in index.html
|
|
742
|
+
export { addChat, updateChatPreview }
|