fedipod 1.50.0 → 1.50.2
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/lib/client/masto/render.mjs +11 -4
- package/lib/core/graphview.mjs +9 -1
- package/lib/core/publisher/restore.mjs +1 -0
- package/lib/core/store.mjs +27 -6
- package/package.json +2 -2
- package/web/app/agent.mjs +3 -1
- package/web/app/dist/sw.js +34 -8
- package/web/app/dist/sw.js.map +2 -2
|
@@ -10,6 +10,13 @@ import { sanitizeHtml, followsNeedApproval, publicHandle } from '../../core/wire
|
|
|
10
10
|
import { profileUrl, postUrl } from '../../connections/bskyfeed.mjs';
|
|
11
11
|
import { htmlToText } from './body.mjs';
|
|
12
12
|
|
|
13
|
+
// Posts stored from 2026-09-13 to 2026-10-10 hold a Mastodon post's text twice,
|
|
14
|
+
// joined by a comma; shown once.
|
|
15
|
+
export const once = (html) => {
|
|
16
|
+
const m = (html.length - 1) / 2;
|
|
17
|
+
return Number.isInteger(m) && html[m] === ',' && html.slice(0, m) === html.slice(m + 1) ? html.slice(0, m) : html;
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
// 1x1 transparent PNG — placeholder header, and the instance thumbnail. A
|
|
14
21
|
// banner nobody set should take up no room and show nothing; an avatar is a
|
|
15
22
|
// different matter (below).
|
|
@@ -273,7 +280,7 @@ export function status(api, s, { all, depth = 0 } = {}) {
|
|
|
273
280
|
created_at: s.published || new Date().toISOString(),
|
|
274
281
|
in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
|
|
275
282
|
in_reply_to_account_id: null,
|
|
276
|
-
sensitive: !!(s.spoiler || s.sensitive), spoiler_text: s.spoiler || '',
|
|
283
|
+
sensitive: !!(s.spoiler || s.sensitive), spoiler_text: once(s.spoiler || ''),
|
|
277
284
|
visibility: s.visibility || 'public', language: null,
|
|
278
285
|
edited_at: s.editedAt || null,
|
|
279
286
|
uri: s.noteId, url: s.link || s.noteId,
|
|
@@ -284,7 +291,7 @@ export function status(api, s, { all, depth = 0 } = {}) {
|
|
|
284
291
|
favourited: !!s.favourited || (s.sourceAccts || []).some(v => v.favourited),
|
|
285
292
|
reblogged: !!s.reblogged || (s.sourceAccts || []).some(v => v.reblogged),
|
|
286
293
|
muted: false, bookmarked: !!s.bookmarked, pinned: !!s.pinned,
|
|
287
|
-
content: s.content || '',
|
|
294
|
+
content: once(s.content || ''),
|
|
288
295
|
reblog: null, application: null,
|
|
289
296
|
account: api.account(s.actor),
|
|
290
297
|
media_attachments: (s.attachments || []).map(a => api.mediaJson(a)),
|
|
@@ -323,7 +330,7 @@ export function status(api, s, { all, depth = 0 } = {}) {
|
|
|
323
330
|
// information it uses to decide anyway.
|
|
324
331
|
export function filtersFor(api, s) {
|
|
325
332
|
const now = Date.now();
|
|
326
|
-
const hay = `${s.content || ''} ${s.spoiler || ''}`
|
|
333
|
+
const hay = `${once(s.content || '')} ${once(s.spoiler || '')}`
|
|
327
334
|
.replace(/<[^>]*>/gu, ' ') // the text, not the markup around it
|
|
328
335
|
.toLowerCase();
|
|
329
336
|
if (!hay.trim()) return [];
|
|
@@ -403,7 +410,7 @@ export function pushNotify(api, n) {
|
|
|
403
410
|
return api.push.notify(n, {
|
|
404
411
|
notification_id: n.id, notification_type: n.type, preferred_locale: 'en',
|
|
405
412
|
title: `${acct.display_name || acct.acct} ${verbs[n.type] || n.type}`,
|
|
406
|
-
body: s ? htmlToText(s.content || '').slice(0, 140) : '',
|
|
413
|
+
body: s ? htmlToText(once(s.content || '')).slice(0, 140) : '',
|
|
407
414
|
icon: acct.avatar || '',
|
|
408
415
|
});
|
|
409
416
|
}
|
package/lib/core/graphview.mjs
CHANGED
|
@@ -227,6 +227,8 @@ function fromLiteral(term) {
|
|
|
227
227
|
return term.value;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
const isTagged = (term) => term.termType === 'Literal' && !!term.language;
|
|
231
|
+
|
|
230
232
|
function toValue(term, ctx, depth, path) {
|
|
231
233
|
if (term.termType === 'Literal') return fromLiteral(term);
|
|
232
234
|
if (isListHead(term, ctx)) return readList(term.value, ctx, depth, path);
|
|
@@ -248,8 +250,14 @@ function readProperty(subject, name, ctx, depth = 0, path = new Set()) {
|
|
|
248
250
|
|
|
249
251
|
const term = TERMS.get(name);
|
|
250
252
|
if (!term) return undefined;
|
|
251
|
-
|
|
253
|
+
let values = preds.get(term.iri);
|
|
252
254
|
if (!values || !values.length) return undefined;
|
|
255
|
+
// `content` and `contentMap` are one predicate, and Mastodon sends a post's
|
|
256
|
+
// text both ways. The plain term is the untagged copy, else the first tagged.
|
|
257
|
+
if (values.some(isTagged)) {
|
|
258
|
+
const plain = values.filter(v => !isTagged(v));
|
|
259
|
+
values = plain.length ? plain : [values.find(isTagged)];
|
|
260
|
+
}
|
|
253
261
|
|
|
254
262
|
// `items` and `orderedItems` are the same predicate; the list is what tells
|
|
255
263
|
// them apart, so asking for the ordered one only answers when it is ordered.
|
|
@@ -210,6 +210,7 @@ export async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
|
|
|
210
210
|
// merge-only rebuild when something is.
|
|
211
211
|
export async function healStatuses(publisher) {
|
|
212
212
|
const { urls, store } = publisher;
|
|
213
|
+
if (store.unread?.has('statuses.json')) return { missing: 0, recovered: 0 }; // nothing to compare with
|
|
213
214
|
const own = store.read('outbox.json', [])
|
|
214
215
|
.map(i => (typeof i === 'string' ? i : null))
|
|
215
216
|
.filter(id => id && id.startsWith(urls.notes));
|
package/lib/core/store.mjs
CHANGED
|
@@ -90,6 +90,10 @@ export class PodStore {
|
|
|
90
90
|
this._held = 0;
|
|
91
91
|
this.chain = Promise.resolve(); // serialized writes
|
|
92
92
|
this.verdicts = new Map(); // name → did its last write land? (read by commit)
|
|
93
|
+
// Documents the pod holds that could not be read, and that nothing here
|
|
94
|
+
// has a copy of. Never written to until they are read: what this store
|
|
95
|
+
// would send is not the document, and would replace it on the pod.
|
|
96
|
+
this.unread = new Set();
|
|
93
97
|
// Moves on each time queued writes are dropped (discardPending): work that
|
|
94
98
|
// began in an earlier generation has lost results, whatever a commit says.
|
|
95
99
|
this.generation = 0;
|
|
@@ -106,7 +110,7 @@ export class PodStore {
|
|
|
106
110
|
attach(storage) {
|
|
107
111
|
// A different tree is different state: carrying the old cache and its
|
|
108
112
|
// ETags across would serve one container's documents as another's.
|
|
109
|
-
if (this.storage && this.storage.base !== storage.base) { this.cache.clear(); this.etags.clear(); this.lastText.clear(); }
|
|
113
|
+
if (this.storage && this.storage.base !== storage.base) { this.cache.clear(); this.etags.clear(); this.lastText.clear(); this.unread.clear(); }
|
|
110
114
|
this.storage = storage;
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -137,8 +141,8 @@ export class PodStore {
|
|
|
137
141
|
if (listing.missing && this.storage.kind === 'pod') {
|
|
138
142
|
throw new Error(`state container missing (HTTP 404) at ${this.base} — pod unreachable or state gone`);
|
|
139
143
|
}
|
|
140
|
-
this.etags.set('', listing.etag);
|
|
141
144
|
const names = listing.names.filter(n => n.endsWith('.json'));
|
|
145
|
+
for (const name of this.unread) if (!names.includes(name)) this.unread.delete(name); // gone: nothing to write over
|
|
142
146
|
let fetched = 0;
|
|
143
147
|
const skipped = [];
|
|
144
148
|
for (const name of names) {
|
|
@@ -147,17 +151,20 @@ export class PodStore {
|
|
|
147
151
|
if (r.notModified) continue; // ours is current
|
|
148
152
|
if (!r.ok) {
|
|
149
153
|
// One unreadable document must not restart the whole sweep — skip it,
|
|
150
|
-
// keep whatever we already hold, and try again next load (
|
|
151
|
-
// recorded, so the
|
|
154
|
+
// keep whatever we already hold, and try again next load (neither its
|
|
155
|
+
// etag nor the container's is recorded, so the next load lists and
|
|
156
|
+
// reads it again). config.json is the
|
|
152
157
|
// exception: without it the caller cannot tell "never set up" from
|
|
153
158
|
// "could not read", and would tell the user to run setup.
|
|
154
159
|
if (name === 'config.json' && !this.cache.has(name)) {
|
|
155
160
|
throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
|
|
156
161
|
}
|
|
162
|
+
if (!this.lastText.has(name)) this.unread.add(name);
|
|
157
163
|
skipped.push(`${name} (HTTP ${r.status})`);
|
|
158
164
|
continue;
|
|
159
165
|
}
|
|
160
166
|
fetched++;
|
|
167
|
+
this.unread.delete(name);
|
|
161
168
|
this.etags.set(name, r.etag);
|
|
162
169
|
try { this.cache.set(name, JSON.parse(r.body)); this.lastText.set(name, r.body); }
|
|
163
170
|
catch (e) { this.log(`state load ${name}: unparsable (${e.message})`); }
|
|
@@ -165,7 +172,12 @@ export class PodStore {
|
|
|
165
172
|
// Kept for /status: a document skipped here is a timeline or a contact
|
|
166
173
|
// list quietly missing, and the owner should be able to see that.
|
|
167
174
|
this.lastSkipped = skipped;
|
|
168
|
-
|
|
175
|
+
// The container's ETag vouches for every document in it, so it is kept
|
|
176
|
+
// only when every one was read; otherwise the next load would stop at
|
|
177
|
+
// "not modified" and never retry what was skipped.
|
|
178
|
+
if (skipped.length) this.etags.delete(''); else this.etags.set('', listing.etag);
|
|
179
|
+
if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(', ')}`
|
|
180
|
+
+ (this.unread.size ? ` — not written until read: ${[...this.unread].join(', ')}` : ''));
|
|
169
181
|
this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
|
|
170
182
|
this.onLoaded?.();
|
|
171
183
|
}
|
|
@@ -177,6 +189,7 @@ export class PodStore {
|
|
|
177
189
|
for (const { name, text, etag } of docs) {
|
|
178
190
|
try { this.cache.set(name, JSON.parse(text)); } catch { continue; }
|
|
179
191
|
this.lastText.set(name, text);
|
|
192
|
+
this.unread.delete(name);
|
|
180
193
|
if (etag) this.etags.set(name, etag);
|
|
181
194
|
}
|
|
182
195
|
if (listingEtag) this.etags.set('', listingEtag);
|
|
@@ -193,11 +206,12 @@ export class PodStore {
|
|
|
193
206
|
const r = await this.storage.read(name, etag ? { etag } : undefined);
|
|
194
207
|
if (r.notModified) continue;
|
|
195
208
|
if (r.status === 404) {
|
|
196
|
-
this.cache.delete(name); this.lastText.delete(name); this.etags.delete(name);
|
|
209
|
+
this.cache.delete(name); this.lastText.delete(name); this.etags.delete(name); this.unread.delete(name);
|
|
197
210
|
} else if (!r.ok) {
|
|
198
211
|
throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
|
|
199
212
|
} else {
|
|
200
213
|
this.cache.set(name, JSON.parse(r.body)); this.lastText.set(name, r.body); this.etags.set(name, r.etag);
|
|
214
|
+
this.unread.delete(name);
|
|
201
215
|
}
|
|
202
216
|
this.onSettled?.(name);
|
|
203
217
|
}
|
|
@@ -216,6 +230,8 @@ export class PodStore {
|
|
|
216
230
|
write(name, obj) {
|
|
217
231
|
this.cache.set(name, structuredClone(obj));
|
|
218
232
|
if (!this.storage) return;
|
|
233
|
+
// Not sent: the next commit says it did not land, and reads it first.
|
|
234
|
+
if (this.unread.has(name)) { this.verdicts.set(name, false); return; }
|
|
219
235
|
// The same bytes the pod already holds are not sent again. A like, a
|
|
220
236
|
// bookmark, a sweep that found nothing new each used to upload the whole
|
|
221
237
|
// timeline index and the whole people cache, unchanged — the largest
|
|
@@ -263,6 +279,7 @@ export class PodStore {
|
|
|
263
279
|
// the returned promise so one failure cannot poison the queue.
|
|
264
280
|
_put(name) {
|
|
265
281
|
const done = this.chain.then(async () => {
|
|
282
|
+
if (this.unread.has(name)) return false;
|
|
266
283
|
const body = serialise(this.cache.get(name));
|
|
267
284
|
await Promise.resolve(this.onWriting?.(name, body)).catch(() => {});
|
|
268
285
|
for (let attempt = 1; attempt <= PUT_RETRIES; attempt++) {
|
|
@@ -313,6 +330,10 @@ export class PodStore {
|
|
|
313
330
|
// `since`: the generation the caller's work began in. Anything dropped since
|
|
314
331
|
// makes the answer false.
|
|
315
332
|
async commit({ since } = {}) {
|
|
333
|
+
// A document that could not be read is read now, replacing whatever was
|
|
334
|
+
// written to it here; those writes already count as not landed, so the
|
|
335
|
+
// drain keeps its items and takes them again against the real document.
|
|
336
|
+
if (this.unread.size && this.storage) await this.refresh([...this.unread]).catch(() => {});
|
|
316
337
|
const pending = [];
|
|
317
338
|
// Held names too, or a sweep that suspended the debounce would delete the
|
|
318
339
|
// pod's copy of an item whose result had not been written down — the one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.2",
|
|
4
4
|
"description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"install-service": "node bin/fedipod.mjs install-service",
|
|
56
56
|
"uninstall-service": "node bin/fedipod.mjs uninstall-service",
|
|
57
57
|
"prepublishOnly": "npm test",
|
|
58
|
-
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/hold-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node --test claude/smoke-tests/forum-signup-form.test.mjs claude/smoke-tests/door-verify.test.mjs claude/smoke-tests/oauth-signin.test.mjs claude/smoke-tests/app-registration.test.mjs claude/smoke-tests/push-signout.test.mjs claude/smoke-tests/token-fetches.test.mjs claude/smoke-tests/signin-hardening.test.mjs claude/smoke-tests/load-fixes.test.mjs && node scripts/check-sizes.mjs && node scripts/test-dependents.mjs && node claude/validation/browser-agent/all.mjs",
|
|
58
|
+
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/hold-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node --test claude/smoke-tests/forum-signup-form.test.mjs claude/smoke-tests/door-verify.test.mjs claude/smoke-tests/oauth-signin.test.mjs claude/smoke-tests/app-registration.test.mjs claude/smoke-tests/push-signout.test.mjs claude/smoke-tests/token-fetches.test.mjs claude/smoke-tests/signin-hardening.test.mjs claude/smoke-tests/load-fixes.test.mjs claude/smoke-tests/unread-docs.test.mjs claude/smoke-tests/post-text-once.test.mjs && node scripts/check-sizes.mjs && node scripts/test-dependents.mjs && node claude/validation/browser-agent/all.mjs",
|
|
59
59
|
"verify-ui": "node scripts/verify-ui.mjs",
|
|
60
60
|
"build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
|
|
61
61
|
"build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
|
package/web/app/agent.mjs
CHANGED
|
@@ -516,7 +516,9 @@ export class BrowserAgent {
|
|
|
516
516
|
let unread = null;
|
|
517
517
|
const fullLoad = () => this.store.load().catch((e) => { unread = e; });
|
|
518
518
|
// Warm: only documents caught mid-write are read again, else a full load.
|
|
519
|
-
|
|
519
|
+
// A kept copy without the container's ETag is one whose last load could
|
|
520
|
+
// not read everything: what it lacks is still on the pod, unread.
|
|
521
|
+
if (this._warm && warmFrom.listingEtag) await this.store.refresh(this._warm.pending).catch(fullLoad);
|
|
520
522
|
else await fullLoad();
|
|
521
523
|
// Config: handed in on sign-up, or read from the pod on a returning sign-in.
|
|
522
524
|
const cfg = config || this.store.getConfig();
|
package/web/app/dist/sw.js
CHANGED
|
@@ -45791,6 +45791,7 @@ var PodStore = class {
|
|
|
45791
45791
|
this._held = 0;
|
|
45792
45792
|
this.chain = Promise.resolve();
|
|
45793
45793
|
this.verdicts = /* @__PURE__ */ new Map();
|
|
45794
|
+
this.unread = /* @__PURE__ */ new Set();
|
|
45794
45795
|
this.generation = 0;
|
|
45795
45796
|
this.onLoaded = null;
|
|
45796
45797
|
this.onWriting = null;
|
|
@@ -45804,6 +45805,7 @@ var PodStore = class {
|
|
|
45804
45805
|
this.cache.clear();
|
|
45805
45806
|
this.etags.clear();
|
|
45806
45807
|
this.lastText.clear();
|
|
45808
|
+
this.unread.clear();
|
|
45807
45809
|
}
|
|
45808
45810
|
this.storage = storage;
|
|
45809
45811
|
}
|
|
@@ -45826,8 +45828,8 @@ var PodStore = class {
|
|
|
45826
45828
|
if (listing.missing && this.storage.kind === "pod") {
|
|
45827
45829
|
throw new Error(`state container missing (HTTP 404) at ${this.base} \u2014 pod unreachable or state gone`);
|
|
45828
45830
|
}
|
|
45829
|
-
this.etags.set("", listing.etag);
|
|
45830
45831
|
const names = listing.names.filter((n) => n.endsWith(".json"));
|
|
45832
|
+
for (const name of this.unread) if (!names.includes(name)) this.unread.delete(name);
|
|
45831
45833
|
let fetched = 0;
|
|
45832
45834
|
const skipped = [];
|
|
45833
45835
|
for (const name of names) {
|
|
@@ -45838,10 +45840,12 @@ var PodStore = class {
|
|
|
45838
45840
|
if (name === "config.json" && !this.cache.has(name)) {
|
|
45839
45841
|
throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
|
|
45840
45842
|
}
|
|
45843
|
+
if (!this.lastText.has(name)) this.unread.add(name);
|
|
45841
45844
|
skipped.push(`${name} (HTTP ${r.status})`);
|
|
45842
45845
|
continue;
|
|
45843
45846
|
}
|
|
45844
45847
|
fetched++;
|
|
45848
|
+
this.unread.delete(name);
|
|
45845
45849
|
this.etags.set(name, r.etag);
|
|
45846
45850
|
try {
|
|
45847
45851
|
this.cache.set(name, JSON.parse(r.body));
|
|
@@ -45851,7 +45855,9 @@ var PodStore = class {
|
|
|
45851
45855
|
}
|
|
45852
45856
|
}
|
|
45853
45857
|
this.lastSkipped = skipped;
|
|
45854
|
-
if (skipped.length) this.
|
|
45858
|
+
if (skipped.length) this.etags.delete("");
|
|
45859
|
+
else this.etags.set("", listing.etag);
|
|
45860
|
+
if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}` + (this.unread.size ? ` \u2014 not written until read: ${[...this.unread].join(", ")}` : ""));
|
|
45855
45861
|
this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
|
|
45856
45862
|
this.onLoaded?.();
|
|
45857
45863
|
}
|
|
@@ -45866,6 +45872,7 @@ var PodStore = class {
|
|
|
45866
45872
|
continue;
|
|
45867
45873
|
}
|
|
45868
45874
|
this.lastText.set(name, text);
|
|
45875
|
+
this.unread.delete(name);
|
|
45869
45876
|
if (etag) this.etags.set(name, etag);
|
|
45870
45877
|
}
|
|
45871
45878
|
if (listingEtag) this.etags.set("", listingEtag);
|
|
@@ -45884,12 +45891,14 @@ var PodStore = class {
|
|
|
45884
45891
|
this.cache.delete(name);
|
|
45885
45892
|
this.lastText.delete(name);
|
|
45886
45893
|
this.etags.delete(name);
|
|
45894
|
+
this.unread.delete(name);
|
|
45887
45895
|
} else if (!r.ok) {
|
|
45888
45896
|
throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
|
|
45889
45897
|
} else {
|
|
45890
45898
|
this.cache.set(name, JSON.parse(r.body));
|
|
45891
45899
|
this.lastText.set(name, r.body);
|
|
45892
45900
|
this.etags.set(name, r.etag);
|
|
45901
|
+
this.unread.delete(name);
|
|
45893
45902
|
}
|
|
45894
45903
|
this.onSettled?.(name);
|
|
45895
45904
|
}
|
|
@@ -45908,6 +45917,10 @@ var PodStore = class {
|
|
|
45908
45917
|
write(name, obj) {
|
|
45909
45918
|
this.cache.set(name, structuredClone(obj));
|
|
45910
45919
|
if (!this.storage) return;
|
|
45920
|
+
if (this.unread.has(name)) {
|
|
45921
|
+
this.verdicts.set(name, false);
|
|
45922
|
+
return;
|
|
45923
|
+
}
|
|
45911
45924
|
if (this.lastText.get(name) === serialise(obj)) {
|
|
45912
45925
|
clearTimeout(this.timers.get(name));
|
|
45913
45926
|
this.timers.delete(name);
|
|
@@ -45957,6 +45970,7 @@ var PodStore = class {
|
|
|
45957
45970
|
// the returned promise so one failure cannot poison the queue.
|
|
45958
45971
|
_put(name) {
|
|
45959
45972
|
const done = this.chain.then(async () => {
|
|
45973
|
+
if (this.unread.has(name)) return false;
|
|
45960
45974
|
const body = serialise(this.cache.get(name));
|
|
45961
45975
|
await Promise.resolve(this.onWriting?.(name, body)).catch(() => {
|
|
45962
45976
|
});
|
|
@@ -46011,6 +46025,8 @@ var PodStore = class {
|
|
|
46011
46025
|
// `since`: the generation the caller's work began in. Anything dropped since
|
|
46012
46026
|
// makes the answer false.
|
|
46013
46027
|
async commit({ since } = {}) {
|
|
46028
|
+
if (this.unread.size && this.storage) await this.refresh([...this.unread]).catch(() => {
|
|
46029
|
+
});
|
|
46014
46030
|
const pending = [];
|
|
46015
46031
|
for (const name of /* @__PURE__ */ new Set([...this.timers.keys(), ...this.dirty])) {
|
|
46016
46032
|
const t = this.timers.get(name);
|
|
@@ -57169,6 +57185,7 @@ function fromLiteral(term3) {
|
|
|
57169
57185
|
}
|
|
57170
57186
|
return term3.value;
|
|
57171
57187
|
}
|
|
57188
|
+
var isTagged = (term3) => term3.termType === "Literal" && !!term3.language;
|
|
57172
57189
|
function toValue(term3, ctx, depth, path) {
|
|
57173
57190
|
if (term3.termType === "Literal") return fromLiteral(term3);
|
|
57174
57191
|
if (isListHead(term3, ctx)) return readList(term3.value, ctx, depth, path);
|
|
@@ -57187,8 +57204,12 @@ function readProperty(subject, name, ctx, depth = 0, path = /* @__PURE__ */ new
|
|
|
57187
57204
|
}
|
|
57188
57205
|
const term3 = TERMS.get(name);
|
|
57189
57206
|
if (!term3) return void 0;
|
|
57190
|
-
|
|
57207
|
+
let values = preds.get(term3.iri);
|
|
57191
57208
|
if (!values || !values.length) return void 0;
|
|
57209
|
+
if (values.some(isTagged)) {
|
|
57210
|
+
const plain = values.filter((v) => !isTagged(v));
|
|
57211
|
+
values = plain.length ? plain : [values.find(isTagged)];
|
|
57212
|
+
}
|
|
57192
57213
|
const listed = values.filter((v) => isListHead(v, ctx));
|
|
57193
57214
|
if (term3.list) {
|
|
57194
57215
|
if (!listed.length) return void 0;
|
|
@@ -58104,6 +58125,7 @@ async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
|
|
|
58104
58125
|
}
|
|
58105
58126
|
async function healStatuses(publisher) {
|
|
58106
58127
|
const { urls, store } = publisher;
|
|
58128
|
+
if (store.unread?.has("statuses.json")) return { missing: 0, recovered: 0 };
|
|
58107
58129
|
const own = store.read("outbox.json", []).map((i) => typeof i === "string" ? i : null).filter((id) => id && id.startsWith(urls.notes));
|
|
58108
58130
|
if (!own.length) return { missing: 0, recovered: 0 };
|
|
58109
58131
|
const have = new Set(store.getStatuses().map((s) => s.noteId));
|
|
@@ -68288,6 +68310,10 @@ var BskyFeed = class {
|
|
|
68288
68310
|
};
|
|
68289
68311
|
|
|
68290
68312
|
// lib/client/masto/render.mjs
|
|
68313
|
+
var once = (html) => {
|
|
68314
|
+
const m = (html.length - 1) / 2;
|
|
68315
|
+
return Number.isInteger(m) && html[m] === "," && html.slice(0, m) === html.slice(m + 1) ? html.slice(0, m) : html;
|
|
68316
|
+
};
|
|
68291
68317
|
var TRANSPARENT_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
|
68292
68318
|
var DEFAULT_AVATAR = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABd0lEQVR42u3ZoW4CQRDG8Xk6NBLbVCJ5gBpMTQ2iBlFR29RXNsFhz1JHXV9hydfkEkoobW5n2F34i0kIHMfN73ZvdwYbTabpmsMAAAAAAAAAAAAAAAAAAACiY3wzS/ePz+n1bZW6zTZ9fH59h17rPX2mYy4OQEktnl5+JP1b6Bgdey6IcIDb2Ty9r7s/Ez8MfUffbRpACfznrp8aDdEIFjnsh9z5YyMhcjqEAWge5ybfh87VFIDuWM7QPzYVokZBCICWM6/k+9A5mwHQmu4NoHM2A+A5/PenQTMA3sn3AQBTgIcgyyAbIbbCFEOUwzREaInRFKUtDgAAFQLcPSxDCqH9Z4V+o0oAXVhU4ofhiWCtJe+NYKV3e6V3idkAHvv9nDqhKIBnxVeqUrRaav5SvQKrqetTols0GGDdbaoB0LWcFUBP31qS72PoimCtD//caWC1tL1Ltc2ttbXfe08wCKC25HP+OgMAAAAAAAAAAAAAAAAArhNgB1PE/x/JOiuNAAAAAElFTkSuQmCC";
|
|
68293
68319
|
var selfIcon = (api, self2, cached) => (self2 ? api.store.getConfig()?.icon : null) || cached.icon;
|
|
@@ -68511,7 +68537,7 @@ function status(api, s, { all, depth = 0 } = {}) {
|
|
|
68511
68537
|
in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
|
|
68512
68538
|
in_reply_to_account_id: null,
|
|
68513
68539
|
sensitive: !!(s.spoiler || s.sensitive),
|
|
68514
|
-
spoiler_text: s.spoiler || "",
|
|
68540
|
+
spoiler_text: once(s.spoiler || ""),
|
|
68515
68541
|
visibility: s.visibility || "public",
|
|
68516
68542
|
language: null,
|
|
68517
68543
|
edited_at: s.editedAt || null,
|
|
@@ -68528,7 +68554,7 @@ function status(api, s, { all, depth = 0 } = {}) {
|
|
|
68528
68554
|
muted: false,
|
|
68529
68555
|
bookmarked: !!s.bookmarked,
|
|
68530
68556
|
pinned: !!s.pinned,
|
|
68531
|
-
content: s.content || "",
|
|
68557
|
+
content: once(s.content || ""),
|
|
68532
68558
|
reblog: null,
|
|
68533
68559
|
application: null,
|
|
68534
68560
|
account: api.account(s.actor),
|
|
@@ -68570,7 +68596,7 @@ function status(api, s, { all, depth = 0 } = {}) {
|
|
|
68570
68596
|
}
|
|
68571
68597
|
function filtersFor(api, s) {
|
|
68572
68598
|
const now = Date.now();
|
|
68573
|
-
const hay = `${s.content || ""} ${s.spoiler || ""}`.replace(/<[^>]*>/gu, " ").toLowerCase();
|
|
68599
|
+
const hay = `${once(s.content || "")} ${once(s.spoiler || "")}`.replace(/<[^>]*>/gu, " ").toLowerCase();
|
|
68574
68600
|
if (!hay.trim()) return [];
|
|
68575
68601
|
const out = [];
|
|
68576
68602
|
for (const f of api.store.getFilters?.() || []) {
|
|
@@ -68657,7 +68683,7 @@ function pushNotify(api, n) {
|
|
|
68657
68683
|
notification_type: n.type,
|
|
68658
68684
|
preferred_locale: "en",
|
|
68659
68685
|
title: `${acct.display_name || acct.acct} ${verbs[n.type] || n.type}`,
|
|
68660
|
-
body: s ? htmlToText(s.content || "").slice(0, 140) : "",
|
|
68686
|
+
body: s ? htmlToText(once(s.content || "")).slice(0, 140) : "",
|
|
68661
68687
|
icon: acct.avatar || ""
|
|
68662
68688
|
});
|
|
68663
68689
|
}
|
|
@@ -74022,7 +74048,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
74022
74048
|
const fullLoad = () => this.store.load().catch((e) => {
|
|
74023
74049
|
unread = e;
|
|
74024
74050
|
});
|
|
74025
|
-
if (this._warm) await this.store.refresh(this._warm.pending).catch(fullLoad);
|
|
74051
|
+
if (this._warm && warmFrom.listingEtag) await this.store.refresh(this._warm.pending).catch(fullLoad);
|
|
74026
74052
|
else await fullLoad();
|
|
74027
74053
|
const cfg = config || this.store.getConfig();
|
|
74028
74054
|
if (!cfg) throw await accountNotRead({ unread, podBase: remotePod, state: this.urls.state, webId, actorUrl: this.urls.actor });
|