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.
@@ -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
  }
@@ -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
- const values = preds.get(term.iri);
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));
@@ -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 (no etag is
151
- // recorded, so the retry is unconditional). config.json is 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
- if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(', ')}`);
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.0",
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
- if (this._warm) await this.store.refresh(this._warm.pending).catch(fullLoad);
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();
@@ -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.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}`);
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
- const values = preds.get(term3.iri);
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 });