solid-chat 0.0.17 → 0.0.19
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 +20 -3
- package/index.html +118 -11
- package/package.json +1 -1
- package/src/chatListPane.js +2 -1
- package/src/longChatPane.js +86 -9
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Modern, decentralized chat app built on the [Solid](https://solidproject.org) pr
|
|
|
21
21
|
### Real-time
|
|
22
22
|
- WebSocket updates (NSS `Updates-Via` header)
|
|
23
23
|
- WebSocketChannel2023 support (CSS/solidcommunity.net)
|
|
24
|
-
- Notification sounds
|
|
24
|
+
- Notification sounds (toggleable)
|
|
25
25
|
|
|
26
26
|
### Messaging
|
|
27
27
|
- Send and receive messages
|
|
@@ -56,6 +56,19 @@ Modern, decentralized chat app built on the [Solid](https://solidproject.org) pr
|
|
|
56
56
|
- Clickable timestamps (permalinks to message URI)
|
|
57
57
|
- "(edited)" indicator for edited messages
|
|
58
58
|
- Message count in header
|
|
59
|
+
- Unread message badges
|
|
60
|
+
- Remember current room across refreshes
|
|
61
|
+
|
|
62
|
+
### Themes
|
|
63
|
+
- Wave (WhatsApp-style green)
|
|
64
|
+
- Signal (clean blue)
|
|
65
|
+
- Telegram (blue with patterns)
|
|
66
|
+
- Solid (purple/gradient)
|
|
67
|
+
|
|
68
|
+
### Saved Messages
|
|
69
|
+
- Telegram-style "notes to self"
|
|
70
|
+
- Auto-created on your pod
|
|
71
|
+
- Private by default
|
|
59
72
|
|
|
60
73
|
## Quick Start
|
|
61
74
|
|
|
@@ -154,11 +167,15 @@ This app implements the [Solid Chat specification](https://solid.github.io/chat/
|
|
|
154
167
|
- [x] Create new chats
|
|
155
168
|
- [x] Deep link sharing (`?chat=<url>`)
|
|
156
169
|
- [x] Type Index registration
|
|
170
|
+
- [x] Mobile responsive sidebar
|
|
171
|
+
- [x] Unread message badges
|
|
172
|
+
- [x] Theme support
|
|
173
|
+
- [x] Saved Messages
|
|
157
174
|
- [ ] @mentions with notifications
|
|
158
175
|
- [ ] End-to-end encryption
|
|
159
|
-
- [ ] Mobile responsive sidebar
|
|
160
|
-
- [ ] Unread message badges
|
|
161
176
|
- [ ] Message search
|
|
177
|
+
- [ ] Push notifications
|
|
178
|
+
- [ ] Offline support
|
|
162
179
|
|
|
163
180
|
## License
|
|
164
181
|
|
package/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
6
|
<title>Solid Chat</title>
|
|
7
7
|
<meta name="description" content="Modern, decentralized chat app for Solid pods. Your messages, your control.">
|
|
8
8
|
|
|
@@ -216,18 +216,18 @@ async function handleAuthRedirect() {
|
|
|
216
216
|
|
|
217
217
|
if (currentSession.info.isLoggedIn) {
|
|
218
218
|
currentWebId = currentSession.info.webId
|
|
219
|
-
updateAuthUI(true)
|
|
219
|
+
await updateAuthUI(true)
|
|
220
220
|
} else {
|
|
221
|
-
updateAuthUI(false)
|
|
221
|
+
await updateAuthUI(false)
|
|
222
222
|
}
|
|
223
223
|
} catch (err) {
|
|
224
224
|
console.error('Auth redirect error:', err)
|
|
225
|
-
updateAuthUI(false)
|
|
225
|
+
await updateAuthUI(false)
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
// Update UI based on auth state
|
|
230
|
-
function updateAuthUI(isLoggedIn) {
|
|
230
|
+
async function updateAuthUI(isLoggedIn) {
|
|
231
231
|
if (isLoggedIn && currentWebId) {
|
|
232
232
|
const shortId = currentWebId.split('//')[1]?.split('/')[0] || currentWebId
|
|
233
233
|
userStatus.innerHTML = `Logged in as <a href="${currentWebId}" target="_blank">${shortId}</a>`
|
|
@@ -237,6 +237,12 @@ function updateAuthUI(isLoggedIn) {
|
|
|
237
237
|
// Update avatar with user initial
|
|
238
238
|
const initial = shortId.charAt(0).toUpperCase()
|
|
239
239
|
document.getElementById('userAvatar').textContent = initial
|
|
240
|
+
|
|
241
|
+
// Add Saved Messages to chat list (pinned at top)
|
|
242
|
+
const savedMessagesUrl = await ensureSavedMessagesChat()
|
|
243
|
+
if (savedMessagesUrl) {
|
|
244
|
+
addChat(savedMessagesUrl, '📌 Saved Messages', 'Private notes to yourself')
|
|
245
|
+
}
|
|
240
246
|
} else {
|
|
241
247
|
userStatus.textContent = 'Not logged in'
|
|
242
248
|
loginArea.innerHTML = `
|
|
@@ -280,7 +286,7 @@ async function handleLogout() {
|
|
|
280
286
|
await logout()
|
|
281
287
|
currentSession = null
|
|
282
288
|
currentWebId = null
|
|
283
|
-
updateAuthUI(false)
|
|
289
|
+
await updateAuthUI(false)
|
|
284
290
|
} catch (err) {
|
|
285
291
|
console.error('Logout error:', err)
|
|
286
292
|
}
|
|
@@ -375,7 +381,7 @@ let reconnectTimeout = null
|
|
|
375
381
|
let soundEnabled = localStorage.getItem('solidchat-sound') !== 'false'
|
|
376
382
|
|
|
377
383
|
function playNotificationSound() {
|
|
378
|
-
if (!soundEnabled
|
|
384
|
+
if (!soundEnabled) return
|
|
379
385
|
|
|
380
386
|
const ctx = new AudioContext()
|
|
381
387
|
|
|
@@ -492,7 +498,7 @@ async function subscribeWebSocketChannel2023(uri, authFetch) {
|
|
|
492
498
|
if (document.hidden) {
|
|
493
499
|
incrementUnread(uri)
|
|
494
500
|
}
|
|
495
|
-
setTimeout(() => refreshChat(),
|
|
501
|
+
setTimeout(() => refreshChat(), 1500)
|
|
496
502
|
}
|
|
497
503
|
|
|
498
504
|
currentWebSocket.onclose = () => {
|
|
@@ -532,7 +538,7 @@ function connectLegacyWebSocket(updatesVia, uri) {
|
|
|
532
538
|
if (document.hidden) {
|
|
533
539
|
incrementUnread(uri)
|
|
534
540
|
}
|
|
535
|
-
setTimeout(() => refreshChat(),
|
|
541
|
+
setTimeout(() => refreshChat(), 1500)
|
|
536
542
|
}
|
|
537
543
|
}
|
|
538
544
|
}
|
|
@@ -645,6 +651,9 @@ async function loadChat(uri) {
|
|
|
645
651
|
currentChatUri = uri
|
|
646
652
|
subscribeToUpdates(uri)
|
|
647
653
|
|
|
654
|
+
// Remember current chat for page refresh
|
|
655
|
+
localStorage.setItem('solidchat-current-room', uri)
|
|
656
|
+
|
|
648
657
|
} catch (err) {
|
|
649
658
|
console.error('Error loading chat:', err)
|
|
650
659
|
chatContainer.innerHTML = `
|
|
@@ -764,6 +773,88 @@ async function setPublicReadACL(resourceUrl) {
|
|
|
764
773
|
}
|
|
765
774
|
}
|
|
766
775
|
|
|
776
|
+
// Set private ACL (owner-only) for Saved Messages
|
|
777
|
+
async function setPrivateACL(resourceUrl) {
|
|
778
|
+
const aclUrl = resourceUrl + '.acl'
|
|
779
|
+
const authFetch = getAuthFetch()
|
|
780
|
+
|
|
781
|
+
const acl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
782
|
+
|
|
783
|
+
<#owner>
|
|
784
|
+
a acl:Authorization ;
|
|
785
|
+
acl:agent <${currentWebId}> ;
|
|
786
|
+
acl:accessTo <${resourceUrl}> ;
|
|
787
|
+
acl:mode acl:Read, acl:Write, acl:Control .
|
|
788
|
+
`
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
await authFetch(aclUrl, {
|
|
792
|
+
method: 'PUT',
|
|
793
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
794
|
+
body: acl
|
|
795
|
+
})
|
|
796
|
+
} catch (e) {
|
|
797
|
+
console.warn('Failed to set private ACL:', e)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Get URI for Saved Messages chat
|
|
802
|
+
function getSavedMessagesUri(podRoot) {
|
|
803
|
+
return podRoot + 'private/saved-messages/chat.ttl'
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Ensure Saved Messages chat exists, create if not
|
|
807
|
+
async function ensureSavedMessagesChat() {
|
|
808
|
+
if (!currentWebId) return null
|
|
809
|
+
|
|
810
|
+
const podRoot = await getMyPodRoot()
|
|
811
|
+
if (!podRoot) return null
|
|
812
|
+
|
|
813
|
+
const chatUrl = getSavedMessagesUri(podRoot)
|
|
814
|
+
const authFetch = getAuthFetch()
|
|
815
|
+
|
|
816
|
+
// Check if chat already exists
|
|
817
|
+
try {
|
|
818
|
+
const response = await authFetch(chatUrl, { method: 'HEAD' })
|
|
819
|
+
if (response.ok) {
|
|
820
|
+
return chatUrl // Already exists
|
|
821
|
+
}
|
|
822
|
+
} catch (e) {
|
|
823
|
+
// Doesn't exist, create it
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Create the chat
|
|
827
|
+
const now = new Date().toISOString()
|
|
828
|
+
const turtle = `@prefix dct: <http://purl.org/dc/terms/>.
|
|
829
|
+
@prefix meeting: <http://www.w3.org/ns/pim/meeting#>.
|
|
830
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
|
|
831
|
+
|
|
832
|
+
<>
|
|
833
|
+
a meeting:LongChat ;
|
|
834
|
+
dct:title "Saved Messages" ;
|
|
835
|
+
dct:created "${now}"^^xsd:dateTime .
|
|
836
|
+
`
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const response = await authFetch(chatUrl, {
|
|
840
|
+
method: 'PUT',
|
|
841
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
842
|
+
body: turtle
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
if (response.ok) {
|
|
846
|
+
// Set private ACL
|
|
847
|
+
await setPrivateACL(chatUrl)
|
|
848
|
+
console.log('Created Saved Messages chat:', chatUrl)
|
|
849
|
+
return chatUrl
|
|
850
|
+
}
|
|
851
|
+
} catch (e) {
|
|
852
|
+
console.warn('Failed to create Saved Messages chat:', e)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return null
|
|
856
|
+
}
|
|
857
|
+
|
|
767
858
|
// Register chat in user's Type Index
|
|
768
859
|
async function registerInTypeIndex(chatUrl, isPublic = true) {
|
|
769
860
|
if (!currentWebId) return
|
|
@@ -905,11 +996,26 @@ function showToast(message) {
|
|
|
905
996
|
window.solidChat = { createChat, copyShareLink, getMyPodRoot }
|
|
906
997
|
window.toggleSound = toggleSound
|
|
907
998
|
|
|
999
|
+
// Detect bottom nav bar overlay and set CSS variable
|
|
1000
|
+
function updateBottomOffset() {
|
|
1001
|
+
if (window.visualViewport) {
|
|
1002
|
+
const offset = window.innerHeight - window.visualViewport.height;
|
|
1003
|
+
document.documentElement.style.setProperty('--bottom-offset', `${Math.max(0, offset)}px`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
908
1007
|
// Initialize: handle auth redirect first, then render sidebar
|
|
909
1008
|
async function init() {
|
|
910
1009
|
// Initialize theme
|
|
911
1010
|
initTheme()
|
|
912
1011
|
|
|
1012
|
+
// Set up viewport detection for Android nav bar
|
|
1013
|
+
updateBottomOffset()
|
|
1014
|
+
if (window.visualViewport) {
|
|
1015
|
+
window.visualViewport.addEventListener('resize', updateBottomOffset)
|
|
1016
|
+
}
|
|
1017
|
+
window.addEventListener('resize', updateBottomOffset)
|
|
1018
|
+
|
|
913
1019
|
// Sound button is now created in chatListPane with onclick handler
|
|
914
1020
|
|
|
915
1021
|
// Clear unread count when user returns to tab
|
|
@@ -946,13 +1052,14 @@ async function init() {
|
|
|
946
1052
|
discoverBtn.parentNode.insertBefore(themeSelect.parentNode, discoverBtn)
|
|
947
1053
|
}
|
|
948
1054
|
|
|
949
|
-
// Check for ?chat= deep link first, then ?uri= (legacy), then default
|
|
1055
|
+
// Check for ?chat= deep link first, then ?uri= (legacy), then saved room, then default
|
|
950
1056
|
const deepLinkedChat = await handleDeepLink()
|
|
951
1057
|
if (deepLinkedChat) {
|
|
952
1058
|
loadChat(deepLinkedChat)
|
|
953
1059
|
} else {
|
|
954
1060
|
const params = new URLSearchParams(window.location.search)
|
|
955
|
-
const
|
|
1061
|
+
const savedRoom = localStorage.getItem('solidchat-current-room')
|
|
1062
|
+
const initialUri = params.get('uri') || savedRoom || DEFAULT_CHAT_URI
|
|
956
1063
|
loadChat(initialUri)
|
|
957
1064
|
}
|
|
958
1065
|
}
|
package/package.json
CHANGED
package/src/chatListPane.js
CHANGED
|
@@ -104,6 +104,7 @@ const styles = `
|
|
|
104
104
|
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
|
105
105
|
color: white;
|
|
106
106
|
padding: 16px 16px;
|
|
107
|
+
min-height: 76px;
|
|
107
108
|
display: flex;
|
|
108
109
|
align-items: center;
|
|
109
110
|
justify-content: space-between;
|
|
@@ -156,7 +157,7 @@ const styles = `
|
|
|
156
157
|
.chat-list {
|
|
157
158
|
flex: 1;
|
|
158
159
|
overflow-y: auto;
|
|
159
|
-
padding: 8px 0;
|
|
160
|
+
padding: 0 0 8px 0;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
.chat-item {
|
package/src/longChatPane.js
CHANGED
|
@@ -235,6 +235,7 @@ const styles = `
|
|
|
235
235
|
.input-area {
|
|
236
236
|
background: white;
|
|
237
237
|
padding: 12px 20px;
|
|
238
|
+
padding-bottom: calc(12px + var(--bottom-offset, 0px));
|
|
238
239
|
display: flex;
|
|
239
240
|
align-items: flex-end;
|
|
240
241
|
gap: 12px;
|
|
@@ -1189,10 +1190,44 @@ export const longChatPane = {
|
|
|
1189
1190
|
try {
|
|
1190
1191
|
const authFetch = context.authFetch ? context.authFetch() : fetch
|
|
1191
1192
|
const doc = subject.doc ? subject.doc() : subject
|
|
1192
|
-
|
|
1193
|
-
// Build SPARQL DELETE for all statements about this message
|
|
1194
1193
|
const msgUri = message.uri
|
|
1195
|
-
|
|
1194
|
+
|
|
1195
|
+
// Get all triples about this message from the store
|
|
1196
|
+
const msgNode = store.sym(msgUri)
|
|
1197
|
+
const statements = store.statementsMatching(msgNode, null, null, doc)
|
|
1198
|
+
|
|
1199
|
+
if (statements.length === 0) {
|
|
1200
|
+
throw new Error('No statements found for this message')
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Build DELETE DATA with explicit triples (more compatible than DELETE WHERE with variables)
|
|
1204
|
+
const triples = statements.map(st => {
|
|
1205
|
+
const obj = st.object
|
|
1206
|
+
let objStr
|
|
1207
|
+
if (obj.termType === 'NamedNode') {
|
|
1208
|
+
objStr = `<${obj.value}>`
|
|
1209
|
+
} else if (obj.termType === 'Literal') {
|
|
1210
|
+
// Escape special characters in literal value (Turtle escaping)
|
|
1211
|
+
const escaped = obj.value
|
|
1212
|
+
.replace(/\\/g, '\\\\')
|
|
1213
|
+
.replace(/"/g, '\\"')
|
|
1214
|
+
.replace(/\n/g, '\\n')
|
|
1215
|
+
.replace(/\r/g, '\\r')
|
|
1216
|
+
.replace(/\t/g, '\\t')
|
|
1217
|
+
if (obj.datatype && obj.datatype.value !== 'http://www.w3.org/2001/XMLSchema#string') {
|
|
1218
|
+
objStr = `"${escaped}"^^<${obj.datatype.value}>`
|
|
1219
|
+
} else if (obj.language) {
|
|
1220
|
+
objStr = `"${escaped}"@${obj.language}`
|
|
1221
|
+
} else {
|
|
1222
|
+
objStr = `"${escaped}"`
|
|
1223
|
+
}
|
|
1224
|
+
} else {
|
|
1225
|
+
objStr = `"${obj.value}"`
|
|
1226
|
+
}
|
|
1227
|
+
return `<${st.subject.value}> <${st.predicate.value}> ${objStr} .`
|
|
1228
|
+
}).join('\n')
|
|
1229
|
+
|
|
1230
|
+
const deleteQuery = `DELETE DATA {\n${triples}\n}`
|
|
1196
1231
|
|
|
1197
1232
|
const response = await authFetch(doc.value || doc.uri, {
|
|
1198
1233
|
method: 'PATCH',
|
|
@@ -1204,14 +1239,22 @@ export const longChatPane = {
|
|
|
1204
1239
|
throw new Error(`Delete failed: ${response.status}`)
|
|
1205
1240
|
}
|
|
1206
1241
|
|
|
1242
|
+
// Show brief success indicator
|
|
1243
|
+
statusEl.textContent = '✓ Deleted'
|
|
1244
|
+
setTimeout(() => { statusEl.textContent = `${messages.length - 1} messages` }, 1000)
|
|
1245
|
+
|
|
1246
|
+
// Remove from local store (prevents ghost re-render on WebSocket refresh)
|
|
1247
|
+
statements.forEach(st => store.remove(st))
|
|
1248
|
+
|
|
1207
1249
|
// Remove from UI
|
|
1208
1250
|
rowEl.remove()
|
|
1209
1251
|
renderedUris.delete(message.uri)
|
|
1210
1252
|
messages = messages.filter(m => m.uri !== message.uri)
|
|
1211
|
-
statusEl.textContent = `${messages.length} messages`
|
|
1212
1253
|
|
|
1213
1254
|
} catch (err) {
|
|
1214
1255
|
console.error('Delete error:', err)
|
|
1256
|
+
statusEl.textContent = '✗ Delete failed'
|
|
1257
|
+
setTimeout(() => { statusEl.textContent = `${messages.length} messages` }, 2000)
|
|
1215
1258
|
alert('Failed to delete: ' + err.message)
|
|
1216
1259
|
}
|
|
1217
1260
|
}
|
|
@@ -1384,7 +1427,7 @@ export const longChatPane = {
|
|
|
1384
1427
|
}
|
|
1385
1428
|
|
|
1386
1429
|
// Load messages from store
|
|
1387
|
-
async function loadMessages() {
|
|
1430
|
+
async function loadMessages(skipFetch = false) {
|
|
1388
1431
|
if (isFirstLoad) {
|
|
1389
1432
|
statusEl.textContent = 'Loading messages...'
|
|
1390
1433
|
messagesContainer.innerHTML = ''
|
|
@@ -1399,9 +1442,11 @@ export const longChatPane = {
|
|
|
1399
1442
|
const DCT = ns('http://purl.org/dc/terms/')
|
|
1400
1443
|
const FOAF = ns('http://xmlns.com/foaf/0.1/')
|
|
1401
1444
|
|
|
1402
|
-
// Fetch the document
|
|
1445
|
+
// Fetch the document (skip if refresh already loaded fresh data)
|
|
1403
1446
|
const doc = subject.doc ? subject.doc() : subject
|
|
1404
|
-
|
|
1447
|
+
if (!skipFetch) {
|
|
1448
|
+
await store.fetcher.load(doc)
|
|
1449
|
+
}
|
|
1405
1450
|
|
|
1406
1451
|
// Get chat title from the subject or document
|
|
1407
1452
|
const chatNode = subject.uri.includes('#') ? subject : $rdf.sym(subject.uri + '#this')
|
|
@@ -1480,6 +1525,17 @@ export const longChatPane = {
|
|
|
1480
1525
|
// Find messages that haven't been rendered yet
|
|
1481
1526
|
const unrenderedMessages = allMessages.filter(m => !renderedUris.has(m.uri))
|
|
1482
1527
|
|
|
1528
|
+
// Find messages that were rendered but no longer exist (deleted)
|
|
1529
|
+
const currentUris = new Set(allMessages.map(m => m.uri))
|
|
1530
|
+
const deletedUris = [...renderedUris].filter(uri => !currentUris.has(uri))
|
|
1531
|
+
|
|
1532
|
+
// Remove deleted messages from UI
|
|
1533
|
+
for (const uri of deletedUris) {
|
|
1534
|
+
const el = messagesContainer.querySelector(`[data-uri="${uri}"]`)
|
|
1535
|
+
if (el) el.remove()
|
|
1536
|
+
renderedUris.delete(uri)
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1483
1539
|
// Render only new messages (or all on first load)
|
|
1484
1540
|
if (isFirstLoad) {
|
|
1485
1541
|
if (allMessages.length === 0) {
|
|
@@ -1669,8 +1725,29 @@ export const longChatPane = {
|
|
|
1669
1725
|
container.refresh = async function() {
|
|
1670
1726
|
// Re-fetch the document
|
|
1671
1727
|
const doc = subject.doc ? subject.doc() : subject
|
|
1672
|
-
|
|
1673
|
-
|
|
1728
|
+
const docUri = doc.uri || doc.value
|
|
1729
|
+
|
|
1730
|
+
// Clear existing statements for this document before reloading
|
|
1731
|
+
const existingStatements = store.statementsMatching(null, null, null, doc)
|
|
1732
|
+
existingStatements.forEach(st => store.remove(st))
|
|
1733
|
+
|
|
1734
|
+
// Fetch with cache-busting to bypass browser cache
|
|
1735
|
+
const authFetch = context.authFetch ? context.authFetch() : fetch
|
|
1736
|
+
const cacheBustUrl = docUri + (docUri.includes('?') ? '&' : '?') + '_t=' + Date.now()
|
|
1737
|
+
|
|
1738
|
+
const response = await authFetch(cacheBustUrl, {
|
|
1739
|
+
headers: { 'Accept': 'text/turtle, application/ld+json, application/rdf+xml' },
|
|
1740
|
+
cache: 'no-store'
|
|
1741
|
+
})
|
|
1742
|
+
|
|
1743
|
+
if (response.ok) {
|
|
1744
|
+
const text = await response.text()
|
|
1745
|
+
const contentType = response.headers.get('content-type') || 'text/turtle'
|
|
1746
|
+
$rdf.parse(text, store, docUri, contentType)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// skipFetch=true because we already loaded fresh data above
|
|
1750
|
+
await loadMessages(true)
|
|
1674
1751
|
}
|
|
1675
1752
|
|
|
1676
1753
|
// Initial load
|