PageSourceSearch

https://shared.sx/assets/ProfileView-DgjWdFkD.js

js shared.sx collected 2026-10-03 22:06:48 UTC 137,329 bytes, 1,742 lines download raw bytes

1import{k as Ae,r as u,X as be,A as Ye,l as r,o as n,q as e,m as me,D as b,K as L,F as K,n as ge,t as i,p as te,u as E,v as ce,W as Et,d as At,f as q,a0 as Nt,a1 as Tt,a2 as Dt,a3 as Rt,L as Ee,N as R,O as j,H as st,S as Ut,V as qe,B as Mt,I as Ze,R as et,a4 as Q,Q as Ft,w as Ve,x as Pt,a5 as Lt,E as $t}from"./vendor-D0Q8KstH.js";import{d as fe,b as nt,u as rt,e as ot,g as Bt,a as re}from"./index-DQqGazYE.js";import{s as Ie}from"./subscriptions-Cu18K4xM.js";import{N as Gt}from"./NavigationBar-B81tMCza.js";import{C as qt}from"./ChatAppSetupCard-whnvbLpL.js";import{c as Oe}from"./chat-1141wjaq.js";import{w as Vt}from"./x402-DMAZw0Lu.js";import"./sharedText512-eMELDve_.js";const je={getBillingInfo(){return fe.get("/billing/info/")},createCryptoPayment(o,p){const c={amount:o};return p&&(c.user_id=p),fe.post("/billing/crypto/create-payment/",c)},checkPaymentStatus(o){return fe.get(`/billing/crypto/status/${o}/`)},getWalletAddress(){return fe.get("/billing/wallet/")},setWalletAddress(o){return fe.put("/billing/wallet/",{wallet_address:o})}},Wt=`
2  // -- tiny utils ----------------------------------------------------------
3  function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
4  function hash(str) {
5    var h = 5381;
6    for (var i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0;
7    return (h >>> 0).toString(36);
8  }
9  function log() {
10    if (!CONFIG.DEBUG) return;
11    var a = ['%c[bridge]', 'color:#4f6bed'].concat([].slice.call(arguments));
12    console.log.apply(console, a);
13  }
14  async function waitFor(fn, timeoutMs, stepMs) {
15    var end = Date.now() + (timeoutMs || 5000);
16    while (Date.now() < end) { if (fn()) return true; await sleep(stepMs || 200); }
17    return false;
18  }
19
20  // -- keep the SPA "awake" in a background tab -----------------------------
21  // Fansly/OnlyFans pause live message + conversation-list updates when the tab
22  // is hidden (Page Visibility / focus), so a message from a NEW user never
23  // appears until a manual reload. Spoof "always visible + focused" so the site
24  // keeps its websocket alive and keeps rendering new threads. No reloads.
25  function keepAwake() {
26    try {
27      var visible = function () { return false; };
28      var state = function () { return 'visible'; };
29      Object.defineProperty(document, 'hidden', { get: visible, configurable: true });
30      Object.defineProperty(document, 'visibilityState', { get: state, configurable: true });
31      try { Object.defineProperty(document, 'webkitHidden', { get: visible, configurable: true }); } catch (e) {}
32      try { Object.defineProperty(document, 'webkitVisibilityState', { get: state, configurable: true }); } catch (e) {}
33      document.hasFocus = function () { return true; };
34      // visibilitychange is neutralised by the getter above (handlers read
35      // document.hidden === false). Block the focus-loss / lifecycle signals
36      // that some SPAs also pause on, without touching visibilitychange so a
37      // synthetic "resume" can still get through.
38      ['blur', 'pagehide', 'freeze'].forEach(function (t) {
39        window.addEventListener(t, function (e) { e.stopImmediatePropagation(); }, true);
40      });
41      // Nudge an already-backgrounded SPA to re-evaluate as visible.
42      try { document.dispatchEvent(new Event('visibilitychange')); } catch (e) {}
43      try { window.dispatchEvent(new Event('focus')); } catch (e) {}
44      log('keep-awake: visibility + focus spoofed');
45    } catch (e) { log('keep-awake failed', e); }
46  }
47  keepAwake();
48
49  // Unread count from the tab title (e.g. "(3) Messages"). Updates over the
50  // websocket even when the list DOM lags — a live signal that new mail arrived.
51  var _lastUnread = -1;
52  function titleUnread() {
53    var m = (document.title || '').match(/\\((\\d+)\\)/);
54    return m ? parseInt(m[1], 10) : 0;
55  }
56  function checkUnread() {
57    var u = titleUnread();
58    if (u !== _lastUnread) {
59      if (u > _lastUnread && _lastUnread >= 0) log('tab-title unread rose to', u, '(new message)');
60      _lastUnread = u;
61    }
62    return u;
63  }
64
65  // -- persistence (survives reloads so we don't re-send on refresh) --------
66  function loadSet(name) {
67    try { return new Set(JSON.parse(GM_getValue(name, '[]'))); }
68    catch (e) { return new Set(); }
69  }
70  function saveSet(name, set, cap) {
71    var arr = Array.from(set);
72    if (cap && arr.length > cap) arr = arr.slice(arr.length - cap);
73    try { GM_setValue(name, JSON.stringify(arr)); } catch (e) {}
74  }
75
76  // Storage namespace is scoped to the TOKEN: regenerating the bridge token
77  // (e.g. after purging rooms server-side) resets the dedupe state so history
78  // is re-delivered into the fresh rooms. Same token → state survives reloads.
79  var NS = CONFIG.PLATFORM + ':' + hash(CONFIG.TOKEN || '');
80  var seen = loadSet('seen:' + NS);
80          // delivered message keys
81  var seenMedia = loadSet('seenMedia:' + NS); // delivered image keys
82  var backfilled = loadSet('backfilled:' + NS); // conv ids fully scrolled
83  var queue = [];
84
85  // -- diagnostics ---------------------------------------------------------
86  // Several selectors here are still guesses (which class marks a Fansly row
87  // unread, where the contact name lives) and the interesting moments — a row
88  // flipping to unread, a failed open, an HTTP error — happen unattended. So
89  // the runtime records them AS THEY HAPPEN: a rolling console log for the live
90  // view, plus a small set of FINDINGS persisted to GM storage, which survive
91  // a /refresh and are still there tomorrow. In the console:
92  //   __bridge.help()    what's available
93  //   __bridge.report()  everything gathered, ready to copy out
94  var Diag = {
95    ring: [],                 // recent events, this session only
96    onceKeys: {},
97    findings: {},             // kind -> [{at, data}], persisted
98    started: new Date().toISOString(),
99    load: function () {
100      try { Diag.findings = JSON.parse(GM_getValue('diag:' + NS, '{}')) || {}; }
101      catch (e) { Diag.findings = {}; }
102    },
103    persist: function () {
104      try { GM_setValue('diag:' + NS, JSON.stringify(Diag.findings)); } catch (e) {}
105    },
106    // Keep the first few examples of a KIND of observation, permanently. Capped
107    // so a recurring condition can't grow storage without bound.
108    finding: function (kind, data, cap) {
109      var list = Diag.findings[kind] || (Diag.findings[kind] = []);
110      if (list.length >= (cap || 3)) return false;
111      list.push({ at: new Date().toISOString(), url: location.pathname, data: data });
112      Diag.persist();
113      console.log('%c[bridge] FINDING ' + kind, 'color:#0a0;font-weight:bold', data);
114      return true;
115    },
116    record: function (tag, data) {
117      Diag.ring.push({ t: new Date().toISOString(), tag: tag, data: data });
118      if (Diag.ring.length > 400) Diag.ring.shift();
119      if (CONFIG.VERBOSE) log(tag, data);
120    },
121    // Log/record something only the first time it's seen (per session).
122    once: function (key, tag, data) {
123      if (Diag.onceKeys[key]) return false;
124      Diag.onceKeys[key] = 1;
125      Diag.record(tag, data);
126      return true;
127    },
128    // Repeat-suppression. A pending command that can't open retries on every
129    // 5s poll; logging that for half an hour buries everything else. Log it,
130    // hold it down, then report how many were suppressed when it recurs.
131    _lastAt: {},
132    _suppressed: {},
133    throttled: function (key, ms) {
134      var now = Date.now();
135      if (now - (Diag._lastAt[key] || 0) < (ms || CONFIG.DIAG_THROTTLE_MS)) {
136        Diag._suppressed[key] = (Diag._suppressed[key] || 0) + 1;
137        return true;
138      }
139      Diag._lastAt[key] = now;
140      return false;
141    },
142    // Loud: something went wrong and is worth keeping.
143    problem: function (kind, data, cap) {
144      if (!Diag.throttled('problem:' + kind)) {
145        var n = Diag._suppressed['problem:' + kind] || 0;
146        Diag._suppressed['problem:' + kind] = 0;
147        Diag.record(
148          'PROBLEM ' + kind + (n ? ' (' + n + ' more since the last one)' : ''), data,
149        );
150      }
151      Diag.finding('problem:' + kind, data, cap || 5);
152    },
153  };
154  Diag.load();
155
156  // -- DOM description helpers (used only for diagnostics) -----------------
157  function classesOf(el) {
158    var out = [];
159    try {
160      (el.getAttribute('class') || '').split(/\\s+/).forEach(function (c) { if (c) out.push(c); });
161    } catch (e) {}
162    return out;
163  }
164  // Every class on an element AND its descendants. The unread marker is
165  // somewhere in here — we just don't know its name yet, so capture the lot and
166  // let a before/after diff point at it.
167  function classSet(el) {
168    var out = [];
169    try {
170      classesOf(el).forEach(function (c) { if (out.indexOf(c) === -1) out.push(c); });
171      el.querySelectorAll('*').forEach(function (n) {
172        classesOf(n).forEach(function (c) { if (out.indexOf(c) === -1) out.push(c); });
173      });
174    } catch (e) {}
175    return out;
176  }
177  // Leaf text nodes with their classes — reveals where the contact name, the
178  // preview line and any unread badge/count actually live.
179  function leafTexts(el, max) {
180    var out = [];
181    try {
182      el.querySelectorAll('*').forEach(function (n) {
183        if (out.length >= (max || 14)) return;
184        if (n.children && n.children.length) return;
185        var t = (n.textContent || '').replace(/\\s+/g, ' ').trim();
186        if (!t) return;
187        out.push({ cls: (n.getAttribute('class') || '').slice(0, 60), text: t.slice(0, 48) });
188      });
189    } catch (e) {}
190    return out;
191  }
192  function rowHtml(el, max) {
193    try { return (el.outerHTML || '').slice(0, max || 900); } catch (e) { return ''; }
194  }
195  function describeRow(el) {
196    return { classes: classSet(el), leaves: leafTexts(el), html: rowHtml(el) };
197  }
198
199  // Class-set snapshot per conversation row, so the moment a row changes we can
200  // diff what was ADDED (→ the unread marker) and what was REMOVED when it is
201  // opened again (→ the same marker, confirmed from the other direction).
202  var rowClassCache = {};
203  function classDiff(id, el) {
204    if (!el) return null;
205    var now = classSet(el);
206    var prev = rowClassCache[id];
207    rowClassCache[id] = now;
208    if (!prev) return null;
209    return {
210      added: now.filter(function (x) { return prev.indexOf(x) === -1; }),
211      removed: prev.filter(function (x) { return now.indexOf(x) === -1; }),
212    };
213  }
214
215  // -- images --------------------------------------------------------------
216  // CDN URLs are signed (query string changes every page load), so dedupe and
217  // keys use the URL PATH only — that part is stable per media item.
218  function imagePath(src) {
219    try { return new URL(src, location.href).pathname; } catch (e) { return src; }
220  }
221  function guessImageType(path) {
222    var m = (path || '').toLowerCase().match(/\\.(png|gif|webp|avif|jpe?g)(?:$|[?#])/);
223    if (!m) return 'image/jpeg';
224    var ext = m[1] === 'jpg' ? 'jpeg' : m[1];
225    return 'image/' + ext;
226  }
227  // Collect real chat-media <img>s inside a message node: skip avatars, emoji
228  // and other tiny decorative images.
229  function collectMessageImages(root) {
230    // Never throws — this runs inside extractMessages, and an error on one
231    // odd <img> must not take down text extraction for the whole chat.
232    var out = [], have = {};
233    // Quote-reply blocks belong to the quoted ORIGINAL message — their images
234    // must not be re-collected under the reply (Scraper.QUOTE_WRAPPER).
235    var excl = '[class*="avatar" i], [class*="emoji" i]';
236    if (typeof Scraper !== 'undefined' && Scraper.QUOTE_WRAPPER) {
237      excl += ', ' + Scraper.QUOTE_WRAPPER;
238    }
239    try {
240      root.querySelectorAll('img').forEach(function (img) {
241        try {
242          var src = img.currentSrc || img.src || '';
243          if (!src || src.indexOf('data:') === 0) return;
244          if (img.closest(excl)) return;
245          var w = img.naturalWidth || img.width || 0;
246          var h = img.naturalHeight || img.height || 0;
247          if (w && h && (w < 64 || h < 64)) return; // emoji / icons
248          var p = imagePath(src);
249          if (have[p]) return;
250          have[p] = 1;
251          out.push({ url: src, path: p, name: (p.split('/').pop() || 'image') });
252        } catch (e) { log('image collect error', e); }
253      });
254    } catch (e) { log('image collect error', e); }
255    return out;
256  }
257
258  // Fetch image bytes with the browser's own session (the backend cannot reach
259  // the platforms' auth'd/signed CDN URLs) and forward them to the bridge.
260  function fetchViaPage(url, cb) {
261    // In-page fetch — used for page-local blob: URLs and as a CORS-permitting
262    // fallback when GM_xmlhttpRequest is unavailable/denied for the host.
263    try {
264      fetch(url, { credentials: 'include' })
265        .then(function (r) { return r.ok ? r.blob() : null; })
266        .then(function (b) { cb(b); }, function () { cb(null); });
267    } catch (e) { cb(null); }
268  }
269  function fetchImageBlob(url, cb) {
270    // blob: URLs are page-local — GM_xmlhttpRequest can't fetch them.
271    if (url.indexOf('blob:') === 0) { fetchViaPage(url, cb); return; }
272    try {
273      GM_xmlhttpRequest({
274        method: 'GET',
275        url: url,
276        responseType: 'blob',
277        onload: function (res) {
278          cb(res.status >= 200 && res.status < 300 ? res.response : null);
279        },
280        onerror: function () { fetchViaPage(url, cb); },
281      });
282    } catch (e) {
283      // Some managers THROW here (e.g. host not in @connect) — never let that
284      // propagate into the capture loop; try the page's own fetch instead.
285      log('GM image fetch threw', e);
286      fetchViaPage(url, cb);
287    }
288  }
289  function fetchBlobAsync(url) {
290    return new Promise(function (resolve) { fetchImageBlob(url, resolve); });
291  }
292
293  // Signature of a conversation-list row, used to decide whether an ALREADY
294  // unread thread has received something new. Digits are stripped so the
295  // relative timestamp ticking over ("2m" → "3m") doesn't read as a new
296  // message, while a changed preview line does.
297  function rowSignature(el) {
298    try {
299      var t = (el.innerText || el.textContent || '')
300        .replace(/\\d+/g, '').replace(/\\s+/g, ' ').trim();
301      return hash(t);
302    } catch (e) { return ''; }
303  }
304
305  // -- ordered delivery ----------------------------------------------------
306  // ONE queue carries text and images in capture (DOM) order, drained
307  // strictly sequentially: an image message blocks the queue until its
308  // upload completes. The old parallel text/media pipelines delivered
309  // whenever their POSTs happened to finish, which scrambled the room
310  // timeline — Matrix orders events by arrival, not by any timestamp we send.
311  // Runs of consecutive text-only messages still batch into a single POST.
312  var TEXT_BATCH_MAX = 40;
313
314  function deliverBatch(msgs) {
315    var chatWith = msgs[0].chatWith;
316    if (CONFIG.DRY_RUN) {
317      log('DRY_RUN — would POST', chatWith, msgs);
318      setStatus('dry-run: ' + msgs.length + ' msg (not sent)');
319      return Promise.resolve(true);
320    }
321    return new Promise(function (resolve) {
322      try {
323        GM_xmlhttpRequest({
324          method: 'POST',
325          url: CONFIG.INGEST_URL,
326          headers: { 'Content-Type': 'application/json', 'X-Bridge-Token': CONFIG.TOKEN },
327          data: JSON.stringify({
328            platform: CONFIG.PLATFORM,
329            chatWith: chatWith,
330            conversationId: msgs[0].convId || '',
331            messages: msgs.map(function (m) {
332              return { sender: m.sender, text: m.text, time: m.time, key: m.key };
333            }),
334          }),
335          onload: function (res) {
336            var ok = res.status >= 200 && res.status < 300;
337            setStatus(ok ? 'synced ' + msgs.length + ' msg' : 'sync error ' + res.status);
338            log('POST', chatWith, res.status, res.responseText);
339            if (!ok) {
340              Diag.problem('ingest-http', {
341                status: res.status, chatWith: chatWith, count: msgs.length,
342                reply: (res.responseText || '').slice(0, 300),
343              });
344            }
345            resolve(ok);
346          },
347          onerror: function () {
348            setStatus('network error');
349            Diag.problem('ingest-network', { chatWith: chatWith, count: msgs.length });
350            resolve(false);
351          },
352        });
353      } catch (e) { log('text POST threw', e); resolve(false); }
354    });
355  }
356
357  async function uploadImageOrdered(m, img) {
358    if (CONFIG.DRY_RUN) {
359      log('DRY_RUN — would upload image', img.path, 'from', m.sender);
360      return true;
361    }
362    var blob = await fetchBlobAsync(img.url);
363    if (!blob || !blob.size) {
364      // Usually a @connect host missing from the header, or a CDN URL that has
365      // already expired — both worth keeping, they're invisible otherwise.
366      Diag.problem('image-fetch-failed', {
367        path: img.path,
368        host: (function () { try { return new URL(img.url, location.href).host; } catch (e) { return ''; } })(),
369      });
370      return false;
371    }
372    var type = (blob.type && blob.type.indexOf('image/') === 0)
373      ? blob.type : guessImageType(img.path);
374    var body = new Blob([blob], { type: type });
375    var name = img.name || 'image';
376    var fd = new FormData();
377    fd.append('platform', CONFIG.PLATFORM);
378    fd.append('chatWith', m.chatWith);
379    fd.append('conversationId', m.convId || '');
380    fd.append('sender', m.sender);
381    fd.append('key', img.mkey);
382    fd.append('filename', name);
383    fd.append('image', body, name);
384    return new Promise(function (resolve) {
385      try {
386        GM_xmlhttpRequest({
387          method: 'POST',
388          url: CONFIG.MEDIA_URL,
389          headers: { 'X-Bridge-Token': CONFIG.TOKEN },
390          data: fd,
391          onload: function (res) {
392            var ok = res.status >= 200 && res.status < 300;
393            setStatus(ok ? 'synced image' : 'image sync error ' + res.status);
394            log('POST image', img.path, res.status);
395            if (!ok) {
396              Diag.problem('media-http', {
397                status: res.status, path: img.path, type: type, bytes: blob.size,
398                reply: (res.responseText || '').slice(0, 300),
399              });
400            }
401            resolve(ok);
402          },
403          onerror: function () {
404            setStatus('image network error');
405            Diag.problem('media-network', { path: img.path });
406            resolve(false);
407          },
408        });
409      } catch (e) { log('image upload threw', e); resolve(false); }
410    });
411  }
412
413  var draining = false;
414  async function drainQueue() {
415    if (draining) return;
416    draining = true;
417    try {
418      while (queue.length) {
419        var item = queue[0];
420        var hasImages = item.images && item.images.length;
421        if (item.text && !hasImages) {
422          // Batch the run of consecutive text-only messages for this chat.
423          var batch = [];
424          while (queue.length && batch.length < TEXT_BATCH_MAX) {
425            var n = queue[0];
426            if (!n.text || (n.images && n.images.length) || n.chatWith !== item.chatWith) break;
427            batch.push(queue.shift());
428          }
429          if (!(await deliverBatch(batch))) {
430            queue = batch.concat(queue); // keep order; retry next round
431            break;
432          }
433          continue;
434        }
435        // Image-bearing (or media-only) message: caption first, then each
436        // image, all completed before the next queue item goes out.
437        queue.shift();
438        if (item.text && !(await deliverBatch([item]))) {
439          queue.unshift(item);
440          break;
441        }
442        var imgs = item.images || [];
443        for (var i = 0; i < imgs.length; i++) {
444          var ok = false;
445          try { ok = await uploadImageOrdered(item, imgs[i]); }
446          catch (e) { log('image deliver error', e); }
447          if (!ok) seenMedia.delete(imgs[i].mkey); // retro sweep retries later
448        }
449        saveSet('seenMedia:' + NS, seenMedia, 5000);
450      }
451    } catch (e) { log('drain error', e); }
452    finally { draining = false; }
453  }
454
455  // -- capture the open conversation --------------------------------------
456  var mediaStalls = {}; // message key -> scans spent waiting for its media
457  function capture() {
458    var msgs;
459    try { msgs = Scraper.extractMessages(); }
460    catch (e) {
461      Diag.problem('extract-error', {
462        conv: Scraper.getConversationId(), error: String(e),
463        stack: (e && e.stack ? String(e.stack) : '').slice(0, 400),
464      });
465      return 0;
466    }
467    // A thread is open but nothing came out of it. Usually an empty new
468    // conversation — but it's also exactly what a broken message selector looks
469    // like, and that would otherwise fail completely silently.
470    if (!msgs.length) {
471      Diag.problem('no-messages-extracted', {
472        conv: Scraper.getConversationId(),
473        scrollElFound: !!Scraper.getMessageScrollEl(),
474        nodesInScroller: (function () {
475          try {
476            var s = Scraper.getMessageScrollEl();
477            return s ? s.querySelectorAll('*').length : 0;
478          } catch (e) { return -1; }
479        })(),
480      });
481      return 0;
482    }
483    // Ordered-media gate: if an UNSEEN message's media hasn't rendered real
484    // URLs yet (lazy load), stop capturing AT that message — delivering
485    // anything after it would drop the image into the room out of order once
486    // it finally loads. Scroll it into view to force the lazy-loader and
487    // retry next scan; give up after MEDIA_WAIT_SCANS scans so one broken row
488    // can't stall the sync forever. Slow and in-order beats fast and scrambled.
489    var cut = msgs.length;
490    if (Scraper.isMessageReady) {
491      for (var gi = 0; gi < msgs.length; gi++) {
492        var gm = msgs[gi];
493        if (seen.has(gm.key)) continue;
494        var ready = true;
495        try { ready = Scraper.isMessageReady(gm); } catch (e) {}
496        if (ready) { delete mediaStalls[gm.key]; continue; }
497        var stalls = (mediaStalls[gm.key] || 0) + 1;
498        mediaStalls[gm.key] = stalls;
499        if (stalls > CONFIG.MEDIA_WAIT_SCANS) {
500          Diag.problem('media-never-loaded', {
501            key: gm.key, conv: gm.convId, scans: stalls,
502            html: gm.el ? rowHtml(gm.el, 500) : '',
503          });
504          continue;
505        }
506        try { if (gm.el) gm.el.scrollIntoView({ block: 'center' }); } catch (e) {}
507        setStatus('waiting for media to load…');
508        log('waiting for media on', gm.key, '(scan ' + stalls + ')');
509        cut = gi;
510        break;
511      }
512    }
513    var freshCount = 0;
514    msgs.slice(0, cut).forEach(function (m) {
515      // Claim this message's not-yet-delivered images now (released again on
516      // upload failure so the retro path below retries them).
517      var pending = [];
518      (m.images || []).forEach(function (img) {
519        var mkey = 'img:' + CONFIG.PLATFORM + ':' + (m.convId || '') + ':' + hash(img.path);
520        if (seenMedia.has(mkey)) return;
521        seenMedia.add(mkey);
522        pending.push({ url: img.url, path: img.path, name: img.name, mkey: mkey });
523      });
524      var isFresh = (m.text || (m.images && m.images.length)) && !seen.has(m.key);
525      if (isFresh) {
526        seen.add(m.key);
527        m.images = pending;
528        queue.push(m);
529        freshCount++;
530      } else if (pending.length) {
531        // Message already delivered but an image only just appeared (lazy
532        // load) or previously failed — send it now; its original timeline
533        // slot is gone, so append-at-end is the best we can do.
534        queue.push({
535          chatWith: m.chatWith, convId: m.convId, sender: m.sender,
536          time: m.time, text: '', key: m.key + ':retro', images: pending,
537        });
538      }
539    });
540    if (freshCount) {
541      saveSet('seen:' + NS, seen, 5000);
542      setStatus('queued ' + freshCount + ' new');
543      log('captured', freshCount, 'new of', msgs.length);
544    } else if (msgs.length && cut === msgs.length) {
545      // Make dedupe suppression visible — "nothing arrived" and "everything
546      // was already delivered" look identical without this. (Suppressed while
547      // the media gate is holding capture back — that's already reported.)
548      log('all', msgs.length, 'messages already delivered (dedupe) — regenerate the bridge token for a clean re-sync');
549    }
550    return freshCount;
551  }
552
553  async function processCurrent() {
554    var convId = Scraper.getConversationId();
555    if (!convId) {
556      // Normal idle state with AUTO_OPEN off: nothing is open, unread threads
557      // are reported to Matrix and wait there for a /read.
558      if (!CONFIG.AUTO_OPEN) setStatus('watching for unread…');
559      else setStatus('open a conversation…');
560      return;
561    }
562    var scroll = Scraper.getMessageScrollEl();
563    if (scroll && CONFIG.BACKFILL && !backfilled.has(convId)) {
564      setStatus('loading full history…');
565      try { await Scraper.backfill(scroll); } catch (e) { log('backfill error', e); }
566      backfilled.add(convId);
567      saveSet('backfilled:' + NS, backfilled);
568    }
569    capture();
570  }
571
572  // -- outbound: Matrix reply -> platform ----------------------------------
573  var outboxSeen = new Set();     // ids handled this session (avoid re-typing)
574  // Executed COMMAND ids, persisted: /refresh reloads the page, so an
575  // in-session-only guard would let a lost ack re-fire it in a reload loop.
576  var cmdDone = loadSet('cmdDone:' + NS);
577  var lastOutboxPoll = 0;
578
579  function fetchOutbox() {
580    return new Promise(function (resolve) {
581      GM_xmlhttpRequest({
582        method: 'GET',
583        url: CONFIG.OUTBOX_URL,
584        headers: { 'X-Bridge-Token': CONFIG.TOKEN },
585        onload: function (res) {
586          if (res.status < 200 || res.status >= 300) {
587            Diag.problem('outbox-http', {
588              status: res.status, reply: (res.responseText || '').slice(0, 300),
589            });
590            resolve([]);
591            return;
592          }
593          var msgs = [];
594          try { msgs = (JSON.parse(res.responseText) || {}).messages || []; }
595          catch (e) {
596            Diag.problem('outbox-parse', { reply: (res.responseText || '').slice(0, 300) });
597            resolve([]);
598            return;
599          }
600          // Keyed on the ids so a NEW item logs at once, while the same stuck
601          // item sitting in the queue doesn't reprint every poll.
602          var key = 'outbox:' + msgs.map(function (m) { return m.id; }).join(',');
603          if (msgs.length && !Diag.throttled(key)) {
604            Diag.record('outbox has work', msgs.map(function (m) {
605              return {
606                id: m.id, command: m.command || '(reply)', conv: m.conversation_id,
607                to: m.remote_username, stageOnly: !!m.stage_only,
608                bodyLen: (m.body || '').length,
609              };
610            }));
611          }
612          resolve(msgs);
613        },
614        onerror: function () { Diag.problem('outbox-network', {}); resolve([]); },
615      });
616    });
617  }
618  // Awaitable: /refresh must not reload the page until its ack has landed, or
619  // the command comes back pending after the reload and loops forever.
620  function ackOutbox(sentIds, failedIds) {
621    return new Promise(function (resolve) {
622      try {
623        GM_xmlhttpRequest({
624          method: 'POST',
625          url: CONFIG.OUTBOX_URL,
626          headers: { 'Content-Type': 'application/json', 'X-Bridge-Token': CONFIG.TOKEN },
627          data: JSON.stringify({ sent: sentIds || [], failed: failedIds || [] }),
628          onload: function (res) {
629            var ok = res.status >= 200 && res.status < 300;
630            Diag.record('outbox ack', { sent: sentIds, failed: failedIds, status: res.status });
631            // A lost ack is how a command comes back and runs twice — never
632            // silent, even though the local cmdDone guard also covers it.
633            if (!ok) Diag.problem('outbox-ack-http', { status: res.status, sent: sentIds });
634            resolve(ok);
635          },
636          onerror: function () {
637            Diag.problem('outbox-ack-network', { sent: sentIds, failed: failedIds });
638            resolve(false);
639          },
640        });
641      } catch (e) { Diag.problem('outbox-ack-threw', { error: String(e) }); resolve(false); }
642    });
643  }
644
645  // -- unread reporting (no reading!) --------------------------------------
646  // Report which conversations the LIST shows as unread. Reading the list never
647  // marks anything read on the platform — opening a thread does, which is why
648  // that only happens on an explicit /read.
649  var lastUnreadSig = null;
650  var lastUnreadPost = 0;
651  function postUnread(items, total) {
652    return new Promise(function (resolve) {
653      try {
654        GM_xmlhttpRequest({
655          method: 'POST',
656          url: CONFIG.UNREAD_URL,
657          headers: { 'Content-Type': 'application/json', 'X-Bridge-Token': CONFIG.TOKEN },
658          data: JSON.stringify({
659            platform: CONFIG.PLATFORM, total: total, conversations: items,
660          }),
661          onload: function (res) {
662            var ok = res.status >= 200 && res.status < 300;
663            Diag.record('unread report → server', {
664              unread: items.length, total: total, status: res.status,
665              reply: (res.responseText || '').slice(0, 200),
666              conversations: items,
667            });
668            if (!ok) {
669              Diag.problem('unread-report-http', {
670                status: res.status, reply: (res.responseText || '').slice(0, 300),
671              });
672            }
673            resolve(ok);
674          },
675          onerror: function () { Diag.problem('unread-report-net
675work', {}); resolve(false); },
676        });
677      } catch (e) { log('unread report threw', e); resolve(false); }
678    });
679  }
680  // Second unread signal. The platforms' own "unread" markers are class-name
681  // guesses (see Scraper._isUnread), so a row whose CONTENT changes while it is
682  // not the open thread is also treated as unread — that is almost always a new
683  // message landing in it. Sticky (the change is a one-off event) until the
684  // thread is opened. Survives a /refresh so a reload can't lose the flag.
685  var rowSigs = {};
686  var changedUnread = loadSet('changedUnread:' + NS);
687  function trackRowChanges(convs) {
688    if (!CONFIG.UNREAD_FROM_CHANGES) return;
689    var dirty = false;
690    convs.forEach(function (c) {
691      if (!c.id) return;
692      var prev = rowSigs[c.id];
693      rowSigs[c.id] = c.sig;
694      if (c.selected) {
695        // Row just became the OPEN thread: whatever classes disappear here are
696        // the platform's "read" transition — the same marker, seen in reverse.
697        var closeDiff = classDiff(c.id, c.el);
698        if (changedUnread.delete(c.id)) {
699          dirty = true;
700          if (closeDiff && (closeDiff.removed.length || closeDiff.added.length)) {
701            Diag.finding('read-transition-classes', {
702              id: c.id, removed: closeDiff.removed, added: closeDiff.added,
703              unreadGuessNow: c.unread,
704            }, 5);
705          }
706        }
707        return;
708      }
709      if (prev !== undefined && prev !== c.sig && !changedUnread.has(c.id)) {
710        // THE moment worth capturing: a closed row's content just changed, so a
711        // message almost certainly landed in it. Whatever classes appeared are
712        // the platform's unread marker — this is what Scraper._isUnread should
713        // be looking for.
714        var diff = classDiff(c.id, c.el);
715        Diag.record('unread transition (row changed while closed)', {
716          id: c.id, name: c.name, unreadGuess: c.unread,
717          classesAdded: diff ? diff.added : null,
718          classesRemoved: diff ? diff.removed : null,
719        });
720        Diag.finding('unread-transition-classes', {
721          id: c.id, unreadGuess: c.unread,
722          added: diff ? diff.added : null,
723          removed: diff ? diff.removed : null,
724          leaves: leafTexts(c.el),
725          html: rowHtml(c.el),
726        }, 5);
727        // The selector guess disagreeing with an obvious new message is the
728        // clearest possible signal that _isUnread needs fixing.
729        if (!c.unread) {
730          Diag.finding('unread-marker-missed', {
731            id: c.id, added: diff ? diff.added : null, allClasses: classSet(c.el),
732          }, 5);
733        }
734        changedUnread.add(c.id);
735        dirty = true;
736      } else if (prev === undefined) {
737        classDiff(c.id, c.el); // prime the cache on first sight
738      }
739    });
740    if (dirty) saveSet('changedUnread:' + NS, changedUnread, 500);
741  }
742
743  async function reportUnread(convs) {
744    if (!CONFIG.REPORT_UNREAD || !convs || !convs.length) return;
745    trackRowChanges(convs);
746    var items = [];
747    convs.forEach(function (c) {
748      if (!c.id) return;
749      if (!c.unread && !changedUnread.has(c.id)) return;
750      items.push({ id: String(c.id), name: c.name || '', sig: c.sig || '' });
751    });
752    var sig = items.map(function (i) { return i.id + '~' + i.sig; }).join('|');
753    var now = Date.now();
754    // Send on change; otherwise heartbeat occasionally so a dropped report
755    // (network blip) still converges.
756    if (sig === lastUnreadSig && now - lastUnreadPost < CONFIG.UNREAD_HEARTBEAT_MS) return;
757    lastUnreadSig = sig;
758    lastUnreadPost = now;
759    if (items.length) setStatus(items.length + ' unread (send /read in Matrix)');
760    var ok = await postUnread(items, convs.length);
761    if (!ok) lastUnreadSig = null; // retry on the next scan
762  }
763
764  // Split a multi-line reply into separate platform messages (how Me actually
765  // chats). Blank lines are dropped. Disabled when CONFIG.SPLIT_LINES is false.
766  function splitOutboundLines(text) {
767    // NB: escapes must be doubled — this source lives in a JS template literal,
768    // so a lone \\n here would emit a real newline INSIDE the regex literal and
769    // break the whole userscript at parse time.
770    return String(text || '').split(/\\r?\\n/).map(function (l) {
771      return l.replace(/^\\s+|\\s+$/g, '');
772    }).filter(function (l) { return l.length > 0; });
773  }
774  function lineGapMs() {
775    var lo = CONFIG.LINE_GAP_MIN_MS || 2000;
776    var hi = CONFIG.LINE_GAP_MAX_MS || 5000;
777    if (hi < lo) hi = lo;
778    return lo + Math.floor(Math.random() * (hi - lo + 1));
779  }
780  // Send (or stage) an outbox body. On real send, each non-empty line is its
781  // own message with a random 2–5s pause before the next Enter — so a 2-line
782  // suggestion becomes two bubbles, not one. Staging keeps the full text in
783  // the composer for review (no auto-submit, no inter-line delay).
784  async function sendOutboundBody(body, opts) {
785    var stageOnly = !!(opts && opts.stageOnly);
786    var split = CONFIG.SPLIT_LINES !== false;
787    var lines = split ? splitOutboundLines(body) : [String(body || '')];
788    if (!lines.length) return false;
789    if (stageOnly || lines.length === 1) {
790      var one = stageOnly ? String(body || '') : lines[0];
791      return await Scraper.sendMessage(one, opts);
792    }
793    var lastRes = false;
794    for (var li = 0; li < lines.length; li++) {
795      setStatus(
796        'outbound: line ' + (li + 1) + '/' + lines.length
797        + ' → ' + lines[li].slice(0, 40)
798      );
799      try { lastRes = await Scraper.sendMessage(lines[li], opts); }
800      catch (e) { log('outbound line send error', e); return false; }
801      if (!lastRes) return false;
802      if (lastRes === 'dryrun' || lastRes === 'staged') return lastRes;
803      if (li < lines.length - 1) {
804        var gap = lineGapMs();
805        setStatus('outbound: next line in ' + Math.round(gap / 1000) + 's…');
806        log('outbound: waiting', gap, 'ms before line', li + 2);
807        await sleep(gap);
808      }
809    }
810    return lastRes;
811  }
812
813  // -- outbound: control commands (/read, /refresh) -------------------------
814  // These carry no text for the platform — they drive the userscript itself.
815  // The creator types them in a bridge room; /read is what actually opens (and
816  // therefore marks read) that room's conversation.
817  async function runCommand(m) {
818    var cmd = String(m.command || '').toLowerCase();
819    var label = m.remote_username || m.conversation_id || '';
820    Diag.record('command received', {
821      id: m.id, command: cmd, conv: m.conversation_id, to: m.remote_username,
822      openNow: Scraper.getConversationId(),
823    });
824
825    if (cmd === 'refresh') {
826      log('command: /refresh — reloading');
827      setStatus('/refresh — reloading page…');
828      // Mark done and ack BEFORE the reload lands, so the command can't come
829      // back pending and reload us again.
830      cmdDone.add(String(m.id));
831      saveSet('cmdDone:' + NS, cmdDone, 200);
832      await ackOutbox([m.id], []);
833      await sleep(500);
834      location.reload();
835      await sleep(30000); // page is tearing down — stop doing work
836      return;
837    }
838
839    if (cmd === 'read') {
840      if (!m.conversation_id || !Scraper.openConversation) {
841        Diag.problem('read-unusable', {
842          id: m.id, conv: m.conversation_id,
843          why: m.conversation_id ? 'scraper cannot open conversations' : 'no conversation id on the outbox row',
844        });
845        outboxSeen.add(m.id);
846        await ackOutbox([], [m.id]);
847        return;
848      }
849      setStatus('/read — opening ' + label);
850      var opened = await Scraper.openConversation(m.conversation_id);
851      if (!opened) {
852        // Not in the loaded list — leave pending and retry on a later poll.
853        // Record what WAS in the list: if the id isn't there at all, the room's
854        // stored conversation_id is stale or the list needs scrolling.
855        var known = (Scraper.listConversations ? Scraper.listConversations() : [])
856          .map(function (c) { return c.id; });
857        Diag.problem('read-open-failed', {
858          conv: m.conversation_id,
859          inList: known.indexOf(String(m.conversation_id)) !== -1,
860          listSize: known.length, listIds: known.slice(0, 30),
861          openNow: Scraper.getConversationId(),
862        });
863        return;
864      }
865      if (Scraper.getConversationId() !== String(m.conversation_id)) {
866        Diag.problem('read-conversation-mismatch', {
867          wanted: String(m.conversation_id), got: Scraper.getConversationId(),
868        });
869        return;
870      }
871      await sleep(1200); // let the thread render before capturing
872      if (changedUnread.delete(String(m.conversation_id))) {
873        saveSet('changedUnread:' + NS, changedUnread, 500);
874      }
875      var before = queue.length;
876      try { await processCurrent(); }
877      catch (e) { Diag.problem('read-capture-failed', { conv: m.conversation_id, error: String(e) }); }
878      Diag.record('/read synced', {
879        conv: m.conversation_id, queued: queue.length - before,
880        queueDepth: queue.length, seenTotal: seen.size,
881      });
882      // Nothing new after opening an unread thread means either the messages
883      // were already delivered, or extraction is failing on this thread.
884      if (queue.length === before) {
885        Diag.finding('read-produced-nothing', {
886          conv: m.conversation_id,
887          messagesOnScreen: (function () {
888            try { return Scraper.extractMessages().length; } catch (e) { return 'extract error: ' + e; }
889          })(),
890        }, 5);
891      }
892      drainQueue();
893      cmdDone.add(String(m.id));
894      saveSet('cmdDone:' + NS, cmdDone, 200);
895      outboxSeen.add(m.id);
896      await ackOutbox([m.id], []);
897      setStatus('read + syncing ' + label);
898      return;
899    }
900
901    Diag.problem('unknown-command', { id: m.id, command: cmd });
902    outboxSeen.add(m.id);
903    await ackOutbox([], [m.id]);
904  }
905
906  async function drainOutbox() {
907    if (Date.now() - lastOutboxPoll < CONFIG.OUTBOX_POLL_MS) return;
908    lastOutboxPoll = Date.now();
909
910    var msgs = await fetchOutbox();
911    for (var i = 0; i < msgs.length; i++) {
912      var m = msgs[i];
913      if (outboxSeen.has(m.id) || cmdDone.has(String(m.id))) continue;
914      if (m.command) { await runCommand(m); continue; }
915      // Replies (text to type) — commands above run even with OUTBOUND off.
916      if (!CONFIG.OUTBOUND || !Scraper.openConversation || !Scraper.sendMessage) continue;
917      if (!m.conversation_id) { outboxSeen.add(m.id); ackOutbox([], [m.id]); continue; }
918
919      setStatus('outbound: opening ' + (m.remote_username || m.conversation_id));
920      var opened = await Scraper.openConversation(m.conversation_id);
921      if (!opened) {
922        // Not in the loaded list — leave pending, retry a later poll.
923        Diag.problem('outbound-open-failed', {
924          conv: m.conversation_id, to: m.remote_username,
925          openNow: Scraper.getConversationId(),
926        });
927        continue;
928      }
929      // Guard: only ever send while the intended conversation is actually open.
930      if (Scraper.getConversationId() !== String(m.conversation_id)) {
931        log('outbound: conversation mismatch, skipping', m.conversation_id);
932        continue;
933      }
934
935      var res;
936      // Per-message stage_only (from the assistant bot's /chat or reaction
937      // chips): type the reply into the composer but DON'T submit — the creator
938      // reviews and sends by hand. Real sends split multi-line bodies.
939      try { res = await sendOutboundBody(m.body, { stageOnly: !!m.stage_only }); }
940      catch (e) { log('outbound send error', e); res = false; }
941
942      outboxSeen.add(m.id);
943      if (res === 'staged') {
944        // Typed, not submitted. Ack as done so a page reload doesn't re-type
945        // over the creator's in-progress edits.
946        ackOutbox([m.id], []);
947        setStatus('staged (typed, not sent) → ' + (m.remote_username || m.conversation_id));
948        log('outbound STAGED: typed into', m.conversation_id, JSON.stringify(m.body));
949      } else if (res === 'dryrun') {
950        setStatus('DRY-RUN typed reply (not sent) → ' + (m.remote_username || m.conversation_id));
951        log('outbound DRY-RUN: typed into', m.conversation_id, JSON.stringify(m.body));
952      } else if (res) {
953        ackOutbox([m.id], []);
954        var nLines = splitOutboundLines(m.body).length;
955        setStatus(
956          (nLines > 1 ? ('sent ' + nLines + ' lines') : 'sent reply')
957          + ' → ' + (m.remote_username || m.conversation_id)
958        );
959      } else {
960        ackOutbox([], [m.id]);
961        setStatus('reply FAILED → ' + (m.remote_username || m.conversation_id));
962      }
963    }
964  }
965
966  // -- main loop -----------------------------------------------------------
967  var visitedThisRun = new Set();
968  async function loop() {
969    while (true) {
970      try {
971        await drainOutbox();
972        var unread = checkUnread();
973        var convs = Scraper.listConversations ? Scraper.listConversations() : [];
974        if (convs.length) {
975          if (CONFIG.DEBUG) Scraper.debugConversations && Scraper.debugConversations(convs);
976          // Diagnostic: the tab title reports unread mail but the conversation
977          // list shows none — the list DOM did not live-update even with
978          // keep-awake, so a new thread is being missed. (Report to tune a
979          // targeted list-resync if this ever prints.)
980          var unreadInList = convs.filter(function (c) { return c.unread; }).length;
981          if (unread > 0 && unreadInList === 0) {
982            Diag.problem('unread-mismatch', {
983              titleSays: unread, listSays: 0, rows: convs.length,
984              changeDetected: changedUnread.size,
985              note: 'either the list DOM is stale, or _isUnread does not match this platform',
986              sample: convs[0] ? describeRow(convs[0].el) : null,
987            }, 3);
988          }
989          // Tell Matrix which threads are unread WITHOUT opening them. Opening
990          // is what marks a thread read on the platform, so it only ever
991          // happens on an explicit /read (or with AUTO_OPEN on, below).
992          await reportUnread(convs);
993        }
994        if (CONFIG.AUTO_OPEN && convs.length) {
995          // Prefer unread threads we haven't opened yet this run.
996          var next = convs.filter(function (c) {
997            return c.unread && !c.selected && !visitedThisRun.has(c.id);
998          })[0];
999          // Bootstrap: sitting on the conversation LIST with nothing open and
1000          // nothing unread, there is no thread to cycle to, so the loop idles
1001          // on "open a conversation…" forever. Open the top thread just to get
1002          // a conversation on screen — after this the normal unread cycle runs.
1003          if (!next && !Scraper.getConversationId() && convs.length) {
1004            next = convs.filter(function (c) { return !visitedThisRun.has(c.id); })[0];
1005            if (next) log('bootstrap: no conversation open, opening first thread');
1006          }
1007          if (next) {
1008            setStatus('opening unread thread…');
1009            visitedThisRun.add(next.id);
1010            await Scraper.selectConversation(next);
1011            await sleep(1500);
1012          } else if (visitedThisRun.size && !convs.some(function (c) { return c.unread; })) {
1013            // No unread left — reset so future new unread threads get picked up.
1014            visitedThisRun.clear();
1015          }
1016        }
1017        await processCurrent();
1018        drainQueue(); // deliberately not awaited — a long image backfill must
1019                      // not stall scanning or the outbox; ordering is kept by
1020                      // the queue itself, not by who calls the drain.
1021        heartbeat(convs);
1022      } catch (e) {
1023        Diag.problem('loop-error', {
1024          error: String(e), stack: (e && e.stack ? String(e.stack) : '').slice(0, 400),
1025        }, 10);
1026      }
1027      await sleep(CONFIG.SCAN_INTERVAL_MS);
1028    }
1029  }
1030
1031  // Periodic one-line summary so a console left open overnight still shows what
1032  // the bridge believed at any point — without printing on every 3s scan.
1033  var lastHeartbeat = 0;
1034  function heartbeat(convs) {
1035    if (Date.now() - lastHeartbeat < CONFIG.HEARTBEAT_MS) return;
1036    lastHeartbeat = Date.now();
1037    Diag.record('heartbeat', {
1038      open: Scraper.getConversationId() || '(none)',
1039      rows: convs.length,
1040      unreadByMarker: convs.filter(function (c) { return c.unread; }).length,
1041      unreadByChange: changedUnread.size,
1042      titleUnread: titleUnread(),
1043      queued: queue.length,
1044      delivered: seen.size,
1045      findings: Object.keys(Diag.findings).length,
1046    });
1047  }
1048
1049  // -- status pill ---------------------------------------------------------
1050  var pill;
1051  function setStatus(text) {
1052    if (!pill) {
1053      pill = document.createElement('div');
1054      pill.style.cssText =
1055        'position:fixed;bottom:12px;right:12px;z-index:2147483647;background:#111;' +
1056        'color:#fff;font:12px/1.4 sans-serif;padding:6px 10px;border-radius:8px;' +
1057        'opacity:.85;pointer-events:none;max-width:260px;';
1058      (document.body || document.documentElement).appendChild(pill);
1059    }
1060    var unread = titleUnread();
1061    pill.textContent = 'Shared bridge' + (CONFIG.DRY_RUN ? ' (dry-run)' : '')
1062      + (unread ? ' [' + unread + ' unread]' : '') + ': ' + text;
1063  }
1064
1065  // -- console API ---------------------------------------------------------
1066  // Everything the runtime has learned, reachable by hand. Exposed on the PAGE
1067  // window (via unsafeWindow) so the devtools console reaches it without having
1068  // to switch to the userscript sandbox context. The bridge token is never put
1069  // on it; the tradeoff is that page scripts can call these too, which on the
1070  // creator's own logged-in tab is not a meaningful escalation.
1071  function safeConfig() {
1072    var out = {};
1073    Object.keys(CONFIG).forEach(function (k) {
1074      out[k] = k === 'TOKEN' ? '(redacted)' : CONFIG[k];
1075    });
1076    return out;
1077  }
1078  var api = {
1079    help: function () {
1080      console.log([
1081        'Shared chat bridge v' + CONFIG.VERSION + ' — console commands:',
1082        '  __bridge.report()      everything gathered (copy this out)',
1083        '  __bridge.findings()    persisted findings (survive reloads)',
1084        '  __bridge.log()         recent events, this session',
1085        '  __bridge.state()       what the bridge believes right now',
1086        '  __bridge.rows()        conversation list as the scraper sees it',
1087        '  __bridge.inspect(id)   full DOM description of one list row',
1088        '  __bridge.messages()    messages extracted from the open thread',
1089        '  __bridge.read(id)      open + sync a conversation now (marks it read)',
1090        '  __bridge.forget()      clear findings (keeps delivery state)',
1091        '  __bridge.resync()      forget delivered keys — re-sends this thread',
1092      ].join('\\n'));
1093      return CONFIG.VERSION;
1094    },
1095    config: safeConfig,
1096    findings: function () { return Diag.findings; },
1097    log: function (n) { return Diag.ring.slice(-(n || 60)); },
1098    state: function () {
1099      var convs = Scraper.listConversations ? Scraper.listConversations() : [];
1100      return {
1101        version: CONFIG.VERSION,
1102        platform: CONFIG.PLATFORM,
1103        startedAt: Diag.started,
1104        openConversation: Scraper.getConversationId() || '(none)',
1105        rows: convs.length,
1106        unreadByMarker: convs.filter(function (c) { return c.unread; }).map(function (c) { return c.id; }),
1107        unreadByChange: Array.from(changedUnread),
1108        titleUnread: titleUnread(),
1109        queueDepth: queue.length,
1110        deliveredKeys: seen.size,
1111        deliveredImages: seenMedia.size,
1112        commandsDone: Array.from(cmdDone),
1113        lastUnreadReport: lastUnreadPost ? new Date(lastUnreadPost).toISOString() : null,
1114      };
1115    },
1116    rows: function () {
1117      return (Scraper.listConversations ? Scraper.listConversations() : []).map(function (c) {
1118        return {
1119          id: c.id, name: c.name, selected: c.selected,
1120          unreadByMarker: c.unread, unreadByChange: changedUnread.has(c.id), sig: c.sig,
1121        };
1122      });
1123    },
1124    inspect: function (id) {
1125      var convs = Scraper.listConversations ? Scraper.listConversations() : [];
1126      var row = convs.filter(function (c) { return String(c.id) === String(id); })[0];
1127      return row ? describeRow(row.el) : 'no such row; try __bridge.rows()';
1128    },
1129    messages: function () {
1130      try {
1131        return Scraper.extractMessages().map(function (m) {
1132          return {
1133            key: m.key, sender: m.sender, time: m.time,
1134            text: (m.text || '').slice(0, 60),
1135            images: (m.images || []).length, delivered: seen.has(m.key),
1136          };
1137        });
1138      } catch (e) { return 'extract error: ' + e; }
1139    },
1140    read: async function (id) {
1141      return await runCommand({ id: 'manual', command: 'read', conversation_id: String(id) });
1142    },
1143    forget: function () {
1144      Diag.findings = {};
1145      Diag.persist();
1146      Diag.onceKeys = {};
1147      return 'findings cleared';
1148    },
1149    resync: function () {
1150      seen.clear();
1151      saveSet('seen:' + NS, seen);
1152      return 'delivered-message state cleared — this thread will re-send on the next scan';
1153    },
1154    report: function () {
1155      var r = {
1156        version: CONFIG.VERSION, platform: CONFIG.PLATFORM,
1157        userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) || '',
1158        config: safeConfig(), state: api.state(), findings: Diag.findings,
1159        recent: Diag.ring.slice(-80),
1160      };
1161      console.log(r);
1162      try { console.log('%c[bridge] copy(__bridge.report()) puts this on the clipboard', 'color:#4f6bed'); } catch (e) {}
1163      return r;
1164    },
1165  };
1166  try {
1167    (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window).__bridge = api;
1168  } catch (e) {
1169    try { window.__bridge = api; } catch (e2) {}
1170  }
1171
1172  // Anything thrown outside the loop's own try (event handlers, async gaps)
1173  // would otherwise vanish into the console at 3am.
1174  try {
1175    window.addEventListener('error', function (ev) {
1176      if (!ev || !ev.message) return;
1177      Diag.problem('window-error', {
1178        message: String(ev.message).slice(0, 200),
1179        at: (ev.filename || '') + ':' + (ev.lineno || ''),
1180      }, 10);
1181    });
1182    window.addEventListener('unhandledrejection', function (ev) {
1183      Diag.problem('unhandled-rejection', {
1184        reason: String((ev && ev.reason && (ev.reason.stack || ev.reason.message)) || (ev && ev.reason) || '').slice(0, 300),
1185      }, 10);
1186    });
1187  } catch (e) {}
1188
1189  console.log(
1190    '%c[bridge] v' + CONFIG.VERSION + ' starting — type __bridge.help() for diagnostics',
1191    'color:#4f6bed;font-weight:bold',
1192    {
1193      platform: CONFIG.PLATFORM,
1194      autoOpen: CONFIG.AUTO_OPEN,
1195      reportUnread: CONFIG.REPORT_UNREAD,
1196      outbound: CONFIG.OUTBOUND && !CONFIG.OUTBOUND_DRYRUN,
1197      delivered: seen.size,
1198      pendingUnreadFromChanges: Array.from(changedUnread),
1199    },
1200  );
1201  if (Object.keys(Diag.findings).length) {
1202    console.log(
1203      '%c[bridge] findings carried over from earlier sessions — __bridge.findings()',
1204      'color:#0a0;font-weight:bold',
1205      Object.keys(Diag.findings).map(function (k) {
1206        return k + ' ×' + Diag.findings[k].length;
1207      }),
1208    );
1209  }
1210
1211  setStatus('starting…');
1212  loop();
1213  setInterval(drainQueue, CONFIG.FLUSH_INTERVAL_MS);
1214`,Ht=`
1215  var Scraper = {
1216    getConversationId: function () {
1217      var m = location.pathname.match(/\\/messages\\/(\\d+)/);
1218      return m ? m[1] : '';
1219    },
1220    getPartnerName: function () {
1221      // Target the name span specifically — NOT the .message-content-header-contact
1222      // container, which also holds the action menu ("Edit User Notes", "Block
1223      // user", etc.). display-name is what's shown; user-name is the @handle.
1224      var root = document.querySelector('.message-content-header-contact');
1225      var el = root && (root.querySelector('.display-name') || root.querySelector('.user-name'));
1226      var name = el ? el.textContent.trim() : '';
1227      if (!name) {
1228        // Fallback: the selected conversation-list row's contact name.
1229        var row = document.querySelector('.message-list a.selected .message-contact');
1230        name = row ? row.textContent.trim() : '';
1231      }
1232      // Collapse any stray whitespace/newlines.
1233      name = name.replace(/\\s+/g, ' ').trim();
1234      if (name) return name;
1235      // Falling back to conv_<id> names the Matrix room after a number. Capture
1236      // the header DOM so the right selector can be added.
1237      var hdr = document.querySelector('.message-content-header-contact');
1238      Diag.finding('partner-name-fallback', {
1239        conv: Scraper.getConversationId(),
1240        headerFound: !!hdr,
1241        headerLeaves: hdr ? leafTexts(hdr) : null,
1242        headerHtml: hdr ? rowHtml(hdr, 600) : '',
1243      }, 3);
1244      return 'conv_' + Scraper.getConversationId();
1245    },
1246    getMessageScrollEl: function () {
1247      return document.querySelector('.message-content-list');
1248    },
1249    // Quote replies embed the quoted ORIGINAL as a nested
1250    // app-group-message.message-reply — anything inside it (text, images)
1251    // belongs to the original message and must never be re-captured. NB: the
1252    // class must be .message-reply, NOT .message-reply-wrapper — Fansly puts
1253    // the wrapper class on the container holding the reply's OWN text too.
1254    QUOTE_WRAPPER: '.message-reply',
1255    _messageText: function (el) {
1256      // Take only this message's OWN .message-text nodes — innerText of the
1257      // whole bubble would also capture the quoted original (duplicating it)
1258      // and the quote footer ("Burgeman, 1m").
1259      var parts = [];
1260      el.querySelectorAll('.message-text').forEach(function (t) {
1261        if (t.closest(Scraper.QUOTE_WRAPPER)) return;      // quoted original
1262        if (t.closest('app-group-message') !== el) return; // any other nesting
1263        var s = (t.innerText || '').trim();
1264        if (s) parts.push(s);
1265      });
1266      if (parts.length) return parts.join('\\n');
1267      // Fallback for message types without .message-text (tips, GIFs, …):
1268      // innerText minus the quote block and UI chrome.
1269      try {
1270        var clone = el.cloneNode(true);
1271        clone.querySelectorAll(
1272          Scraper.QUOTE_WRAPPER + ', .actions, .tap-backs, .reply-footer'
1273        ).forEach(function (n) { n.remove(); });
1274        return (clone.innerText || clone.textContent || '').trim();
1275      } catch (e) { return (el.innerText || '').trim(); }
1276    },
1277    _collectionTime: function (el) {
1278      var coll = el.closest('app-group-message-collection');
1279      var t = coll && coll.querySelector('.timestamp .margin-right-text');
1280      return t ? t.textContent.trim() : '';
1281    },
1282    extractMessages: function () {
1283      var scroll = Scraper.getMessageScrollEl();
1284      if (!scroll) return [];
1285      var convId = Scraper.getConversationId();
1286      var partner = Scraper.getPartnerName();
1287      var nodes = scroll.querySelectorAll('app-group-message');
1288      var out = [];
1289      nodes.forEach(function (el) {
1290        // The quoted original inside a quote reply is itself an
1291        // app-group-message — skip it or it re-delivers as a new message.
1292        if (el.classList.contains('message-reply') || el.closest(Scraper.QUOTE_WRAPPER)) return;
1293        var text = Scraper._messageText(el);
1294        var images = collectMessageImages(el);
1295        if (!text && !images.length) return;
1296        var outgoing = el.classList.contains('my-message');
1297        var time = Scraper._collectionTime(el);
1298        var domId = el.getAttribute('data-id') || el.id || '';
1299        // Dedupe on content only — Fansly's timestamp is RELATIVE ("now"/"5s"/
1300        // "6m") and ticks on every rescan, so including it re-sent every message.
1301        // Media-only messages key on the stable image paths instead.
1302        var content = text || images.map(function (i) { return i.path; }).join(',');
1303        var key = 'f:' + convId + ':' + (domId || hash((outgoing ? '1' : '0') + '|' + content));
1304        out.push({
1305          chatWith: partner,
1306          convId: convId,
1307          sender: outgoing ? CONFIG.SELF_LABEL : partner,
1308          text: text,
1309          time: time,
1310          key: key,
1311          images: images,
1312          el: el,
1313        });
1314      });
1315      return out;
1316    },
1317    isMessageReady: function (m) {
1318      // Fansly lazy-loads message media: the bubble renders first, the real
1319      // image URL arrives later. A message is "ready" once every non-avatar,
1320      // non-emoji <img> has a real (non-data:) src, and no attachment shell
1321      // is still waiting to create its <img> at all.
1322      var el = m.el;
1323      if (!el) return true;
1324      var pending = false;
1325      var count = 0;
1326      el.querySelectorAll('img').forEach(function (img) {
1327        // Quote-block images belong to the quoted original — never gate on them.
1328        if (img.closest('[class*="avatar" i], [class*="emoji" i]')) return;
1329        if (img.closest(Scraper.QUOTE_WRAPPER)) return;
1330        count++;
1331        var src = img.currentSrc || img.src || '';
1332        if (!src || src.indexOf('data:') === 0) pending = true;
1333      });
1334      if (pending) return false;
1335      if (!count && !Scraper._messageText(el) &&
1336          el.querySelector('app-media-preview, [class*="attachment" i]')) {
1337        return false;
1338      }
1339      return true;
1340    },
1341    listConversations: function () {
1342      var links = document.querySelectorAll('.message-list a[href*="/messages/"]');
1343      return Array.prototype.map.call(links, function (a) {
1344        var m = (a.getAttribute('href') || '').match(/\\/messages\\/(\\d+)/);
1345        return {
1346          el: a,
1347          id: m ? m[1] : (a.getAttribute('href') || ''),
1348          selected: a.classList.contains('selected'),
1349          unread: Scraper._isUnread(a),
1350          // Name + signature come from the LIST row only — reading the list
1351          // never marks anything read, unlike opening the thread.
1352          name: Scraper._rowName(a),
1353          sig: rowSignature(a),
1354        };
1355      });
1356    },
1357    _rowName: function (a) {
1358      var sels = ['.display-name', '.user-name', '.message-contact'];
1359      for (var i = 0; i < sels.length; i++) {
1360        var el = null;
1361        try { el = a.querySelector(sels[i]); } catch (e) { continue; }
1362        var name = (el ? el.textContent : '').replace(/\\s+/g, ' ').trim();
1363        if (name) {
1364          Diag.once('rowname:' + sels[i], 'list-row name selector works', { selector: sels[i] });
1365          return name;
1366        }
1367      }
1368      // No name from the list row → the room would be created as "conv_<id>".
1369      // Capture the row's leaf text so the right selector can be added.
1370      Diag.finding('row-name-missing', {
1371        href: a.getAttribute('href') || '', leaves: leafTexts(a), html: rowHtml(a),
1372      }, 3);
1373      return '';
1374    },
1375    // Best-guess until confirmed. Fansly tends to mark unread rows with a
1376    // badge/dot element or an 'unread'/'unseen' class. Tried one at a time so
1377    // the log says WHICH guess fired (and on what element) rather than just
1378    // "unread: true" — that's what pins the real selector down.
1379    _UNREAD_SELECTORS: [
1380      '[class*="unread" i]', '[class*="unseen" i]', '[class*="new-message" i]',
1381      '[class*="badge" i]', '[class*="notification" i]',
1382    ],
1383    _isUnread: function (a) {
1384      for (var i = 0; i < Scraper._UNREAD_SELECTORS.length; i++) {
1385        var sel = Scraper._UNREAD_SELECTORS[i];
1386        var hit = null;
1387        try { hit = a.querySelector(sel); } catch (e) { continue; }
1388        if (!hit) continue;
1389        Diag.once('unreadsel:' + sel, 'unread marker matched', {
1390          selector: sel,
1391          onClass: (hit.getAttribute('class') || '').slice(0, 80),
1392          text: (hit.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 24),
1393        });
1394        Diag.finding('unread-selector-hits', {
1395          selector: sel, onClass: (hit.getAttribute('class') || '').slice(0, 80),
1396        }, 5);
1397        return true;
1398      }
1399      return false;
1400    },
1401    selectConversation: async function (conv) {
1402      conv.el.click();
1403      await waitFor(function () { return Scraper.getConversationId() === conv.id; }, 6000, 200);
1404      // give the message list a moment to render
1405      await waitFor(function () {
1406        var s = Scraper.getMessageScrollEl();
1407        return s && s.querySelector('app-group-message');
1408      }, 6000, 200);
1409    },
1410    backfill: async function (scroll) {
1411      var last = -1, stable = 0;
1412      for (var i = 0; i < 100 && stable < 3; i++) {
1413        scroll.scrollTop = 0;
1414        await sleep(650);
1415        var h = scroll.scrollHeight;
1416        if (h === last) stable++; else { stable = 0; last = h; }
1417      }
1418      scroll.scrollTop = scroll.scrollHeight; // return to newest
1419      await sleep(300);
1420    },
1421    // ---- outbound (Matrix reply -> Fansly) ----
1422    openConversation: async function (convId) {
1423      convId = String(convId);
1424      if (Scraper.getConversationId() === convId) return true;
1425      var links = document.querySelectorAll('.message-list a[href*="/messages/"]');
1426      var target = null;
1427      Array.prototype.forEach.call(links, function (a) {
1428        var m = (a.getAttribute('href') || '').match(/\\/messages\\/(\\d+)/);
1429        if (m && m[1] === convId) target = a;
1430      });
1431      if (!target) return false; // not in the loaded list
1432      target.click();
1433      return await waitFor(function () {
1434        return Scraper.getConversationId() === convId &&
1435          !!document.querySelector('textarea.message-input');
1436      }, 7000, 200);
1437    },
1438    sendMessage: async function (text, opts) {
1439      var editor = document.querySelector('app-group-message-input textarea.message-input, textarea.message-input');
1440      if (!editor) return false;
1441      editor.focus();
1442      // Angular reactive-form textarea: set via the native value setter, then
1443      // fire 'input' so the form control model updates and send is allowed.
1444      try {
1445        var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
1446        setter.call(editor, text);
1447      } catch (e) { editor.value = text; }
1448      editor.dispatchEvent(new Event('input', { bubbles: true }));
1449      await sleep(250);
1450      if ((editor.value || '').indexOf(text.slice(0, Math.min(24, text.length))) === -1) {
1451        log('outbound: textarea did not accept text');
1452        return false;
1453      }
1454      // Per-message stage-only: leave the text in the composer, don't submit.
1455      if (opts && opts.stageOnly) return 'staged';
1456      if (CONFIG.OUTBOUND_DRYRUN) return 'dryrun';
1457      // Prefer an explicit send button in the composer; else press Enter.
1458      var btn = document.querySelector(
1459        'app-group-message-input [class*="send" i], app-group-message-input .fa-paper-plane'
1460      );
1461      if (btn) {
1462        var clickable = btn.closest('button, [role="button"], a, .icon-wrapper') || btn;
1463        clickable.click();
1464      } else {
1465        ['keydown', 'keypress', 'keyup'].forEach(function (t) {
1466          editor.dispatchEvent(new KeyboardEvent(t, {
1467            key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true,
1468          }));
1469        });
1470      }
1471      // Success signal: Fansly clears the textarea once the message is sent.
1472      var cleared = await waitFor(function () { return (editor.value || '') === ''; }, 3000, 150);
1473      return cleared;
1474    },
1475    debugConversations: function (convs) {
1476      // One-time-ish dump so we can confirm the real unread marker. Prints only
1477      // structural class names — no message text, no names.
1478      if (Scraper._dumped) return;
1479      Scraper._dumped = true;
1480      log('conversation rows:', convs.length);
1481      convs.slice(0, 12).forEach(function (c, i) {
1482        var group = c.el.querySelector('app-messaging-group') || c.el;
1483        var classes = new Set();
1484        group.querySelectorAll('*').forEach(function (n) {
1485          (n.getAttribute('class') || '').split(/\\s+/).forEach(function (t) { if (t) classes.add(t); });
1486        });
1487        log('row ' + i, { id: c.id, selected: c.selected, unreadGuess: c.unread,
1488          rowClass: c.el.getAttribute('class') || '', classes: Array.from(classes) });
1489      });
1490      log('^ compare an UNREAD row vs a READ row above to find the unread class.');
1491    },
1492  };
1493`,jt=`
1494  var Scraper = {
1495    getConversationId: function () {
1496      var m = location.pathname.match(/chat\\/(\\d+)/);
1497      return m ? m[1] : '';
1498    },
1499    _currentItem: function () {
1500      var convId = Scraper.getConversationId();
1501      return document.querySelector('.b-chats__item.current') ||
1502        (convId ? document.getElementById(convId) : null);
1503    },
1504    getPartnerName: function () {
1505      // Try every DISPLAY-NAME source before any handle-based fallback: fans
1506      // without a custom username get a default 'u<id>' handle, so the avatar
1507      // href must be the LAST resort or rooms end up named "OnlyFans: u1839…".
1508      var clean = function (s) { return (s || '').replace(/\\s+/g, ' ').trim(); };
1509      var scroll = Scraper.getMessageScrollEl();
1510      // 1) Chat header (present whenever a conversation is open, all layouts).
1511      var hdr = document.querySelector(
1512        '.b-chat__header .g-user-name, .b-chat__header .g-user-realname'
1513      );
1514      var name = hdr ? clean(hdr.textContent) : '';
1515      if (name) return name;
1516      // 2) Incoming-message avatar alt — the partner's display name.
1517      if (scroll) {
1518        var img = scroll.querySelector('.b-chat__message:not(.m-from-me) .g-avatar img[alt]');
1519        name = img ? clean(img.getAttribute('alt')) : '';
1520        if (name) return name;
1521      }
1522      // 3) The conversation-list row (desktop layout only).
1523      var item = Scraper._currentItem();
1524      var el = item && item.querySelector('.g-user-name');
1525      name = el ? clean(el.textContent) : '';
1526      if (name) return name;
1527      // 4) Avatar profile-link handle — may be a default 'u<id>' username.
1528      if (scroll) {
1529        var a = scroll.querySelector('.b-chat__message:not(.m-from-me) a.g-avatar[href]');
1530        if (a) {
1531          var handle = (a.getAttribute('href') || '')
1532            .replace(/^https?:\\/\\/[^/]+/, '').replace(/^\\/+|\\/+$/g, '');
1533          if (handle && !/^(my|chats?)\\b/.test(handle) && handle.indexOf('/') === -1) {
1534            Diag.once('partner-name-handle', 'partner name came from the profile handle', {
1535              handle: handle,
1536            });
1537            return handle;
1538          }
1539        }
1540      }
1541      var hdr2 = document.querySelector('.b-chat__header');
1542      Diag.finding('partner-name-fallback', {
1543        conv: Scraper.getConversationId(),
1544        headerFound: !!hdr2,
1545        headerLeaves: hdr2 ? leafTexts(hdr2) : null,
1546        headerHtml: hdr2 ? rowHtml(hdr2, 600) : '',
1547      }, 3);
1548      return 'conv_' + Scraper.getConversationId();
1549    },
1550    getMessageScrollEl: function () {
1551      return document.querySelector('.b-chat__messages');
1552    },
1553    extractMessages: function () {
1554      var scroll = Scraper.getMessageScrollEl();
1555      if (!scroll) return [];
1556      var convId = Scraper.getConversationId();
1557      var partner = Scraper.getPartnerName();
1558      var nodes = scroll.querySelectorAll('[at-attr="chat_message"]');
1559      var out = [];
1560      nodes.forEach(function (el) {
1561        var textEl = el.querySelector('[at-attr="message_text"], .b-chat__message__text-holder');
1562        var text = textEl ? (textEl.innerText || '').trim() : '';
1563        var images = collectMessageImages(el);
1564        if (!text && !images.length) return; // tip-only messages — skip
1565        var outgoing = el.classList.contains('m-from-me');
1566        var timeEl = el.querySelector('.b-chat__message__time');
1567        var time = timeEl ? timeEl.textContent.trim() : '';
1568        // Content-only key (no volatile time) — keeps dedupe stable across
1569        // rescans. Media-only messages key on the stable image paths.
1570        var content = text || images.map(function (i) { return i.path; }).join(',');
1571        var key = 'o:' + convId + ':' + hash((outgoing ? '1' : '0') + '|' + content);
1572        out.push({
1573          chatWith: partner,
1574          convId: convId,
1575          sender: outgoing ? CONFIG.SELF_LABEL : partner,
1576          text: text,
1577          time: time,
1578          key: key,
1579          images: images,
1580          el: el,
1581        });
1582      });
1583      return out;
1584    },
1585    listConversations: function () {
1586      var items = document.querySelectorAll('.b-chats__item[id]');
1587      return Array.prototype.map.call(items, function (el) {
1588        return {
1589          el: el,
1590          id: el.id,
1591          selected: el.classList.contains('current'),
1592          unread: el.classList.contains('m-unread'),
1593          link: el.querySelector('a.b-chats__item__link'),
1594          // From the LIST row only — reading the list never marks a chat read.
1595          name: Scraper._rowName(el),
1596          sig: rowSignature(el),
1597        };
1598      });
1599    },
1600    _rowName: function (el) {
1601      var n = el.querySelector('.g-user-name, .g-user-realname');
1602      var name = (n ? n.textContent : '').replace(/\\s+/g, ' ').trim();
1603      if (!name) {
1604        Diag.finding('row-name-missing', {
1605          id: el.id || '', leaves: leafTexts(el), html: rowHtml(el),
1606        }, 3);
1607      }
1608      return name;
1609    },
1610    selectConversation: async function (conv) {
1611      var target = conv.link || conv.el;
1612      target.click();
1613      await waitFor(function () { return Scraper.getConversationId() === conv.id; }, 6000, 200);
1614      await waitFor(function () {
1615        var s = Scraper.getMessageScrollEl();
1616        return s && s.querySelector('[at-attr="chat_message"]');
1617      }, 6000, 200);
1618    },
1619    backfill: async function (scroll) {
1620      // Scrolling to the top triggers OnlyFans' infinite-loader (.b-chat__loading-top)
1621      // to fetch older messages. Repeat until the height stops growing.
1622      var last = -1, stable = 0;
1623      for (var i = 0; i < 120 && stable < 3; i++) {
1624        scroll.scrollTop = 0;
1625        await sleep(700);
1626        var h = scroll.scrollHeight;
1627        if (h === last) stable++; else { stable = 0; last = h; }
1628      }
1629      scroll.scrollTop = scroll.scrollHeight;
1630      await sleep(300);
1631    },
1632    // ---- outbound (Matrix reply -> OnlyFans) ----
1633    openConversation: async function (convId) {
1634      convId = String(convId);
1635      if (Scraper.getConversationId() === convId) return true;
1636      var item = document.getElementById(convId);
1637      var link = item && item.querySelector('a.b-chats__item__link');
1638      if (!link) return false; // not in the loaded list
1639      link.click();
1640      return await waitFor(function () {
1641        return Scraper.getConversationId() === convId &&
1642          !!document.querySelector('#make_post_form .tiptap, #make_post_form [contenteditable="true"]');
1643      }, 7000, 200);
1644    },
1645    sendMessage: async function (text, opts) {
1646      var editor = document.querySelector('#make_post_form .tiptap, #make_post_form [contenteditable="true"]');
1647      var btn = document.querySelector('#make_post_form [at-attr="send_btn"], #make_post_form .b-chat__btn-submit');
1648      if (!editor || !btn) return false;
1649      // TipTap/ProseMirror: execCommand('insertText') fires the beforeinput
1650      // events the editor listens for, so its state updates and send enables.
1651      editor.focus();
1652      try { document.execCommand('selectAll', false, null); } catch (e) {}
1653      try { document.execCommand('insertText', false, text); } catch (e) {}
1654      await sleep(250);
1655      // Verify the text actually landed before we consider sending.
1656      if ((editor.textContent || '').indexOf(text.slice(0, Math.min(24, text.length))) === -1) {
1657        log('outbound: editor did not accept text');
1658        return false;
1659      }
1660      // Per-message stage-only: leave the text in the composer, don't submit.
1661      if (opts && opts.stageOnly) return 'staged';
1662      var ready = await waitFor(function () { return !btn.disabled; }, 3000, 100);
1663      if (!ready) return false;
1664      if (CONFIG.OUTBOUND_DRYRUN) return 'dryrun';
1665      btn.click();
1666      await sleep(500);
1667      return true;
1668    },
1669  };
1670`,tt={fansly:{scraper:Ht,dryRun:!1,autoOpen:!1,outbound:!0,grants:["GM_xmlhttpRequest","GM_getValue","GM_setValue","unsafeWindow"],connect:["fansly.com"]},onlyfans:{scraper:jt,dryRun:!1,autoOpen:!1,outbound:!0,grants:["GM_xmlhttpRequest","GM_getValue","GM_setValue","unsafeWindow"],connect:["onlyfans.com","onlyfansassets.com"]}};function Yt(o){return o.replace(/bridge-ingest\/?$/,"bridge-outbox/")}function Kt(o){return o.replace(/bridge-ingest\/?$/,"bridge-ingest-media/")}function zt(o){return o.replace(/bridge-ingest\/?$/,"bridge-unread/")}function Xt(o){try{return new URL(o).host}catch{return""}}function Qt({platform:o,label:p,token:c,ingestUrl:f,match:v,chatUrl:y}){const d=tt[o]||tt.fansly,F=Xt(f),x="2.11.0",N=d.grants.map(D=>`// @grant        ${D}`).join(`
1671`),C=[F].concat(d.connect||[]).filter(Boolean).map(D=>`// @connect      ${D}`).join(`
1672`),P={VERSION:x,PLATFORM:o,INGEST_URL:f,OUTBOX_URL:Yt(f),MEDIA_URL:Kt(f),UNREAD_URL:zt(f),TOKEN:c,DEBUG:!0,VERBOSE:!0,HEARTBEAT_MS:3e5,DIAG_THROTTLE_MS:6e4,DRY_RUN:d.dryRun,AUTO_OPEN:!!d.autoOpen,REPORT_UNREAD:!0,UNREAD_FROM_CHANGES:!0,UNREAD_HEARTBEAT_MS:12e4,BACKFILL:!0,OUTBOUND:!!d.outbound,OUTBOUND_DRYRUN:!1,SELF_LABEL:"me",MEDIA_WAIT_SCANS:10,SCAN_INTERVAL_MS:3e3,FLUSH_INTERVAL_MS:4e3,OUTBOX_POLL_MS:5e3,SPLIT_LINES:!0,LINE_GAP_MIN_MS:2e3,LINE_GAP_MAX_MS:5e3},T=d.dryRun?`  console.log('%c[bridge] ${p} is in DIAGNOSTIC mode — capturing + logging only, NOT sending. Send the inspector report to enable delivery.', 'color:#e0a500;font-weight:bold');
1673`:"";return`// ==UserScript==
1674// @name         Shared Chat Bridge — ${p}
1675// @namespace    shared.chat.bridge
1676// @version      ${x}
1677// @description  Mirrors your ${p} chat into Shared (Matrix). Keep the chat tab open.
1678// @author       Shared
1679// @match        ${v}
1680${C}
1681${N}
1682// @run-at       document-idle
1683// @noframes
1684// ==/UserScript==
1685
1686/* eslint-disable */
1687(function () {
1688  'use strict';
1689
1690  // -- generated for your account ------------------------------------------
1691  var CONFIG = ${JSON.stringify(P,null,2)};
1692
1693${T}${d.scraper}
1694${Wt}
1695})();
1696`}const Jt={class:"matrix-section"},Zt={class:"section-head"},ea={class:"scrapers"},ta={key:0,class:"status"},aa={key:1,class:"status error"},sa={key:2,class:"platform-list"},na={class:"platform-head"},ra={class:"platform-name"},oa={class:"key-row"},ia={class:"key"},la=["onClick"],da=["onClick"],ca={class:"actions"},ua=["onClick"],pa=["disabled","onClick"],ma={key:1,class:"actions"},ga=["disabled","onClick"],fa={class:"scrapers"},va={key:0,class:"status"},ha={key:0,class:"platform-card"},ya={class:"actions"},ba=["disabled"],wa={key:1,class:"platform-card"},_a={class:"bot-field"},ka={class:"key"},Sa={class:"bot-field"},xa={class:"key"},Ca={class:"bot-field"},Ia={class:"key"},Oa={class:"bot-note"},Ea={class:"actions"},Aa=["disabled"],Na=["disabled"],Ta={class:"modal-content"},Da={class:"modal-actions"},Ra={__name:"MatrixSection",setup(o){const p=nt(),c=u(!0),f=u(!1),v=u(""),y=u([]),d=be({}),F=be({}),x=u(!1),N=u(!0),C=u(!1),P=u(!1),T=be({exists:!1,matrix_user_id:null,homeserver_url:"",access_token:null,room_count:0});function D(m){return m?`${m.slice(0,6)}…${m.slice(-4)}`:""}function I(m){F[m]=!F[m]}async function U(m){try{await navigator.clipboard.writeText(m),p.success("Copied.")}catch{}}const $=U;Ye(async()=>{try{const{data:m}=await Oe.getBridgeConfig();v.value=m.ingest_url,y.value=m.platforms}catch{f.value=!0}finally{c.value=!1}try{const{data:m}=await Oe.getBridgeBot();Object.assign(T,m)}catch{}finally{N.value=!1}});async function G(m){if(!(m==="remove"&&!window.confirm("Remove the assistant bot? Its token stops working and it leaves your bridge rooms."))){C.value=!0;try{const{data:l}=await Oe.bridgeBotAction(m);Object.assign(T,l),m==="remove"&&(P.value=!1),p.success(m==="remove"?"Assistant bot removed.":m==="regenerate"?"Token regenerated.":"Assistant bot created.")}catch(l){p.error(l?.response?.data?.error||"Could not update the assistant bot.")}finally{C.value=!1}}}async function w(m,l){d[m.platform]=!0;try{const{data:g}=await Oe.generateBridgeToken(m.platform,l);v.value=g.ingest_url;const B=y.value.findIndex(z=>z.platform===m.platform);B!==-1&&(y.value[B]=g.platform),p.success(l?"Key regenerated.":"Key generated.")}catch(g){p.error(g?.response?.data?.error||"Could not generate key.")}finally{d[m.platform]=!1}}function A(m){return m.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function M(m){const l=Qt({platform:m.platform,label:m.label,token:m.token,ingestUrl:v.value,match:m.match,chatUrl:m.chat_url}),g=window.open("","_blank");if(!g){p.error("Your browser blocked the popup. Allow popups and try again.");return}const B=A(l);g.document.write(`<!doctype html>
1697<html>
1698<head>
1699<meta charset="utf-8" />
1700<title>${m.label} chat bridge — userscript</title>
1701<style>
1702  body { margin: 0; font-family: system-ui, sans-serif; background: #0f0f14; color: #e8e8ef; }
1703  header { padding: 16px 20px; border-bottom: 1px solid #2a2a35; }
1704  h1 { font-size: 16px; margin: 0 0 4px; }
1705  p { margin: 4px 0; font-size: 13px; color: #b7b7c6; }
1706  a { color: #7aa2ff; }
1707  .bar { display: flex; gap: 8px; padding: 12px 20px; }
1708  button { background: #4f6bed; color: #fff; border: 0; border-radius: 6px; padding: 8px 14px; font-size: 13px; cursor: pointer; }
1709  button.copied { background: #2e9e5b; }
1710  textarea { width: 100%; height: calc(100vh - 190px); box-sizing: border-box; border: 0; border-top: 1px solid #2a2a35; background: #16161f; color: #e8e8ef; font-family: ui-monospace, monospace; font-size: 12px; padding: 16px 20px; resize: none; }
1711</style>
1712</head>
1713<body>
1714<header>
1715  <h1>${m.label} chat bridge userscript</h1>
1716  <p>Your key and domain are already filled in below.</p>
1717  <p>1. Install <a href="https://violentmonkey.github.io/get-it/" target="_blank" rel="noopener">Violentmonkey</a>.
1718     2. Copy this script. 3. In Violentmonkey, create a new script, paste, and save.
1719     4. Keep your ${m.label} chat window open.</p>
1720</header>
1721<div class="bar">
1722  <button id="copy">Copy script</button>
1723</div>
1724<textarea id="src" readonly spellcheck="false">${B}</textarea>
1725<script>
1726  var btn = document.getElementById('copy');
1727  btn.addEventListener('click', function () {
1728    var ta = document.getElementById('src');
1729    ta.select();
1730    ta.setSelectionRange(0, ta.value.length);
1731    try {
1732      navigator.clipboard.writeText(ta.value);
1733    } catch (e) {
1734      document.execCommand('copy');
1735    }
1736    btn.textContent = 'Copied!';
1737    btn.className = 'copied';
1738    setTimeout(function () { btn.textContent = 'Copy script'; btn.className = ''; }, 1800);
1739  });
1740<\/script>
1741</body>
1742</html>`),g.document.close()}return(m,l)=>(n(),r("div",Jt,[e("div",Zt,[l[10]||(l[10]=e("h2",{class:"section-title"},"Matrix & Chat",-1)),e("button",{type:"button",class:"info-btn",title:"How the chat bridge works",onClick:l[0]||(l[0]=g=>x.value=!0)}," ⓘ How it works ")]),l[21]||(l[21]=e("p",{class:"section-intro"},[L(" Read and answer all of your messages from one place. Connect a Matrix app, and optionally bridge your "),e("strong",null,"Fansly"),L(" and "),e("strong",null,"OnlyFans"),L(" DMs so they land in the same inbox. ")],-1)),me(qt),e("div",ea,[l[11]||(l[11]=e("h3",{class:"scrapers-title"},"Chat scrapers",-1)),l[12]||(l[12]=e("p",{class:"scrapers-intro"},[L(" Generate a key, install the userscript in "),e("a",{href:"https://violentmonkey.github.io/get-it/",target:"_blank",rel:"noopener"},"Violentmonkey"),L(", then keep the platform's chat window open in your browser — new messages are mirrored into a Matrix room automatically. ")],-1)),c.value?(n(),r("div",ta,"Loading…")):f.value?(n(),r("div",aa," Could not load bridge settings. Reload to try again. ")):(n(),r("div",sa,[(n(!0),r(K,null,ge(y.value,g=>(n(),r("div",{key:g.platform,class:"platform-card"},[e("div",na,[e("span",ra,i(g.label),1),e("span",{class:te(["badge",g.token?"badge-on":"badge-off"])},i(g.token?"Key active":"No key"),3)]),g.token?(n(),r(K,{key:0},[e("div",oa,[e("code",ia,i(F[g.platform]?g.token:D(g.token)),1),e("button",{type:"button",class:"btn-ghost",onClick:B=>I(g.platform)},i(F[g.platform]?"Hide":"Reveal"),9,la),e("button",{type:"button",class:"btn-ghost",onClick:B=>E($)(g.token)},"Copy",8,da)]),e("div",ca,[e("button",{type:"button",class:"btn-primary",onClick:B=>M(g)}," Open userscript ",8,ua),e("button",{type:"button",class:"btn-secondary",disabled:d[g.platform],onClick:B=>w(g,!0)},i(d[g.platform]?"Working…":"Regenerate key"),9,pa)])],64)):(n(),r("div",ma,[e("button",{type:"button",class:"btn-primary",disabled:d[g.platform],onClick:B=>w(g,!1)},i(d[g.platform]?"Generating…":"Generate key"),9,ga)]))]))),128))]))]),e("div",fa,[l[18]||(l[18]=e("h3",{class:"scrapers-title"},"Assistant bot",-1)),l[19]||(l[19]=e("p",{class:"scrapers-intro"},[L(" Create a Matrix identity for an external assistant/analysis bot. It joins all your private chats — direct messages and bridge rooms — so it can read conversations and post suggestions, and it's added to new ones automatically. Its messages stay in Matrix; only your "),e("code",null,">>"),L("-prefixed replies are sent out to a platform. ")],-1)),N.value?(n(),r("div",va,"Loading…")):(n(),r(K,{key:1},[T.exists?(n(),r("div",wa,[e("div",_a,[l[13]||(l[13]=e("span",{class:"bot-label"},"Homeserver",-1)),e("code",ka,i(T.homeserver_url),1),e("button",{type:"button",class:"btn-ghost",onClick:l[2]||(l[2]=g=>U(T.homeserver_url))},"Copy")]),e("div",Sa,[l[14]||(l[14]=e("span",{class:"bot-label"},"Bot user ID",-1)),e("code",xa,i(T.matrix_user_id),1),e("button",{type:"button",class:"btn-ghost",onClick:l[3]||(l[3]=g=>U(T.matrix_user_id))},"Copy")]),e("div",Ca,[l[15]||(l[15]=e("span",{class:"bot-label"},"Access token",-1)),e("code",Ia,i(P.value?T.access_token:D(T.access_token)),1),e("button",{type:"button",class:"btn-ghost",onClick:l[4]||(l[4]=g=>P.value=!P.value)},i(P.value?"Hide":"Reveal"),1),e("button",{type:"button",class:"btn-ghost",onClick:l[5]||(l[5]=g=>U(T.access_token))},"Copy")]),e("p",Oa,[l[16]||(l[16]=L(" In ",-1)),e("strong",null,i(T.room_count),1),l[17]||(l[17]=L(" private chat(s). Treat the token like a password — anyone with it can post as this bot. ",-1))]),e("div",Ea,[e("button",{type:"button",class:"btn-secondary",disabled:C.value,onClick:l[6]||(l[6]=g=>G("regenerate"))},i(C.value?"Working…":"Regenerate token"),9,Aa),e("button",{type:"button",class:"btn-ghost danger",disabled:C.value,onClick:l[7]||(l[7]=g=>
1742G("remove"))}," Remove bot ",8,Na)])])):(n(),r("div",ha,[e("div",ya,[e("button",{type:"button",class:"btn-primary",disabled:C.value,onClick:l[1]||(l[1]=g=>G("create"))},i(C.value?"Creating…":"Create assistant bot"),9,ba)])]))],64))]),x.value?(n(),r("div",{key:0,class:"modal-overlay",onClick:l[9]||(l[9]=ce(g=>x.value=!1,["self"]))},[e("div",Ta,[l[20]||(l[20]=Et('<h3 class="modal-title" data-v-1eb56104>How the chat bridge works</h3><div class="modal-body" data-v-1eb56104><ol data-v-1eb56104><li data-v-1eb56104><strong data-v-1eb56104>Install Violentmonkey.</strong> It&#39;s a free browser extension that runs small scripts on specific sites. Get it from <a href="https://violentmonkey.github.io/get-it/" target="_blank" rel="noopener" data-v-1eb56104> violentmonkey.github.io</a> (Chrome, Edge, Firefox). </li><li data-v-1eb56104><strong data-v-1eb56104>Generate a key</strong> for the platform you want to bridge, then click <em data-v-1eb56104>Open userscript</em>. A window opens with the script already filled in with your key and the correct domain. </li><li data-v-1eb56104><strong data-v-1eb56104>Add it to Violentmonkey.</strong> Copy the script, open the Violentmonkey dashboard, create a new script, paste, and save. </li><li data-v-1eb56104><strong data-v-1eb56104>Keep the chat window open.</strong> The script only runs while the platform&#39;s chat page is open in your browser. Each conversation gets its own Matrix room here. If you close the tab, mirroring pauses until you open it again. </li><li data-v-1eb56104><strong data-v-1eb56104>Unread stays unread.</strong> The script never opens a conversation by itself, so nothing is marked as read behind your back. When a thread has unread mail you get a <code data-v-1eb56104>🔔 Unread message</code> notice in its Matrix room — no message text, nothing read yet. Send <code data-v-1eb56104>/read</code> in that room when you want it opened and synced across; the messages then appear as normal. <code data-v-1eb56104>/refresh</code> (in any bridge room) reloads the platform tab if it ever gets stuck. </li><li data-v-1eb56104><strong data-v-1eb56104>Reply from Matrix.</strong> Your bridged conversations appear alongside your Shared chats. To send a reply back out to the platform, start your message with <code data-v-1eb56104>&gt;&gt;</code> — e.g. <code data-v-1eb56104>&gt;&gt; thanks, talk soon!</code>. Messages without the <code data-v-1eb56104>&gt;&gt;</code> prefix stay in Matrix only, so notes and assistant bots in the room are never sent out. The reply is typed into the platform&#39;s chat and sent, so keep that tab open too. </li></ol><p class="modal-note" data-v-1eb56104> Your key authenticates the script — treat it like a password. If it ever leaks, hit <em data-v-1eb56104>Regenerate key</em> and update the script. </p><p class="modal-note" data-v-1eb56104> Something not working? The script keeps a diagnostic record as things happen. Open the browser console on the platform&#39;s tab and run <code data-v-1eb56104>__bridge.report()</code> — it prints what it has seen (and what failed), including notes kept from earlier sessions. </p></div>',2)),e("div",Da,[e("button",{type:"button",class:"btn-primary",onClick:l[8]||(l[8]=g=>x.value=!1)},"Got it")])])])):b("",!0)]))}},Ua=Ae(Ra,[["__scopeId","data-v-1eb56104"]]),We="0x14a34",ye=84532,ve=u(null);let at=!1;const Ma=new Proxy({},{get(o,p,c){const f=ve.value;if(!f)throw new Error("x402 wallet is not connected");const v=Reflect.get(f,p,c);return typeof v=="function"?v.bind(f):v},has(o,p){const c=ve.value;return c?p in c:!1},ownKeys(o){const p=ve.value;return p?Reflect.ownKeys(p):[]},getOwnPropertyDescriptor(o,p){const c=ve.value;if(c)return Object.getOwnPropertyDescriptor(c,p)}});function Fa(){at||(Vt(fe,Ma),at=!0)}async function He(o,p=!1){if(!o)return"";if(!p&&o.selectedAddress)return o.selectedAddress;const c=await o.request({method:p?"eth_requestAccounts":"eth_accounts"});return Array.isArray(c)&&c.length>0?c[0]:""}const Pa=At("x402Wallet",()=>{const o=u(""),p=u(!1),c=u(null),f=u(!1),v=u(""),y=typeof window<"u",d=q(()=>y&&window.ethereum||null),F=q(()=>d.value!==null),x=q(()=>c.value===ye),N=q(()=>"Base Sepolia"),C=q(()=>p.value&&x.value);function P(){o.value="",p.value=!1,c.value=null,ve.value=null}async function T(w){try{return await w.request({method:"wallet_switchEthereumChain",params:[{chainId:We}]}),c.value=ye,!0}catch(A){if(A.code===4902||A?.message?.includes("Unrecognized chain ID"))try{return await w.request({method:"wallet_addEthereumChain",params:[{chainId:We,chainName:"Base Sepolia",rpcUrls:["https://sepolia.base.org"],nativeCurrency:{name:"Ether",symbol
1742:"ETH",decimals:18},blockExplorerUrls:["https://sepolia.basescan.org"]}]}),c.value=ye,!0}catch(M){return v.value=M?.message||"Failed to add Base network to wallet",!1}return v.value=A?.message||"Wallet must be connected to the Base network",!1}}async function D(w,A){const M=A||await He(w,!0);if(!M)throw new Error("Wallet did not return an account");const m=Nt({chain:Dt,transport:Tt(w)}),l=new Proxy(m,{get(g,B,z){if(B==="account"){const V=w?.selectedAddress||o.value||M||M?.toString?.();return V?{address:V}:void 0}return B==="signTypedData"?async V=>{const J=w?.selectedAddress||o.value||M||await He(w,!0);if(!J)throw new Error("Unable to determine active wallet account");return g.signTypedData({...V,account:J})}:Reflect.get(g,B,z)}});ve.value=l,Fa()}async function I(){if(!F.value)return v.value="No compatible Ethereum wallet detected. Please install Coinbase Wallet or MetaMask.",!1;f.value=!0,v.value="";const w=d.value;try{const A=await w.request({method:"eth_chainId"});if(c.value=parseInt(A,16),A.toLowerCase()!==We&&!await T(w))return f.value=!1,!1;const M=await He(w,!0);if(!M)throw new Error("Wallet did not return any accounts");return o.value=M,p.value=!0,c.value=ye,await D(w,M),!0}catch(A){return v.value=A?.message||"Failed to connect wallet",P(),!1}finally{f.value=!1}}function U(){P(),v.value=""}async function $(w){if(!w||w.length===0){P();return}const A=d.value,M=w[0];if(o.value=M,p.value=!0,!A){v.value="Wallet provider unavailable";return}await D(A,M)}async function G(w){const A=w.startsWith("0x")?parseInt(w,16):Number(w);if(c.value=A,A!==ye){v.value=`Please switch your wallet to the ${N.value} network to continue.`;return}v.value="",p.value&&d.value&&o.value&&await D(d.value,o.value)}if(y){const w=d.value;w&&(w.removeListener?.("accountsChanged",$),w.on?.("accountsChanged",$),w.removeListener?.("chainChanged",G),w.on?.("chainChanged",G))}return{address:o,chainId:c,connected:p,loading:f,error:v,isSupported:F,isCorrectNetwork:x,isReady:C,networkName:N,connect:I,disconnect:U}});function La(o,p=null){const c=parseFloat(o)||0,f=p?.fee_percent??.02,v=p?.fee_fixed??.1,y=c*f+v,d=c+y;return{baseAmount:c,feeAmount:y,totalAmount:d}}function $a(o){return parseFloat(o).toFixed(2).replace(/\.?0+$/,"")}const Ba={class:"modal-content"},Ga={class:"modal-body"},qa={key:0,class:"input-step"},Va={class:"amount-input-group"},Wa={key:0,class:"error-message"},Ha={key:1,class:"fee-breakdown"},ja={class:"fee-row"},Ya={class:"fee-value"},Ka={class:"fee-row"},za={class:"fee-label"},Xa={class:"fee-value"},Qa={class:"fee-row fee-total"},Ja={class:"fee-value"},Za={key:2,class:"fee-explanation"},es={class:"explanation-text"},ts={class:"wallet-section"},as={key:0},ss={key:1},ns={key:2},rs=["disabled"],os={key:0,class:"loading-spinner"},is={key:1},ls={key:2},ds={key:3},cs={key:0,class:"error-message"},us={key:1,class:"error-message"},ps=["disabled"],ms={key:0,class:"loading-spinner"},gs={key:1,class:"processing-step"},fs={class:"processing-amount"},vs={class:"processing-description"},hs={key:2,class:"success-step"},ys={key:0,class:"status-message success"},bs={key:1,class:"success-details"},ws={key:3,class:"error-step"},_s={__name:"AddFundsModal",props:{show:{type:Boolean,default:!1}},emits:["close","credit-updated"],setup(o,{emit:p}){const c=o,f=p,v=Pa(),y=rt(),d=ot(),{address:F,connected:x,isReady:N,isSupported:C,loading:P,error:T}=Rt(v),D=u("input"),I=u(""),U=u(!1),$=u(""),G=u(""),w=u(""),A=u(""),M=q(()=>{const O=parseFloat(I.value);return I.value?isNaN(O)?"Please enter a valid number":O<.01?"Minimum amount is $0.01":O>1e4?"Maximum amount is $10,000.00":"":""}),m=q(()=>!M.value&&parseFloat(I.value)>=.01),l=q(()=>{if(!m.value)return null;const O=d.billingInfo?.fee_config||null;return La(I.value,O)});function g(O){return $a(O)}const B=q(()=>F.value?`${F.value.slice(0,6)}...${F.value.slice(-4)}`:""),z=q(()=>T.value);function V(){$.value="",G.value=""}async function J(){await v.connect()}async function ae(){if(m.value){if(!C.value){T.value="No compatible wallet detected. Please install Coinbase Wallet or MetaMask.";return}if(!(!N.value&&!await v.connect())){U.value=!0,G.value="info",$.value="Waiting for wallet confirmation...",D.value="processing";try{const O=await je.createCryptoPayment(I.value,y.user?.id||y.user?.pk),_=O.headers?.["x-payment-response"];_&&($.value=_),A.value=O.data.transaction_id,$.value=O.data.message||"Payment c
1742onfirmed!",G.value="success",D.value="success",f("credit-updated")}catch(O){console.error("Error creating payment:",O),O.response?.data&&console.error("x402 response payload:",O.response.data);let _=O.response?.data?.error||O?.message||"Failed to create payment";!O.response&&(O?.code===4001||/user rejected/i.test(_))&&(_="Transaction was rejected in your wallet. Please try again."),D.value="error",w.value=_}finally{U.value=!1}}}}function ie(){D.value="input",I.value="",$.value="",G.value="",w.value="",A.value=""}function Z(){ie(),f("close")}return Ee(()=>c.show,O=>{O||ie()}),(O,_)=>o.show?(n(),r("div",{key:0,class:"modal-overlay",onClick:ce(Z,["self"])},[e("div",Ba,[e("div",{class:"modal-header"},[_[1]||(_[1]=e("h2",null,"Add Credit",-1)),e("button",{onClick:Z,class:"close-button"},"×")]),e("div",Ga,[D.value==="input"?(n(),r("div",qa,[_[5]||(_[5]=e("p",{class:"description"}," Add credit to your account using cryptocurrency. Minimum amount: $0.01 ",-1)),e("div",Va,[_[4]||(_[4]=e("label",{for:"amount"},"Credit Amount ($):",-1)),R(e("input",{id:"amount","onUpdate:modelValue":_[0]||(_[0]=he=>I.value=he),type:"number",min:"0.01",max:"10000",step:"0.01",placeholder:"0.01",class:"amount-input",onInput:V},null,544),[[j,I.value]]),M.value?(n(),r("p",Wa,i(M.value),1)):b("",!0),m.value&&l.value?(n(),r("div",Ha,[e("div",ja,[_[2]||(_[2]=e("span",{class:"fee-label"},"Credit Amount:",-1)),e("span",Ya,"$"+i(g(l.value.baseAmount)),1)]),e("div",Ka,[e("span",za," Processing Fee ("+i(((E(d).billingInfo?.fee_config?.fee_percent||.02)*100).toFixed(0))+"% + $"+i(g(E(d).billingInfo?.fee_config?.fee_fixed||.1))+"): ",1),e("span",Xa,"$"+i(g(l.value.feeAmount)),1)]),e("div",Qa,[_[3]||(_[3]=e("span",{class:"fee-label"},"Total to Pay:",-1)),e("span",Ja,"$"+i(g(l.value.totalAmount)),1)])])):b("",!0),m.value&&l.value?(n(),r("div",Za,[e("p",es," In order for us to guarantee the creator received 100% of what you spend on them, we need to cover costs of both receiving and sending money. We have kept it as low as possible. "+i(((E(d).billingInfo?.fee_config?.fee_percent||.02)*100).toFixed(0))+"% plus "+i((E(d).billingInfo?.fee_config?.fee_fixed||.1)*100)+"c per transaction. The creator receives 100% of what they withdraw. Their fees are covered by this too! ",1)])):b("",!0)]),e("div",ts,[e("div",{class:te(["wallet-status",{connected:E(N)}])},[E(N)?(n(),r("span",as,"Connected wallet: "+i(B.value),1)):E(x)?(n(),r("span",ss,"Please switch your wallet to the Base network to continue.")):(n(),r("span",ns,"Connect a Base-compatible wallet (Coinbase Wallet or MetaMask) to continue."))],2),e("button",{onClick:J,disabled:E(P),class:"btn btn-secondary wallet-button"},[E(P)?(n(),r("span",os)):b("",!0),E(N)?(n(),r("span",is,"Wallet Connected")):E(x)?(n(),r("span",ls,"Switch Network")):(n(),r("span",ds,"Connect Wallet"))],8,rs),z.value?(n(),r("p",cs,i(z.value),1)):E(C)?b("",!0):(n(),r("p",us," No compatible wallet detected. Install Coinbase Wallet or MetaMask. "))]),e("button",{onClick:ae,disabled:!m.value||U.value||E(P),class:"btn btn-primary payment-button"},[U.value?(n(),r("span",ms)):b("",!0),!U.value&&l.value?(n(),r(K,{key:1},[L(" Pay $"+i(g(l.value.totalAmount))+" USDC ",1)],64)):U.value?(n(),r(K,{key:3},[L(" Creating Payment... ")],64)):(n(),r(K,{key:2},[L(" Pay with Crypto ")],64))],8,ps)])):D.value==="processing"?(n(),r("div",gs,[_[6]||(_[6]=e("div",{class:"processing-icon"},"⏳",-1)),_[7]||(_[7]=e("h3",null,"Confirm in Your Wallet",-1)),e("p",fs,"Total Amount: $"+i(l.value?g(l.value.totalAmount):I.value),1),e("p",vs," Approve the transaction in your connected wallet. Once confirmed on Base, we will automatically credit your account with $"+i(I.value)+". ",1),$.value?(n(),r("div",{key:0,class:te(["status-message",G.value])},i($.value),3)):b("",!0)])):D.value==="success"?(n(),r("div",hs,[_[8]||(_[8]=e("div",{class:"success-icon"},"✓",-1)),_[9]||(_[9]=e("h3",null,"Payment Successful!",-1)),e("p",null,"Your account has been credited 
1742with $"+i(I.value),1),$.value?(n(),r("p",ys,i($.value),1)):b("",!0),A.value?(n(),r("p",bs,"Transaction ID: "+i(A.value),1)):b("",!0),e("button",{onClick:Z,class:"btn btn-primary"},"Done")])):D.value==="error"?(n(),r("div",ws,[_[10]||(_[10]=e("div",{class:"error-icon"},"✗",-1)),_[11]||(_[11]=e("h3",null,"Payment Failed",-1)),e("p",null,i(w.value),1),e("div",{class:"error-actions"},[e("button",{onClick:ie,class:"btn btn-secondary"},"Try Again"),e("button",{onClick:Z,class:"btn btn-outline"},"Cancel")])])):b("",!0)])])])):b("",!0)}},ks=Ae(_s,[["__scopeId","data-v-312cedca"]]),Ss=["aria-live"],xs={class:"error-content"},Cs={key:0,class:"error-icon"},Is={key:0,width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Os={key:1,width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Es={class:"error-text"},As={key:0,class:"error-title"},Ns={class:"error-description"},Ts={key:1,class:"error-details"},Ds={key:1,class:"error-actions"},Rs=["disabled","aria-label"],Us={key:0,class:"spinner",width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},Ms={key:0,class:"error-progress"},Fs={__name:"ErrorMessage",props:{show:{type:Boolean,default:!1},type:{type:String,default:"error",validator:o=>["error","warning","info","success"].includes(o)},title:{type:String,default:""},message:{type:String,required:!0},details:{type:String,default:""}
1742,showDetails:{type:Boolean,default:!1},canRetry:{type:Boolean,default:!1},canDismiss:{type:Boolean,default:!0},retryText:{type:String,default:"Retry"},action:{type:String,default:""},showIcon:{type:Boolean,default:!0},showProgress:{type:Boolean,default:!1},progress:{type:Number,default:null,validator:o=>o===null||o>=0&&o<=100},autoHide:{type:Boolean,default:!1},autoHideDelay:{type:Number,default:5e3},isLiveRegion:{type:Boolean,default:!0}},emits:["retry","dismiss","update:show"],setup(o,{emit:p}){const c=o,f=p,v=u(!1),y=u(!navigator.onLine),d=u(null),F=q(()=>{const I=[`error-${c.type}`];return y.value&&I.push("error-offline"),I});function x(){v.value||(v.value=!0,f("retry"),setTimeout(()=>{v.value=!1},1e3))}function N(){f("dismiss"),f("update:show",!1)}function C(){y.value=!1}function P(){y.value=!0}function T(){c.autoHide&&c.show&&(d.value=setTimeout(()=>{f("update:show",!1)},c.autoHideDelay))}function D(){d.value&&(clearTimeout(d.value),d.value=null)}return Ee(()=>c.show,I=>{I?T():D()}),Ye(()=>{window.addEventListener("online",C),window.addEventListener("offline",P),c.show&&c.autoHide&&T()}),st(()=>{window.removeEventListener("online",C),window.removeEventListener("offline",P),D()}),(I,U)=>o.show?(n(),r("div",{key:0,class:te(["error-message",F.value]),role:"alert","aria-live":o.isLiveRegion?"assertive":"polite","aria-atomic":!0},[e("div",xs,[o.showIcon?(n(),r("div",Cs,[y.value?(n(),r("svg",Is,[...U[0]||(U[0]=[e("path",{d:"M21 12a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9c2.4 0 4.6.9 6.3 2.4"},null,-1),e("path",{d:"M21 3v6h-6"},null,-1),e("path",{d:"M3 12a9 9 0 0 0 9 9"},null,-1)])])):(n(),r("svg",Os,[...U[1]||(U[1]=[e("circle",{cx:"12",cy:"12",r:"10"},null,-1),e("path",{d:"m15 9-6 6"},null,-1),e("path",{d:"m9 9 6 6"},null,-1)])]))])):b("",!0),e("div",Es,[o.title?(n(),r("div",As,i(o.title),1)):b("",!0),e("div",Ns,i(o.message),1),o.showDetail
1742s&&o.details?(n(),r("div",Ts,i(o.details),1)):b("",!0)]),o.canRetry||o.canDismiss?(n(),r("div",Ds,[o.canRetry?(n(),r("button",{key:0,onClick:x,disabled:v.value,class:"btn btn-retry btn-touch","aria-label":`Retry ${o.action||"action"}`},[v.value?(n(),r("svg",Us,[...U[2]||(U[2]=[e("path",{d:"M21 12a9 9 0 0 1-9 9 9 9 0 0 1-9-9 9 9 0 0 1 9-9c2.4 0 4.6.9 6.3 2.4"},null,-1)])])):b("",!0),L(" "+i(v.value?"Retrying...":o.retryText),1)],8,Rs)):b("",!0),o.canDismiss?(n(),r("button",{key:1,onClick:N,class:"btn btn-dismiss btn-touch","aria-label":"Dismiss error message"},[...U[3]||(U[3]=[e("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},[e("path",{d:"m18 6-12 12"}),e("path",{d:"m6 6 12 12"})],-1)])])):b("",!0)])):b("",!0)]),o.showProgress&&o.progress!==null?(n(),r("div",Ms,[e("div",{class:"progress-bar",style:Ut({width:`${o.progress}%`})},null,4)])):b("",!0)],10,Ss)):b("",!0)}},Ps=Ae(Fs,[["__scopeId","data-v-b833b666"]]),S={NETWORK:"network",AUTHENTICATION:"authentication",AUTHORIZATION:"authorization",VALIDATION:"validation",SERVER:"server",CLIENT:"client",OFFLINE:"offline"},oe={[S.NETWORK]:"Network connection failed. Please check your internet connection.",[S.AUTHENTICATION]:"Your session has expired. Please log in again.",[S.AUTHORIZATION]:"You don't have permission to perform this action.",[S.VALIDATION]:"Please check your input and try again.",[S.SERVER]:"Server error occurred. Please try again later.",[S.CLIENT]:"An unexpected error occurred.",[S.OFFLINE]:"You appear to be offline. Please check your connection."};function Ls(o,p={}){const{fallbackMessage:c=oe[S.CLIENT],includeDetails:f=!1,action:v=""}=p;let y=S.CLIENT,d=c,F="",x=!1,N=null;if(!o)return{type:y,message:d,details:F,canRetry:x,statusCode:N};if(o.response){N=o.response.status;const C=o.response.data;N===401?(y=S.AUTHENTICATION,d=oe[S.AUTHENTICATION]):N===403?(y=S.AUTHORIZATION,d=oe[S.AUTHORIZATION]):N===400?(y=S.VALIDATION,d=$s(C)):N>=500?(y=S.SERVER,d=oe[S.SERVER],x=!0):N>=400&&(y=S.CLIENT,d=C?.message||C?.detail||c),f&&(F=JSON.stringify(C,null,2))}else o.request?(y=S.NETWORK,d=oe[S.NETWORK],x=!0,navigator.onLine||(y=S.OFFLINE,d=oe[S.OFFLINE])):(d=o.message||c,f&&(F=o.stack||""));return{type:y,message:d,details:F,canRetry:x,statusCode:N,action:v}}function $s(o){if(!o)return oe[S.VALIDATION];if(typeof o=="object"){const p=[];o.non_field_errors&&p.push(...o.non_field_errors);for(const[c,f]of Object.entries(o))c!=="non_field_errors"&&Array.isArray(f)&&p.push(...f);if(p.length>0)return p.join(" ");if(o.message)return o.message;if(o.detail)return o.detail}return oe[S.VALIDATION]}function Bs(o,p={}){const c=Ls(o,p);return{show:!0,type:c.type===S.OFFLINE?"warning":"error",title:Gs(c.type),message:c.message,details:c.details,canRetry:c.canRetry,canDismiss:!0,retryText:"Retry",action:c.action,showIcon:!0,showDetail
1742s:p.includeDetails||!1,autoHide:c.type===S.SUCCESS,autoHideDelay:5e3}}function Gs(o){return{[S.NETWORK]:"Connection Error",[S.AUTHENTICATION]:"Authentication Required",[S.AUTHORIZATION]:"Access Denied",[S.VALIDATION]:"Invalid Input",[S.SERVER]:"Server Error",[S.CLIENT]:"Error",[S.OFFLINE]:"Offline"}[o]||"Error"}const qs={class:"settings-page"},Vs={class:"container"},Ws={class:"section-nav","aria-label":"Settings sections"},Hs=["href","onClick"],js={id:"profile",class:"settings-section"},Ys={key:0,class:"section-card"},Ks={class:"profile-picture-container"},zs={class:"avatar-wrapper"},Xs=["src"],Qs={key:1,class:"avatar-placeholder"},Js={class:"avatar-controls"},Zs={class:"section-card"},en={class:"section-card"},tn={class:"section-card"},an={class:"checkbox-label"},sn=["disabled"],nn={class:"section-card"},rn={class:"radio-options"},on={class:"radio-label"},ln={class:"radio-label"},dn={class:"radio-label"},cn={class:"radio-label"},un={class:"section-card"},pn={class:"radio-options"},mn={class:"radio-label"},gn={class:"radio-label"},fn={class:"radio-label"},vn={class:"section-card"},hn={class:"form-stack"},yn={class:"password-buttons"},bn=["disabled"],wn={key:0,id:"creator-settings",class:"settings-section"},_n={class:"section-card"},kn={class:"section-card"},Sn={class:"radio-options"},xn={class:"radio-label"},Cn={class:"radio-label"},In={class:"radio-label"},On={class:"section-card"},En={key:1,id:"viewer-details",class:"settings-section"},An={class:"section-card"},Nn=["min"],Tn=["disabled"],Dn={class:"section-card"},Rn={key:0,class:"loading"},Un={key:1,class:"empty-message"},Mn={key:2,class:"viewers-list"},Fn={class:"viewer-info"},Pn={class:"viewer-details"},Ln={class:"detail-item"},$n={class:"detail-item"},Bn={key:0,class:"detail-item"},Gn={key:0,class:"expired-label"},qn={class:"viewer-actions"},Vn=["onClick"],Wn=["onClick"],Hn={key:2,id:"subscription",class:"settings-section"},jn={class:"section-card"},Yn={class:"checkbox-label"},Kn={key:0,class:"pricing-grid"},zn={class:"pricing-item"},Xn={class:"price-input-group"},Qn={class:"pricing-item"},Jn={class:"price-input-group"},Zn={class:"form-actions"},er=["disabled"],tr={id:"billing",class:"settings-section"},ar={class:"section-card"},sr={class:"credit-balance-display"},nr={class:"credit-amount"},rr={key:0,class:"section-card"},or={class:"wallet-input-group"},ir={key:0,class:"error-text"},lr=["disabled"],dr={key:1,class:"section-card"},cr={class:"billing-header"},ur={key:0,class:"loading"},pr={key:1,class:"error-text"},mr={key:2,class:"billing-content"},gr={class:"billing-cards"},fr={class:"billing-card"},vr={class:"credit-amount-sm"},hr={key:0,class:"credit-warning"},yr={key:1,class:"credit-depleted"},br={class:"billing-card"},wr={class:"usage-amount"},_r={class:"cost-amount"},kr={class:"billing-card"},Sr={class:"usage-amount"},xr={class:"cost-amount"},Cr={class:"billing-card"},Ir={class:"viewer-breakdown"},Or={key:0,class:"empty-message"},Er={key:1,class:"viewer-table"},Ar={key:1},Nr={class:"section-card"},Tr={key:0,class:"loading"},Dr={key:1,class:"empty-message"},Rr={key:2},Ur={class:"transactions-list"},Mr={class:"transaction-main"},Fr={class:"transaction-type"},Pr={class:"transaction-details"},Lr={class:"transaction-parties"},$r={key:0},Br={key:1},Gr={key:0},qr={key:1},Vr={key:0,class:"transaction-description"},Wr={class:"transaction-date"},Hr={key:0,class:"pagination-controls"},jr=["disabled"],Yr={class:"pagination-info"},Kr={key:0},zr=["disabled"],Xr={class:"about-link"},Qr={class:"modal-body"},Jr={key:0,class:"loading"},Zr={key:1,class:"empty-message"},eo={key:2,class:"followers-table"},to={class:"follower-info"},ao=["src","alt"],so={key:1,class:"follower-avatar-placeholder"},no={class:"follower-username"},ro={class:"follower-email"},oo=["href"],io={key:1,class:"follower-email-hidden"},lo={__name:"ProfileView",setup(o){const p=$t(),c=Mt(),f=rt(),v=ot(),y=nt(),d=q(()=>f.user),F=q(()=>f.isViewer),x=q(()=>f.isCreator),N=Bt(),C=u(N.isOnline),P=[{id:"profile",label:"Profile",creatorOnly:!1},{id:"creator-settings",label:"Creator Settings",creatorOnly:!0},{id:"viewer-details",label:"Viewer Details",creatorOnly:!0},{id:"subscription",label:"Subscription",creatorOnly:!0},{id:"billing",label:"Billing and Usage",creatorOnly:!1}],T=q(()=>P.filter(s=>!s.creatorOnly||x.value)),D=u("profile"),I=u(null),U=u(null),$=u(!1),G=u(""),w=u("success"),A=u(0),M=u([]),m=u(!1),l=u(1),g=u(1),B=u(0),z=u(!1),V=u(""),J=u(!1),ae=u(""),ie=u(!1),Z=u(null),O=u(null),_=u([]),he=u(!1),we=u(!1),Ne=u(!1),Te=u([]),De=u(!1),_e=u(!1),le=u(!0),Re=u(!1),h=be({bio:"",email:"",watermark_text:"",avatar:null,nsfw_access_mode:1,notification_preference:"ALL"}),W=be({old_password:"",new_password:"",new_password2:""}),Y=u({viewerUsername:"",accessLevel:"FREE",expiresAt:Ke()}),H=u({subscriptions_enabled:!1,member_price:null,vip_price:null}),ee=u({profile_state:1});function it(){return new Date().toISOString().split("T")[0]}function Ke(){const s=new Date;return s.setDate(s.getDate()+30),s.toISOString().split("T")[0]}function ke(s){return parseFloat(s||0).toFixed(2).replace(/\.?0+$/,"")}function Se(s){const t=parseFloat(s||0);return t>=1e3?`${(t/1e3).toFixed(1)}TB`:t>=1?`${t.toFixed(1)}GB`:`${(t*1e3).toFixed(0)}MB`}function Ue(s){return new Date(s).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function X(s,t="success"){G.value=s,w.value=t,setTimeout(()=>{G.value=""},5e3)}function se(s,t={}){O.value=Bs(s,{action:t.action||"operation",includeDetails:t.includeDetails||!1})}function ue(){O.value=null}function lt(){ue()}function Me(s){const t=document.getElementById(s);if(!t)return;D.value=s;const a=t.getBoundingClientRect().top+window.scrollY-130;window.scrollTo({top:a,behavior:"smooth"}),history.replaceState(null,"",`#${s}`)}function ze(){I.value&&I.value.disconnect();const t=T.value.map(k=>k.id).map(k=>
1742document.getElementById(k)).filter(Boolean);t.length&&(I.value=new IntersectionObserver(k=>{const a=k.filter(ne=>ne.isIntersecting).sort((ne,Ce)=>Ce.intersectionRatio-ne.intersectionRatio);a[0]&&(D.value=a[0].target.id)},{rootMargin:"-20% 0px -60% 0px",threshold:[0,.25,.5]}),t.forEach(k=>I.value.observe(k)))}async function dt(s){const t=s.target.files[0];if(!t)return;const k=new Image,a=document.createElement("canvas"),ne=a.getContext("2d");k.onload=()=>{a.width=200,a.height=200;const pe=Math.max(200/k.width,200/k.height),It=(200-k.width*pe)/2,Ot=(200-k.height*pe)/2;ne.drawImage(k,It,Ot,k.width*pe,k.height*pe),h.avatar=a.toDataURL("image/png")};const Ce=new FileReader;Ce.onload=pe=>{k.src=pe.target.result},Ce.readAsDataURL(t),await ut(t)}function ct(s,t){const k=d.value?.[s];return String(k??"")===String(t??"")}async function de(s,t){if(!ct(s,t)){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"saving profile"});return}ue();try{await f.updateProfile({[s]:t??""}),X("Saved","success")}catch(k){console.error("Error updating profile:",k),se(k,{action:"saving profile"}),d.value&&s in h&&(h[s]=d.value[s]??(s==="notification_preference"?"ALL":""))}}}async function ut(s){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"saving profile"});return}ue();try{const t=new FormData;t.append("avatar",s),await f.updateProfile(t),X("Profile picture saved","success")}catch(t){console.error("Error updating avatar:",t),se(t,{action:"saving profile"}),h.avatar=d.value?.avatar||null}}async function pt(){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"changing password"});return}if(W.new_password!==W.new_password2){X("New passwords do not match","error");return}if(W.new_password.length<8){X("Password must be at least 8 characters long","error");return}$.value=!0,ue();try{await re.changePassword(W),X("Password changed successfully!","success"),W.old_password="",W.new_password="",W.new_password2=""}catch(s){console.error("Error changing password:",s),se(s,{action:"changing password"})}finally{$.value=!1}}function mt(){f.logout(),p.push("/login")}function gt(s){return s.source_user&&s.source_user.id===d.value?.id}function Fe(s){return s.source_user&&s.source_user.id===d.value?.id?-Math.abs(s.amount):Math.abs(s.amount)}async function Xe(){try{const s=await re.getCreditBalance();A.value=parseFloat(s.data.credit_balance||0)}catch(s){console.error("Error loading credit balance:",s),A.value=0}}async function Pe(s=1){m.value=!0;try{const t=await re.getTransactionHistory({page_size:5,page:s});M.value=t.data.results||[],l.value=t.data.page||1,g.value=Math.ceil((t.data.count||0)/5),B.value=t.data.count||0}catch(t){console.error("Error loading transaction history:",t),M.value=[]}finally{m.value=!1}}function Qe(s){s>=1&&s<=g.value&&Pe(s)}async function ft(){try{const s=await je.getWalletAddress();V.value=s.data.wallet_address||""}catch(s){console.error("Error loading wallet address:",s),V.value=""}}async function vt(){if(!C.value){se(new Error("You are currently offline. Please check your connection and try again."),{action:"saving wallet address"});return}const s=V.value.trim();if(!s){ae.value="Wallet address is required";return}if(!s.startsWith("0x")||s.length!==42){ae.value="Invalid wallet address format. Must be a valid Ethereum address (0x...)";return}J.value=!0,ae.value="",ue();try{await je.setWalletAddress(s),X("Wallet address saved successfully!","success")}catch(t){console.error("Error saving wallet address:",t),se(t,{action:"saving wallet address"})}finally{J.value=!1}}function ht(){Xe(),Pe(),x.value&&v.fetchBillingInfo()}async function Le(){if(h.nsfw_access_mode===2&&d.value.nsfw_access_mode!==2)Z.value=2,ie.value=!0,h.nsfw_access_mode=d.value.nsfw_access_mode??1;else try{await f.updateProfile({nsfw_access_mode:h.nsfw_access_mode}),X("18+ access setting updated successfully","success")}catch(s){console.error("Failed to update NSFW access mode:",s),X("Failed to update 18+ access setting","error"),h.nsfw_access_mode=d.value.nsfw_access_mode??1}}function $e(){ie.value=!1,Z.value=null,h.nsfw_access_mode=d.value.nsfw_access_mode??1}async function yt(){try{await f.updateProfile({nsfw_access_mode:Z.value}),h.nsfw_access_mode=Z.value,X("18+ access setting updated successfully","success")}catch(s){console.error("Failed to update NSFW access mode:",s),X("Failed to update 18+ access setting","error"),h.nsfw_access_mode=d.value.nsfw_access_mode??1}$e()}async function Be(){try{const s=await re.updateProfile({profile_state:ee.value.profile_state});f.user={...f.user,...s.data},y.success("Page visibility updated")}catch(s){console.error("Failed to update page state:",s),y.error("Failed to update page visibility"),ee.value.profile_state=f.user?.profile_state??1}}async function xe(){he.value=!0;try{const s=await Ie.getMyViewers();_.value=s.data}catch(s){console.error("Failed to fetch viewers:",s),y.error("Failed to load viewers")}finally{he.value=!1}}async function bt(){we.value=!0;try{const s={access_level:Y.value.accessLevel};if(Y.value.expiresAt){const t=new Date,k=new Date(Y.value.expiresAt);k.setHours(t.getHours(),t.getMinutes(),t.getSeconds()),s.expires_at=k.toISOString()}await Ie.grantAccessByUsername(Y.value.viewerUsername,s),y.success("Access granted successfully!"),Y.value={viewerUsername:"",accessLevel:"FREE",expiresAt:Ke()},await xe()}catch(s){console.error("Failed to grant access:",s),y.error("Failed to grant access. Please check the username and try again.")}finally{we.value=!1}}async function wt(s){const t=prompt("Enter new access level (FREE, MEMBER, VIP):");if(!t)return;const k=prompt("Enter expiration date (YYYY-MM-DD) or leave empty:");try{const a={access_level:t.toUpperCase()};k&&(a.expires_at=new Date(k).toISOString()),await Ie.grantAccessByUsername(s.username,a),y.success("Access updated successfully!"),await xe()}catch(a){console.error("Failed to update access:",a),y.error("Failed to update access")}}async function _t(s){if(confirm("Are you sure you want to revoke access for this viewer?"))try{await Ie.revokeAccessByUsername(s.username),y.success("Access revoked successfully!"),await xe()}catch(t){console.error("Failed to revoke access:",t),y.error("Failed to revoke access")}}async function kt(){_e.value=!0;try{const s=await re.updateProfile({subscriptions_enabled:H.value.subscriptions_enabled,member_price:H.value.subscriptions_enabled?H.value.member_price:null,vip_price:H.value.subscriptions_enabled?H.value.vip_price:null});f.user={...f.user,...s.data},y.success("Subscription settings updated successfully!")}catch(s){console.error("Failed to update subscription settings:",s),y.error("Failed to update subscription settings. Please try again.")}finally{_e.value=!1}}async function St(){Ne.value=!0,De.value=!0;try{const s=await re.getFollowers();Te.value=s.data.results||[]}catch(s){console.error("Failed to fetch followers:",s),y.error("Failed to load followers")}finally{De.value=!1}}function Je(){Ne.value=!1}
1742async function xt(){try{const s=await re.getEmailSharingPreference();le.value=s.data.share_email_with_creators!==!1}catch(s){console.error("Failed to load email sharing preference:",s)}}async function Ct(){Re.value=!0;try{const s=await re.updateEmailSharingPreference(le.value);le.value=s.data.share_email_with_creators,y.success(s.data.message||"Email sharing preference updated")}catch(s){console.error("Failed to update email sharing preference:",s),le.value=!le.value,y.error("Failed to update email sharing preference")}finally{Re.value=!1}}let Ge=null;return Ye(async()=>{Ge=N.addListener(t=>{C.value=t.isOnline}),d.value&&(h.bio=d.value.bio||"",h.email=d.value.email||"",h.watermark_text=d.value.watermark_text||"",h.avatar=d.value.avatar||null,h.nsfw_access_mode=d.value.nsfw_access_mode??1,h.notification_preference=d.value.notification_preference||"ALL",xt()),Xe(),Pe(),x.value&&(ft(),v.fetchBillingInfo(),xe(),f.user&&(H.value={subscriptions_enabled:f.user.subscriptions_enabled||!1,member_price:f.user.member_price||null,vip_price:f.user.vip_price||null},ee.value={profile_state:f.user.profile_state??1})),await qe(),ze();const s=(c.hash||window.location.hash||"").replace(/^#/,"");s&&T.value.some(t=>t.id===s)&&setTimeout(()=>Me(s),50)}),st(()=>{Ge&&Ge(),I.value&&I.value.disconnect()}),Ee(x,async()=>{await qe(),ze()}),Ee(()=>c.hash,s=>{const t=(s||"").replace(/^#/,"");t&&T.value.some(k=>k.id===t)&&qe(()=>Me(t))}),(s,t)=>{const k=Pt("router-link");return n(),r("div",qs,[me(Gt),e("div",Vs,[t[108]||(t[108]=e("h1",{class:"page-title"},"Settings",-1)),e("nav",Ws,[(n(!0),r(K,null,ge(T.value,a=>(n(),r("a",{key:a.id,href:`#${a.id}`,class:te({active:D.value===a.id}),onClick:ce(ne=>Me(a.id),["prevent"])},i(a.label),11,Hs))),128))]),e("section",js,[t[60]||(t[60]=e("h2",{class:"section-heading"},"Profile",-1)),F.value?b("",!0):(n(),r("div",Ys,[t[39]||(t[39]=e("h3",{class:"card-title"},"Profile Picture",-1)),e("div",Ks,[e("div",zs,[h.avatar?(n(),r("img",{key:0,src:h.avatar,alt:"Profile picture",class:"avatar-image",loading:"lazy"},null,8,Xs)):(n(),r("div",Qs,i(d.value?.username?.[0]?.toUpperCase()||"?"),1))]),e("div",Js,[e("input",{ref_key:"avatarInput",ref:U,type:"file",accept:"image/*",onChange:dt,class:"hidden"},null,544),e("button",{onClick:t[0]||(t[0]=a=>s.$refs.avatarInput.click()),class:"btn btn-primary btn-touch"}," Choose Image "),t[38]||(t[38]=e("p",{class:"recommended-text"}," Recommended: Square image, at least 200x200 pixels ",-1))])])])),e("div",Zs,[t[40]||(t[40]=e("h3",{class:"card-title"},"Biography",-1)),R(e("textarea",{"onUpdate:modelValue":t[1]||(t[1]=a=>h.bio=a),rows:"5",placeholder:"Tell us about yourself...",class:"input-touch",onBlur:t[2]||(t[2]=a=>de("bio",h.bio))},null,544),[[j,h.bio]])]),e("div",en,[t[41]||(t[41]=e("h3",{class:"card-title"},"Email",-1)),R(e("input",{"onUpdate:modelValue":t[3]||(t[3]=a=>h.email=a),type:"email",placeholder:"[email protected]",class:"input-touch",onBlur:t[4]||(t[4]=a=>de("email",h.email))},null,544),[[j,h.email]])]),e("div",tn,[t[43]||(t[43]=e("h3",{class:"card-title"},"Share Email with Creators",-1)),t[44]||(t[44]=e("p",{class:"help-text"}," When you follow a creator, they can see your email so they can contact you. Turn this off to keep your email private from creators you follow. ",-1)),e("label",an,[R(e("input",{type:"checkbox","onUpdate:modelValue":t[5]||(t[5]=a=>le.value=a),class:"checkbox-input",disabled:Re.value,onChange:Ct},null,40,sn),[[et,le.value]]),t[42]||(t[42]=L(" Share my email with creators I follow ",-1))])]),e("div",nn,[t[49]||(t[49]=e("h3",{class:"card-title"},"Notification Preferences",-1)),t[50]||(t[50]=e("p",{class:"help-text"}," Choose which notifications you want to receive. Browser push notifications will be sent based on your preference. ",-1)),e("div",rn,[e("label",on,[R(e("input",{type:"radio","onUpdate:modelValue":t[6]||(t[6]=a=>h.notification_preference=a),value:"SILENT",class:"radio-input",onChange:t[7]||(t[7]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[45]||(t[45]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Silent (None)"),e("div",{class:"radio-description"},"No notifications will be sent")],-1))]),e("label",ln,[R(e("input",{type:"radio","onUpdate:modelValue":t[8]||(t[8]=a=>h.notification_preference=a),value:"CRITICAL",class:"radio-input",onChange:t[9]||(t[9]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[46]||(t[46]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Critical (Account Issues)"),e("div",{class:"radio-description"},"Only critical account-related notifications")],-1))]),e("label",dn,[R(e("input",{type:"radio","onUpdate:modelValue":t[10]||(t[10]=a=>h.notification_preference=a),value:"MESSAGES",class:"radio-input",onChange:t[11
1742]||(t[11]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[47]||(t[47]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Messages (Only Messages and Critical)"),e("div",{class:"radio-description"},"Messages and critical notifications only")],-1))]),e("label",cn,[R(e("input",{type:"radio","onUpdate:modelValue":t[12]||(t[12]=a=>h.notification_preference=a),value:"ALL",class:"radio-input",onChange:t[13]||(t[13]=a=>de("notification_preference",h.notification_preference))},null,544),[[Q,h.notification_preference]]),t[48]||(t[48]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"All (All Notifications)"),e("div",{class:"radio-description"},"Receive all notifications (default)")],-1))])])]),e("div",un,[t[54]||(t[54]=e("h3",{class:"card-title"},"18+ Content Access",-1)),t[55]||(t[55]=e("p",{class:"help-text"}," Control your access to adult content on the platform. ",-1)),e("div",pn,[e("label",mn,[R(e("input",{type:"radio",value:0,"onUpdate:modelValue":t[14]||(t[14]=a=>h.nsfw_access_mode=a),class:"radio-input",onChange:Le},null,544),[[Q,h.nsfw_access_mode]]),t[51]||(t[51]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Hidden"),e("div",{class:"radio-description"},"Do not show NSFW creators in Discover")],-1))]),e("label",gn,[R(e("input",{type:"radio",value:1,"onUpdate:modelValue":t[15]||(t[15]=a=>h.nsfw_access_mode=a),class:"radio-input",onChange:Le},null,544),[[Q,h.nsfw_access_mode]]),t[52]||(t[52]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"SFW Mode"),e("div",{class:"radio-description"},"Show NSFW creators as restricted tiles in Discover")],-1))]),e("label",fn,[R(e("input",{type:"radio",value:2,"onUpdate:modelValue":t[16]||(t[16]=a=>h.nsfw_access_mode=a),class:"radio-input",onChange:Le},null,544),[[Q,h.nsfw_access_mode]]),t[53]||(t[53]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"NSFW Mode"),e("div",{class:"radio-description"},"Full access to all adult content")],-1))])])]),e("div",vn,[t[59]||(t[59]=e("h3",{class:"card-title"},"Change Password",-1)),e("div",hn,[e("div",null,[t[56]||(t[56]=e("label",null,"Current Password",-1)),R(e("input",{"onUpdate:modelValue":t[17]||(t[17]=a=>W.old_password=a),type:"password",class:"input-touch"},null,512),[[j,W.old_password]])]),e("div",null,[t[57]||(t[57]=e("label",null,"New Password",-1)),R(e("input",{"onUpdate:modelValue":t[18]||(t[18]=a=>W.new_password=a),type:"password",class:"input-touch"},null,512),[[j,W.new_password]])]),e("div",null,[t[58]||(t[58]=e("label",null,"Confirm New Password",-1)),R(e("input",{"onUpdate:modelValue":t[19]||(t[19]=a=>W.new_password2=a),type:"password",class:"input-touch"},null,512),[[j,W.new_password2]])]),e("div",yn,[e("button",{onClick:pt,disabled:$.value,class:"btn btn-primary btn-touch"},i($.value?"Changing...":"Change Password"),9,bn),e("button",{onClick:mt,class:"btn btn-secondary btn-touch"}," Logout ")])])])]),x.value?(n(),r("section",wn,[t[68]||(t[68]=e("h2",{class:"section-heading"},"Creator Settings",-1)),e("div",_n,[t[61]||(t[61]=e("h3",{class:"card-title"},"Watermark",-1)),t[62]||(t[62]=e("p",{class:"help-text"}," Add a watermark text that will appear on your media. Leave empty to disable watermarks. ",-1)),R(e("input",{"onUpdate:modelValue":t[20]||(t[20]=a=>h.watermark_text=a),type:"text",placeholder:"e.g., @yourusername",maxlength:"100",class:"input-touch",onBlur:t[21]||(t[21]=a=>de("watermark_text",h.watermark_text||""))},null,544),[[j,h.watermark_text]])]),e("div",kn,[t[66]||(t[66]=e("h3",{class:"card-title"},"Page Visibility",-1)),t[67]||(t[67]=e("p",{class:"help-text"}," Control who can find and visit your creator page. ",-1)),e("div",Sn,[e("label",xn,[R(e("input",{type:"radio",value:0,"onUpdate:modelValue":t[22]||(t[22]=a=>ee.value.profile_state=a),class:"radio-input",onChange:Be},null,544),[[Q,ee.value.profile_state]]),t[63]||(t[63]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"Private"),e("div",{class:"radio-description"},"No one can access your profile")],-1))]),e("label",Cn,[R(e("input",{type:"radio",value:1,"onUpdate:modelValue":t[23]||(t[23]=a=>ee.value.profile_state=a),class:"radio-input",onChange:Be},null,544),[[Q,ee.value.profile_state]]),t[64]||(t[64]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"SFW"),e("div",{class:"radio-description"},"Suitable for all ages")],-1))]),e("label",In,[R(e("input",{type:"radio",value:2,"onUpdate:modelValue":t[24]||(t[24]=a=>ee.value.profile_state=a),class:"radio-input",onChange:Be},null,544),[[Q,ee.value.profile_state]]),t[65]||(t[65]=e("div",{class:"radio-content"},[e("div",{class:"radio-title"},"NSFW"),e("div",{class:"radio-description"},"Suitable only for 18+ audiences")],-1))])])]),e("div",On,[me(Ua)])])):b("",!0),x.value?(n(),r("section",En,[t[79]||(t[79]=e("h2",{class:"section-heading"},"Viewer Details",-1)),e("div",An,[t[73]||(t[73]=e("h3",{class:"card-title"}
1742,"Grant Access to a Viewer",-1)),e("form",{onSubmit:ce(bt,["prevent"]),class:"form-stack"},[e("div",null,[t[69]||(t[69]=e("label",null,"Viewer Username *",-1)),R(e("input",{"onUpdate:modelValue":t[25]||(t[25]=a=>Y.value.viewerUsername=a),type:"text",required:"",placeholder:"Enter viewer username"},null,512),[[j,Y.value.viewerUsername]])]),e("div",null,[t[71]||(t[71]=e("label",null,"Access Level *",-1)),R(e("select",{"onUpdate:modelValue":t[26]||(t[26]=a=>Y.value.accessLevel=a),required:""},[...t[70]||(t[70]=[e("option",{value:"FREE"},"Free",-1),e("option",{value:"MEMBER"},"Member",-1),e("option",{value:"VIP"},"VIP",-1)])],512),[[Ft,Y.value.accessLevel]])]),e("div",null,[t[72]||(t[72]=e("label",null,"Expiration Date (optional)",-1)),R(e("input",{"onUpdate:modelValue":t[27]||(t[27]=a=>Y.value.expiresAt=a),type:"date",min:it()},null,8,Nn),[[j,Y.value.expiresAt]])]),e("button",{type:"submit",disabled:we.value,class:"btn btn-primary"},i(we.value?"Granting...":"Grant Access"),9,Tn)],32)]),e("div",Dn,[t[78]||(t[78]=e("h3",{class:"card-title"},"Current Viewers",-1)),he.value?(n(),r("div",Rn,[...t[74]||(t[74]=[e("div",{class:"loading-spinner"},null,-1)])])):_.value.length===0?(n(),r("div",Un," No viewers yet ")):(n(),r("div",Mn,[(n(!0),r(K,null,ge(_.value,a=>(n(),r("div",{key:a.id,class:"viewer-item"},[e("div",Fn,[e("h4",null,i(a.viewer.username),1),e("p",null,i(a.viewer.email),1)]),e("div",Pn,[e("div",Ln,[t[75]||(t[75]=e("span",{class:"label"},"Access Level:",-1)),e("span",{class:te(["badge",`level-${a.access_level.toLowerCase()}`])},i(a.access_level),3)]),e("div",$n,[t[76]||(t[76]=e("span",{class:"label"},"Granted:",-1)),e("span",null,i(Ue(a.granted_at)),1)]),a.expires_at?(n(),r("div",Bn,[t[77]||(t[77]=e("span",{class:"label"},"Expires:",-1)),e("span",{class:te({expired:a.is_expired})},[L(i(Ue(a.expires_at))+" ",1),a.is_expired?(n(),r("span",Gn,"(Expired)")):b("",!0)],2)])):b("",!0)]),e("div",qn,[e("button",{onClick:ne=>wt(a.viewer),class:"btn-update"},"Update",8,Vn),e("button",{onClick:ne=>_t(a.viewer),class:"btn-revoke"},"Revoke",8,Wn)])]))),128))]))])])):b("",!0),x.value?(n(),r("section",Hn,[t[90]||(t[90]=e("h2",{class:"section-heading"},"Subscription",-1)),e("div",jn,[e("form",{onSubmit:ce(kt,["prevent"]),class:"form-stack"},[e("label",Yn,[R(e("input",{type:"checkbox","onUpdate:modelValue":t[28]||(t[28]=a=>H.value.subscriptions_enabled=a),class:"checkbox-input"},null,512),[[et,H.value.subscriptions_enabled]]),t[80]||(t[80]=L(" Enable Subscriptions ",-1))]),t[89]||(t[89]=e("p",{class:"help-text indented"}," Allow viewers to subscribe to your content for recurring access ",-1)),H.value.subscriptions_enabled?(n(),r("div",Kn,[e("div",zn,[t[83]||(t[83]=e("h4",null,"Member",-1)),e("div",Xn,[t[81]||(t[81]=e("span",{class:"currency"},"$",-1)),R(e("input",{"onUpdate:modelValue":t[29]||(t[29]=a=>H.value.member_price=a),type:"number",step:"0.01",min:"0",placeholder:"0.00",class:"price-input"},null,512),[[j,H.value.member_price,void 0,{number:!0}]]),t[82]||(t[82]=e("span",{class:"period"},"/month",-1))]),t[84]||(t[84]=e("p",{class:"tier-description"},"Basic access to member-only content",-1))]),e("div",Qn,[t[87]||(t[87]=e("h4",null,"VIP",-1)),e("div",Jn,[t[85]||(t[85]=e("span",{class:"currency"},"$",-1)),R(e("input",{"onUpdate:modelValue":t[30]||(t[30]=a=>H.value.vip_price=a),type:"number",step:"0.01",min:"0",placeholder:"0.00",class:"price-input"},null,512),[[j,H.value.vip_price,void 0,{number:!0}]]),t[86]||(t[86]=e("span",{class:"period"},"/month",-1))]),t[88]||(t[88]=e("p",{class:"tier-description"},"Full access including VIP-exclusive content",-1))])])):b("",!0),e("div",Zn,[e("button",{type:"submit",disabled:_e.value,class:"btn btn-primary"},i(_e.value?"Updating...":"Update Settings"),9,er)])],32)])])):b("",!0),e("section",tr,[t[106]||(t[106]=e("h2",{class:"section-heading"},"Billing and Usage",-1)),e("div",ar,[t[92]||(t[92]=e("h3",{class:"card-title"},"Credit Balance",-1)),e("div",sr,[e("div",nr," $"+i(A.value.toFixed(2)),1),t[91]||(t[91]=e("p",{class:"credit-description"},"Your current credit balance",-1)),e("button",{onClick:t[31]||(t[31]=a=>z.value=!0),class:"btn btn-primary add-funds-button btn-touch"}," Add Funds ")])]),x.value?(n(),r("div",rr,[t[94]||(t[94]=e("h3",{class:"card-title"},"Wallet Address",-1)),t[95]||(t[95]=e("p",{class:"help-text"}," Set your USDC wallet address for future withdrawals. This should be a valid Ethereum address on the Base network. ",-1)),e("div",or,[t[93]||(t[93]=e("label",{for:"wallet-address"},"Wallet Address:",-1)),R(e("input",{id:"wallet-address","onUpdate:modelValue":t[32]||(t[32]=a=>V.value=a),type:"text",placeholder:"0x...",class:"wallet-input input-touch",onInput:t[33]||(t[33]=a=>ae.value="")},null,544),[[j,V.value]]),ae.value?(n(),r("p",ir,i(ae.value),1)):b("",!0),e("button",{onClick:vt,disabled:J.value||!V.value.trim(),class:"btn btn-primary save-wallet-button btn-touch"},i(J.value?"Saving...":"Save Wallet Address"),9,lr)])])):b("",!0),x.value?(n(),r("div",dr,[e("div",cr,[t[97]||(t[97]=e("h3",{class:"card-title"},"Usage Overview",-1)),me(k,{to:{name:"CreatorStats",params:{username:d.value?.username}},class:"stats-link"},{default:Ve(()=>[...t[96]||(t[96]=[L("View full statistics →",-1)])]),_:1},8,["to"])]),E(v).loading?(n(),r("div",ur,"Loading billing info...")):E(v).error?(n(),r("div",pr," Error loading billing info: "+i(E(v).error),1)):(n(),r("div",mr,[e("div",gr,[e("div",fr,[t[98]||(t[98]=e("h4",null,"Current Credit",-1)),e("div",vr,"$"+i(ke(E(v).billingInfo.credit_balance)),1),E(v).billingInfo.credit_estimate_days?(n(),r("div",hr," Estimated "+i(E(v).billingInfo.credit_estimate_days)+" days remaining ",1)):E(v).billingInfo.credit_balance<=0?(n(),r("div",yr," Credit depleted - content is private ")):b("",!0)]),e("div",br,[t[99]||(t[99]=e("h4",null,"Storage Usage",-1)),e("div",wr,i(Se(E(v).billingInfo.storage_usage_gb)),1),e("div",_r,"$"+i(ke(E(v).billingInfo.storage_cost))+"/month",1)]),e("div",kr,[t[100]||(t[100]=e("h4",null,"Bandwidth (This Month)",-1)),e("div",Sr,i(Se(E(v).billingInfo.monthly_bandwidth_gb)),1),e("div",xr,"$"+i(ke(E(v).billingInfo.monthly_bandwidth_cost)),1)]),e("div",Cr,[t[101]||(t[101]=e("h4",null,"Followers",-1)),e("div",{class:"usage-amount clickable",onClick:St},i(E(v).billingInfo.follower_count||0),1),t[102]||(t[102]=e("div",{class:"cost-amount"},"Click to view details",-1))])]),e("div",Ir,[t[104]||(t[104]=e("h4",null,"Top Viewers This Month",-1)),E(v).billingInfo.viewer_breakdown?.length?(n(),r("div",Er,[t[103]||(t[103]=e("div",{class:"viewer-header"}
1742,[e("span",null,"Viewer"),e("span",null,"Bandwidth"),e("span",null,"Cost")],-1)),(n(!0),r(K,null,ge(E(v).billingInfo.viewer_breakdown,a=>(n(),r("div",{key:a.viewer__username,class:"viewer-row"},[a.viewer__username==="Public Embeds"?(n(),Ze(k,{key:0,to:{name:"CreatorStats",params:{username:d.value?.username},query:{public:"1"}},class:"public-embeds-link",title:"See which content anonymous visitors viewed and where they came from"},{default:Ve(()=>[L(i(a.viewer__username)+" →",1)]),_:2},1032,["to"])):(n(),r("span",Ar,i(a.viewer__username),1)),e("span",null,i(Se(a.total_bytes/1024**3)),1),e("span",null,"$"+i(ke(a.total_cost)),1)]))),128))])):(n(),r("div",Or," No viewer data available "))])]))])):b("",!0),e("div",Nr,[t[105]||(t[105]=e("h3",{class:"card-title"},"Transaction History",-1)),m.value?(n(),r("div",Tr,"Loading transactions...")):M.value.length===0?(n(),r("div",Dr,"No transactions yet")):(n(),r("div",Rr,[e("div",Ur,[(n(!0),r(K,null,ge(M.value,a=>(n(),r("div",{key:a.id,class:"transaction-item"},[e("div",Mr,[e("div",Fr,i(a.transaction_type_display),1),e("div",{class:te(["transaction-amount",Fe(a)>=0?"amount-positive":"amount-negative"])},i(Fe(a)>=0?"+":"")+"$"+i(Math.abs(Fe(a)).toFixed(2)),3)]),e("div",Pr,[e("div",Lr,[gt(a)?(n(),r("span",$r," To: "+i(a.destination_user.username),1)):(n(),r("span",Br,[a.source_user?(n(),r("span",Gr," From: "+i(a.source_user.username),1)):(n(),r("span",qr,"From: System"))]))]),a.description?(n(),r("div",Vr,i(a.description),1)):b("",!0),e("div",Wr,i(Ue(a.created_at)),1)])]))),128))]),g.value>1?(n(),r("div",Hr,[e("button",{onClick:t[34]||(t[34]=a=>Qe(l.value-1)),disabled:l.value===1||m.value,class:"pagination-btn"}," Previous ",8,jr),e("span",Yr,[L(" Page "+i(l.value)+" of "+i(g.value)+" ",1),B.value>0?(n(),r("span",Kr," ("+i(B.value)+" total)",1)):b("",!0)]),e("button",{onClick:t[35]||(t[35]=a=>Qe(l.value+1)),disabled:l.value===g.value||m.value,class:"pagination-btn"}," Next ",8,zr)])):b("",!0)]))])]),G.value?(n(),r("div",{key:3,class:te(["message",w.value==="success"?"message-success":"message-error"])},i(G.value),3)):b("",!0),O.value?(n(),Ze(Ps,Lt({key:4},O.value,{onRetry:lt,onDismiss:ue,class:"mb-4"}),null,16)):b("",!0),e("p",Xr,[me(k,{to:"/about"},{default:Ve(()=>[...t[107]||(t[107]=[L("About, TOS, and FAQ",-1)])]),_:1})])]),me(ks,{show:z.value,onClose:t[36]||(t[36]=a=>z.value=!1),onCreditUpdated:ht},null,8,["show"]),ie.value?(n(),r("div",{key:0,class:"modal-overlay",onClick:ce($e,["self"])},[e("div",{class:"modal-content nsfw-modal"},[t[109]||(t[109]=e("h3",{class:"modal-title"},"18+ Content Warning",-1)),t[110]||(t[110]=e("div",{class:"modal-body"},[e("p",null,"This site allows access to NSFW content, featuring age-restricted materials such as nudity and explicit sexual depictions."),e("p",null,"By proceeding, you confirm that you are at least 18 years old or the legal adult age in your region, and you agree to view sexually explicit material.")],-1)),e("div",{class:"modal-actions"},[e("button",{class:"btn btn-secondary btn-touch",onClick:$e},"Cancel"),e("button",{class:"btn btn-primary btn-touch",onClick:yt},"Yes, I am 18+")])])])):b("",!0),Ne.value?(n(),r("div",{key:1,class:"modal-overlay",onClick:Je},[e("div",{class:"modal-content followers-modal",onClick:t[37]||(t[37]=ce(()=>{},["stop"]))},[e("div",{class:"modal-header"},[t[111]||(t[111]=e("h3",null,"Followers",-1)),e("button",{class:"modal-close",onClick:Je},"×")]),e("div",Qr,[t[113]||(t[113]=e("p",{class:"help-text followers-contact-help"}," Contact emails for followers who share theirs with you. Use these to reach fans directly. ",-1)),De.value?(n(),r("div",Jr,"Loading followers...")):Te.value.length===0?(n(),r("div",Zr,"No followers yet")):(n(),r("div",eo,[t[112]||(t[112]=e("div",{class:"followers-header"},[e("span",null,"Username"),e("span",null,"Email"),e("span",null,"Days Following"),e("span",null,"Bandwidth Used")],-1)),(n(!0),r(K,null,ge(Te.value,a=>(n(),r("div",{key:a.id,class:"follower-row"},[e("div",to,[a.avatar?(n(),r("img",{key:0,src:a.avatar,alt:a.username,class:"follower-avatar"},null,8,ao)):(n(),r("div",so,i(a.username.charAt(0).toUpperCase()),1)),e("span",no,i(a.username),1)]),e("span",ro,[a.email_shared&&a.email?(n(),r("a",{key:0,href:`mailto:${a.email}`,class:"follower-email-link"},i(a.email),9,oo)):(n(),r("span",io,"Hidden"))]),e("span",null,i(a.days_following)+" days",1),e("span",null,i(Se(a.bandwidth_used_gb)),1)]))),128))]))])])])):b("",!0)])}}},yo=Ae(lo,[["__scopeId","data-v-cc25f2f7"]]);export{yo as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.