a demonstration replicated social networking web app built with anproto
wiredove.net/
social
ed25519
protocols
1const CACHE_KEY = 'wiredove.feedRowCache.v1'
2const MAX_ROWS = 3000
3
4let rows = null
5let persistTimer = null
6
7const nowMs = () => Date.now()
8
9const loadRows = () => {
10 if (rows) { return rows }
11 rows = new Map()
12 if (typeof localStorage === 'undefined') { return rows }
13 try {
14 const raw = localStorage.getItem(CACHE_KEY)
15 if (!raw) { return rows }
16 const parsed = JSON.parse(raw)
17 if (!Array.isArray(parsed)) { return rows }
18 for (const item of parsed) {
19 if (!item || typeof item.hash !== 'string' || item.hash.length !== 44) { continue }
20 rows.set(item.hash, item)
21 }
22 } catch (err) {
23 console.warn('feed row cache load failed', err)
24 }
25 return rows
26}
27
28const schedulePersist = () => {
29 if (persistTimer) { return }
30 persistTimer = setTimeout(() => {
31 persistTimer = null
32 if (typeof localStorage === 'undefined') { return }
33 try {
34 const data = Array.from(loadRows().values())
35 localStorage.setItem(CACHE_KEY, JSON.stringify(data))
36 } catch (err) {
37 console.warn('feed row cache persist failed', err)
38 }
39 }, 500)
40}
41
42const trimRows = () => {
43 const map = loadRows()
44 while (map.size > MAX_ROWS) {
45 const firstKey = map.keys().next().value
46 if (!firstKey) { break }
47 map.delete(firstKey)
48 }
49}
50
51export const parseOpenedTimestamp = (opened) => {
52 if (!opened || opened.length < 13) { return 0 }
53 const ts = Number.parseInt(opened.substring(0, 13), 10)
54 return Number.isFinite(ts) ? ts : 0
55}
56
57const summarize = (txt, maxLen = 140) => {
58 if (!txt || typeof txt !== 'string') { return '' }
59 const single = txt.replace(/\s+/g, ' ').trim()
60 if (single.length <= maxLen) { return single }
61 return single.substring(0, maxLen) + '...'
62}
63
64export const makeFeedRow = ({ hash, opened = null, author = '', contentHash = '', yaml = null, ts = 0 } = {}) => {
65 if (!hash || typeof hash !== 'string' || hash.length !== 44) { return null }
66 const openedTs = ts || parseOpenedTimestamp(opened)
67 const preview = yaml && yaml.body
68 ? summarize(yaml.body)
69 : (yaml && yaml.bio ? summarize(yaml.bio) : '')
70 const name = yaml && yaml.name ? yaml.name.trim() : ''
71 return {
72 hash,
73 ts: openedTs || 0,
74 opened: opened || null,
75 author: author || '',
76 contentHash: contentHash || '',
77 name,
78 preview,
79 replyCount: 0,
80 updatedAt: nowMs()
81 }
82}
83
84export const upsertFeedRow = (row) => {
85 if (!row || !row.hash) { return false }
86 const map = loadRows()
87 const prev = map.get(row.hash) || {}
88 const next = {
89 ...prev,
90 ...row,
91 updatedAt: nowMs()
92 }
93 map.delete(row.hash)
94 map.set(row.hash, next)
95 trimRows()
96 schedulePersist()
97 return true
98}
99
100export const getFeedRow = (hash) => {
101 if (!hash || typeof hash !== 'string') { return null }
102 const map = loadRows()
103 return map.get(hash) || null
104}
105
106export const attachCachedRows = (log) => {
107 if (!Array.isArray(log) || !log.length) { return log || [] }
108 const map = loadRows()
109 return log.map((entry) => {
110 if (!entry || !entry.hash) { return entry }
111 const row = map.get(entry.hash)
112 if (!row) { return entry }
113 return { ...entry, row }
114 })
115}