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,837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long Chat Pane - WhatsApp-style chat interface for Solid
|
|
3
|
+
*
|
|
4
|
+
* Experimental pane inspired by Wave messenger design.
|
|
5
|
+
* Uses vanilla JS DOM manipulation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CHAT = {
|
|
9
|
+
namespace: 'http://www.w3.org/2007/ont/chat#',
|
|
10
|
+
Message: 'http://www.w3.org/2007/ont/chat#Message',
|
|
11
|
+
Chat: 'http://www.w3.org/2007/ont/chat#Chat'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SIOC = {
|
|
15
|
+
namespace: 'http://rdfs.org/sioc/ns#',
|
|
16
|
+
Post: 'http://rdfs.org/sioc/ns#Post',
|
|
17
|
+
content: 'http://rdfs.org/sioc/ns#content',
|
|
18
|
+
Container: 'http://rdfs.org/sioc/ns#Container'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const FLOW = {
|
|
22
|
+
namespace: 'http://www.w3.org/2005/01/wf/flow#',
|
|
23
|
+
message: 'http://www.w3.org/2005/01/wf/flow#message',
|
|
24
|
+
Message: 'http://www.w3.org/2005/01/wf/flow#Message'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// CSS styles as a string (will be injected)
|
|
28
|
+
const styles = `
|
|
29
|
+
.long-chat-pane {
|
|
30
|
+
--gradient-start: #667eea;
|
|
31
|
+
--gradient-end: #9f7aea;
|
|
32
|
+
--bg-chat: #f7f8fc;
|
|
33
|
+
--bg-message-in: #ffffff;
|
|
34
|
+
--bg-message-out: linear-gradient(135deg, #e8e4f4 0%, #f0ecf8 100%);
|
|
35
|
+
--text: #2d3748;
|
|
36
|
+
--text-secondary: #4a5568;
|
|
37
|
+
--text-muted: #a0aec0;
|
|
38
|
+
--border: #e2e8f0;
|
|
39
|
+
--accent: #805ad5;
|
|
40
|
+
|
|
41
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
height: 100%;
|
|
45
|
+
border: none;
|
|
46
|
+
border-radius: 0;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
background: var(--bg-chat);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.long-chat-pane * {
|
|
52
|
+
box-sizing: border-box;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.chat-header {
|
|
56
|
+
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
57
|
+
color: white;
|
|
58
|
+
padding: 16px 20px;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 14px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.chat-avatar {
|
|
65
|
+
width: 44px;
|
|
66
|
+
height: 44px;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
background: rgba(255,255,255,0.2);
|
|
69
|
+
backdrop-filter: blur(4px);
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
font-size: 18px;
|
|
75
|
+
border: 2px solid rgba(255,255,255,0.3);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.chat-title {
|
|
79
|
+
flex: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.chat-name {
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
color: white;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
opacity: 0.95;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.chat-name:hover {
|
|
91
|
+
text-decoration: underline;
|
|
92
|
+
opacity: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.chat-status {
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
opacity: 0.8;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.messages-container {
|
|
101
|
+
flex: 1;
|
|
102
|
+
overflow-y: auto;
|
|
103
|
+
padding: 20px;
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
gap: 8px;
|
|
107
|
+
background: linear-gradient(180deg, #f7f8fc 0%, #f0f2f8 100%);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.message-row {
|
|
111
|
+
display: flex;
|
|
112
|
+
margin-bottom: 2px;
|
|
113
|
+
align-items: flex-end;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.message-row.sent {
|
|
118
|
+
justify-content: flex-end;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.message-row.received {
|
|
122
|
+
justify-content: flex-start;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.message-avatar {
|
|
126
|
+
width: 32px;
|
|
127
|
+
height: 32px;
|
|
128
|
+
border-radius: 50%;
|
|
129
|
+
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
130
|
+
color: white;
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.message-avatar img {
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
object-fit: cover;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.message-row.sent .message-avatar {
|
|
147
|
+
order: 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.message-bubble {
|
|
151
|
+
max-width: 70%;
|
|
152
|
+
padding: 10px 14px 10px;
|
|
153
|
+
border-radius: 18px;
|
|
154
|
+
position: relative;
|
|
155
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.message-bubble.sent {
|
|
159
|
+
background: #ede9fe;
|
|
160
|
+
border-bottom-right-radius: 4px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.message-bubble.sent .message-time {
|
|
164
|
+
color: #a0aec0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.message-bubble.received {
|
|
168
|
+
background: var(--bg-message-in);
|
|
169
|
+
border-bottom-left-radius: 4px;
|
|
170
|
+
border: 1px solid #e8e8f0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.message-text {
|
|
174
|
+
font-size: 14.2px;
|
|
175
|
+
line-height: 19px;
|
|
176
|
+
color: var(--text);
|
|
177
|
+
white-space: pre-wrap;
|
|
178
|
+
word-wrap: break-word;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.message-meta {
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: flex-end;
|
|
185
|
+
gap: 4px;
|
|
186
|
+
margin-top: 2px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.message-time {
|
|
190
|
+
font-size: 11px;
|
|
191
|
+
color: var(--text-muted);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.message-author {
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
color: #805ad5;
|
|
198
|
+
margin-bottom: 4px;
|
|
199
|
+
text-decoration: none;
|
|
200
|
+
display: inline-block;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
transition: color 0.2s;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.message-author:hover {
|
|
206
|
+
color: #667eea;
|
|
207
|
+
text-decoration: underline;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.input-area {
|
|
211
|
+
background: white;
|
|
212
|
+
padding: 12px 20px;
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: flex-end;
|
|
215
|
+
gap: 12px;
|
|
216
|
+
border-top: 1px solid #e8e8f0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.input-wrapper {
|
|
220
|
+
flex: 1;
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: flex-end;
|
|
223
|
+
background: #f7f8fc;
|
|
224
|
+
border-radius: 24px;
|
|
225
|
+
padding: 10px 16px;
|
|
226
|
+
border: 1px solid #e2e8f0;
|
|
227
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.input-wrapper:focus-within {
|
|
231
|
+
border-color: #805ad5;
|
|
232
|
+
box-shadow: 0 0 0 3px rgba(128, 90, 213, 0.1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.message-input {
|
|
236
|
+
flex: 1;
|
|
237
|
+
border: none;
|
|
238
|
+
background: transparent;
|
|
239
|
+
font-size: 15px;
|
|
240
|
+
font-family: inherit;
|
|
241
|
+
color: var(--text);
|
|
242
|
+
resize: none;
|
|
243
|
+
max-height: 100px;
|
|
244
|
+
line-height: 20px;
|
|
245
|
+
outline: none;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.message-input::placeholder {
|
|
249
|
+
color: var(--text-muted);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.send-btn {
|
|
253
|
+
width: 44px;
|
|
254
|
+
height: 44px;
|
|
255
|
+
border-radius: 50%;
|
|
256
|
+
border: none;
|
|
257
|
+
background: linear-gradient(135deg, #667eea 0%, #9f7aea 100%);
|
|
258
|
+
color: white;
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
justify-content: center;
|
|
263
|
+
flex-shrink: 0;
|
|
264
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
265
|
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.send-btn:hover {
|
|
269
|
+
transform: scale(1.05);
|
|
270
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.send-btn:disabled {
|
|
274
|
+
background: #e2e8f0;
|
|
275
|
+
box-shadow: none;
|
|
276
|
+
cursor: not-allowed;
|
|
277
|
+
transform: none;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.emoji-btn {
|
|
281
|
+
width: 36px;
|
|
282
|
+
height: 36px;
|
|
283
|
+
border-radius: 50%;
|
|
284
|
+
border: none;
|
|
285
|
+
background: transparent;
|
|
286
|
+
color: var(--text-muted);
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
display: flex;
|
|
289
|
+
align-items: center;
|
|
290
|
+
justify-content: center;
|
|
291
|
+
font-size: 20px;
|
|
292
|
+
transition: background 0.2s, color 0.2s;
|
|
293
|
+
flex-shrink: 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.emoji-btn:hover {
|
|
297
|
+
background: #f0f2f8;
|
|
298
|
+
color: var(--text);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.emoji-picker {
|
|
302
|
+
position: absolute;
|
|
303
|
+
bottom: 100%;
|
|
304
|
+
left: 0;
|
|
305
|
+
margin-bottom: 8px;
|
|
306
|
+
background: white;
|
|
307
|
+
border-radius: 12px;
|
|
308
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
309
|
+
padding: 8px;
|
|
310
|
+
display: none;
|
|
311
|
+
z-index: 100;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.emoji-picker.open {
|
|
315
|
+
display: block;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.emoji-grid {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: repeat(8, 1fr);
|
|
321
|
+
gap: 2px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.emoji-grid button {
|
|
325
|
+
width: 32px;
|
|
326
|
+
height: 32px;
|
|
327
|
+
border: none;
|
|
328
|
+
background: transparent;
|
|
329
|
+
font-size: 18px;
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
border-radius: 6px;
|
|
332
|
+
transition: background 0.15s;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.emoji-grid button:hover {
|
|
336
|
+
background: #f0f2f8;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.input-area {
|
|
340
|
+
position: relative;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.send-btn svg {
|
|
344
|
+
width: 20px;
|
|
345
|
+
height: 20px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.empty-chat {
|
|
349
|
+
flex: 1;
|
|
350
|
+
display: flex;
|
|
351
|
+
flex-direction: column;
|
|
352
|
+
align-items: center;
|
|
353
|
+
justify-content: center;
|
|
354
|
+
color: var(--text-muted);
|
|
355
|
+
text-align: center;
|
|
356
|
+
padding: 40px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.empty-chat-icon {
|
|
360
|
+
font-size: 48px;
|
|
361
|
+
margin-bottom: 16px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.loading {
|
|
365
|
+
text-align: center;
|
|
366
|
+
padding: 20px;
|
|
367
|
+
color: var(--text-muted);
|
|
368
|
+
}
|
|
369
|
+
`
|
|
370
|
+
|
|
371
|
+
// Inject styles once
|
|
372
|
+
let stylesInjected = false
|
|
373
|
+
function injectStyles(dom) {
|
|
374
|
+
if (stylesInjected) return
|
|
375
|
+
const styleEl = dom.createElement('style')
|
|
376
|
+
styleEl.textContent = styles
|
|
377
|
+
dom.head.appendChild(styleEl)
|
|
378
|
+
stylesInjected = true
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Format timestamp
|
|
382
|
+
function formatTime(date) {
|
|
383
|
+
if (!date) return ''
|
|
384
|
+
const d = new Date(date)
|
|
385
|
+
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get initials from name
|
|
389
|
+
function getInitials(name) {
|
|
390
|
+
if (!name) return '?'
|
|
391
|
+
return name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Avatar cache
|
|
395
|
+
const avatarCache = new Map()
|
|
396
|
+
|
|
397
|
+
// Fetch avatar from WebID profile
|
|
398
|
+
async function fetchAvatar(webId, store, $rdf) {
|
|
399
|
+
if (!webId) return null
|
|
400
|
+
if (avatarCache.has(webId)) return avatarCache.get(webId)
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const profile = $rdf.sym(webId)
|
|
404
|
+
await store.fetcher.load(profile.doc())
|
|
405
|
+
|
|
406
|
+
const ns = $rdf.Namespace
|
|
407
|
+
const FOAF = ns('http://xmlns.com/foaf/0.1/')
|
|
408
|
+
const VCARD = ns('http://www.w3.org/2006/vcard/ns#')
|
|
409
|
+
|
|
410
|
+
const avatar = store.any(profile, FOAF('img'))?.value ||
|
|
411
|
+
store.any(profile, FOAF('depiction'))?.value ||
|
|
412
|
+
store.any(profile, VCARD('hasPhoto'))?.value
|
|
413
|
+
|
|
414
|
+
avatarCache.set(webId, avatar)
|
|
415
|
+
return avatar
|
|
416
|
+
} catch (e) {
|
|
417
|
+
avatarCache.set(webId, null)
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Create message element
|
|
423
|
+
function createMessageElement(dom, message, isOwn) {
|
|
424
|
+
const row = dom.createElement('div')
|
|
425
|
+
row.className = `message-row ${isOwn ? 'sent' : 'received'}`
|
|
426
|
+
|
|
427
|
+
// Avatar
|
|
428
|
+
const avatar = dom.createElement('div')
|
|
429
|
+
avatar.className = 'message-avatar'
|
|
430
|
+
avatar.textContent = getInitials(message.author || '?')
|
|
431
|
+
avatar.dataset.webid = message.authorUri || ''
|
|
432
|
+
row.appendChild(avatar)
|
|
433
|
+
|
|
434
|
+
const bubble = dom.createElement('div')
|
|
435
|
+
bubble.className = `message-bubble ${isOwn ? 'sent' : 'received'}`
|
|
436
|
+
|
|
437
|
+
// Author (for received messages in group chats)
|
|
438
|
+
if (!isOwn && message.author) {
|
|
439
|
+
const author = dom.createElement('a')
|
|
440
|
+
author.className = 'message-author'
|
|
441
|
+
author.textContent = message.author
|
|
442
|
+
if (message.authorUri) {
|
|
443
|
+
author.href = message.authorUri
|
|
444
|
+
author.target = '_blank'
|
|
445
|
+
author.rel = 'noopener'
|
|
446
|
+
}
|
|
447
|
+
bubble.appendChild(author)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const text = dom.createElement('div')
|
|
451
|
+
text.className = 'message-text'
|
|
452
|
+
text.textContent = message.content || ''
|
|
453
|
+
bubble.appendChild(text)
|
|
454
|
+
|
|
455
|
+
const meta = dom.createElement('div')
|
|
456
|
+
meta.className = 'message-meta'
|
|
457
|
+
|
|
458
|
+
const time = dom.createElement('span')
|
|
459
|
+
time.className = 'message-time'
|
|
460
|
+
time.textContent = formatTime(message.date)
|
|
461
|
+
meta.appendChild(time)
|
|
462
|
+
|
|
463
|
+
bubble.appendChild(meta)
|
|
464
|
+
row.appendChild(bubble)
|
|
465
|
+
|
|
466
|
+
return row
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Main pane definition
|
|
470
|
+
export const longChatPane = {
|
|
471
|
+
icon: 'https://solid.github.io/solid-ui/src/icons/noun_Forum_3572062.svg',
|
|
472
|
+
name: 'long-chat',
|
|
473
|
+
|
|
474
|
+
label: function(subject, context) {
|
|
475
|
+
const dominated = async function() { return false }
|
|
476
|
+
const store = context.session.store
|
|
477
|
+
const dominated2 = async function() { return false }
|
|
478
|
+
const dominated3 = async function() { return false }
|
|
479
|
+
|
|
480
|
+
// Check for chat types
|
|
481
|
+
const dominated4 = async function() { return false }
|
|
482
|
+
const dominated5 = async function() { return false }
|
|
483
|
+
|
|
484
|
+
const dominated6 = async function() { return false }
|
|
485
|
+
const dominated7 = async function() { return false }
|
|
486
|
+
|
|
487
|
+
// Check if it's a chat resource
|
|
488
|
+
const dominated8 = async function() { return false }
|
|
489
|
+
const dominated9 = async function() { return false }
|
|
490
|
+
|
|
491
|
+
const dominated10 = async function() { return false }
|
|
492
|
+
const dominated11 = async function() { return false }
|
|
493
|
+
|
|
494
|
+
const dominated12 = async function() { return false }
|
|
495
|
+
const dominated13 = async function() { return false }
|
|
496
|
+
const dominated14 = async function() { return false }
|
|
497
|
+
const dominated15 = async function() { return false }
|
|
498
|
+
|
|
499
|
+
const dominated16 = async function() { return false }
|
|
500
|
+
const dominated17 = async function() { return false }
|
|
501
|
+
const dominated18 = async function() { return false }
|
|
502
|
+
const $rdf = context.session.store.rdflib || globalThis.$rdf
|
|
503
|
+
|
|
504
|
+
if (!$rdf) return null
|
|
505
|
+
|
|
506
|
+
const ns = $rdf.Namespace
|
|
507
|
+
const RDF = ns('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
|
|
508
|
+
const MEETING = ns('http://www.w3.org/ns/pim/meeting#')
|
|
509
|
+
const FLOW = ns('http://www.w3.org/2005/01/wf/flow#')
|
|
510
|
+
|
|
511
|
+
// Check various chat types
|
|
512
|
+
if (store.holds(subject, RDF('type'), MEETING('LongChat')) ||
|
|
513
|
+
store.holds(subject, RDF('type'), FLOW('Chat')) ||
|
|
514
|
+
subject.uri?.includes('/chat') ||
|
|
515
|
+
subject.uri?.endsWith('.ttl') && subject.uri?.includes('chat')) {
|
|
516
|
+
return 'Long Chat'
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return null
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
render: function(subject, context, options) {
|
|
523
|
+
const dom = context.dom
|
|
524
|
+
const store = context.session.store
|
|
525
|
+
const $rdf = store.rdflib || globalThis.$rdf
|
|
526
|
+
|
|
527
|
+
injectStyles(dom)
|
|
528
|
+
|
|
529
|
+
// Create main container
|
|
530
|
+
const container = dom.createElement('div')
|
|
531
|
+
container.className = 'long-chat-pane'
|
|
532
|
+
|
|
533
|
+
// Header
|
|
534
|
+
const header = dom.createElement('div')
|
|
535
|
+
header.className = 'chat-header'
|
|
536
|
+
|
|
537
|
+
const avatar = dom.createElement('div')
|
|
538
|
+
avatar.className = 'chat-avatar'
|
|
539
|
+
avatar.textContent = 'C'
|
|
540
|
+
header.appendChild(avatar)
|
|
541
|
+
|
|
542
|
+
const titleDiv = dom.createElement('div')
|
|
543
|
+
titleDiv.className = 'chat-title'
|
|
544
|
+
|
|
545
|
+
const nameEl = dom.createElement('a')
|
|
546
|
+
nameEl.className = 'chat-name'
|
|
547
|
+
nameEl.href = subject.uri
|
|
548
|
+
nameEl.target = '_blank'
|
|
549
|
+
nameEl.rel = 'noopener'
|
|
550
|
+
nameEl.textContent = subject.uri
|
|
551
|
+
titleDiv.appendChild(nameEl)
|
|
552
|
+
|
|
553
|
+
const statusEl = dom.createElement('div')
|
|
554
|
+
statusEl.className = 'chat-status'
|
|
555
|
+
statusEl.textContent = 'Loading...'
|
|
556
|
+
titleDiv.appendChild(statusEl)
|
|
557
|
+
|
|
558
|
+
header.appendChild(titleDiv)
|
|
559
|
+
container.appendChild(header)
|
|
560
|
+
|
|
561
|
+
// Messages container
|
|
562
|
+
const messagesContainer = dom.createElement('div')
|
|
563
|
+
messagesContainer.className = 'messages-container'
|
|
564
|
+
container.appendChild(messagesContainer)
|
|
565
|
+
|
|
566
|
+
// Input area
|
|
567
|
+
const inputArea = dom.createElement('div')
|
|
568
|
+
inputArea.className = 'input-area'
|
|
569
|
+
|
|
570
|
+
// Emoji picker
|
|
571
|
+
const emojiPicker = dom.createElement('div')
|
|
572
|
+
emojiPicker.className = 'emoji-picker'
|
|
573
|
+
const emojiGrid = dom.createElement('div')
|
|
574
|
+
emojiGrid.className = 'emoji-grid'
|
|
575
|
+
const emojis = ['😀','😂','😊','🥰','😎','🤔','😢','😡','👍','👎','❤️','🔥','🎉','✨','💬','👋','🙏','💪','✅','❌','⭐','💡','📌','🚀','☕','🌟','💯','🤝']
|
|
576
|
+
emojis.forEach(e => {
|
|
577
|
+
const btn = dom.createElement('button')
|
|
578
|
+
btn.textContent = e
|
|
579
|
+
btn.type = 'button'
|
|
580
|
+
btn.onclick = () => {
|
|
581
|
+
input.value += e
|
|
582
|
+
input.focus()
|
|
583
|
+
sendBtn.disabled = !input.value.trim()
|
|
584
|
+
}
|
|
585
|
+
emojiGrid.appendChild(btn)
|
|
586
|
+
})
|
|
587
|
+
emojiPicker.appendChild(emojiGrid)
|
|
588
|
+
inputArea.appendChild(emojiPicker)
|
|
589
|
+
|
|
590
|
+
// Emoji button
|
|
591
|
+
const emojiBtn = dom.createElement('button')
|
|
592
|
+
emojiBtn.className = 'emoji-btn'
|
|
593
|
+
emojiBtn.textContent = '😊'
|
|
594
|
+
emojiBtn.type = 'button'
|
|
595
|
+
emojiBtn.onclick = () => {
|
|
596
|
+
emojiPicker.classList.toggle('open')
|
|
597
|
+
}
|
|
598
|
+
inputArea.appendChild(emojiBtn)
|
|
599
|
+
|
|
600
|
+
const inputWrapper = dom.createElement('div')
|
|
601
|
+
inputWrapper.className = 'input-wrapper'
|
|
602
|
+
|
|
603
|
+
const input = dom.createElement('textarea')
|
|
604
|
+
input.className = 'message-input'
|
|
605
|
+
input.placeholder = 'Type a message'
|
|
606
|
+
input.rows = 1
|
|
607
|
+
inputWrapper.appendChild(input)
|
|
608
|
+
inputArea.appendChild(inputWrapper)
|
|
609
|
+
|
|
610
|
+
const sendBtn = dom.createElement('button')
|
|
611
|
+
sendBtn.className = 'send-btn'
|
|
612
|
+
sendBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>'
|
|
613
|
+
sendBtn.disabled = true
|
|
614
|
+
inputArea.appendChild(sendBtn)
|
|
615
|
+
|
|
616
|
+
// Close emoji picker when clicking elsewhere
|
|
617
|
+
dom.addEventListener('click', (e) => {
|
|
618
|
+
if (!emojiPicker.contains(e.target) && e.target !== emojiBtn) {
|
|
619
|
+
emojiPicker.classList.remove('open')
|
|
620
|
+
}
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
container.appendChild(inputArea)
|
|
624
|
+
|
|
625
|
+
// State
|
|
626
|
+
let messages = []
|
|
627
|
+
let currentUser = null
|
|
628
|
+
|
|
629
|
+
// Get current user
|
|
630
|
+
const authn = context.session?.logic?.authn || globalThis.SolidLogic?.authn
|
|
631
|
+
if (authn) {
|
|
632
|
+
currentUser = authn.currentUser()?.value
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Load messages from store
|
|
636
|
+
async function loadMessages() {
|
|
637
|
+
statusEl.textContent = 'Loading messages...'
|
|
638
|
+
messagesContainer.innerHTML = ''
|
|
639
|
+
messages = []
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
// Define namespaces
|
|
643
|
+
const ns = $rdf.Namespace
|
|
644
|
+
const FLOW = ns('http://www.w3.org/2005/01/wf/flow#')
|
|
645
|
+
const SIOC = ns('http://rdfs.org/sioc/ns#')
|
|
646
|
+
const DC = ns('http://purl.org/dc/elements/1.1/')
|
|
647
|
+
const DCT = ns('http://purl.org/dc/terms/')
|
|
648
|
+
const FOAF = ns('http://xmlns.com/foaf/0.1/')
|
|
649
|
+
|
|
650
|
+
// Fetch the document
|
|
651
|
+
const doc = subject.doc ? subject.doc() : subject
|
|
652
|
+
await store.fetcher.load(doc)
|
|
653
|
+
|
|
654
|
+
// Get chat title from the subject or document
|
|
655
|
+
const chatNode = subject.uri.includes('#') ? subject : $rdf.sym(subject.uri + '#this')
|
|
656
|
+
const title = store.any(chatNode, DCT('title'), null, doc)?.value ||
|
|
657
|
+
store.any(chatNode, DC('title'), null, doc)?.value ||
|
|
658
|
+
store.any(subject, DCT('title'), null, doc)?.value ||
|
|
659
|
+
store.any(null, DCT('title'), null, doc)?.value
|
|
660
|
+
if (title) {
|
|
661
|
+
// Show title with URI as subtitle
|
|
662
|
+
nameEl.textContent = title
|
|
663
|
+
nameEl.title = subject.uri // Tooltip shows full URI
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Extract all messages with sioc:content from this document
|
|
667
|
+
const contentStatements = store.statementsMatching(null, SIOC('content'), null, doc)
|
|
668
|
+
|
|
669
|
+
for (const st of contentStatements) {
|
|
670
|
+
const msgNode = st.subject
|
|
671
|
+
const content = st.object.value
|
|
672
|
+
|
|
673
|
+
if (!content) continue
|
|
674
|
+
|
|
675
|
+
const date = store.any(msgNode, DCT('created'), null, doc)?.value ||
|
|
676
|
+
store.any(msgNode, DC('created'), null, doc)?.value ||
|
|
677
|
+
store.any(msgNode, DC('date'), null, doc)?.value
|
|
678
|
+
|
|
679
|
+
const maker = store.any(msgNode, FOAF('maker'), null, doc) ||
|
|
680
|
+
store.any(msgNode, DC('author'), null, doc) ||
|
|
681
|
+
store.any(msgNode, DCT('creator'), null, doc)
|
|
682
|
+
|
|
683
|
+
let authorName = null
|
|
684
|
+
if (maker) {
|
|
685
|
+
// Try to get name from loaded profile or use URI fragment
|
|
686
|
+
authorName = store.any(maker, FOAF('name'))?.value ||
|
|
687
|
+
maker.value?.split('//')[1]?.split('.')[0] ||
|
|
688
|
+
'Unknown'
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
messages.push({
|
|
692
|
+
uri: msgNode.value,
|
|
693
|
+
content,
|
|
694
|
+
date: date ? new Date(date) : new Date(),
|
|
695
|
+
author: authorName,
|
|
696
|
+
authorUri: maker?.value
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Sort by date
|
|
701
|
+
messages.sort((a, b) => (a.date || 0) - (b.date || 0))
|
|
702
|
+
|
|
703
|
+
// Keep only last 100 messages for performance
|
|
704
|
+
if (messages.length > 100) {
|
|
705
|
+
messages = messages.slice(-100)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Render messages
|
|
709
|
+
if (messages.length === 0) {
|
|
710
|
+
const empty = dom.createElement('div')
|
|
711
|
+
empty.className = 'empty-chat'
|
|
712
|
+
empty.innerHTML = '<div class="empty-chat-icon">💬</div><div>No messages yet</div><div>Be the first to say hello!</div>'
|
|
713
|
+
messagesContainer.appendChild(empty)
|
|
714
|
+
} else {
|
|
715
|
+
for (const msg of messages) {
|
|
716
|
+
const isOwn = currentUser && msg.authorUri === currentUser
|
|
717
|
+
const el = createMessageElement(dom, msg, isOwn)
|
|
718
|
+
messagesContainer.appendChild(el)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
statusEl.textContent = `${messages.length} messages`
|
|
723
|
+
|
|
724
|
+
// Scroll to bottom
|
|
725
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight
|
|
726
|
+
|
|
727
|
+
// Load avatars asynchronously
|
|
728
|
+
const uniqueWebIds = [...new Set(messages.map(m => m.authorUri).filter(Boolean))]
|
|
729
|
+
for (const webId of uniqueWebIds) {
|
|
730
|
+
fetchAvatar(webId, store, $rdf).then(avatarUrl => {
|
|
731
|
+
if (avatarUrl) {
|
|
732
|
+
// Update all avatars for this WebID
|
|
733
|
+
const avatars = messagesContainer.querySelectorAll(`.message-avatar[data-webid="${webId}"]`)
|
|
734
|
+
avatars.forEach(el => {
|
|
735
|
+
el.innerHTML = `<img src="${avatarUrl}" alt="" />`
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.error('Error loading chat:', err)
|
|
743
|
+
statusEl.textContent = 'Error loading chat'
|
|
744
|
+
messagesContainer.innerHTML = `<div class="loading">Error: ${err.message}</div>`
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Send message
|
|
749
|
+
async function sendMessage() {
|
|
750
|
+
const text = input.value.trim()
|
|
751
|
+
if (!text) return
|
|
752
|
+
|
|
753
|
+
sendBtn.disabled = true
|
|
754
|
+
input.disabled = true
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
const ns = $rdf.Namespace
|
|
758
|
+
const FLOW = ns('http://www.w3.org/2005/01/wf/flow#')
|
|
759
|
+
const SIOC = ns('http://rdfs.org/sioc/ns#')
|
|
760
|
+
const DCT = ns('http://purl.org/dc/terms/')
|
|
761
|
+
const FOAF = ns('http://xmlns.com/foaf/0.1/')
|
|
762
|
+
const RDF = ns('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
|
|
763
|
+
|
|
764
|
+
const msgId = `#msg-${Date.now()}`
|
|
765
|
+
const msgNode = $rdf.sym(subject.uri + msgId)
|
|
766
|
+
const now = new Date().toISOString()
|
|
767
|
+
|
|
768
|
+
const ins = [
|
|
769
|
+
$rdf.st(subject, FLOW('message'), msgNode, subject.doc()),
|
|
770
|
+
$rdf.st(msgNode, RDF('type'), FLOW('Message'), subject.doc()),
|
|
771
|
+
$rdf.st(msgNode, SIOC('content'), text, subject.doc()),
|
|
772
|
+
$rdf.st(msgNode, DCT('created'), $rdf.lit(now, null, $rdf.sym('http://www.w3.org/2001/XMLSchema#dateTime')), subject.doc())
|
|
773
|
+
]
|
|
774
|
+
|
|
775
|
+
if (currentUser) {
|
|
776
|
+
ins.push($rdf.st(msgNode, FOAF('maker'), $rdf.sym(currentUser), subject.doc()))
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
await store.updater.update([], ins)
|
|
780
|
+
|
|
781
|
+
// Add to UI immediately
|
|
782
|
+
const msg = {
|
|
783
|
+
uri: msgNode.value,
|
|
784
|
+
content: text,
|
|
785
|
+
date: new Date(now),
|
|
786
|
+
author: 'You',
|
|
787
|
+
authorUri: currentUser
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Remove empty state if present
|
|
791
|
+
const empty = messagesContainer.querySelector('.empty-chat')
|
|
792
|
+
if (empty) empty.remove()
|
|
793
|
+
|
|
794
|
+
const el = createMessageElement(dom, msg, true)
|
|
795
|
+
messagesContainer.appendChild(el)
|
|
796
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight
|
|
797
|
+
|
|
798
|
+
messages.push(msg)
|
|
799
|
+
statusEl.textContent = `${messages.length} messages`
|
|
800
|
+
|
|
801
|
+
input.value = ''
|
|
802
|
+
input.style.height = 'auto'
|
|
803
|
+
|
|
804
|
+
} catch (err) {
|
|
805
|
+
console.error('Error sending message:', err)
|
|
806
|
+
alert('Failed to send message: ' + err.message)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
sendBtn.disabled = !input.value.trim()
|
|
810
|
+
input.disabled = false
|
|
811
|
+
input.focus()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Event listeners
|
|
815
|
+
input.addEventListener('input', () => {
|
|
816
|
+
sendBtn.disabled = !input.value.trim()
|
|
817
|
+
input.style.height = 'auto'
|
|
818
|
+
input.style.height = Math.min(input.scrollHeight, 100) + 'px'
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
input.addEventListener('keydown', (e) => {
|
|
822
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
823
|
+
e.preventDefault()
|
|
824
|
+
sendMessage()
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
sendBtn.addEventListener('click', sendMessage)
|
|
829
|
+
|
|
830
|
+
// Initial load
|
|
831
|
+
loadMessages()
|
|
832
|
+
|
|
833
|
+
return container
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export default longChatPane
|