solid-chat 0.0.1 → 0.0.3
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/README.md +119 -25
- package/package.json +9 -3
- package/src/chatListPane.js +233 -41
- package/src/index.test.js +71 -0
- package/src/longChatPane.js +870 -28
package/README.md
CHANGED
|
@@ -2,17 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
Modern, decentralized chat app built on the [Solid](https://solidproject.org) protocol.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Live App:** [solid-chat.com/app](https://solid-chat.com/app)
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
-
|
|
9
|
+
### Core
|
|
10
|
+
- Clean, WhatsApp-style messenger UI
|
|
11
|
+
- Decentralized - messages stored in Solid pods
|
|
11
12
|
- Implements the [Solid Chat specification](https://solid.github.io/chat/)
|
|
12
|
-
- Works with any Solid pod provider
|
|
13
|
-
- Clickable WebID links on author names
|
|
13
|
+
- Works with any Solid pod provider (solidweb.org, solidcommunity.net, inrupt.net, etc.)
|
|
14
14
|
- No React/heavy frameworks - vanilla JS
|
|
15
15
|
|
|
16
|
+
### Authentication
|
|
17
|
+
- Solid OIDC authentication
|
|
18
|
+
- Multi-provider support (enter any IDP)
|
|
19
|
+
- Persistent sessions
|
|
20
|
+
|
|
21
|
+
### Real-time
|
|
22
|
+
- WebSocket updates (NSS `Updates-Via` header)
|
|
23
|
+
- WebSocketChannel2023 support (CSS/solidcommunity.net)
|
|
24
|
+
- Notification sounds when tab is hidden (toggleable)
|
|
25
|
+
|
|
26
|
+
### Messaging
|
|
27
|
+
- Send and receive messages
|
|
28
|
+
- Edit your own messages (requires Write permission)
|
|
29
|
+
- Delete your own messages (requires Write permission)
|
|
30
|
+
- Simple markdown: `*bold*`, `_italic_`, `~strike~`, `` `code` ``, ` ```code blocks``` `
|
|
31
|
+
- Emoji picker
|
|
32
|
+
- Emoji reactions: 👍 ❤️ 😂 😮 😢 🎉
|
|
33
|
+
|
|
34
|
+
### Media
|
|
35
|
+
- File upload to your pod (📎 button)
|
|
36
|
+
- Paste images directly (Ctrl+V)
|
|
37
|
+
- Auto-expand images, video, and audio inline
|
|
38
|
+
- Clickable URLs
|
|
39
|
+
|
|
40
|
+
### Sidebar
|
|
41
|
+
- Chat list with recent chats
|
|
42
|
+
- Type Index auto-discovery of your chats
|
|
43
|
+
- Create new chats on your pod
|
|
44
|
+
- Manual chat URL addition
|
|
45
|
+
- Persistent storage (localStorage)
|
|
46
|
+
|
|
47
|
+
### Sharing & Deep Links
|
|
48
|
+
- Share button (📋) copies shareable link
|
|
49
|
+
- Deep link support: `?chat=<url>` loads or creates chat
|
|
50
|
+
- Auto-create: links to your pod create rooms on demand
|
|
51
|
+
- Type Index registration for discoverability
|
|
52
|
+
|
|
53
|
+
### UI/UX
|
|
54
|
+
- Avatar display from WebID profiles
|
|
55
|
+
- Clickable author names (links to WebID)
|
|
56
|
+
- Clickable timestamps (permalinks to message URI)
|
|
57
|
+
- "(edited)" indicator for edited messages
|
|
58
|
+
- Message count in header
|
|
59
|
+
|
|
16
60
|
## Quick Start
|
|
17
61
|
|
|
18
62
|
```bash
|
|
@@ -20,8 +64,10 @@ Modern, decentralized chat app built on the [Solid](https://solidproject.org) pr
|
|
|
20
64
|
git clone https://github.com/solid-chat/app.git
|
|
21
65
|
cd app
|
|
22
66
|
|
|
23
|
-
# Serve locally
|
|
67
|
+
# Serve locally (choose one)
|
|
24
68
|
npx serve .
|
|
69
|
+
# or
|
|
70
|
+
npx vite
|
|
25
71
|
|
|
26
72
|
# Open in browser
|
|
27
73
|
open http://localhost:3000
|
|
@@ -29,32 +75,67 @@ open http://localhost:3000
|
|
|
29
75
|
|
|
30
76
|
## Usage
|
|
31
77
|
|
|
32
|
-
1. Enter
|
|
33
|
-
2.
|
|
34
|
-
3.
|
|
78
|
+
1. Enter your Solid IDP (e.g., `solidweb.org`) and click Login
|
|
79
|
+
2. Select a chat from the sidebar or add a new one
|
|
80
|
+
3. Start chatting!
|
|
81
|
+
|
|
82
|
+
### Default Global Chats
|
|
83
|
+
- `https://solid-chat.solidweb.org/public/global/chat.ttl` (faster)
|
|
84
|
+
- `https://solid-chat.solidcommunity.net/public/global/chat.ttl`
|
|
85
|
+
|
|
86
|
+
### Deep Links
|
|
87
|
+
|
|
88
|
+
Share chats with a URL:
|
|
89
|
+
```
|
|
90
|
+
https://solid-chat.com/app?chat=https://you.solidweb.org/public/chats/my-room.ttl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Behavior:**
|
|
94
|
+
- If chat exists → loads it
|
|
95
|
+
- If chat doesn't exist AND it's your pod → creates it automatically
|
|
96
|
+
- Change one character → instant new room
|
|
97
|
+
|
|
98
|
+
**Create rooms on demand:**
|
|
99
|
+
```
|
|
100
|
+
https://solid-chat.com/app?chat=https://you.pod/public/chats/meeting-monday.ttl
|
|
101
|
+
https://solid-chat.com/app?chat=https://you.pod/public/chats/meeting-tuesday.ttl
|
|
102
|
+
```
|
|
35
103
|
|
|
36
|
-
|
|
37
|
-
- `https://solidos.solidcommunity.net/Team/SolidOs%20team%20chat/2022/02/09/chat.ttl`
|
|
104
|
+
Click the share button (📋) in any chat to copy the shareable link.
|
|
38
105
|
|
|
39
106
|
## Chat Data Format
|
|
40
107
|
|
|
41
|
-
|
|
108
|
+
Messages use standard Solid chat vocabularies (RDF/Turtle):
|
|
42
109
|
|
|
43
110
|
```turtle
|
|
44
|
-
@prefix flow: <http://www.w3.org/2005/01/wf/flow#>.
|
|
45
111
|
@prefix sioc: <http://rdfs.org/sioc/ns#>.
|
|
46
112
|
@prefix dct: <http://purl.org/dc/terms/>.
|
|
47
113
|
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
|
48
114
|
|
|
49
|
-
<#
|
|
50
|
-
a <http://www.w3.org/
|
|
51
|
-
flow:message :msg1 .
|
|
52
|
-
|
|
53
|
-
:msg1
|
|
54
|
-
a flow:Message ;
|
|
115
|
+
<#msg-1234567890>
|
|
116
|
+
a <http://www.w3.org/2005/01/wf/flow#Message> ;
|
|
55
117
|
sioc:content "Hello world!" ;
|
|
56
118
|
dct:created "2024-12-30T10:00:00Z"^^xsd:dateTime ;
|
|
57
|
-
foaf:maker <https://
|
|
119
|
+
foaf:maker <https://you.solidweb.org/profile/card#me> .
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Reactions use Schema.org:
|
|
123
|
+
|
|
124
|
+
```turtle
|
|
125
|
+
@prefix schema: <http://schema.org/>.
|
|
126
|
+
|
|
127
|
+
<#reaction-1234567890>
|
|
128
|
+
a schema:ReactAction ;
|
|
129
|
+
schema:about <#msg-123> ;
|
|
130
|
+
schema:agent <https://you.solidweb.org/profile/card#me> ;
|
|
131
|
+
schema:name "👍" .
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Testing
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm install
|
|
138
|
+
npm test
|
|
58
139
|
```
|
|
59
140
|
|
|
60
141
|
## Specification
|
|
@@ -63,11 +144,24 @@ This app implements the [Solid Chat specification](https://solid.github.io/chat/
|
|
|
63
144
|
|
|
64
145
|
## Roadmap
|
|
65
146
|
|
|
66
|
-
- [
|
|
67
|
-
- [
|
|
68
|
-
- [
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
147
|
+
- [x] Solid OIDC authentication
|
|
148
|
+
- [x] Chat list sidebar
|
|
149
|
+
- [x] Real-time updates (WebSocket + WebSocketChannel2023)
|
|
150
|
+
- [x] Notification sounds
|
|
151
|
+
- [x] File upload
|
|
152
|
+
- [x] Image paste
|
|
153
|
+
- [x] Media auto-expand
|
|
154
|
+
- [x] Simple markdown
|
|
155
|
+
- [x] Edit/delete messages
|
|
156
|
+
- [x] Emoji reactions
|
|
157
|
+
- [x] Create new chats
|
|
158
|
+
- [x] Deep link sharing (`?chat=<url>`)
|
|
159
|
+
- [x] Type Index registration
|
|
160
|
+
- [ ] @mentions with notifications
|
|
161
|
+
- [ ] End-to-end encryption
|
|
162
|
+
- [ ] Mobile responsive sidebar
|
|
163
|
+
- [ ] Unread message badges
|
|
164
|
+
- [ ] Message search
|
|
71
165
|
|
|
72
166
|
## License
|
|
73
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-chat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Modern chat panes for Solid pods - longChatPane and chatListPane",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "npx serve .",
|
|
18
|
-
"dev": "npx serve . -p 8080"
|
|
18
|
+
"dev": "npx serve . -p 8080",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest"
|
|
19
21
|
},
|
|
20
22
|
"peerDependencies": {
|
|
21
23
|
"rdflib": ">=2.2.0"
|
|
@@ -38,5 +40,9 @@
|
|
|
38
40
|
},
|
|
39
41
|
"homepage": "https://solid-chat.com",
|
|
40
42
|
"author": "Solid Chat Contributors",
|
|
41
|
-
"license": "AGPL-3.0"
|
|
43
|
+
"license": "AGPL-3.0",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"jsdom": "^27.4.0",
|
|
46
|
+
"vitest": "^4.0.16"
|
|
47
|
+
}
|
|
42
48
|
}
|
package/src/chatListPane.js
CHANGED
|
@@ -20,13 +20,21 @@ const MEETING = {
|
|
|
20
20
|
// Storage key for localStorage
|
|
21
21
|
const STORAGE_KEY = 'solidchat-chats'
|
|
22
22
|
|
|
23
|
-
// Default global
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// Default global chats
|
|
24
|
+
const DEFAULT_CHATS = [
|
|
25
|
+
{
|
|
26
|
+
uri: 'https://solid-chat.solidweb.org/public/global/chat.ttl',
|
|
27
|
+
title: 'Solid Chat Global (solidweb.org)',
|
|
28
|
+
lastMessage: 'Welcome! Faster server.',
|
|
29
|
+
timestamp: '2025-12-31T12:00:00Z'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
uri: 'https://solid-chat.solidcommunity.net/public/global/chat.ttl',
|
|
33
|
+
title: 'Solid Chat Global (solidcommunity.net)',
|
|
34
|
+
lastMessage: 'Welcome to the global chat!',
|
|
35
|
+
timestamp: '2025-12-31T09:00:00Z'
|
|
36
|
+
}
|
|
37
|
+
]
|
|
30
38
|
|
|
31
39
|
// CSS styles
|
|
32
40
|
const styles = `
|
|
@@ -308,6 +316,47 @@ const styles = `
|
|
|
308
316
|
box-shadow: none;
|
|
309
317
|
}
|
|
310
318
|
|
|
319
|
+
/* Modal tabs */
|
|
320
|
+
.modal-tabs {
|
|
321
|
+
display: flex;
|
|
322
|
+
gap: 8px;
|
|
323
|
+
margin-bottom: 16px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.modal-tab {
|
|
327
|
+
flex: 1;
|
|
328
|
+
padding: 8px 12px;
|
|
329
|
+
border: 1px solid var(--border);
|
|
330
|
+
border-radius: 6px;
|
|
331
|
+
background: var(--bg);
|
|
332
|
+
color: var(--text-secondary);
|
|
333
|
+
font-size: 13px;
|
|
334
|
+
font-weight: 500;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
transition: all 0.2s;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.modal-tab:hover {
|
|
340
|
+
background: var(--bg-hover);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.modal-tab.active {
|
|
344
|
+
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
345
|
+
color: white;
|
|
346
|
+
border-color: transparent;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.modal-url-preview {
|
|
350
|
+
font-size: 11px;
|
|
351
|
+
color: var(--text-muted);
|
|
352
|
+
word-break: break-all;
|
|
353
|
+
padding: 8px 12px;
|
|
354
|
+
background: var(--bg-hover);
|
|
355
|
+
border-radius: 6px;
|
|
356
|
+
margin-bottom: 16px;
|
|
357
|
+
display: none;
|
|
358
|
+
}
|
|
359
|
+
|
|
311
360
|
/* Delete button on chat item */
|
|
312
361
|
.chat-item-delete {
|
|
313
362
|
width: 24px;
|
|
@@ -364,18 +413,21 @@ function loadChatList() {
|
|
|
364
413
|
chatList = []
|
|
365
414
|
}
|
|
366
415
|
|
|
367
|
-
// Add default global
|
|
416
|
+
// Add default global chats if list is empty
|
|
368
417
|
if (chatList.length === 0) {
|
|
369
|
-
chatList.push({ ...
|
|
418
|
+
chatList.push(...DEFAULT_CHATS.map(c => ({ ...c })))
|
|
370
419
|
saveChatList()
|
|
371
420
|
}
|
|
372
421
|
|
|
373
|
-
// Ensure default
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
chatList.
|
|
377
|
-
|
|
422
|
+
// Ensure all default chats exist in list
|
|
423
|
+
let added = false
|
|
424
|
+
for (const defaultChat of DEFAULT_CHATS) {
|
|
425
|
+
if (!chatList.some(c => c.uri === defaultChat.uri)) {
|
|
426
|
+
chatList.unshift({ ...defaultChat })
|
|
427
|
+
added = true
|
|
428
|
+
}
|
|
378
429
|
}
|
|
430
|
+
if (added) saveChatList()
|
|
379
431
|
|
|
380
432
|
return chatList
|
|
381
433
|
}
|
|
@@ -535,31 +587,108 @@ function renderChatList() {
|
|
|
535
587
|
})
|
|
536
588
|
}
|
|
537
589
|
|
|
538
|
-
// Show add chat modal
|
|
539
|
-
function showAddModal(dom) {
|
|
590
|
+
// Show add/create chat modal
|
|
591
|
+
function showAddModal(dom, webId) {
|
|
540
592
|
const overlay = dom.createElement('div')
|
|
541
593
|
overlay.className = 'modal-overlay'
|
|
542
594
|
|
|
543
595
|
const modal = dom.createElement('div')
|
|
544
596
|
modal.className = 'modal'
|
|
545
597
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<
|
|
552
|
-
|
|
553
|
-
|
|
598
|
+
// Check if user is logged in (can create chats)
|
|
599
|
+
const canCreate = !!webId
|
|
600
|
+
|
|
601
|
+
if (canCreate) {
|
|
602
|
+
modal.innerHTML = `
|
|
603
|
+
<div class="modal-title">Add or Create Chat</div>
|
|
604
|
+
<div class="modal-tabs">
|
|
605
|
+
<button class="modal-tab active" data-tab="add">Add Existing</button>
|
|
606
|
+
<button class="modal-tab" data-tab="create">Create New</button>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="modal-tab-content" data-content="add">
|
|
609
|
+
<input type="url" class="modal-input" id="addUrlInput" placeholder="Enter chat URL..." />
|
|
610
|
+
</div>
|
|
611
|
+
<div class="modal-tab-content" data-content="create" style="display: none;">
|
|
612
|
+
<input type="text" class="modal-input" id="createTitleInput" placeholder="Chat title (e.g. Team Standup)" />
|
|
613
|
+
<select class="modal-input" id="createLocationSelect">
|
|
614
|
+
<option value="/public/chats/">Public (anyone with link)</option>
|
|
615
|
+
<option value="/private/chats/">Private (only you)</option>
|
|
616
|
+
</select>
|
|
617
|
+
<div class="modal-url-preview" id="urlPreview"></div>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="modal-buttons">
|
|
620
|
+
<button class="modal-btn modal-btn-cancel">Cancel</button>
|
|
621
|
+
<button class="modal-btn modal-btn-add" id="actionBtn">Add</button>
|
|
622
|
+
</div>
|
|
623
|
+
`
|
|
624
|
+
} else {
|
|
625
|
+
modal.innerHTML = `
|
|
626
|
+
<div class="modal-title">Add Chat</div>
|
|
627
|
+
<input type="url" class="modal-input" id="addUrlInput" placeholder="Enter chat URL..." />
|
|
628
|
+
<p style="font-size: 12px; color: #a0aec0; margin-bottom: 16px;">Login to create new chats on your pod.</p>
|
|
629
|
+
<div class="modal-buttons">
|
|
630
|
+
<button class="modal-btn modal-btn-cancel">Cancel</button>
|
|
631
|
+
<button class="modal-btn modal-btn-add" id="actionBtn">Add</button>
|
|
632
|
+
</div>
|
|
633
|
+
`
|
|
634
|
+
}
|
|
554
635
|
|
|
555
636
|
overlay.appendChild(modal)
|
|
556
637
|
dom.body.appendChild(overlay)
|
|
557
638
|
|
|
558
|
-
const input = modal.querySelector('.modal-input')
|
|
559
639
|
const cancelBtn = modal.querySelector('.modal-btn-cancel')
|
|
560
|
-
const
|
|
640
|
+
const actionBtn = modal.querySelector('#actionBtn')
|
|
641
|
+
const addUrlInput = modal.querySelector('#addUrlInput')
|
|
642
|
+
|
|
643
|
+
let currentTab = 'add'
|
|
644
|
+
|
|
645
|
+
// Tab switching
|
|
646
|
+
if (canCreate) {
|
|
647
|
+
const tabs = modal.querySelectorAll('.modal-tab')
|
|
648
|
+
const createTitleInput = modal.querySelector('#createTitleInput')
|
|
649
|
+
const createLocationSelect = modal.querySelector('#createLocationSelect')
|
|
650
|
+
const urlPreview = modal.querySelector('#urlPreview')
|
|
651
|
+
|
|
652
|
+
// Update URL preview
|
|
653
|
+
const updatePreview = () => {
|
|
654
|
+
const title = createTitleInput.value.trim()
|
|
655
|
+
const location = createLocationSelect.value
|
|
656
|
+
if (title && webId) {
|
|
657
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
658
|
+
const podRoot = getPodRootFromWebId(webId)
|
|
659
|
+
const fullUrl = `${podRoot}${location.slice(1)}${slug}.ttl`
|
|
660
|
+
urlPreview.textContent = fullUrl
|
|
661
|
+
urlPreview.style.display = 'block'
|
|
662
|
+
} else {
|
|
663
|
+
urlPreview.style.display = 'none'
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
createTitleInput.addEventListener('input', updatePreview)
|
|
668
|
+
createLocationSelect.addEventListener('change', updatePreview)
|
|
669
|
+
|
|
670
|
+
tabs.forEach(tab => {
|
|
671
|
+
tab.onclick = () => {
|
|
672
|
+
tabs.forEach(t => t.classList.remove('active'))
|
|
673
|
+
tab.classList.add('active')
|
|
674
|
+
currentTab = tab.dataset.tab
|
|
675
|
+
|
|
676
|
+
modal.querySelectorAll('.modal-tab-content').forEach(c => {
|
|
677
|
+
c.style.display = c.dataset.content === currentTab ? 'block' : 'none'
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
actionBtn.textContent = currentTab === 'add' ? 'Add' : 'Create'
|
|
561
681
|
|
|
562
|
-
|
|
682
|
+
if (currentTab === 'add') {
|
|
683
|
+
addUrlInput.focus()
|
|
684
|
+
} else {
|
|
685
|
+
createTitleInput.focus()
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
addUrlInput?.focus()
|
|
563
692
|
|
|
564
693
|
const close = () => {
|
|
565
694
|
overlay.remove()
|
|
@@ -571,26 +700,89 @@ function showAddModal(dom) {
|
|
|
571
700
|
|
|
572
701
|
cancelBtn.onclick = close
|
|
573
702
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
onSelectCallback
|
|
703
|
+
actionBtn.onclick = async () => {
|
|
704
|
+
if (currentTab === 'add') {
|
|
705
|
+
const uri = addUrlInput.value.trim()
|
|
706
|
+
if (uri) {
|
|
707
|
+
addChat(uri)
|
|
708
|
+
activeUri = uri
|
|
709
|
+
renderChatList()
|
|
710
|
+
if (onSelectCallback) {
|
|
711
|
+
onSelectCallback(uri)
|
|
712
|
+
}
|
|
713
|
+
close()
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
// Create new chat
|
|
717
|
+
const createTitleInput = modal.querySelector('#createTitleInput')
|
|
718
|
+
const createLocationSelect = modal.querySelector('#createLocationSelect')
|
|
719
|
+
|
|
720
|
+
const title = createTitleInput.value.trim()
|
|
721
|
+
const location = createLocationSelect.value
|
|
722
|
+
|
|
723
|
+
if (!title) {
|
|
724
|
+
createTitleInput.focus()
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
729
|
+
const podRoot = getPodRootFromWebId(webId)
|
|
730
|
+
const chatUrl = `${podRoot}${location.slice(1)}${slug}.ttl`
|
|
731
|
+
|
|
732
|
+
actionBtn.disabled = true
|
|
733
|
+
actionBtn.textContent = 'Creating...'
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
// Use the createChat from index.html
|
|
737
|
+
if (window.solidChat?.createChat) {
|
|
738
|
+
await window.solidChat.createChat(chatUrl, title)
|
|
739
|
+
} else {
|
|
740
|
+
throw new Error('createChat not available')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
addChat(chatUrl, title)
|
|
744
|
+
activeUri = chatUrl
|
|
745
|
+
renderChatList()
|
|
746
|
+
|
|
747
|
+
// Copy share link
|
|
748
|
+
if (window.solidChat?.copyShareLink) {
|
|
749
|
+
window.solidChat.copyShareLink(chatUrl)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (onSelectCallback) {
|
|
753
|
+
onSelectCallback(chatUrl)
|
|
754
|
+
}
|
|
755
|
+
close()
|
|
756
|
+
} catch (e) {
|
|
757
|
+
console.error('Failed to create chat:', e)
|
|
758
|
+
alert('Failed to create chat: ' + e.message)
|
|
759
|
+
actionBtn.disabled = false
|
|
760
|
+
actionBtn.textContent = 'Create'
|
|
582
761
|
}
|
|
583
|
-
close()
|
|
584
762
|
}
|
|
585
763
|
}
|
|
586
764
|
|
|
587
|
-
|
|
765
|
+
// Handle Enter key
|
|
766
|
+
const handleKeydown = (e) => {
|
|
588
767
|
if (e.key === 'Enter') {
|
|
589
|
-
|
|
768
|
+
actionBtn.click()
|
|
590
769
|
} else if (e.key === 'Escape') {
|
|
591
770
|
close()
|
|
592
771
|
}
|
|
593
772
|
}
|
|
773
|
+
|
|
774
|
+
addUrlInput?.addEventListener('keydown', handleKeydown)
|
|
775
|
+
modal.querySelector('#createTitleInput')?.addEventListener('keydown', handleKeydown)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Get pod root from WebID (simple extraction)
|
|
779
|
+
function getPodRootFromWebId(webId) {
|
|
780
|
+
try {
|
|
781
|
+
const url = new URL(webId)
|
|
782
|
+
return `${url.protocol}//${url.host}/`
|
|
783
|
+
} catch {
|
|
784
|
+
return ''
|
|
785
|
+
}
|
|
594
786
|
}
|
|
595
787
|
|
|
596
788
|
// Discover chats from Type Index
|
|
@@ -670,8 +862,8 @@ export const chatListPane = {
|
|
|
670
862
|
const addBtn = dom.createElement('button')
|
|
671
863
|
addBtn.className = 'add-chat-btn'
|
|
672
864
|
addBtn.textContent = '+'
|
|
673
|
-
addBtn.title = 'Add chat'
|
|
674
|
-
addBtn.onclick = () => showAddModal(dom)
|
|
865
|
+
addBtn.title = 'Add or create chat'
|
|
866
|
+
addBtn.onclick = () => showAddModal(dom, options.webId)
|
|
675
867
|
|
|
676
868
|
header.appendChild(title)
|
|
677
869
|
header.appendChild(addBtn)
|
|
@@ -739,4 +931,4 @@ export const chatListPane = {
|
|
|
739
931
|
}
|
|
740
932
|
|
|
741
933
|
// Export for use in index.html
|
|
742
|
-
export { addChat, updateChatPreview }
|
|
934
|
+
export { addChat, removeChat, updateChatPreview }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { longChatPane, chatListPane, addChat, removeChat } from './index.js'
|
|
3
|
+
|
|
4
|
+
describe('solid-chat exports', () => {
|
|
5
|
+
it('exports longChatPane', () => {
|
|
6
|
+
expect(longChatPane).toBeDefined()
|
|
7
|
+
expect(longChatPane.name).toBe('long-chat')
|
|
8
|
+
expect(typeof longChatPane.render).toBe('function')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('exports chatListPane', () => {
|
|
12
|
+
expect(chatListPane).toBeDefined()
|
|
13
|
+
expect(chatListPane.name).toBe('chat-list')
|
|
14
|
+
expect(typeof chatListPane.render).toBe('function')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('exports addChat and removeChat', () => {
|
|
18
|
+
expect(typeof addChat).toBe('function')
|
|
19
|
+
expect(typeof removeChat).toBe('function')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('chatListPane', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
localStorage.clear()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders a container element', () => {
|
|
29
|
+
const context = { dom: document, session: { store: {} } }
|
|
30
|
+
const options = { onSelectChat: () => {} }
|
|
31
|
+
const element = chatListPane.render(context, options)
|
|
32
|
+
|
|
33
|
+
expect(element).toBeInstanceOf(HTMLElement)
|
|
34
|
+
expect(element.className).toBe('chat-list-pane')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('includes default global chat', () => {
|
|
38
|
+
const context = { dom: document, session: { store: {} } }
|
|
39
|
+
const options = { onSelectChat: () => {} }
|
|
40
|
+
const element = chatListPane.render(context, options)
|
|
41
|
+
|
|
42
|
+
expect(element.textContent).toContain('Solid Chat Global')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('addChat persists to localStorage', () => {
|
|
46
|
+
const context = { dom: document, session: { store: {} } }
|
|
47
|
+
chatListPane.render(context, { onSelectChat: () => {} })
|
|
48
|
+
|
|
49
|
+
addChat('https://example.com/chat.ttl', 'Test Chat')
|
|
50
|
+
|
|
51
|
+
const stored = JSON.parse(localStorage.getItem('solidchat-chats'))
|
|
52
|
+
expect(stored.some(c => c.uri === 'https://example.com/chat.ttl')).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('removeChat removes from localStorage', () => {
|
|
56
|
+
const context = { dom: document, session: { store: {} } }
|
|
57
|
+
chatListPane.render(context, { onSelectChat: () => {} })
|
|
58
|
+
|
|
59
|
+
addChat('https://example.com/chat.ttl', 'Test Chat')
|
|
60
|
+
removeChat('https://example.com/chat.ttl')
|
|
61
|
+
|
|
62
|
+
const stored = JSON.parse(localStorage.getItem('solidchat-chats'))
|
|
63
|
+
expect(stored.some(c => c.uri === 'https://example.com/chat.ttl')).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('longChatPane', () => {
|
|
68
|
+
it('has label function', () => {
|
|
69
|
+
expect(typeof longChatPane.label).toBe('function')
|
|
70
|
+
})
|
|
71
|
+
})
|