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 CHANGED
@@ -2,17 +2,61 @@
2
2
 
3
3
  Modern, decentralized chat app built on the [Solid](https://solidproject.org) protocol.
4
4
 
5
- 🌐 **Live App:** [solid-chat.com/app](https://solid-chat.com/app)
5
+ **Live App:** [solid-chat.com/app](https://solid-chat.com/app)
6
6
 
7
7
  ## Features
8
8
 
9
- - Clean, modern messenger-style UI
10
- - Decentralized - messages stored in your Solid pod
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 a Solid chat URI in the input field
33
- 2. Click "Load Chat" or press Enter
34
- 3. Messages from the chat will be displayed
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
- Example chat URIs:
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
- The app expects Turtle/RDF data using standard Solid chat vocabularies:
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
- <#this>
50
- a <http://www.w3.org/ns/pim/meeting#LongChat> ;
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://example.com/profile#me> .
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
- - [ ] Solid OIDC authentication
67
- - [ ] Chat list sidebar
68
- - [ ] Create new chats
69
- - [ ] Real-time updates
70
- - [ ] Nostr DID integration
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.1",
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
  }
@@ -20,13 +20,21 @@ const MEETING = {
20
20
  // Storage key for localStorage
21
21
  const STORAGE_KEY = 'solidchat-chats'
22
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
- }
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 chat if list is empty
416
+ // Add default global chats if list is empty
368
417
  if (chatList.length === 0) {
369
- chatList.push({ ...DEFAULT_CHAT })
418
+ chatList.push(...DEFAULT_CHATS.map(c => ({ ...c })))
370
419
  saveChatList()
371
420
  }
372
421
 
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()
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
- 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
- `
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 addBtn = modal.querySelector('.modal-btn-add')
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
- input.focus()
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
- 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)
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
- input.onkeydown = (e) => {
765
+ // Handle Enter key
766
+ const handleKeydown = (e) => {
588
767
  if (e.key === 'Enter') {
589
- addBtn.click()
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
+ })