a demonstration replicated social networking web app built with anproto wiredove.net/
social ed25519 protocols

Preserve visited routes and update inactive feeds efficiently

+72 -36
+49 -7
adder.js
··· 167 167 window.__feedController = { 168 168 feeds: new Map(), 169 169 getFeed(src) { 170 - const state = this.feeds.get(src) 171 - if (state && state.container && !document.body.contains(state.container)) { 172 - this.feeds.delete(src) 173 - return null 174 - } 175 - return state || null 170 + return this.feeds.get(src) || null 176 171 }, 177 172 deactivateFeed(src) { 178 173 const state = this.getFeed(src) ··· 187 182 return window.__feedController 188 183 } 189 184 185 + const makeRouteMatcher = (src) => { 186 + if (src === '') { 187 + return () => true 188 + } 189 + if (src.length === 44) { 190 + return (entry) => entry?.author === src 191 + } 192 + if (src.length < 44 && !src.startsWith('?') && src !== 'settings' && src !== 'import') { 193 + return (entry) => { 194 + const aliasesRaw = localStorage.getItem(src) 195 + if (!aliasesRaw) { return false } 196 + try { 197 + const aliases = JSON.parse(aliasesRaw) 198 + if (!Array.isArray(aliases)) { return false } 199 + return aliases.includes(entry?.author) 200 + } catch { 201 + return false 202 + } 203 + } 204 + } 205 + return () => false 206 + } 207 + 190 208 const normalizeTimestamp = (ts) => { 191 209 const value = Number.parseInt(ts, 10) 192 210 return Number.isNaN(value) ? 0 : value ··· 366 384 } 367 385 } 368 386 369 - const enqueuePost = async (state, entry) => { 387 + const enqueuePost = async (state, entry) => { 370 388 if (!entry || !entry.hash || !entry.ts) { return } 371 389 const insertedAt = insertEntry(state, entry) 372 390 if (insertedAt < 0) { return } 391 + if (!state.active) { 392 + state.pending.push(entry) 393 + updateBanner(state) 394 + return 395 + } 373 396 if (!state.latestVisibleTs) { 374 397 await renderEntry(state, entry) 375 398 state.latestVisibleTs = entry.ts ··· 408 431 return true 409 432 } 410 433 434 + window.__feedEnqueueMatching = async (entry) => { 435 + if (!entry || !entry.hash || !entry.ts) { return false } 436 + const controller = getController() 437 + let matched = false 438 + const states = Array.from(controller.feeds.values()) 439 + for (const state of states) { 440 + if (!state?.matches?.(entry)) { continue } 441 + await enqueuePost(state, entry) 442 + matched = true 443 + } 444 + return matched 445 + } 446 + 411 447 const getStatusState = () => { 412 448 const controller = getController() 413 449 const src = window.location.hash.substring(1) ··· 474 510 const state = { 475 511 src, 476 512 container: div, 513 + matches: makeRouteMatcher(src), 514 + active: true, 477 515 entries, 478 516 cursor: 0, 479 517 seen: new Set(entries.map(entry => entry.hash)), ··· 527 565 528 566 const loadNext = async () => { 529 567 if (loading) { return } 568 + if (!state.active) { return false } 530 569 if (window.location.hash.substring(1) !== src) { return } 531 570 loading = true 532 571 try { ··· 579 618 observer.observe(sentinel) 580 619 state.observer = observer 581 620 state.deactivate = () => { 621 + state.active = false 582 622 detachArmScroll() 583 623 state.observer?.disconnect() 584 624 } 585 625 state.activate = () => { 626 + state.active = true 586 627 if (state.sentinel) { 587 628 state.observer?.observe(state.sentinel) 588 629 } 589 630 attachArmScroll() 631 + updateBanner(state) 590 632 } 591 633 }
+10 -22
render.js
··· 558 558 if (beforeNode && beforeNode.parentNode === container) { 559 559 container.insertBefore(div, beforeNode) 560 560 } else { 561 - const sentinel = container.querySelector('#scroll-sentinel') 561 + const sentinel = container.querySelector('.scroll-sentinel') 562 562 if (sentinel && sentinel.parentNode === container) { 563 563 container.insertBefore(div, sentinel) 564 564 } else { ··· 1331 1331 const inDom = document.getElementById(hash) 1332 1332 if (opened && !inDom) { 1333 1333 await noteSeen(blob.substring(0, 44)) 1334 - const src = window.location.hash.substring(1) 1335 - const al = [] 1336 - const aliases = localStorage.getItem(src) 1337 - if (aliases) { 1338 - const parse = JSON.parse(aliases) 1339 - al.push(...parse) 1340 - console.log(al) 1341 - } 1342 1334 let yaml = null 1343 1335 const msg = await apds.get(opened.substring(13)) 1344 1336 if (msg) { ··· 1362 1354 } 1363 1355 return 1364 1356 } 1365 - if (scroller && (authorKey === src || hash === src || al.includes(authorKey))) { 1366 - if (window.__feedEnqueue) { 1367 - const queued = await window.__feedEnqueue(src, { hash, ts, blob, opened }) 1368 - if (queued) { return } 1369 - } 1370 - return 1371 - } 1372 - if (scroller && src === '') { 1373 - if (window.__feedEnqueue) { 1374 - const queued = await window.__feedEnqueue(src, { hash, ts, blob, opened }) 1375 - if (queued) { return } 1376 - } 1377 - return 1357 + if (scroller && window.__feedEnqueueMatching) { 1358 + const queued = await window.__feedEnqueueMatching({ 1359 + hash, 1360 + ts, 1361 + blob, 1362 + opened, 1363 + author: authorKey 1364 + }) 1365 + if (queued) { return } 1378 1366 } 1379 1367 } 1380 1368 }
+13 -7
route.js
··· 34 34 35 35 const getRoutePanel = (scroller, key) => { 36 36 let panel = routePanels.get(key) 37 - if (panel && !document.body.contains(panel)) { 38 - routePanels.delete(key) 39 - panel = null 40 - } 41 37 if (!panel) { 42 38 panel = h('div', { classList: 'route-panel' }) 43 39 panel.dataset.routeKey = key 44 - panel.style.display = 'none' 45 40 routePanels.set(key, panel) 46 41 scroller.appendChild(panel) 47 42 } else if (panel.parentNode !== scroller) { ··· 55 50 const previous = routePanels.get(activePanelKey) 56 51 if (previous) { 57 52 routeScrollTop.set(activePanelKey, window.scrollY || 0) 58 - previous.style.display = 'none' 53 + previous.remove() 59 54 } 60 55 window.__feedController?.deactivateFeed?.(panelKeyToSrc(activePanelKey)) 61 56 } 62 - panel.style.display = '' 63 57 activePanelKey = key 64 58 window.__feedController?.activateFeed?.(panelKeyToSrc(key)) 65 59 const savedTop = routeScrollTop.get(key) ··· 172 166 } 173 167 174 168 if (src === 'settings') { 169 + if (panel.dataset.ready === 'true') { return } 175 170 panel.replaceChildren() 176 171 if (await apds.pubkey()) { 177 172 panel.appendChild(await settings()) 178 173 } else { 179 174 panel.appendChild(await importKey()) 180 175 } 176 + panel.dataset.ready = 'true' 181 177 return 182 178 } 183 179 184 180 if (src === 'import') { 181 + if (panel.dataset.ready === 'true') { return } 185 182 panel.replaceChildren() 186 183 panel.appendChild(await importBlob()) 184 + panel.dataset.ready = 'true' 187 185 return 188 186 } 189 187 190 188 if (src.length < 44 && !src.startsWith('?')) { 189 + if (panel.dataset.ready === 'true') { return } 191 190 panel.replaceChildren() 192 191 panel.dataset.paginated = 'true' 193 192 scheduleReplyIndexBuild() ··· 202 201 const header = await buildProfileHeader({ label: src, messages: [], canEdit: false, pubkey: primaryKey }) 203 202 if (header) { panel.appendChild(header) } 204 203 } 204 + panel.dataset.ready = 'true' 205 205 return 206 206 } 207 207 208 208 if (src.length === 44) { 209 + if (panel.dataset.ready === 'true') { return } 209 210 panel.replaceChildren() 210 211 if (await isBlockedAuthor(src)) { return } 211 212 const selfKey = await apds.pubkey() ··· 226 227 if (!log || !log[0]) { 227 228 await send(src) 228 229 } 230 + panel.dataset.ready = 'true' 229 231 return 230 232 } 231 233 232 234 if (src.startsWith('?')) { 235 + if (panel.dataset.ready === 'true') { return } 233 236 panel.replaceChildren() 234 237 panel.dataset.paginated = 'true' 235 238 scheduleReplyIndexBuild() ··· 237 240 const { log } = await ctx.orchestrator.startSearch(src) 238 241 if (!ctx.isActive()) { return } 239 242 adder(log || [], src, panel) 243 + panel.dataset.ready = 'true' 240 244 return 241 245 } 242 246 243 247 if (src.length > 44) { 248 + if (panel.dataset.ready === 'true') { return } 244 249 panel.replaceChildren() 245 250 const hash = await apds.hash(src) 246 251 const opened = await apds.open(src) ··· 263 268 if (opened) { div.dataset.opened = opened } 264 269 await render.blob(src, { hash, opened }) 265 270 } 271 + panel.dataset.ready = 'true' 266 272 } 267 273 } finally { 268 274 perfEnd(token)