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

Preserve route panels and localize infinite-scroll sentinels

+91 -28
+5 -4
adder.js
··· 460 460 const entries = buildEntries(log || []) 461 461 let loading = false 462 462 let armed = false 463 - const sentinelId = 'scroll-sentinel' 464 463 465 464 let posts = [] 466 465 const state = { ··· 478 477 bannerButton: null, 479 478 statusMessage: '', 480 479 statusMode: false, 481 - statusTimer: null 480 + statusTimer: null, 481 + sentinel: null 482 482 } 483 483 getController().feeds.set(src, state) 484 484 ensureBanner(state) ··· 499 499 } 500 500 501 501 const ensureSentinel = () => { 502 - let sentinel = document.getElementById(sentinelId) 502 + let sentinel = state.sentinel 503 503 if (!sentinel) { 504 504 sentinel = document.createElement('div') 505 - sentinel.id = sentinelId 505 + sentinel.className = 'scroll-sentinel' 506 506 sentinel.style.height = '1px' 507 + state.sentinel = sentinel 507 508 } 508 509 if (sentinel.parentNode && sentinel.parentNode !== div) { 509 510 sentinel.parentNode.removeChild(sentinel)
+86 -24
route.js
··· 17 17 let activeRouteRun = 0 18 18 let activeRouteController = null 19 19 let activeOrchestrator = null 20 + let activePanelKey = null 21 + const routePanels = new Map() 22 + const routeScrollTop = new Map() 23 + 24 + const getScroller = () => { 25 + let scroller = document.getElementById('scroller') 26 + if (scroller) { return scroller } 27 + scroller = h('div', { id: 'scroller' }) 28 + document.body.appendChild(scroller) 29 + return scroller 30 + } 31 + 32 + const getPanelKey = (src) => (src === '' ? '__home__' : src) 33 + 34 + const getRoutePanel = (scroller, key) => { 35 + let panel = routePanels.get(key) 36 + if (panel && !document.body.contains(panel)) { 37 + routePanels.delete(key) 38 + panel = null 39 + } 40 + if (!panel) { 41 + panel = h('div', { classList: 'route-panel' }) 42 + panel.dataset.routeKey = key 43 + panel.style.display = 'none' 44 + routePanels.set(key, panel) 45 + scroller.appendChild(panel) 46 + } else if (panel.parentNode !== scroller) { 47 + scroller.appendChild(panel) 48 + } 49 + return panel 50 + } 51 + 52 + const activatePanel = (key, panel) => { 53 + if (activePanelKey && activePanelKey !== key) { 54 + const previous = routePanels.get(activePanelKey) 55 + if (previous) { 56 + routeScrollTop.set(activePanelKey, window.scrollY || 0) 57 + previous.style.display = 'none' 58 + } 59 + } 60 + panel.style.display = '' 61 + activePanelKey = key 62 + const savedTop = routeScrollTop.get(key) 63 + if (typeof savedTop === 'number') { 64 + setTimeout(() => { 65 + window.scrollTo(0, savedTop) 66 + }, 0) 67 + } 68 + } 20 69 21 70 const waitForFirstRenderedAuthor = (container, timeoutMs = 6000) => new Promise((resolve) => { 22 71 if (!container) { resolve(null); return } ··· 80 129 export const route = async () => { 81 130 const token = perfStart('route', window.location.hash.substring(1) || 'home') 82 131 const src = window.location.hash.substring(1) 83 - const scroller = h('div', {id: 'scroller'}) 84 - 85 - document.body.appendChild(scroller) 86 - scheduleReplyIndexBuild() 87 - const ctx = makeRouteContext(src, scroller) 132 + const scroller = getScroller() 133 + const panelKey = getPanelKey(src) 134 + const panel = getRoutePanel(scroller, panelKey) 135 + activatePanel(panelKey, panel) 88 136 89 137 try { 90 138 if (src.startsWith('share=')) { ··· 108 156 } 109 157 110 158 if (src === '' || src.startsWith('share=')) { 111 - scroller.dataset.paginated = 'true' 159 + if (panel.dataset.ready === 'true') { return } 160 + panel.replaceChildren() 161 + panel.dataset.paginated = 'true' 162 + scheduleReplyIndexBuild() 163 + const ctx = makeRouteContext(src, panel) 112 164 const { log } = await ctx.orchestrator.startHome() 113 165 if (!ctx.isActive()) { return } 114 - adder(log || [], src, scroller) 166 + adder(log || [], src, panel) 167 + panel.dataset.ready = 'true' 115 168 return 116 169 } 117 170 118 171 if (src === 'settings') { 172 + panel.replaceChildren() 119 173 if (await apds.pubkey()) { 120 - scroller.appendChild(await settings()) 174 + panel.appendChild(await settings()) 121 175 } else { 122 - scroller.appendChild(await importKey()) 176 + panel.appendChild(await importKey()) 123 177 } 124 178 return 125 179 } 126 180 127 181 if (src === 'import') { 128 - scroller.appendChild(await importBlob()) 182 + panel.replaceChildren() 183 + panel.appendChild(await importBlob()) 129 184 return 130 185 } 131 186 132 187 if (src.length < 44 && !src.startsWith('?')) { 133 - scroller.dataset.paginated = 'true' 188 + panel.replaceChildren() 189 + panel.dataset.paginated = 'true' 190 + scheduleReplyIndexBuild() 191 + const ctx = makeRouteContext(src, panel) 134 192 const { query, primaryKey } = await ctx.orchestrator.startAlias(src) 135 193 if (!ctx.isActive()) { return } 136 - adder(query || [], src, scroller) 194 + adder(query || [], src, panel) 137 195 if (query.length) { 138 196 const header = await buildProfileHeader({ label: src, messages: query, canEdit: false, pubkey: primaryKey }) 139 - if (header) { scroller.appendChild(header) } 197 + if (header) { panel.appendChild(header) } 140 198 } else { 141 199 const header = await buildProfileHeader({ label: src, messages: [], canEdit: false, pubkey: primaryKey }) 142 - if (header) { scroller.appendChild(header) } 200 + if (header) { panel.appendChild(header) } 143 201 } 144 202 return 145 203 } 146 204 147 205 if (src.length === 44) { 206 + panel.replaceChildren() 148 207 if (await isBlockedAuthor(src)) { return } 149 208 const selfKey = await apds.pubkey() 150 209 await noteInterest(src) 151 - scroller.dataset.paginated = 'true' 210 + panel.dataset.paginated = 'true' 211 + scheduleReplyIndexBuild() 212 + const ctx = makeRouteContext(src, panel) 152 213 const { log } = await ctx.orchestrator.startAuthor(src) 153 214 if (!ctx.isActive()) { return } 154 - adder(log || [], src, scroller) 215 + adder(log || [], src, panel) 155 216 const canEdit = !!(selfKey && selfKey === src) 156 - void waitForFirstRenderedAuthor(scroller).then(async (author) => { 217 + void waitForFirstRenderedAuthor(panel).then(async (author) => { 157 218 if (!ctx.isActive()) { return } 158 219 if (!author || author !== src) { return } 159 220 const header = await buildProfileHeader({ label: src.substring(0, 10), messages: log || [], canEdit, pubkey: src }) 160 - if (header) { scroller.prepend(header) } 221 + if (header) { panel.prepend(header) } 161 222 }) 162 223 if (!log || !log[0]) { 163 224 await send(src) ··· 166 227 } 167 228 168 229 if (src.startsWith('?')) { 169 - scroller.dataset.paginated = 'true' 230 + panel.replaceChildren() 231 + panel.dataset.paginated = 'true' 232 + scheduleReplyIndexBuild() 233 + const ctx = makeRouteContext(src, panel) 170 234 const { log } = await ctx.orchestrator.startSearch(src) 171 235 if (!ctx.isActive()) { return } 172 - adder(log || [], src, scroller) 236 + adder(log || [], src, panel) 173 237 return 174 238 } 175 239 176 240 if (src.length > 44) { 241 + panel.replaceChildren() 177 242 const hash = await apds.hash(src) 178 243 const opened = await apds.open(src) 179 244 const author = src.substring(0, 44) ··· 190 255 if (Number.isNaN(ts)) { ts = 0 } 191 256 } 192 257 if (!ts) { ts = Date.now() } 193 - const div = render.insertByTimestamp(scroller, hash, ts) 258 + const div = render.insertByTimestamp(panel, hash, ts) 194 259 if (!div) { return } 195 260 if (opened) { div.dataset.opened = opened } 196 261 await render.blob(src, { hash, opened }) ··· 209 274 if (activeRouteController) { 210 275 activeRouteController.abort() 211 276 activeRouteController = null 212 - } 213 - while (document.getElementById('scroller')) { 214 - document.getElementById('scroller').remove() 215 277 } 216 278 if (window.location.hash === '#?') { 217 279 const search = document.getElementById('search')