mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Fix Netlify build by restoring dist build script and config
- Restore `scripts/build-dist.mjs` to clean `dist/`, build CSS, and copy assets. - Update `package.json` to use the new build script and serve `dist`. - Update `netlify.toml` to publish the `dist` directory. - Update `dist/` artifacts to match current source. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
a432d3d4c0
commit
c862cbc358
72 files changed
+1555
-5883
No files matched your search
Vendored
+16
-156
@@ -381,24 +381,6 @@
|
||||
data-profile-mobile-pane
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
id="profilePermissionPrompt"
|
||||
class="card hidden flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<p
|
||||
id="profilePermissionPromptMessage"
|
||||
class="text-sm text-muted"
|
||||
></p>
|
||||
<button
|
||||
id="profilePermissionPromptButton"
|
||||
type="button"
|
||||
class="btn focus-ring"
|
||||
>
|
||||
Enable permissions
|
||||
</button>
|
||||
</div>
|
||||
<div id="profileModalPanes" class="profile-modal__panes">
|
||||
<section
|
||||
id="profilePaneAccount"
|
||||
@@ -705,18 +687,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-base font-semibold text-text">Relays</h3>
|
||||
<button
|
||||
id="relayListRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh relay list"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul id="relayList" class="grid-stack" data-orientation="vertical"></ul>
|
||||
<div id="relayHealthPanel" class="card space-y-4 p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
@@ -734,18 +704,6 @@
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="relayHealthSummary"
|
||||
class="rounded-md border border-border/70 bg-surface/40 p-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<p class="text-2xs font-semibold uppercase tracking-extra-wide text-muted-strong">
|
||||
Relay status
|
||||
</p>
|
||||
<p id="relayHealthCounts" class="text-xs font-semibold text-text"></p>
|
||||
</div>
|
||||
<p id="relayHealthMessage" class="mt-1 text-xs text-muted"></p>
|
||||
</div>
|
||||
<label
|
||||
for="relayHealthTelemetryOptIn"
|
||||
class="inline-flex items-center gap-2 text-xs font-medium text-muted-strong"
|
||||
@@ -962,22 +920,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4 p-5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">Interests</h3>
|
||||
<p class="text-sm text-muted">
|
||||
Videos tagged with these hashtags will be highlighted for you.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
id="profileHashtagInterestRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh interest hashtags"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">Interests</h3>
|
||||
<p class="text-sm text-muted">
|
||||
Videos tagged with these hashtags will be highlighted for you.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<label
|
||||
@@ -1016,22 +963,11 @@
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4 p-5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">Disinterests</h3>
|
||||
<p class="text-sm text-muted">
|
||||
We’ll de-emphasize or hide videos using these hashtags.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
id="profileHashtagDisinterestRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh disinterest hashtags"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">Disinterests</h3>
|
||||
<p class="text-sm text-muted">
|
||||
We’ll de-emphasize or hide videos using these hashtags.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<label
|
||||
@@ -1079,24 +1015,6 @@
|
||||
<p class="text-sm text-muted">
|
||||
These are the creators you’ve subscribed to directly in bitvid.
|
||||
</p>
|
||||
<p
|
||||
id="subscriptionsStatus"
|
||||
class="text-sm text-muted"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></p>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-base font-semibold text-text">Subscriptions</h3>
|
||||
<button
|
||||
id="subscriptionsRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh subscriptions"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="subscriptionsEmpty"
|
||||
class="card border border-dashed border-border/60 p-6 text-center text-sm text-muted"
|
||||
@@ -1115,18 +1033,6 @@
|
||||
<p class="text-sm text-muted">
|
||||
Add friends to spotlight creators you want to keep up with across bitvid.
|
||||
</p>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-base font-semibold text-text">Friends</h3>
|
||||
<button
|
||||
id="friendsRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh friends"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<label class="flex-1 text-sm text-muted-strong" for="friendsInput">
|
||||
<span class="mb-2 block font-medium">Creator npub</span>
|
||||
@@ -1345,18 +1251,6 @@
|
||||
Creators you block are hidden from your bitvid experience. This
|
||||
list is private to your account.
|
||||
</p>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-base font-semibold text-text">Blocked creators</h3>
|
||||
<button
|
||||
id="blockedRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh blocked creators"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<label class="flex-1 text-sm text-muted-strong" for="blockedInput">
|
||||
<span class="mb-2 block font-medium">Creator npub</span>
|
||||
@@ -1387,7 +1281,7 @@
|
||||
</section>
|
||||
<section
|
||||
id="profilePaneMessages"
|
||||
class="profile-pane hidden space-y-4"
|
||||
class="profile-pane hidden"
|
||||
role="tabpanel"
|
||||
aria-labelledby="profileNavMessages"
|
||||
>
|
||||
@@ -1426,11 +1320,10 @@
|
||||
<button
|
||||
id="profileHistoryRefresh"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon focus-ring"
|
||||
class="btn-ghost focus-ring inline-flex items-center gap-2"
|
||||
data-size="sm"
|
||||
aria-label="Refresh watch history"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1526,18 +1419,7 @@
|
||||
|
||||
<div id="adminModeratorsSection" class="space-y-4 hidden" aria-hidden="true">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-base font-semibold text-text">Moderators</h4>
|
||||
<button
|
||||
id="adminModeratorsRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh moderators"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<h4 class="text-base font-semibold text-text">Moderators</h4>
|
||||
<p class="text-xs uppercase tracking-wide text-muted">
|
||||
Super Admin only
|
||||
</p>
|
||||
@@ -1576,18 +1458,7 @@
|
||||
|
||||
<div id="adminWhitelistSection" class="space-y-4">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-base font-semibold text-text">Whitelist</h4>
|
||||
<button
|
||||
id="adminWhitelistRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh whitelist"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<h4 class="text-base font-semibold text-text">Whitelist</h4>
|
||||
</div>
|
||||
<p class="text-sm text-muted">
|
||||
Creators listed here remain visible when whitelist mode is enabled. Accounts can appear in both lists, and the blacklist always takes precedence.
|
||||
@@ -1623,18 +1494,7 @@
|
||||
|
||||
<div id="adminBlacklistSection" class="space-y-4">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-base font-semibold text-text">Blacklist</h4>
|
||||
<button
|
||||
id="adminBlacklistRefreshBtn"
|
||||
type="button"
|
||||
class="btn-ghost btn-icon"
|
||||
data-size="sm"
|
||||
aria-label="Refresh blacklist"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path><path d="M16 21h5v-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<h4 class="text-base font-semibold text-text">Blacklist</h4>
|
||||
</div>
|
||||
<p class="text-sm text-muted">
|
||||
Creators on the blacklist are hidden from bitvid everywhere. Accounts can appear in both lists, and the blacklist always takes precedence.
|
||||
|
||||
Vendored
+5
-9
@@ -17,13 +17,11 @@
|
||||
* Whether the current deployment should enable developer-centric behavior.
|
||||
*
|
||||
* When `true`, bitvid surfaces extra logging, debug helpers, and experimental
|
||||
* UI affordances intended for development environments. Leave the value
|
||||
* `false` for production so end users receive the stable experience; use the
|
||||
* runtime overrides documented in `js/config.js` when you need to toggle dev
|
||||
* mode without editing this file. Downstream modules read this flag via
|
||||
* `js/config.js`.
|
||||
* UI affordances intended for development environments. Set the value to
|
||||
* `false` before deploying to production so that end users receive the stable
|
||||
* experience. Downstream modules read this flag via `js/config.js`.
|
||||
*/
|
||||
export const IS_DEV_MODE = false;
|
||||
export const IS_DEV_MODE = true;
|
||||
|
||||
/**
|
||||
* Whether development builds should surface extra verbose diagnostics.
|
||||
@@ -31,10 +29,8 @@ export const IS_DEV_MODE = false;
|
||||
* Set this to `true` to keep high-volume development warnings (like COUNT
|
||||
* request failures) visible in the console. Flip it to `false` when you want
|
||||
* a quieter dev mode without disabling other development helpers entirely.
|
||||
* Use the runtime override documented in `js/config.js` if you need to toggle
|
||||
* verbosity without editing this file.
|
||||
*/
|
||||
export const IS_VERBOSE_DEV_MODE = false;
|
||||
export const IS_VERBOSE_DEV_MODE = true;
|
||||
|
||||
/**
|
||||
* Whether the current deployment should operate in lockdown mode.
|
||||
|
||||
Vendored
+1
-2
@@ -45,8 +45,7 @@
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color var(--motion-duration-fast) ease,
|
||||
transition: background-color var(--motion-duration-fast) ease,
|
||||
color var(--motion-duration-fast) ease;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-18
@@ -28,30 +28,14 @@ an operator might change it.
|
||||
debug affordances. The value is re-exported as `isDevMode` in
|
||||
`js/config.js` and controls `window.__BITVID_DEV_MODE__` as well as
|
||||
logger routing.
|
||||
- **Why change it:** Defaults to `false` for production builds. Set to `true`
|
||||
for local development, or use the runtime override described below to
|
||||
toggle dev mode without editing this file.
|
||||
- **Why change it:** Set to `false` for production builds so only the stable
|
||||
UX and user-facing logging remain.
|
||||
|
||||
- **`IS_VERBOSE_DEV_MODE`**
|
||||
- **What it does:** Enables high-volume diagnostics inside dev mode (for
|
||||
example, repeated COUNT failures).
|
||||
- **Why change it:** Keep `true` while debugging noisy workflows and set to
|
||||
`false` when you want a quieter dev console without disabling dev mode.
|
||||
Defaults to `false` for production.
|
||||
|
||||
To override the dev flags at runtime (for example in a staging build) without
|
||||
changing `config/instance-config.js`, inject one or both of the following
|
||||
globals **before** `js/config.js` loads:
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.__BITVID_DEV_MODE_OVERRIDE__ = true;
|
||||
window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__ = true;
|
||||
</script>
|
||||
```
|
||||
|
||||
The overrides accept boolean values or the strings `"true"` / `"false"` so they
|
||||
can be injected at build time.
|
||||
|
||||
- **`IS_LOCKDOWN_MODE`**
|
||||
- **What it does:** Toggles a lockdown posture that limits access and switches
|
||||
|
||||
Vendored
+3
-16
@@ -59,23 +59,10 @@ The source of truth lives in `config/instance-config.js`:
|
||||
|
||||
- Set `IS_DEV_MODE = true` while developing locally so the dev logger and other
|
||||
diagnostics are available.
|
||||
- Keep `IS_DEV_MODE = false` for production builds. This value propagates to
|
||||
`isDevMode` (the module export), `window.__BITVID_DEV_MODE__`, and
|
||||
`logger.dev`, silencing development-only messages.
|
||||
- Flip `IS_DEV_MODE = false` before shipping a production build. This change
|
||||
propagates to `isDevMode` (the module export), `window.__BITVID_DEV_MODE__`,
|
||||
and `logger.dev`, silencing development-only messages.
|
||||
|
||||
Always commit the flag change alongside deployment-ready configuration tweaks.
|
||||
Forgetting to update the flag leaves verbose traces in user consoles and can
|
||||
expose experimental behavior intended only for QA environments.
|
||||
|
||||
If you need to enable dev logging without editing `config/instance-config.js`,
|
||||
inject a runtime override before `js/config.js` loads:
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.__BITVID_DEV_MODE_OVERRIDE__ = true;
|
||||
window.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__ = true;
|
||||
</script>
|
||||
```
|
||||
|
||||
Overrides accept boolean values or the strings `"true"` / `"false"` so they can
|
||||
be injected by a build pipeline.
|
||||
Vendored
+70
-54
@@ -1,76 +1,92 @@
|
||||
# NostrClient Architecture Overview
|
||||
# NostrClient Overview
|
||||
|
||||
The `NostrClient` (`js/nostr/client.js`) is the central controller for all Nostr network interactions in the application. It acts as a singleton facade over the `nostr-tools` library, managing relay connections, event subscriptions, state synchronization, and cryptographic signing.
|
||||
`NostrClient` (`js/nostr/client.js`) is the central controller for all Nostr network interactions, state management, and persistence in the application. It acts as the bridge between the raw Nostr protocol (relays, events) and the application's UI domain model (Videos, Profiles).
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Connection Management**: Maintains a persistent connection pool (`SimplePool`) to multiple relays.
|
||||
2. **State Management**: Tracks the global state of video events, including history, active versions, and deletions.
|
||||
3. **Event Ingestion**: Handles high-volume event streams with buffering and deduplication.
|
||||
4. **Caching**: Implements a persistent IndexedDB cache to support "Stale-While-Revalidate" loading (instant UI render).
|
||||
5. **Signer Orchestration**: Manages NIP-07 (Browser Extension), NIP-46 (Remote Signer), and Local (nsec) signers seamlessly.
|
||||
1. **Connection Management**: Maintains a pool of WebSocket connections to relays via `nostr-tools/SimplePool`.
|
||||
2. **Event Processing**: Subscribes to event streams, buffers incoming bursts, validates schemas, and converts raw events into domain objects.
|
||||
3. **State Management**:
|
||||
* **`allEvents`**: A complete history of every video version fetched.
|
||||
* **`activeMap`**: A materialized view of the *latest* valid version of each video series (deduplicated by Root ID).
|
||||
* **`tombstones`**: A set of deletion timestamps to prevent "zombie" events (deleted items reappearing from old caches) from being displayed.
|
||||
4. **Persistence**: Implements a "Stale-While-Revalidate" strategy using IndexedDB (with LocalStorage fallback) to restore the UI instantly on load.
|
||||
5. **Signer Orchestration**: Manages NIP-07 (Browser Extension), NIP-46 (Remote/Bunker), and Local (nsec) signers.
|
||||
|
||||
## State Management
|
||||
## State Management Architecture
|
||||
|
||||
The client maintains three primary state maps to ensure data consistency:
|
||||
The client maintains three primary maps to handle the "Eventual Consistency" nature of Nostr:
|
||||
|
||||
* **`allEvents`** (`Map<string, VideoObject>`):
|
||||
* Stores *every* valid video event fetched from relays, indexed by Event ID.
|
||||
* Used for history preservation and resolving direct links to specific versions.
|
||||
* **`allEvents` (`Map<EventID, Video>`)**:
|
||||
* Stores every valid video event received.
|
||||
* Used for history hydration (viewing edit logs) and debugging.
|
||||
* **`activeMap` (`Map<RootID, Video>`)**:
|
||||
* Stores only the *most recent* version of a video.
|
||||
* Keyed by `videoRootId` (or `pubkey:dTag` for legacy).
|
||||
* **Invariant**: `activeMap.get(id).created_at` is always >= any other version of that ID.
|
||||
* **`tombstones` (`Map<RootID, Timestamp>`)**:
|
||||
* Stores the timestamp of the latest deletion event for a video.
|
||||
* **Invariant**: Any event with `created_at <= tombstone` is implicitly treated as deleted, even if the "deleted" flag isn't set on the event itself. This handles out-of-order delivery where a "delete" arrives before the "video" it deletes.
|
||||
|
||||
* **`activeMap`** (`Map<string, VideoObject>`):
|
||||
* The "materialized view" used by the UI.
|
||||
* Keys are `videoRootId` (prefixed with `ROOT:`) for versioned edits, or `pubkey:dTag` for addressable events.
|
||||
* Values are the *latest* valid version of the video.
|
||||
* **Invariant**: Only one entry exists per "video entity", ensuring the UI displays the most recent edit.
|
||||
## Caching Strategy
|
||||
|
||||
* **`tombstones`** (`Map<string, timestamp>`):
|
||||
* Tracks deletions (Kind 5).
|
||||
* Keys are the `activeKey` of the deleted item.
|
||||
* Values are the timestamp of the deletion.
|
||||
* **Invariant**: Events older than the tombstone are strictly ignored, preventing "zombie" events from reappearing.
|
||||
The client uses a multi-layered approach to ensure fast boot times:
|
||||
|
||||
## Data Ingestion Flow
|
||||
1. **IndexedDB (`events` store)**:
|
||||
* Primary cache for video events.
|
||||
* Stores fingerprints of objects to avoid redundant writes.
|
||||
2. **LocalStorage (`bitvid:eventsCache:v1`)**:
|
||||
* Fallback if IndexedDB is unavailable.
|
||||
* Size-limited (typically < 5MB).
|
||||
3. **Buffering**:
|
||||
* Incoming events are buffered in memory and flushed to IDB periodically (debounced) to avoid I/O thrashing during heavy relay traffic.
|
||||
|
||||
The `subscribeVideos` method implements a buffered ingestion pipeline to handle high-throughput streams without freezing the UI:
|
||||
## Event Processing (Buffering & Debouncing)
|
||||
|
||||
1. **Subscription**: Opens a subscription to all connected relays.
|
||||
2. **Buffering**: Incoming events are pushed to an `eventBuffer` array immediately.
|
||||
3. **Debouncing**: A timer flushes the buffer every ~75ms (or when full).
|
||||
4. **Processing (Flush)**:
|
||||
* Events are parsed and validated.
|
||||
* **Tombstone Check**: If an event predates a known deletion, it is discarded.
|
||||
* **Latest-Wins Resolution**: The new event's `created_at` is compared against the existing entry in `activeMap`.
|
||||
* **Update**: If the new event is newer, `activeMap` is updated, and the UI callback is triggered.
|
||||
5. **Persistence**: The updated state is asynchronously persisted to IndexedDB.
|
||||
When `subscribeVideos` is called, the client expects a flood of events (e.g., 500+ items from relay history). Processing these one-by-one would freeze the UI.
|
||||
|
||||
### Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Relay[Relays] -->|Event Stream| Buffer[Event Buffer]
|
||||
Buffer -->|Debounce Timer| Processor[Processor]
|
||||
Processor -->|Check| Tombstones{Is Deleted?}
|
||||
Tombstones -->|Yes| Discard[Discard]
|
||||
Tombstones -->|No| Compare{Is Newer?}
|
||||
Compare -->|No| Discard
|
||||
Compare -->|Yes| ActiveMap[Update ActiveMap]
|
||||
ActiveMap --> UI[Update UI]
|
||||
ActiveMap --> IDB[(IndexedDB Cache)]
|
||||
sequenceDiagram
|
||||
participant Relay
|
||||
participant ClientBuffer
|
||||
participant Processor
|
||||
participant UI
|
||||
|
||||
Relay->>ClientBuffer: Event 1
|
||||
Relay->>ClientBuffer: Event 2
|
||||
Relay->>ClientBuffer: ... Event 50
|
||||
ClientBuffer->>ClientBuffer: Buffer (Debounce 75ms)
|
||||
|
||||
loop Flush Timer
|
||||
ClientBuffer->>Processor: Flush Batch (50 events)
|
||||
Processor->>Processor: Validate & Convert
|
||||
Processor->>Processor: Apply Tombstones
|
||||
Processor->>Processor: Update activeMap (Last-Write-Wins)
|
||||
Processor->>UI: Callback(onVideo) (Single Render)
|
||||
Processor->>IndexedDB: Persist Snapshot
|
||||
end
|
||||
```
|
||||
|
||||
## Caching Strategy (`EventsCacheStore`)
|
||||
## History Reconstruction
|
||||
|
||||
To provide an "app-like" experience, the client persists its state to IndexedDB (`bitvid-events-cache`).
|
||||
Videos in Nostr are immutable events. "Editing" means publishing a new event with the same identifier. `NostrClient` reconstructs the history chain using:
|
||||
|
||||
* **Fingerprinting**: To minimize I/O, the store tracks JSON fingerprints (hashes) of all persisted events.
|
||||
* **Incremental Persistence**: Only changed events (where fingerprint differs) are written to IDB during a snapshot.
|
||||
* **Stale-While-Revalidate**: On startup (`init()`), the client immediately loads data from IDB to render the UI, then connects to relays to fetch updates.
|
||||
1. **`videoRootId` (V3)**: The canonical identifier for a video series.
|
||||
2. **`d` Tag (NIP-33)**: The addressable identifier.
|
||||
3. **Legacy IDs**: Fallback to Event ID for V1/V2 posts.
|
||||
|
||||
## Signer Integration
|
||||
`hydrateVideoHistory` fetches all events matching these identifiers, sorts them by `created_at` descending, and returns the full edit log.
|
||||
|
||||
The client abstracts the signing mechanism, allowing the user to switch between methods without changing application logic:
|
||||
## Why it works this way
|
||||
|
||||
1. **NIP-07 (Extension)**: Delegates signing to a browser extension (e.g., Alby, nos2x).
|
||||
2. **NIP-46 (Remote/Bunker)**: Uses a partial `NostrClient` instance to communicate with a remote signer via a relay.
|
||||
3. **Local (NIP-01)**: Uses a local private key (nsec) stored in `sessionStorage` (encrypted).
|
||||
* **Eventual Consistency**: The `tombstones` map is essential because relays guarantee neither order nor completeness. A client might receive a "delete" event for a video it hasn't seen yet, or receive an old version of a video after receiving the deletion. Tombstones ensure that once something is known to be deleted, it stays deleted (until a newer re-creation event appears).
|
||||
* **Performance**: The `eventBuffer` mechanism allows the client to ingest thousands of events without blocking the main thread, as React rendering is expensive. Batching updates reduces render cycles from O(N) to O(1) per batch.
|
||||
* **Offline-First**: Restoring from IndexedDB immediately allows the app to be interactive before any network connection is established.
|
||||
|
||||
The `activeSigner` object is normalized to expose a standard interface (`signEvent`, `nip04Encrypt`, `nip44Encrypt`), regardless of the underlying implementation.
|
||||
## When to change
|
||||
|
||||
* **Storage Backend**: If the cache schema becomes too complex for IndexedDB, or if we move to a native wrapper (React Native/Tauri) with a SQL backend, the `EventsCacheStore` class should be swapped out.
|
||||
* **Event Loop**: If we switch to a worker-based architecture (off-main-thread processing), the `subscribeVideos` logic would move to a Web Worker, and `NostrClient` would become a message passer.
|
||||
* **Protocol Upgrades**: If NIP-33 (Parameterized Replaceable Events) is superseded or the video event kind (30078) changes, the filters in `subscribeVideos` and `fetchVideos` must be updated.
|
||||
Vendored
+3
-4
@@ -341,12 +341,11 @@ valid events:
|
||||
|
||||
```bash
|
||||
# Run the validation script
|
||||
node scripts/agent/validate-events.mjs
|
||||
node scripts/agent/validate-events.test.mjs
|
||||
|
||||
# Run the unit test suite
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
When modifying schemas or builders, use `scripts/agent/validate-events.mjs` to
|
||||
verify your changes locally. This tool also scans the codebase to ensure all
|
||||
active event builders are covered by tests.
|
||||
When modifying schemas or builders, use `scripts/agent/validate-events.test.mjs` to
|
||||
verify your changes locally.
|
||||
Vendored
+478
-847
File diff suppressed because it is too large.
Load diff
Vendored
+62
-77
@@ -16,10 +16,6 @@ import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderer
|
||||
import { ALLOW_NSFW_CONTENT } from "./config.js";
|
||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import {
|
||||
fetchProfileMetadata,
|
||||
ensureProfileMetadataSubscription,
|
||||
} from "./services/profileMetadataService.js";
|
||||
import { createModerationStage } from "./feedEngine/stages.js";
|
||||
import {
|
||||
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
||||
@@ -2799,23 +2795,13 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
|
||||
});
|
||||
const videoEvent = getZapVideoEvent();
|
||||
|
||||
let previousOverride;
|
||||
const hasGlobal = typeof globalThis !== "undefined";
|
||||
if (
|
||||
hasGlobal &&
|
||||
typeof overrideFee === "number" &&
|
||||
Number.isFinite(overrideFee)
|
||||
) {
|
||||
previousOverride = globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
|
||||
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = overrideFee;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await splitAndZap(
|
||||
{
|
||||
videoEvent,
|
||||
amountSats: context.shares.total,
|
||||
walletSettings: settings
|
||||
walletSettings: settings,
|
||||
overrideFee
|
||||
},
|
||||
dependencies
|
||||
);
|
||||
@@ -2836,21 +2822,6 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
if (
|
||||
hasGlobal &&
|
||||
typeof overrideFee === "number" &&
|
||||
Number.isFinite(overrideFee)
|
||||
) {
|
||||
if (typeof previousOverride === "number") {
|
||||
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = previousOverride;
|
||||
} else if (
|
||||
globalThis &&
|
||||
"__BITVID_PLATFORM_FEE_OVERRIDE__" in globalThis
|
||||
) {
|
||||
delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2877,25 +2848,37 @@ async function executePendingRetry({ walletSettings }) {
|
||||
|
||||
const aggregatedReceipts = [];
|
||||
const aggregatedTracker = [];
|
||||
const nextRetryShares = [];
|
||||
let lastError = null;
|
||||
|
||||
for (const share of shares) {
|
||||
const overrideFee = share.type === "platform" ? 100 : 0;
|
||||
try {
|
||||
const attempt = await runZapAttempt({
|
||||
const results = await Promise.allSettled(
|
||||
shares.map((share) => {
|
||||
const overrideFee = share.type === "platform" ? 100 : 0;
|
||||
return runZapAttempt({
|
||||
amount: share.amount,
|
||||
overrideFee,
|
||||
walletSettings
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
const share = shares[i];
|
||||
|
||||
if (result.status === "fulfilled") {
|
||||
const attempt = result.value;
|
||||
if (!attempt) {
|
||||
setZapStatus("", "neutral");
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (attempt?.result?.receipts) {
|
||||
aggregatedReceipts.push(...attempt.result.receipts);
|
||||
} else if (Array.isArray(attempt?.shareTracker)) {
|
||||
aggregatedTracker.push(...attempt.shareTracker);
|
||||
}
|
||||
} catch (error) {
|
||||
} else {
|
||||
const error = result.reason;
|
||||
lastError = error;
|
||||
const tracker = Array.isArray(error?.__zapShareTracker)
|
||||
? error.__zapShareTracker
|
||||
: [];
|
||||
@@ -2916,20 +2899,28 @@ async function executePendingRetry({ walletSettings }) {
|
||||
const failureShares = tracker.filter(
|
||||
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
||||
);
|
||||
markZapRetryPending(failureShares.length ? failureShares : [share]);
|
||||
const message = error?.message || "Retry failed.";
|
||||
setZapStatus(message, "error");
|
||||
app?.showError?.(message);
|
||||
renderZapReceipts(
|
||||
aggregatedTracker.length ? aggregatedTracker : tracker,
|
||||
{
|
||||
partial: true
|
||||
}
|
||||
);
|
||||
return;
|
||||
if (failureShares.length) {
|
||||
nextRetryShares.push(...failureShares);
|
||||
} else {
|
||||
nextRetryShares.push(share);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextRetryShares.length > 0) {
|
||||
markZapRetryPending(nextRetryShares);
|
||||
const message = lastError?.message || "Retry failed.";
|
||||
setZapStatus(message, "error");
|
||||
app?.showError?.(message);
|
||||
renderZapReceipts(
|
||||
aggregatedTracker.length ? aggregatedTracker : [],
|
||||
{
|
||||
partial: true
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
renderZapReceipts(
|
||||
aggregatedReceipts.length ? aggregatedReceipts : aggregatedTracker,
|
||||
{ partial: false }
|
||||
@@ -5103,20 +5094,31 @@ async function fetchChannelProfileFromRelays(pubkey) {
|
||||
}
|
||||
|
||||
try {
|
||||
const profileEntry = await fetchProfileMetadata(pubkey, {
|
||||
nostr: nostrClient,
|
||||
relays: relayUrls,
|
||||
logger: devLogger,
|
||||
});
|
||||
const events = await pool.list(relayUrls, [
|
||||
{ kinds: [0], authors: [pubkey], limit: 1 }
|
||||
]);
|
||||
|
||||
if (!profileEntry) {
|
||||
return { event: null, profile: {} };
|
||||
let newestEvent = null;
|
||||
for (const event of Array.isArray(events) ? events : []) {
|
||||
if (!event || !event.content) {
|
||||
continue;
|
||||
}
|
||||
if (!newestEvent || event.created_at > newestEvent.created_at) {
|
||||
newestEvent = event;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
event: profileEntry.event || null,
|
||||
profile: profileEntry.profile || {},
|
||||
};
|
||||
if (newestEvent?.content) {
|
||||
try {
|
||||
const meta = JSON.parse(newestEvent.content);
|
||||
return { event: newestEvent, profile: meta };
|
||||
} catch (error) {
|
||||
userLogger.warn("Failed to parse channel metadata payload:", error);
|
||||
return { event: newestEvent, profile: {} };
|
||||
}
|
||||
}
|
||||
|
||||
return { event: newestEvent, profile: {} };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -5206,23 +5208,6 @@ async function loadUserProfile(pubkey) {
|
||||
npub: currentChannelNpub,
|
||||
loadToken
|
||||
});
|
||||
|
||||
ensureProfileMetadataSubscription({
|
||||
pubkey,
|
||||
nostr: nostrClient,
|
||||
onProfile: ({ profile, event }) => {
|
||||
if (loadToken !== currentProfileLoadToken) {
|
||||
return;
|
||||
}
|
||||
applyChannelProfileMetadata({
|
||||
profile,
|
||||
event,
|
||||
pubkey,
|
||||
npub: currentChannelNpub,
|
||||
loadToken,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (loadToken === currentProfileLoadToken) {
|
||||
userLogger.error("Failed to fetch user profile data:", error);
|
||||
|
||||
Vendored
+2
-31
@@ -47,39 +47,10 @@ import {
|
||||
|
||||
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
|
||||
|
||||
const runtimeScope = typeof globalThis === "undefined" ? null : globalThis;
|
||||
|
||||
const parseBooleanOverride = (value) => {
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "true") {
|
||||
return true;
|
||||
}
|
||||
if (normalized === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const devModeOverride = parseBooleanOverride(
|
||||
runtimeScope?.__BITVID_DEV_MODE_OVERRIDE__
|
||||
);
|
||||
const verboseDevModeOverride = parseBooleanOverride(
|
||||
runtimeScope?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__
|
||||
);
|
||||
|
||||
export const isDevMode =
|
||||
devModeOverride === null ? Boolean(IS_DEV_MODE) : devModeOverride;
|
||||
export const isDevMode = Boolean(IS_DEV_MODE);
|
||||
export { IS_DEV_MODE };
|
||||
|
||||
export const isVerboseDevMode =
|
||||
verboseDevModeOverride === null
|
||||
? Boolean(IS_VERBOSE_DEV_MODE)
|
||||
: verboseDevModeOverride;
|
||||
export const isVerboseDevMode = Boolean(IS_VERBOSE_DEV_MODE);
|
||||
export { IS_VERBOSE_DEV_MODE };
|
||||
|
||||
export const isLockdownMode = Boolean(IS_LOCKDOWN_MODE);
|
||||
|
||||
Vendored
+7
-2
@@ -1,7 +1,6 @@
|
||||
import Application from "./app.js";
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import { convertEventToVideo, getDTagValueFromTags } from "./nostr/index.js";
|
||||
import { sanitizeRelayList } from "./nostr/nip46Client.js";
|
||||
import { resolveVideoPointer } from "./utils/videoPointer.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import { nostrToolsReady } from "./nostrToolsBootstrap.js";
|
||||
@@ -181,7 +180,13 @@ const buildRelayList = (relays) => {
|
||||
...(Array.isArray(nostrClient.relays) ? nostrClient.relays : []),
|
||||
];
|
||||
|
||||
const result = sanitizeRelayList(combined);
|
||||
const result = Array.from(
|
||||
new Set(
|
||||
combined
|
||||
.map((relay) => (typeof relay === "string" ? relay.trim() : ""))
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
diag?.emit("relay-list", { relays: result });
|
||||
return result;
|
||||
};
|
||||
|
||||
Vendored
+4
-5
@@ -261,11 +261,10 @@ export function createResolvePostedAtStage({
|
||||
});
|
||||
}
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
await task();
|
||||
} catch (error) {
|
||||
context?.log?.(`[${stageName}] resolution task failed`, error);
|
||||
const results = await Promise.allSettled(tasks.map((task) => task()));
|
||||
for (const result of results) {
|
||||
if (result.status === "rejected") {
|
||||
context?.log?.(`[${stageName}] resolution task failed`, result.reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+3
-30
@@ -49,36 +49,14 @@ export function trackersFromMagnet(magnet) {
|
||||
return Array.from(deduped);
|
||||
}
|
||||
|
||||
export function webSeedsFromMagnet(magnet) {
|
||||
if (typeof magnet !== "string") {
|
||||
return [];
|
||||
}
|
||||
const parsed = parseMagnetLite(magnet);
|
||||
if (!parsed || !Array.isArray(parsed.ws)) {
|
||||
return [];
|
||||
}
|
||||
const deduped = new Set();
|
||||
parsed.ws.forEach((url) => {
|
||||
if (typeof url !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
deduped.add(trimmed);
|
||||
});
|
||||
return Array.from(deduped);
|
||||
}
|
||||
|
||||
function parseMagnetLite(magnet) {
|
||||
if (typeof magnet !== "string") {
|
||||
return { infoHash: "", announce: [], ws: [] };
|
||||
return { infoHash: "", announce: [] };
|
||||
}
|
||||
|
||||
const trimmed = magnet.trim();
|
||||
if (!trimmed) {
|
||||
return { infoHash: "", announce: [], ws: [] };
|
||||
return { infoHash: "", announce: [] };
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
@@ -86,7 +64,6 @@ function parseMagnetLite(magnet) {
|
||||
const paramsPart = queryIndex >= 0 ? trimmed.slice(queryIndex + 1) : "";
|
||||
const params = paramsPart.split("&");
|
||||
const announce = [];
|
||||
const ws = [];
|
||||
let infoHash = "";
|
||||
|
||||
for (const param of params) {
|
||||
@@ -112,12 +89,8 @@ function parseMagnetLite(magnet) {
|
||||
if (value) {
|
||||
announce.push(value);
|
||||
}
|
||||
} else if (key === "ws" || key === "webseed") {
|
||||
if (value) {
|
||||
ws.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { infoHash, announce, ws };
|
||||
return { infoHash, announce };
|
||||
}
|
||||
+31
-25
@@ -42,7 +42,7 @@ function resolveNip44Module(extension) {
|
||||
return nip44;
|
||||
}
|
||||
|
||||
export async function createNip07Adapter(initialExtension, { preloadedPubkey } = {}) {
|
||||
export async function createNip07Adapter(initialExtension) {
|
||||
const getExtension = () => {
|
||||
if (typeof window !== "undefined" && window.nostr) {
|
||||
return window.nostr;
|
||||
@@ -86,10 +86,15 @@ export async function createNip07Adapter(initialExtension, { preloadedPubkey } =
|
||||
};
|
||||
|
||||
const pubkey =
|
||||
preloadedPubkey ||
|
||||
(typeof bootstrapExtension.getPublicKey === "function"
|
||||
typeof bootstrapExtension.getPublicKey === "function"
|
||||
? normalizeActorKey(await getPublicKey())
|
||||
: "");
|
||||
: "";
|
||||
|
||||
const hasNip04 =
|
||||
bootstrapExtension.nip04 && typeof bootstrapExtension.nip04 === "object";
|
||||
|
||||
const bootstrapNip44 = resolveNip44Module(bootstrapExtension);
|
||||
const hasNip44 = !!bootstrapNip44;
|
||||
|
||||
// Wrapper builders to ensure dynamic lookup at call time
|
||||
const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => {
|
||||
@@ -201,29 +206,30 @@ export async function createNip07Adapter(initialExtension, { preloadedPubkey } =
|
||||
requestPermissions,
|
||||
destroy: async () => {},
|
||||
canSign: () => typeof getExtension()?.signEvent === "function",
|
||||
// Always expose these methods; they perform their own existence checks at runtime.
|
||||
// This handles race conditions where NIP-04/44 APIs are injected lazily.
|
||||
nip04Encrypt: createNip04Encrypt(),
|
||||
nip04Decrypt: createNip04Decrypt(),
|
||||
nip44Encrypt: createNip44Encrypt(),
|
||||
nip44Decrypt: createNip44Decrypt(),
|
||||
capabilities: {
|
||||
sign: typeof bootstrapExtension.signEvent === "function",
|
||||
nip44: hasNip44,
|
||||
nip04: hasNip04,
|
||||
},
|
||||
};
|
||||
|
||||
// Define capabilities as dynamic properties to reflect current extension state
|
||||
Object.defineProperty(signer, "capabilities", {
|
||||
get: () => {
|
||||
const ext = getExtension();
|
||||
const hasNip04 = !!(ext?.nip04 && typeof ext.nip04 === "object");
|
||||
const hasNip44 = !!resolveNip44Module(ext);
|
||||
return {
|
||||
sign: typeof ext?.signEvent === "function",
|
||||
nip44: hasNip44,
|
||||
nip04: hasNip04,
|
||||
};
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
if (hasNip04) {
|
||||
if (typeof bootstrapExtension.nip04.encrypt === "function") {
|
||||
signer.nip04Encrypt = createNip04Encrypt();
|
||||
}
|
||||
if (typeof bootstrapExtension.nip04.decrypt === "function") {
|
||||
signer.nip04Decrypt = createNip04Decrypt();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNip44) {
|
||||
if (typeof bootstrapNip44.encrypt === "function") {
|
||||
signer.nip44Encrypt = createNip44Encrypt();
|
||||
}
|
||||
if (typeof bootstrapNip44.decrypt === "function") {
|
||||
signer.nip44Decrypt = createNip44Decrypt();
|
||||
}
|
||||
}
|
||||
|
||||
return signer;
|
||||
}
|
||||
Vendored
+141
-753
File diff suppressed because it is too large.
Load diff
Vendored
+5
-8
@@ -13,7 +13,6 @@ import { CACHE_POLICIES } from "./cachePolicies.js";
|
||||
import { isSessionActor } from "./sessionActor.js";
|
||||
import { queueSignEvent } from "./signRequestQueue.js";
|
||||
import { getActiveSigner } from "../nostrClientRegistry.js";
|
||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
||||
|
||||
const COMMENT_EVENT_SCHEMA = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
|
||||
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_COMMENT];
|
||||
@@ -33,15 +32,13 @@ function getAllowedCommentKinds() {
|
||||
return ALLOWED_COMMENT_KINDS.slice();
|
||||
}
|
||||
function sanitizeRelayList(primary, fallback) {
|
||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
||||
if (primaryList.length) {
|
||||
return primaryList;
|
||||
if (Array.isArray(primary) && primary.length) {
|
||||
return primary;
|
||||
}
|
||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
||||
if (fallbackList.length) {
|
||||
return fallbackList;
|
||||
if (Array.isArray(fallback) && fallback.length) {
|
||||
return fallback;
|
||||
}
|
||||
return sanitizeRelayUrls(RELAY_URLS);
|
||||
return RELAY_URLS;
|
||||
}
|
||||
|
||||
function normalizeRelay(candidate) {
|
||||
|
||||
Vendored
-2
@@ -2,7 +2,6 @@
|
||||
|
||||
import { NostrClient } from "./client.js";
|
||||
import {
|
||||
DEFAULT_NIP07_CORE_METHODS,
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
} from "./nip07Permissions.js";
|
||||
@@ -27,7 +26,6 @@ export function requestDefaultExtensionPermissions(
|
||||
|
||||
export {
|
||||
NostrClient,
|
||||
DEFAULT_NIP07_CORE_METHODS,
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
};
|
||||
Vendored
+5
-8
@@ -9,20 +9,17 @@ import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { queueSignEvent } from "./signRequestQueue.js";
|
||||
import { getActiveSigner } from "../nostrClientRegistry.js";
|
||||
import { isSessionActor } from "./sessionActor.js";
|
||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
||||
|
||||
const DEFAULT_TYPING_EXPIRY_SECONDS = 15;
|
||||
|
||||
function sanitizeRelayList(primary, fallback) {
|
||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
||||
if (primaryList.length) {
|
||||
return primaryList;
|
||||
if (Array.isArray(primary) && primary.length) {
|
||||
return primary;
|
||||
}
|
||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
||||
if (fallbackList.length) {
|
||||
return fallbackList;
|
||||
if (Array.isArray(fallback) && fallback.length) {
|
||||
return fallback;
|
||||
}
|
||||
return sanitizeRelayUrls(RELAY_URLS);
|
||||
return RELAY_URLS;
|
||||
}
|
||||
|
||||
function normalizeString(value) {
|
||||
|
||||
Vendored
+1
-5
@@ -21,16 +21,12 @@ export const DEFAULT_NIP07_ENCRYPTION_METHODS = Object.freeze([
|
||||
"nip44.v2.decrypt",
|
||||
]);
|
||||
|
||||
export const DEFAULT_NIP07_CORE_METHODS = Object.freeze([
|
||||
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
// Core auth + relay metadata
|
||||
"get_public_key",
|
||||
"sign_event",
|
||||
"read_relays",
|
||||
"write_relays",
|
||||
]);
|
||||
|
||||
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||
...DEFAULT_NIP07_CORE_METHODS,
|
||||
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
]);
|
||||
|
||||
|
||||
Vendored
+1
-37
@@ -43,28 +43,6 @@ export const NIP46_ENCRYPTION_ALGORITHMS = Object.freeze([
|
||||
"nip04",
|
||||
]);
|
||||
|
||||
function isSecureRelayContext() {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
const protocol = window?.location?.protocol;
|
||||
return protocol === "https:";
|
||||
}
|
||||
|
||||
function resolveRelayProtocol(protocol, { enforceTls = false } = {}) {
|
||||
if (!protocol) {
|
||||
return null;
|
||||
}
|
||||
const normalized = protocol.toLowerCase();
|
||||
if (normalized === "wss:") {
|
||||
return "wss:";
|
||||
}
|
||||
if (normalized === "ws:") {
|
||||
return enforceTls ? "wss:" : "ws:";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* resolves the appropriate storage mechanism (localStorage or polyfill) for NIP-46 sessions.
|
||||
* @returns {Storage|null} The storage interface or null if unavailable.
|
||||
@@ -94,7 +72,6 @@ export function sanitizeRelayList(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
return sanitized;
|
||||
}
|
||||
const enforceTls = isSecureRelayContext();
|
||||
|
||||
list.forEach((value) => {
|
||||
if (typeof value !== "string") {
|
||||
@@ -113,29 +90,16 @@ export function sanitizeRelayList(list) {
|
||||
|
||||
let normalized = trimmed.replace(/\/+$/, "");
|
||||
try {
|
||||
const parsed = new URL(normalized);
|
||||
const parsed = new URL(trimmed);
|
||||
if (!parsed.hostname) {
|
||||
return;
|
||||
}
|
||||
const resolvedProtocol = resolveRelayProtocol(parsed.protocol, {
|
||||
enforceTls,
|
||||
});
|
||||
if (!resolvedProtocol) {
|
||||
return;
|
||||
}
|
||||
if (parsed.protocol !== resolvedProtocol) {
|
||||
parsed.protocol = resolvedProtocol;
|
||||
}
|
||||
const pathname = parsed.pathname.replace(/\/+$/, "");
|
||||
normalized = `${parsed.protocol}//${parsed.host}${pathname}${parsed.search || ""}`;
|
||||
} catch (error) {
|
||||
// Ignore URL parsing failures and fall back to the trimmed string.
|
||||
}
|
||||
|
||||
if (enforceTls && normalized.startsWith("ws://")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vendored
+1
-12
@@ -6,10 +6,6 @@ import {
|
||||
resolveInfoJsonUrl,
|
||||
} from "../utils/storagePointer.js";
|
||||
import { getCachedNostrTools } from "./toolkit.js";
|
||||
import {
|
||||
validateEventAgainstSchema,
|
||||
NOTE_TYPES,
|
||||
} from "../nostrEventSchemas.js";
|
||||
|
||||
function stringFromInput(value) {
|
||||
if (value === null || value === undefined) {
|
||||
@@ -938,20 +934,13 @@ export function buildNip71VideoEvent({
|
||||
tags.push(...pointerTags);
|
||||
}
|
||||
|
||||
const event = {
|
||||
return {
|
||||
kind: normalizeNip71Kind(metadata.kind),
|
||||
pubkey: normalizedPubkey,
|
||||
created_at: timestamp,
|
||||
tags,
|
||||
content: summary,
|
||||
};
|
||||
|
||||
validateEventAgainstSchema(
|
||||
event.kind === 22 ? NOTE_TYPES.NIP71_SHORT_VIDEO : NOTE_TYPES.NIP71_VIDEO,
|
||||
event,
|
||||
);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
export function collectNip71PointerRequests(videos = []) {
|
||||
|
||||
Vendored
+5
-8
@@ -7,21 +7,18 @@ import { LRUCache } from "../utils/lruCache.js";
|
||||
import { CACHE_POLICIES } from "./cachePolicies.js";
|
||||
import { NOTE_TYPES } from "../nostrEventSchemas.js";
|
||||
import { queueSignEvent } from "./signRequestQueue.js";
|
||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
||||
|
||||
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_REACTION];
|
||||
const reactionCache = new LRUCache({ maxSize: 100 });
|
||||
|
||||
function sanitizeRelayList(primary, fallback) {
|
||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
||||
if (primaryList.length) {
|
||||
return primaryList;
|
||||
if (Array.isArray(primary) && primary.length) {
|
||||
return primary;
|
||||
}
|
||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
||||
if (fallbackList.length) {
|
||||
return fallbackList;
|
||||
if (Array.isArray(fallback) && fallback.length) {
|
||||
return fallback;
|
||||
}
|
||||
return sanitizeRelayUrls(RELAY_URLS);
|
||||
return RELAY_URLS;
|
||||
}
|
||||
|
||||
function normalizeString(value) {
|
||||
|
||||
Vendored
-11
@@ -487,21 +487,10 @@ export function isSessionActor(nostrClient) {
|
||||
return !!sa && sa.source !== "nsec";
|
||||
}
|
||||
|
||||
function _closeSessionActorDb() {
|
||||
if (sessionActorDbPromise) {
|
||||
return sessionActorDbPromise.then((db) => {
|
||||
db.close();
|
||||
sessionActorDbPromise = null;
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export const __testExports = {
|
||||
arrayBufferToBase64,
|
||||
base64ToUint8Array,
|
||||
deriveSessionEncryptionKey,
|
||||
generateRandomBytes,
|
||||
isSubtleCryptoAvailable,
|
||||
_closeSessionActorDb,
|
||||
};
|
||||
Vendored
+6
-18
@@ -14,7 +14,6 @@ import { normalizePointerInput } from "./watchHistory.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { logViewCountFailure } from "./countDiagnostics.js";
|
||||
import { queueSignEvent } from "./signRequestQueue.js";
|
||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
||||
|
||||
const VIEW_EVENT_GUARD_PREFIX = "bitvid:viewed";
|
||||
|
||||
@@ -341,15 +340,13 @@ function flattenListResults(input) {
|
||||
}
|
||||
|
||||
function sanitizeRelayList(primary, fallback) {
|
||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
||||
if (primaryList.length) {
|
||||
return primaryList;
|
||||
if (Array.isArray(primary) && primary.length) {
|
||||
return primary;
|
||||
}
|
||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
||||
if (fallbackList.length) {
|
||||
return fallbackList;
|
||||
if (Array.isArray(fallback) && fallback.length) {
|
||||
return fallback;
|
||||
}
|
||||
return sanitizeRelayUrls(RELAY_URLS);
|
||||
return RELAY_URLS;
|
||||
}
|
||||
|
||||
export async function listVideoViewEvents(client, pointer, options = {}) {
|
||||
@@ -566,7 +563,6 @@ export async function countVideoViewEvents(client, pointer, options = {}) {
|
||||
perRelay: [],
|
||||
best: null,
|
||||
fallback: true,
|
||||
partial: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -592,26 +588,19 @@ export async function countVideoViewEvents(client, pointer, options = {}) {
|
||||
throw normalizeAbortError();
|
||||
}
|
||||
|
||||
let countAttemptPartial = false;
|
||||
|
||||
try {
|
||||
const result = await client.countEventsAcrossRelays([pointerFilter], {
|
||||
relays: sanitizeRelayList(relayList, client.relays),
|
||||
timeoutMs: options?.timeoutMs,
|
||||
});
|
||||
|
||||
countAttemptPartial = Boolean(result?.partial);
|
||||
|
||||
if (result?.perRelay?.some((entry) => entry && entry.ok)) {
|
||||
return { ...result, fallback: false, partial: countAttemptPartial };
|
||||
return { ...result, fallback: false };
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code !== "count-unsupported") {
|
||||
logViewCountFailure(error);
|
||||
}
|
||||
if (error?.code === "count-timeout" || error?.code === "timeout") {
|
||||
countAttemptPartial = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
@@ -658,7 +647,6 @@ export async function countVideoViewEvents(client, pointer, options = {}) {
|
||||
perRelay: [],
|
||||
best: null,
|
||||
fallback: true,
|
||||
partial: countAttemptPartial,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Vendored
+12
-2
@@ -520,7 +520,13 @@ async function processShare({
|
||||
}
|
||||
|
||||
export async function splitAndZap(
|
||||
{ videoEvent, amountSats, comment = "", walletSettings } = {},
|
||||
{
|
||||
videoEvent,
|
||||
amountSats,
|
||||
comment = "",
|
||||
walletSettings,
|
||||
overrideFee = null,
|
||||
} = {},
|
||||
dependencies = {}
|
||||
) {
|
||||
if (!videoEvent || typeof videoEvent !== "object") {
|
||||
@@ -538,7 +544,11 @@ export async function splitAndZap(
|
||||
throw new Error("This creator has not configured a Lightning address yet.");
|
||||
}
|
||||
|
||||
const platformFee = clampPercent(getOverridePlatformFee());
|
||||
const platformFee = clampPercent(
|
||||
typeof overrideFee !== "undefined" && overrideFee !== null
|
||||
? resolvePlatformFeePercent(overrideFee)
|
||||
: getOverridePlatformFee()
|
||||
);
|
||||
const platformShare = Math.floor((amount * platformFee) / 100);
|
||||
const creatorShare = amount - platformShare;
|
||||
|
||||
|
||||
Vendored
-16
@@ -39,10 +39,6 @@ function normalizeRelayUrl(input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enforceTls =
|
||||
typeof window !== "undefined" &&
|
||||
window?.location?.protocol === "https:";
|
||||
|
||||
let candidate = input.trim();
|
||||
if (!candidate) {
|
||||
return null;
|
||||
@@ -63,18 +59,6 @@ function normalizeRelayUrl(input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (enforceTls && parsed.protocol === "ws:") {
|
||||
try {
|
||||
parsed = new URL(candidate.replace(/^ws:/i, "wss:"));
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (enforceTls && parsed.protocol !== "wss:") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostname = parsed.hostname.toLowerCase();
|
||||
const port = parsed.port ? `:${parsed.port}` : "";
|
||||
let pathname = parsed.pathname || "";
|
||||
|
||||
Vendored
+84
-145
@@ -26,7 +26,6 @@ import { profileCache } from "../state/profileCache.js";
|
||||
import getDefaultAuthProvider, {
|
||||
providers as defaultAuthProviders,
|
||||
} from "./authProviders/index.js";
|
||||
import { fetchProfileMetadata } from "./profileMetadataService.js";
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
@@ -623,12 +622,8 @@ export default class AuthService {
|
||||
postLoginError: null,
|
||||
};
|
||||
|
||||
const { detail: postLoginDetail, completionPromise } = this.applyPostLoginState({
|
||||
deferBlocks: true,
|
||||
});
|
||||
detail.postLogin = postLoginDetail;
|
||||
|
||||
const postLoginPromise = Promise.resolve(completionPromise)
|
||||
const postLoginPromise = Promise.resolve()
|
||||
.then(() => this.applyPostLoginState())
|
||||
.then((postLogin) => {
|
||||
detail.postLogin = postLogin;
|
||||
detail.postLoginError = null;
|
||||
@@ -682,151 +677,75 @@ export default class AuthService {
|
||||
return detail;
|
||||
}
|
||||
|
||||
applyPostLoginState({ deferBlocks = false } = {}) {
|
||||
async applyPostLoginState() {
|
||||
const activePubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
|
||||
const cachedEntry = activePubkey ? this.getProfileCacheEntry(activePubkey) : null;
|
||||
const cachedProfile = cachedEntry?.profile || FALLBACK_PROFILE;
|
||||
const detail = {
|
||||
pubkey: activePubkey || null,
|
||||
blocksLoaded: deferBlocks ? null : false,
|
||||
blocksLoaded: false,
|
||||
relaysLoaded: false,
|
||||
profile: cachedProfile,
|
||||
profile: null,
|
||||
};
|
||||
|
||||
if (!activePubkey) {
|
||||
return { detail, completionPromise: Promise.resolve(detail) };
|
||||
return detail;
|
||||
}
|
||||
|
||||
const runOperation = async (operation) => {
|
||||
try {
|
||||
const value = await operation.promise;
|
||||
const resolved = operation.onFulfilled(value);
|
||||
detail[operation.name] = resolved;
|
||||
if (typeof operation.emit === "function") {
|
||||
operation.emit(resolved, null);
|
||||
}
|
||||
return resolved;
|
||||
} catch (error) {
|
||||
const resolved = operation.onRejected(error);
|
||||
detail[operation.name] = resolved;
|
||||
if (typeof operation.emit === "function") {
|
||||
operation.emit(resolved, error);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
};
|
||||
|
||||
const schedule = (callback) => Promise.resolve().then(() => callback());
|
||||
|
||||
const completionPromise = (async () => {
|
||||
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
|
||||
const relayOp = {
|
||||
name: "relaysLoaded",
|
||||
promise: schedule(() => this.relayManager.loadRelayList(activePubkey)),
|
||||
onFulfilled: () => true,
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load relay list", error);
|
||||
return false;
|
||||
},
|
||||
emit: (value, error) => {
|
||||
this.emit("relaysLoaded", {
|
||||
pubkey: activePubkey,
|
||||
relaysLoaded: value,
|
||||
error: error ?? null,
|
||||
});
|
||||
},
|
||||
};
|
||||
await runOperation(relayOp);
|
||||
}
|
||||
const operations = [];
|
||||
|
||||
const concurrentOps = [];
|
||||
|
||||
if (!deferBlocks) {
|
||||
const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule);
|
||||
if (blocksOperation) {
|
||||
concurrentOps.push(blocksOperation);
|
||||
}
|
||||
}
|
||||
|
||||
concurrentOps.push({
|
||||
name: "profile",
|
||||
promise: schedule(() => this.loadOwnProfile(activePubkey)),
|
||||
onFulfilled: (value) => {
|
||||
const profile = value || FALLBACK_PROFILE;
|
||||
this.setProfileCacheEntry(activePubkey, profile, {
|
||||
persist: true,
|
||||
reason: "post-login",
|
||||
});
|
||||
return profile;
|
||||
},
|
||||
if (this.userBlocks && typeof this.userBlocks.loadBlocks === "function") {
|
||||
operations.push({
|
||||
name: "blocksLoaded",
|
||||
promise: schedule(() => this.userBlocks.loadBlocks(activePubkey)),
|
||||
onFulfilled: () => true,
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load own profile", error);
|
||||
const fallbackProfile = cachedProfile || FALLBACK_PROFILE;
|
||||
this.setProfileCacheEntry(activePubkey, fallbackProfile, {
|
||||
persist: true,
|
||||
reason: "post-login-fallback",
|
||||
});
|
||||
return fallbackProfile;
|
||||
this.log("[AuthService] Failed to load block list", error);
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.allSettled(concurrentOps.map(runOperation));
|
||||
|
||||
return detail;
|
||||
})();
|
||||
|
||||
return { detail, completionPromise };
|
||||
}
|
||||
|
||||
createBlocksLoadOperation(
|
||||
activePubkey,
|
||||
schedule = (callback) => Promise.resolve().then(() => callback()),
|
||||
options = {},
|
||||
) {
|
||||
if (!this.userBlocks || typeof this.userBlocks.loadBlocks !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "blocksLoaded",
|
||||
promise: schedule(() => this.userBlocks.loadBlocks(activePubkey, options)),
|
||||
onFulfilled: () => true,
|
||||
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
|
||||
operations.push({
|
||||
name: "relaysLoaded",
|
||||
promise: schedule(() => this.relayManager.loadRelayList(activePubkey)),
|
||||
onFulfilled: () => true,
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load relay list", error);
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
operations.push({
|
||||
name: "profile",
|
||||
promise: schedule(() => this.loadOwnProfile(activePubkey)),
|
||||
onFulfilled: (value) => value,
|
||||
onRejected: (error) => {
|
||||
this.log("[AuthService] Failed to load block list", error);
|
||||
return false;
|
||||
this.log("[AuthService] Failed to load own profile", error);
|
||||
return null;
|
||||
},
|
||||
emit: (value, error) => {
|
||||
this.emit("blocksLoaded", {
|
||||
pubkey: activePubkey,
|
||||
blocksLoaded: value,
|
||||
error: error ?? null,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async loadBlocksForPubkey(pubkey, options = {}) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey) ||
|
||||
(typeof pubkey === "string" ? pubkey.trim() : "");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const settled = await Promise.allSettled(
|
||||
operations.map((operation) => operation.promise)
|
||||
);
|
||||
|
||||
const operation = this.createBlocksLoadOperation(normalized, undefined, options);
|
||||
if (!operation) {
|
||||
return false;
|
||||
}
|
||||
settled.forEach((result, index) => {
|
||||
const operation = operations[index];
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await operation.promise;
|
||||
const resolved = operation.onFulfilled(value);
|
||||
operation.emit?.(resolved, null);
|
||||
return resolved;
|
||||
} catch (error) {
|
||||
const resolved = operation.onRejected(error);
|
||||
operation.emit?.(resolved, error);
|
||||
return resolved;
|
||||
}
|
||||
if (result.status === "fulfilled") {
|
||||
detail[operation.name] = operation.onFulfilled(result.value);
|
||||
} else {
|
||||
detail[operation.name] = operation.onRejected(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
@@ -844,14 +763,6 @@ export default class AuthService {
|
||||
setCurrentUserNpub(null);
|
||||
setActiveProfilePubkey(null, { persist: true });
|
||||
|
||||
try {
|
||||
if (profileCache && typeof profileCache.reset === "function") {
|
||||
profileCache.reset();
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("[AuthService] profileCache.reset threw", error);
|
||||
}
|
||||
|
||||
try {
|
||||
resetModerationSettings({ persist: true });
|
||||
} catch (error) {
|
||||
@@ -1212,16 +1123,44 @@ export default class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetchProfileMetadata(normalized, {
|
||||
nostr: this.nostrClient,
|
||||
});
|
||||
const results = await Promise.all(
|
||||
this.nostrClient.relays.map((relayUrl) =>
|
||||
this.nostrClient.pool.list([relayUrl], [
|
||||
{ kinds: [0], authors: [normalized], limit: 1 },
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
if (result?.event && typeof this.nostrClient?.handleEvent === "function") {
|
||||
this.nostrClient.handleEvent(result.event);
|
||||
const events = results.flat();
|
||||
let newest = null;
|
||||
for (const event of events) {
|
||||
if (!event || event.pubkey !== normalized || !event.content) {
|
||||
continue;
|
||||
}
|
||||
if (!newest || event.created_at > newest.created_at) {
|
||||
newest = event;
|
||||
}
|
||||
}
|
||||
|
||||
if (result?.profile) {
|
||||
return result.profile;
|
||||
if (newest) {
|
||||
if (this.nostrClient && typeof this.nostrClient.handleEvent === "function") {
|
||||
this.nostrClient.handleEvent(newest);
|
||||
}
|
||||
if (newest.content) {
|
||||
const data = JSON.parse(newest.content);
|
||||
const profile = {
|
||||
name: data.display_name || data.name || FALLBACK_PROFILE.name,
|
||||
picture: data.picture || FALLBACK_PROFILE.picture,
|
||||
about: typeof data.about === "string" ? data.about : FALLBACK_PROFILE.about,
|
||||
website:
|
||||
typeof data.website === "string" ? data.website : FALLBACK_PROFILE.website,
|
||||
banner:
|
||||
typeof data.banner === "string" ? data.banner : FALLBACK_PROFILE.banner,
|
||||
lud16: typeof data.lud16 === "string" ? data.lud16 : FALLBACK_PROFILE.lud16,
|
||||
lud06: typeof data.lud06 === "string" ? data.lud06 : FALLBACK_PROFILE.lud06,
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to fetch profile", error);
|
||||
|
||||
+4
-11
@@ -4,8 +4,6 @@ import { COMMENT_EVENT_KIND } from "../nostr/commentEvents.js";
|
||||
|
||||
const DEFAULT_MAX_DISCUSSION_COUNT_VIDEOS = 24;
|
||||
const COUNT_UNSUPPORTED_TITLE = "Relay does not support NIP-45 COUNT queries.";
|
||||
const COUNT_PARTIAL_TITLE =
|
||||
"Some relays timed out. Comment counts may be partial.";
|
||||
|
||||
function toPositiveInteger(value, fallback) {
|
||||
const numeric = Number(value);
|
||||
@@ -101,11 +99,10 @@ export default class DiscussionCountService {
|
||||
}
|
||||
|
||||
const total = Number(result?.total);
|
||||
const isPartial = Boolean(result?.partial);
|
||||
const normalized =
|
||||
Number.isFinite(total) && total >= 0 ? Math.floor(total) : 0;
|
||||
this.videoDiscussionCountCache.set(video.id, normalized);
|
||||
this.updateDiscussionCountElement(container, normalized, { partial: isPartial });
|
||||
this.updateDiscussionCountElement(container, normalized);
|
||||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -207,7 +204,7 @@ export default class DiscussionCountService {
|
||||
element.removeAttribute("title");
|
||||
}
|
||||
|
||||
updateDiscussionCountElement(element, count, { partial = false } = {}) {
|
||||
updateDiscussionCountElement(element, count) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
@@ -220,13 +217,9 @@ export default class DiscussionCountService {
|
||||
const numeric = Number(count);
|
||||
const safeValue =
|
||||
Number.isFinite(numeric) && numeric >= 0 ? Math.floor(numeric) : 0;
|
||||
element.dataset.countState = partial ? "partial" : "ready";
|
||||
element.dataset.countState = "ready";
|
||||
valueEl.textContent = safeValue.toLocaleString();
|
||||
if (partial) {
|
||||
element.title = COUNT_PARTIAL_TITLE;
|
||||
} else {
|
||||
element.removeAttribute("title");
|
||||
}
|
||||
element.removeAttribute("title");
|
||||
}
|
||||
|
||||
markDiscussionCountError(element, { unsupported = false } = {}) {
|
||||
|
||||
Vendored
+5
-48
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
DEFAULT_RELAY_URLS,
|
||||
ensureNostrTools,
|
||||
resolveSimplePoolConstructor,
|
||||
shimLegacySimplePoolMethods,
|
||||
} from "../nostr/toolkit.js";
|
||||
import { ensureNostrTools, resolveSimplePoolConstructor, shimLegacySimplePoolMethods } from "../nostr/toolkit.js";
|
||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { publishEventToRelay } from "../nostrPublish.js";
|
||||
import logger from "../utils/logger.js";
|
||||
@@ -15,7 +10,6 @@ const DEFAULT_BACKOFF = {
|
||||
};
|
||||
|
||||
const MAX_SEEN_EVENT_IDS = 5000;
|
||||
const DM_RELAY_LIST_TIMEOUT_MS = 5000;
|
||||
export const DM_RELAY_WARNING_FALLBACK = "dm-relays-fallback";
|
||||
|
||||
function normalizeRelayList(relays) {
|
||||
@@ -61,29 +55,6 @@ function extractRelayHintsFromEvent(event) {
|
||||
);
|
||||
}
|
||||
|
||||
function withTimeout(promise, timeoutMs, errorCode) {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
let timeoutId = null;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
const error = new Error("DM relay discovery timed out.");
|
||||
if (errorCode) {
|
||||
error.code = errorCode;
|
||||
}
|
||||
reject(error);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeoutPromise]).finally(() => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveDmRelaySelection({
|
||||
pubkey,
|
||||
relayHints = [],
|
||||
@@ -103,14 +74,11 @@ export async function resolveDmRelaySelection({
|
||||
const discoveryList = sanitizeRelayList(
|
||||
Array.isArray(discoveryRelays) ? discoveryRelays : [],
|
||||
);
|
||||
const candidateFallbackList = sanitizeRelayList(
|
||||
const fallbackList = sanitizeRelayList(
|
||||
Array.isArray(fallbackRelays) && fallbackRelays.length
|
||||
? fallbackRelays
|
||||
: discoveryRelays,
|
||||
);
|
||||
const defaultFallbackList = sanitizeRelayList(Array.from(DEFAULT_RELAY_URLS));
|
||||
const fallbackList =
|
||||
candidateFallbackList.length > 0 ? candidateFallbackList : defaultFallbackList;
|
||||
|
||||
if (
|
||||
normalizedPubkey &&
|
||||
@@ -119,13 +87,9 @@ export async function resolveDmRelaySelection({
|
||||
discoveryList.length
|
||||
) {
|
||||
try {
|
||||
const events = await withTimeout(
|
||||
pool.list(discoveryList, [
|
||||
{ kinds: [10050], authors: [normalizedPubkey], limit: 1 },
|
||||
]),
|
||||
DM_RELAY_LIST_TIMEOUT_MS,
|
||||
"dm-relay-discovery-timeout",
|
||||
);
|
||||
const events = await pool.list(discoveryList, [
|
||||
{ kinds: [10050], authors: [normalizedPubkey], limit: 1 },
|
||||
]);
|
||||
const sorted = Array.isArray(events)
|
||||
? events
|
||||
.filter((entry) => entry && entry.pubkey === normalizedPubkey)
|
||||
@@ -139,13 +103,6 @@ export async function resolveDmRelaySelection({
|
||||
}
|
||||
} catch (error) {
|
||||
resolvedLogger.dev.warn("[dmNostrService] Failed to fetch DM relay hints.", error);
|
||||
if (fallbackList.length) {
|
||||
return {
|
||||
relays: fallbackList,
|
||||
source: "fallback",
|
||||
warning: DM_RELAY_WARNING_FALLBACK,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-5
@@ -46,11 +46,6 @@ export class DmNotificationManager {
|
||||
return this.focusedConversationId;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.conversationCache.clear();
|
||||
this.focusedConversationId = "";
|
||||
}
|
||||
|
||||
setFocusedConversation(conversationId, isFocused = true) {
|
||||
if (typeof conversationId !== "string") {
|
||||
return;
|
||||
|
||||
+9
-25
@@ -6,9 +6,6 @@ import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
||||
const DEFAULT_IDF_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_HISTORY_REFRESH_INTERVAL_MS = 60 * 1000;
|
||||
const DEFAULT_REFRESH_DEBOUNCE_MS = 200;
|
||||
const YIELD_CHUNK_SIZE = 500;
|
||||
|
||||
const yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
function normalizeAddressKey(value) {
|
||||
if (typeof value !== "string") {
|
||||
@@ -102,8 +99,7 @@ export async function buildWatchHistoryTagCounts({
|
||||
return counts;
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = items[i];
|
||||
for (const entry of items) {
|
||||
const pointer = normalizePointerInput(entry?.pointer || entry);
|
||||
let video = entry?.video || entry?.metadata?.video || null;
|
||||
|
||||
@@ -127,16 +123,12 @@ export async function buildWatchHistoryTagCounts({
|
||||
for (const tag of tags) {
|
||||
counts.set(tag, (counts.get(tag) || 0) + 1);
|
||||
}
|
||||
|
||||
if (i > 0 && i % YIELD_CHUNK_SIZE === 0) {
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
export async function buildTagIdf({ videos } = {}) {
|
||||
export function buildTagIdf({ videos } = {}) {
|
||||
const idf = new Map();
|
||||
const list = Array.isArray(videos) ? videos : [];
|
||||
if (!list.length) {
|
||||
@@ -146,31 +138,23 @@ export async function buildTagIdf({ videos } = {}) {
|
||||
const docFrequency = new Map();
|
||||
const totalDocs = list.length;
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const video = list[i];
|
||||
for (const video of list) {
|
||||
const tags = collectVideoTags(video);
|
||||
if (tags.size) {
|
||||
for (const tag of tags) {
|
||||
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
||||
}
|
||||
if (!tags.size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i > 0 && i % YIELD_CHUNK_SIZE === 0) {
|
||||
await yieldToMain();
|
||||
for (const tag of tags) {
|
||||
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
let processedCount = 0;
|
||||
for (const [tag, df] of docFrequency.entries()) {
|
||||
const ratio = (totalDocs + 1) / (df + 1);
|
||||
const value = 1 + Math.log(ratio);
|
||||
if (Number.isFinite(value) && value > 0) {
|
||||
idf.set(tag, value);
|
||||
}
|
||||
processedCount++;
|
||||
if (processedCount % YIELD_CHUNK_SIZE === 0) {
|
||||
await yieldToMain();
|
||||
}
|
||||
}
|
||||
|
||||
return idf;
|
||||
@@ -326,7 +310,7 @@ export default class ExploreDataService {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshTagIdf({ force = false, videos, reason } = {}) {
|
||||
refreshTagIdf({ force = false, videos, reason } = {}) {
|
||||
const now = Date.now();
|
||||
if (!force && this.tagIdfUpdatedAt) {
|
||||
const elapsed = now - this.tagIdfUpdatedAt;
|
||||
@@ -342,7 +326,7 @@ export default class ExploreDataService {
|
||||
: [];
|
||||
|
||||
try {
|
||||
this.tagIdf = await buildTagIdf({ videos: sourceVideos });
|
||||
this.tagIdf = buildTagIdf({ videos: sourceVideos });
|
||||
this.tagIdfUpdatedAt = now;
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
|
||||
+22
-229
@@ -15,17 +15,11 @@ import {
|
||||
assertAnyRelayAccepted,
|
||||
} from "../nostrPublish.js";
|
||||
import { userLogger, devLogger } from "../utils/logger.js";
|
||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
||||
import { relayManager } from "../relayManager.js";
|
||||
import {
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
runNip07WithRetry,
|
||||
NIP07_PRIORITY,
|
||||
} from "../nostr/nip07Permissions.js";
|
||||
import { relaySubscriptionService } from "./relaySubscriptionService.js";
|
||||
import { runNip07WithRetry, NIP07_PRIORITY } from "../nostr/nip07Permissions.js";
|
||||
|
||||
const LOG_PREFIX = "[HashtagPreferences]";
|
||||
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
||||
@@ -99,6 +93,14 @@ function normalizeHexPubkey(pubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeRelayList(candidate) {
|
||||
return Array.isArray(candidate)
|
||||
? candidate
|
||||
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
function normalizeEncryptionToken(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
@@ -236,24 +238,19 @@ class HashtagPreferencesService {
|
||||
this.eventId = null;
|
||||
this.eventCreatedAt = null;
|
||||
this.loaded = false;
|
||||
this.backgroundLoading = false;
|
||||
this.preferencesVersion = DEFAULT_VERSION;
|
||||
this.decryptRetryTimeoutId = null;
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
this.subscriptionKey = null;
|
||||
this.lastLoadError = null;
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
this.reset();
|
||||
if (detail.pubkey) {
|
||||
this.load(detail.pubkey, { allowPermissionPrompt: false });
|
||||
this.load(detail.pubkey);
|
||||
}
|
||||
} else if (event === "runtimeCleared" && detail.pubkey === this.activePubkey) {
|
||||
this.reset();
|
||||
if (this.activePubkey) {
|
||||
this.load(this.activePubkey, { allowPermissionPrompt: false });
|
||||
this.load(this.activePubkey);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -264,74 +261,19 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.stopHashtagPreferencesSubscription();
|
||||
this.interests = new Set();
|
||||
this.disinterests = new Set();
|
||||
this.eventId = null;
|
||||
this.eventCreatedAt = null;
|
||||
this.loaded = false;
|
||||
this.backgroundLoading = false;
|
||||
this.preferencesVersion = DEFAULT_VERSION;
|
||||
this.lastLoadError = null;
|
||||
if (this.decryptRetryTimeoutId) {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
}
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
this.emitChange("reset");
|
||||
}
|
||||
|
||||
stopHashtagPreferencesSubscription() {
|
||||
if (this.subscriptionKey) {
|
||||
relaySubscriptionService.stopSubscription(this.subscriptionKey, "reset");
|
||||
this.subscriptionKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
ensureHashtagPreferencesSubscription(pubkey, relays) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
if (!normalized || !nostrClient?.pool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.HASHTAG_PREFERENCES);
|
||||
const canonicalKind = schema?.kind ?? 30015;
|
||||
const legacyKind = 30005;
|
||||
const kinds = canonicalKind === legacyKind
|
||||
? [canonicalKind]
|
||||
: [canonicalKind, legacyKind];
|
||||
|
||||
const filters = kinds.map((kind) => ({
|
||||
kinds: [kind],
|
||||
authors: [normalized],
|
||||
"#d": [HASHTAG_IDENTIFIER],
|
||||
}));
|
||||
|
||||
const key = `hashtags:${normalized}`;
|
||||
this.subscriptionKey = key;
|
||||
|
||||
return relaySubscriptionService.ensureSubscription({
|
||||
key,
|
||||
pool: nostrClient.pool,
|
||||
relays,
|
||||
filters,
|
||||
label: "hashtag-preferences",
|
||||
onEvent: (event) => this.handleHashtagPreferencesEvent(event),
|
||||
});
|
||||
}
|
||||
|
||||
handleHashtagPreferencesEvent(event) {
|
||||
const normalized = normalizeHexPubkey(event?.pubkey);
|
||||
if (!normalized || normalized !== this.activePubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.load(normalized, { allowPermissionPrompt: false }).catch((error) => {
|
||||
devLogger.warn(`${LOG_PREFIX} Failed to refresh after hashtag event`, error);
|
||||
});
|
||||
}
|
||||
|
||||
saveToCache() {
|
||||
const interests = this.getInterests();
|
||||
const disinterests = this.getDisinterests();
|
||||
@@ -483,7 +425,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
}
|
||||
|
||||
scheduleDecryptRetry(pubkey, error, options = {}) {
|
||||
scheduleDecryptRetry(pubkey, error) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
if (!normalized) {
|
||||
return;
|
||||
@@ -496,7 +438,7 @@ class HashtagPreferencesService {
|
||||
if (this.activePubkey && this.activePubkey !== normalized) {
|
||||
return;
|
||||
}
|
||||
this.load(normalized, options).catch((retryError) => {
|
||||
this.load(normalized).catch((retryError) => {
|
||||
userLogger.warn(`${LOG_PREFIX} Decryption retry failed`, retryError);
|
||||
});
|
||||
}, DECRYPT_RETRY_DELAY_MS);
|
||||
@@ -506,51 +448,14 @@ class HashtagPreferencesService {
|
||||
);
|
||||
}
|
||||
|
||||
async load(pubkey, options = {}) {
|
||||
async load(pubkey) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey === normalized) {
|
||||
devLogger.log(`${LOG_PREFIX} Reusing in-flight preferences load.`, {
|
||||
pubkey: normalized,
|
||||
});
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey && this.loadingPubkey !== normalized) {
|
||||
devLogger.log(`${LOG_PREFIX} Waiting for existing preferences load to settle.`, {
|
||||
previous: this.loadingPubkey,
|
||||
next: normalized,
|
||||
});
|
||||
await this.loadPromise.catch(() => {});
|
||||
}
|
||||
|
||||
const loadPromise = this.loadInternal(normalized, options);
|
||||
this.loadPromise = loadPromise;
|
||||
this.loadingPubkey = normalized;
|
||||
|
||||
try {
|
||||
return await loadPromise;
|
||||
} finally {
|
||||
if (this.loadPromise === loadPromise) {
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadInternal(pubkey, options = {}) {
|
||||
const normalized = normalizeHexPubkey(pubkey);
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
this.lastLoadError = null;
|
||||
let wasLoadedForUser =
|
||||
this.activePubkey &&
|
||||
this.activePubkey === normalized &&
|
||||
(this.interests.size > 0 || this.disinterests.size > 0 || this.loaded);
|
||||
|
||||
this.activePubkey = normalized;
|
||||
if (allowPermissionPrompt) {
|
||||
this.backgroundLoading = false;
|
||||
}
|
||||
|
||||
if (!normalized) {
|
||||
this.reset();
|
||||
@@ -561,12 +466,7 @@ class HashtagPreferencesService {
|
||||
// Try cache first
|
||||
if (this.loadFromCache(normalized)) {
|
||||
wasLoadedForUser = true;
|
||||
if (!allowPermissionPrompt && !this.backgroundLoading) {
|
||||
this.backgroundLoading = true;
|
||||
this.emitChange("background-loading", { background: true });
|
||||
}
|
||||
}
|
||||
const wasBackgroundLoading = this.backgroundLoading;
|
||||
|
||||
if (
|
||||
!nostrClient ||
|
||||
@@ -581,18 +481,10 @@ class HashtagPreferencesService {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Keeping existing preferences despite client unavailability.`,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -603,31 +495,12 @@ class HashtagPreferencesService {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Keeping existing preferences; no relays available for refresh.`,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!nostrClient?.pool && typeof nostrClient?.ensurePool === "function") {
|
||||
try {
|
||||
await nostrClient.ensurePool();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
`${LOG_PREFIX} Failed to initialize relay pool for subscription`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.ensureHashtagPreferencesSubscription(normalized, relays);
|
||||
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.HASHTAG_PREFERENCES);
|
||||
const canonicalKind = schema?.kind ?? 30015;
|
||||
@@ -680,10 +553,6 @@ class HashtagPreferencesService {
|
||||
`${LOG_PREFIX} Failed to load hashtag preferences (keeping local state if any).`,
|
||||
fetchError,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -694,19 +563,11 @@ class HashtagPreferencesService {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Keeping existing preferences despite empty relay response.`,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -739,18 +600,10 @@ class HashtagPreferencesService {
|
||||
|
||||
if (!latest) {
|
||||
if (wasLoadedForUser) {
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -763,18 +616,12 @@ class HashtagPreferencesService {
|
||||
devLogger.log(
|
||||
`${LOG_PREFIX} Ignoring stale/concurrent preferences event (remote: ${latestCreatedAt}, local: ${currentCreatedAt}).`,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let decryptResult;
|
||||
try {
|
||||
const decryptPromise = this.decryptEvent(latest, normalized, {
|
||||
allowPermissionPrompt,
|
||||
});
|
||||
const decryptPromise = this.decryptEvent(latest, normalized);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => {
|
||||
@@ -793,7 +640,6 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
if (!decryptResult.ok) {
|
||||
this.lastLoadError = decryptResult.error || null;
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Failed to decrypt hashtag preferences`,
|
||||
decryptResult.error,
|
||||
@@ -803,15 +649,7 @@ class HashtagPreferencesService {
|
||||
if (!wasLoadedForUser && !this.loaded) {
|
||||
this.loaded = true;
|
||||
}
|
||||
this.scheduleDecryptRetry(normalized, decryptResult.error, {
|
||||
allowPermissionPrompt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!allowPermissionPrompt &&
|
||||
decryptResult.error?.code === "hashtag-preferences-permission-required"
|
||||
) {
|
||||
this.scheduleDecryptRetry(normalized, decryptResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -821,19 +659,11 @@ class HashtagPreferencesService {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Preserving cached preferences despite decryption failure.`,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitChange("background-loaded", { background: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -851,9 +681,6 @@ class HashtagPreferencesService {
|
||||
? latest.created_at
|
||||
: null;
|
||||
this.loaded = true;
|
||||
if (this.backgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
}
|
||||
this.saveToCache();
|
||||
this.emitChange("sync", { scheme: decryptResult.scheme });
|
||||
} catch (error) {
|
||||
@@ -866,7 +693,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
}
|
||||
|
||||
async decryptEvent(event, userPubkey, options = {}) {
|
||||
async decryptEvent(event, userPubkey) {
|
||||
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
||||
if (!ciphertext) {
|
||||
const error = new Error("Preference event missing ciphertext content.");
|
||||
@@ -874,13 +701,8 @@ class HashtagPreferencesService {
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
let signer = getActiveSigner();
|
||||
if (
|
||||
!signer &&
|
||||
typeof nostrClient?.ensureActiveSignerForPubkey === "function" &&
|
||||
allowPermissionPrompt
|
||||
) {
|
||||
if (!signer && typeof nostrClient?.ensureActiveSignerForPubkey === "function") {
|
||||
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
|
||||
}
|
||||
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
||||
@@ -894,36 +716,13 @@ class HashtagPreferencesService {
|
||||
(!signerHasNip44 && requiresNip44) ||
|
||||
(!signerHasNip04 && requiresNip04)
|
||||
) {
|
||||
if (!allowPermissionPrompt) {
|
||||
const error = new Error(
|
||||
"Decrypt permissions are required to read hashtag preferences.",
|
||||
);
|
||||
error.code = "hashtag-preferences-permission-required";
|
||||
return { ok: false, error };
|
||||
}
|
||||
try {
|
||||
const permissionResult = await requestDefaultExtensionPermissions(
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
);
|
||||
if (!permissionResult?.ok) {
|
||||
const error =
|
||||
permissionResult?.error instanceof Error
|
||||
? permissionResult.error
|
||||
: new Error(
|
||||
"Extension encryption permissions are required to read hashtag preferences.",
|
||||
);
|
||||
error.code = "hashtag-preferences-permission-required";
|
||||
return { ok: false, error };
|
||||
}
|
||||
await requestDefaultExtensionPermissions();
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
`${LOG_PREFIX} Extension permissions request failed while loading preferences`,
|
||||
error,
|
||||
);
|
||||
const wrapped =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
wrapped.code = "hashtag-preferences-permission-required";
|
||||
return { ok: false, error: wrapped };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -967,11 +766,7 @@ class HashtagPreferencesService {
|
||||
);
|
||||
}
|
||||
|
||||
const nostrApi = allowPermissionPrompt
|
||||
? typeof window !== "undefined"
|
||||
? window?.nostr
|
||||
: null
|
||||
: null;
|
||||
const nostrApi = typeof window !== "undefined" ? window?.nostr : null;
|
||||
if (nostrApi) {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
@@ -1125,12 +920,10 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
if (signer.type === "extension") {
|
||||
const permissionResult = await requestDefaultExtensionPermissions(
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
const error = new Error(
|
||||
"The active signer must allow encryption before publishing preferences.",
|
||||
"The active signer must allow encryption and signing before publishing preferences.",
|
||||
);
|
||||
error.code = "hashtag-preferences-extension-denied";
|
||||
error.cause = permissionResult?.error || null;
|
||||
|
||||
Vendored
+52
-168
@@ -176,30 +176,6 @@ function sanitizeSnapshotTimestamp(value) {
|
||||
return floored;
|
||||
}
|
||||
|
||||
function resolveDirectMessageCreatedAt(message) {
|
||||
const createdAt = sanitizeSnapshotTimestamp(
|
||||
message?.message?.created_at ??
|
||||
message?.event?.created_at ??
|
||||
message?.created_at ??
|
||||
message?.timestamp,
|
||||
);
|
||||
|
||||
if (createdAt) {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
function resolveDirectMessageSortValue(message) {
|
||||
return sanitizeSnapshotTimestamp(
|
||||
message?.created_at ??
|
||||
message?.timestamp ??
|
||||
message?.message?.created_at ??
|
||||
message?.event?.created_at,
|
||||
);
|
||||
}
|
||||
|
||||
const SNAPSHOT_PREVIEW_LIMIT = 160;
|
||||
|
||||
function sanitizeSnapshotPreview(value) {
|
||||
@@ -264,7 +240,11 @@ function resolvePreviewCacheKey(message) {
|
||||
: typeof message?.remotePubkey === "string"
|
||||
? message.remotePubkey.trim()
|
||||
: "";
|
||||
const timestamp = resolveDirectMessageSortValue(message);
|
||||
const timestamp = Number.isFinite(message?.timestamp)
|
||||
? Math.floor(message.timestamp)
|
||||
: Number.isFinite(message?.message?.created_at)
|
||||
? Math.floor(message.message.created_at)
|
||||
: 0;
|
||||
|
||||
if (remote && timestamp) {
|
||||
return `snapshot:${remote}:${timestamp}`;
|
||||
@@ -421,7 +401,11 @@ function normalizeDirectMessageRecord(message, actorPubkey = "") {
|
||||
receiverPubkey = senderPubkey === actor ? remote : actor;
|
||||
}
|
||||
|
||||
const createdAt = resolveDirectMessageCreatedAt(message);
|
||||
const createdAt = sanitizeSnapshotTimestamp(
|
||||
message?.message?.created_at ??
|
||||
message?.event?.created_at ??
|
||||
message?.timestamp,
|
||||
);
|
||||
|
||||
const kind =
|
||||
Number.isFinite(message?.message?.kind)
|
||||
@@ -475,7 +459,7 @@ function buildSnapshotFromMessages(messages, actorPubkey = "", resolvePreview =
|
||||
continue;
|
||||
}
|
||||
|
||||
const timestamp = resolveDirectMessageSortValue(message);
|
||||
const timestamp = sanitizeSnapshotTimestamp(message.timestamp);
|
||||
const preview =
|
||||
typeof resolvePreview === "function"
|
||||
? resolvePreview(message)
|
||||
@@ -496,60 +480,6 @@ function buildSnapshotFromMessages(messages, actorPubkey = "", resolvePreview =
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDirectMessagePayload(message) {
|
||||
if (!message || message.ok !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const eventId = typeof message?.event?.id === "string" ? message.event.id : "";
|
||||
if (!eventId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createdAt = resolveDirectMessageCreatedAt(message);
|
||||
|
||||
return {
|
||||
...message,
|
||||
created_at: createdAt,
|
||||
timestamp: createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeDirectMessageCache(existingMessages, incomingMessages) {
|
||||
const mergedIndex = new Map();
|
||||
|
||||
const upsert = (message) => {
|
||||
const normalized = normalizeDirectMessagePayload(message);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = normalized.event.id;
|
||||
const existing = mergedIndex.get(eventId);
|
||||
if (
|
||||
!existing ||
|
||||
normalized.created_at >= resolveDirectMessageSortValue(existing)
|
||||
) {
|
||||
mergedIndex.set(eventId, normalized);
|
||||
}
|
||||
};
|
||||
|
||||
for (const message of Array.isArray(existingMessages) ? existingMessages : []) {
|
||||
upsert(message);
|
||||
}
|
||||
|
||||
for (const message of Array.isArray(incomingMessages) ? incomingMessages : []) {
|
||||
upsert(message);
|
||||
}
|
||||
|
||||
const mergedMessages = Array.from(mergedIndex.values());
|
||||
mergedMessages.sort(
|
||||
(a, b) => resolveDirectMessageSortValue(b) - resolveDirectMessageSortValue(a),
|
||||
);
|
||||
|
||||
return { messages: mergedMessages, index: mergedIndex };
|
||||
}
|
||||
|
||||
function hydrateMessagesFromSnapshot(snapshot, actorPubkey = "") {
|
||||
const normalizedActor = normalizeHexPubkey(actorPubkey);
|
||||
const hydrated = [];
|
||||
@@ -629,7 +559,6 @@ export class NostrService {
|
||||
this.dmMessageIndex = new Map();
|
||||
this.dmSubscription = null;
|
||||
this.dmActorPubkey = null;
|
||||
this.dmSubscriptionRelays = null;
|
||||
this.dmHydratedFromSnapshot = false;
|
||||
this.dmNotificationManager = new DmNotificationManager({
|
||||
logger: userLogger,
|
||||
@@ -770,13 +699,6 @@ export class NostrService {
|
||||
if (this.dmPreviewCache) {
|
||||
this.dmPreviewCache.clear();
|
||||
}
|
||||
if (this.dmNotificationManager?.reset) {
|
||||
try {
|
||||
this.dmNotificationManager.reset();
|
||||
} catch (error) {
|
||||
userLogger.warn("[nostrService] Failed to reset DM notifications cache", error);
|
||||
}
|
||||
}
|
||||
const activeActor = normalizeHexPubkey(
|
||||
typeof actorPubkey === "string" && actorPubkey
|
||||
? actorPubkey
|
||||
@@ -871,10 +793,17 @@ export class NostrService {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeDirectMessagePayload(message);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const normalized = {
|
||||
...message,
|
||||
timestamp:
|
||||
Number.isFinite(message?.timestamp)
|
||||
? message.timestamp
|
||||
: Number.isFinite(message?.message?.created_at)
|
||||
? message.message.created_at
|
||||
: Number.isFinite(message?.event?.created_at)
|
||||
? message.event.created_at
|
||||
: Date.now() / 1000,
|
||||
};
|
||||
|
||||
this.dmMessageIndex.set(eventId, normalized);
|
||||
|
||||
@@ -899,9 +828,7 @@ export class NostrService {
|
||||
this.dmMessages.push(normalized);
|
||||
}
|
||||
|
||||
this.dmMessages.sort(
|
||||
(a, b) => resolveDirectMessageSortValue(b) - resolveDirectMessageSortValue(a),
|
||||
);
|
||||
this.dmMessages.sort((a, b) => (b?.timestamp || 0) - (a?.timestamp || 0));
|
||||
if (actor) {
|
||||
this.dmActorPubkey = actor;
|
||||
}
|
||||
@@ -964,14 +891,8 @@ export class NostrService {
|
||||
return this.getDirectMessages();
|
||||
}
|
||||
|
||||
if (force) {
|
||||
this.dmMessages = [];
|
||||
this.dmMessageIndex = new Map();
|
||||
}
|
||||
|
||||
const merged = mergeDirectMessageCache(this.dmMessages, hydrated);
|
||||
this.dmMessageIndex = merged.index;
|
||||
this.dmMessages = merged.messages;
|
||||
this.dmMessages = hydrated;
|
||||
this.dmMessageIndex = new Map();
|
||||
this.dmHydratedFromSnapshot = hydrated.length > 0;
|
||||
|
||||
const snapshot = this.getDirectMessages();
|
||||
@@ -1006,8 +927,6 @@ export class NostrService {
|
||||
: Number.isFinite(decryptLimitCandidate) && decryptLimitCandidate > 0
|
||||
? Math.floor(decryptLimitCandidate)
|
||||
: null;
|
||||
const sinceCandidate = Number(options.since);
|
||||
const untilCandidate = Number(options.until);
|
||||
|
||||
if (resolvedDecryptLimit && resolvedDecryptLimit > resolvedLimit) {
|
||||
resolvedDecryptLimit = resolvedLimit;
|
||||
@@ -1032,27 +951,6 @@ export class NostrService {
|
||||
|
||||
this.dmActorPubkey = normalizedActor;
|
||||
|
||||
let resolvedSince =
|
||||
Number.isFinite(sinceCandidate) && sinceCandidate >= 0
|
||||
? Math.floor(sinceCandidate)
|
||||
: null;
|
||||
const resolvedUntil =
|
||||
Number.isFinite(untilCandidate) && untilCandidate >= 0
|
||||
? Math.floor(untilCandidate)
|
||||
: null;
|
||||
if (resolvedSince === null && initialLoad) {
|
||||
let latestTimestamp = 0;
|
||||
for (const message of Array.isArray(this.dmMessages) ? this.dmMessages : []) {
|
||||
const timestamp = resolveDirectMessageSortValue(message);
|
||||
if (timestamp > latestTimestamp) {
|
||||
latestTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
if (latestTimestamp > 0) {
|
||||
resolvedSince = Math.max(latestTimestamp - 60, 0);
|
||||
}
|
||||
}
|
||||
|
||||
let messages = [];
|
||||
try {
|
||||
messages = await this.nostrClient.listDirectMessages(normalizedActor, {
|
||||
@@ -1060,8 +958,6 @@ export class NostrService {
|
||||
...options,
|
||||
limit: resolvedLimit,
|
||||
decryptLimit: resolvedDecryptLimit,
|
||||
...(resolvedSince !== null ? { since: resolvedSince } : {}),
|
||||
...(resolvedUntil !== null ? { until: resolvedUntil } : {}),
|
||||
onMessage: (message) => {
|
||||
this.applyDirectMessage(message, { reason: "load-incremental" });
|
||||
},
|
||||
@@ -1072,19 +968,37 @@ export class NostrService {
|
||||
}
|
||||
|
||||
const collected = [];
|
||||
const index = new Map();
|
||||
|
||||
for (const message of Array.isArray(messages) ? messages : []) {
|
||||
const normalized = normalizeDirectMessagePayload(message);
|
||||
if (!normalized) {
|
||||
if (!message || message.ok !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const eventId = message?.event?.id;
|
||||
if (typeof eventId !== "string" || !eventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = {
|
||||
...message,
|
||||
timestamp:
|
||||
Number.isFinite(message?.timestamp)
|
||||
? message.timestamp
|
||||
: Number.isFinite(message?.message?.created_at)
|
||||
? message.message.created_at
|
||||
: Number.isFinite(message?.event?.created_at)
|
||||
? message.event.created_at
|
||||
: Date.now() / 1000,
|
||||
};
|
||||
|
||||
index.set(eventId, normalized);
|
||||
collected.push(normalized);
|
||||
}
|
||||
|
||||
const merged = mergeDirectMessageCache(this.dmMessages, collected);
|
||||
this.dmMessageIndex = merged.index;
|
||||
this.dmMessages = merged.messages;
|
||||
collected.sort((a, b) => (b?.timestamp || 0) - (a?.timestamp || 0));
|
||||
this.dmMessageIndex = index;
|
||||
this.dmMessages = collected;
|
||||
|
||||
const snapshot = this.getDirectMessages();
|
||||
|
||||
@@ -1118,38 +1032,16 @@ export class NostrService {
|
||||
}
|
||||
|
||||
async ensureDirectMessageSubscription({ actorPubkey, relays, ...handlers } = {}) {
|
||||
if (this.dmSubscription) {
|
||||
return this.dmSubscription;
|
||||
}
|
||||
|
||||
const activeActor = actorPubkey || this.resolveActiveDmActor();
|
||||
const normalizedActor = normalizeHexPubkey(activeActor);
|
||||
if (!normalizedActor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.dmSubscription) {
|
||||
if (this.dmActorPubkey === normalizedActor) {
|
||||
devLogger.log(
|
||||
"[nostrService] Reusing existing DM subscription for active actor.",
|
||||
{
|
||||
actorPubkey: normalizedActor,
|
||||
relays: Array.isArray(this.dmSubscriptionRelays)
|
||||
? this.dmSubscriptionRelays
|
||||
: [],
|
||||
},
|
||||
);
|
||||
return this.dmSubscription;
|
||||
}
|
||||
devLogger.log(
|
||||
"[nostrService] Replacing existing DM subscription for new actor.",
|
||||
{
|
||||
previous: this.dmActorPubkey,
|
||||
next: normalizedActor,
|
||||
previousRelays: Array.isArray(this.dmSubscriptionRelays)
|
||||
? this.dmSubscriptionRelays
|
||||
: [],
|
||||
},
|
||||
);
|
||||
this.stopDirectMessageSubscription();
|
||||
}
|
||||
|
||||
let resolvedRelays = Array.isArray(relays) ? relays : null;
|
||||
let relaySelection = null;
|
||||
|
||||
@@ -1250,13 +1142,6 @@ export class NostrService {
|
||||
|
||||
this.dmSubscription = subscription;
|
||||
this.dmActorPubkey = normalizedActor;
|
||||
this.dmSubscriptionRelays = Array.isArray(resolvedRelays)
|
||||
? resolvedRelays.slice()
|
||||
: [];
|
||||
devLogger.log("[nostrService] DM subscription activated.", {
|
||||
actorPubkey: normalizedActor,
|
||||
relays: resolvedRelays,
|
||||
});
|
||||
this.emit("directMessages:subscribed", { subscription });
|
||||
if (relaySelection?.warning === DM_RELAY_WARNING_FALLBACK) {
|
||||
this.emit("directMessages:relayWarning", {
|
||||
@@ -1288,7 +1173,6 @@ export class NostrService {
|
||||
}
|
||||
}
|
||||
this.dmSubscription = null;
|
||||
this.dmSubscriptionRelays = null;
|
||||
}
|
||||
|
||||
async persistDirectMessageSnapshot(actorPubkey = null, messages = null) {
|
||||
|
||||
Vendored
+11
-278
@@ -62,160 +62,6 @@ class SimpleEventEmitter {
|
||||
const HOSTED_URL_SUCCESS_MESSAGE = "✅ Streaming from hosted URL";
|
||||
const DEFAULT_UNSUPPORTED_BTITH_MESSAGE =
|
||||
"This magnet link is missing a compatible BitTorrent v1 info hash.";
|
||||
const AUTH_STATUS_CODES = new Set([401, 403]);
|
||||
const SSL_ERROR_PATTERN = /(ssl|cert|certificate)/i;
|
||||
const CORS_ERROR_PATTERN = /cors/i;
|
||||
const PROBE_CACHE_TTL_MS = 45000;
|
||||
|
||||
const getHostedUrlFailureDetails = (probeResult = {}) => {
|
||||
const outcome = probeResult?.outcome || "error";
|
||||
const status = Number.isFinite(probeResult?.status)
|
||||
? probeResult.status
|
||||
: null;
|
||||
const error = probeResult?.error;
|
||||
const isAuthStatus = status !== null && AUTH_STATUS_CODES.has(status);
|
||||
|
||||
if (outcome === "timeout") {
|
||||
return {
|
||||
category: "external",
|
||||
message:
|
||||
"Hosted URL timed out. We’ll try WebTorrent if available.",
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
if (outcome === "bad") {
|
||||
if (isAuthStatus) {
|
||||
return {
|
||||
category: "auth",
|
||||
message:
|
||||
"Hosted URL requires authorization or a signed request. Please log in or re-sign.",
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
return {
|
||||
category: "external",
|
||||
message: "Hosted URL not found (404).",
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
return {
|
||||
category: "external",
|
||||
message: "Hosted URL blocked (403).",
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
if (status && status >= 500) {
|
||||
return {
|
||||
category: "external",
|
||||
message: `Hosted URL unavailable (HTTP ${status}).`,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
if (status) {
|
||||
return {
|
||||
category: "external",
|
||||
message: `Hosted URL failed (HTTP ${status}).`,
|
||||
status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (outcome === "opaque" || outcome === "unknown") {
|
||||
return {
|
||||
category: "external",
|
||||
message:
|
||||
"Hosted URL blocked by browser security (CORS/SSL). We’ll try WebTorrent if available.",
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
if (outcome === "error") {
|
||||
const message = error?.message || "";
|
||||
if (SSL_ERROR_PATTERN.test(message)) {
|
||||
return {
|
||||
category: "external",
|
||||
message:
|
||||
"Hosted URL blocked due to SSL certificate issues.",
|
||||
status,
|
||||
};
|
||||
}
|
||||
if (CORS_ERROR_PATTERN.test(message)) {
|
||||
return {
|
||||
category: "external",
|
||||
message:
|
||||
"Hosted URL blocked by CORS. We’ll try WebTorrent if available.",
|
||||
status,
|
||||
};
|
||||
}
|
||||
return {
|
||||
category: "external",
|
||||
message:
|
||||
"Hosted URL failed to load due to network or security restrictions.",
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
category: "external",
|
||||
message: "Hosted URL failed to load.",
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
const getHostedVideoErrorMessage = (videoElement) => {
|
||||
if (!videoElement?.error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const error = videoElement.error;
|
||||
const code = error.code;
|
||||
const mediaErrorCodes =
|
||||
typeof MediaError !== "undefined"
|
||||
? MediaError
|
||||
: {
|
||||
MEDIA_ERR_ABORTED: 1,
|
||||
MEDIA_ERR_NETWORK: 2,
|
||||
MEDIA_ERR_DECODE: 3,
|
||||
MEDIA_ERR_SRC_NOT_SUPPORTED: 4,
|
||||
};
|
||||
|
||||
switch (code) {
|
||||
case mediaErrorCodes.MEDIA_ERR_ABORTED:
|
||||
return "Hosted playback was interrupted.";
|
||||
case mediaErrorCodes.MEDIA_ERR_NETWORK:
|
||||
return "Hosted playback failed due to network/CORS restrictions.";
|
||||
case mediaErrorCodes.MEDIA_ERR_DECODE:
|
||||
return "Hosted playback failed to decode the video.";
|
||||
case mediaErrorCodes.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||
return "Hosted playback failed: source not supported or blocked.";
|
||||
default:
|
||||
return "Hosted playback failed to load.";
|
||||
}
|
||||
};
|
||||
|
||||
const getTorrentErrorMessage = (error) => {
|
||||
if (!error) {
|
||||
return "WebTorrent could not start. Please try again.";
|
||||
}
|
||||
const message = error.message || "";
|
||||
if (message.includes(DEFAULT_UNSUPPORTED_BTITH_MESSAGE)) {
|
||||
return DEFAULT_UNSUPPORTED_BTITH_MESSAGE;
|
||||
}
|
||||
if (/tracker|announce|peer/i.test(message)) {
|
||||
return "WebTorrent could not reach any peers or trackers.";
|
||||
}
|
||||
if (/permission|blocked|denied/i.test(message)) {
|
||||
return "WebTorrent was blocked by the browser or network.";
|
||||
}
|
||||
return "WebTorrent could not start. Please try again.";
|
||||
};
|
||||
|
||||
export class PlaybackService {
|
||||
constructor({
|
||||
@@ -242,9 +88,6 @@ export class PlaybackService {
|
||||
this.analyticsCallbacks = analyticsCallbacks || {};
|
||||
this.playbackStartTimeout = playbackStartTimeout;
|
||||
this.currentSession = null;
|
||||
this.probeCache = new Map();
|
||||
this.probeInFlight = new Map();
|
||||
this.probeCacheTtlMs = PROBE_CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
log(...args) {
|
||||
@@ -274,76 +117,6 @@ export class PlaybackService {
|
||||
}
|
||||
}
|
||||
|
||||
getProbeCacheKey({ url, magnet }) {
|
||||
const trimmedUrl = typeof url === "string" ? url.trim() : "";
|
||||
const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : "";
|
||||
return `${trimmedUrl}::${trimmedMagnet}`;
|
||||
}
|
||||
|
||||
pruneProbeCache(now = Date.now()) {
|
||||
for (const [key, entry] of this.probeCache.entries()) {
|
||||
if (!entry || !Number.isFinite(entry.expiresAt) || entry.expiresAt <= now) {
|
||||
this.probeCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCachedProbeResult(cacheKey, now = Date.now()) {
|
||||
this.pruneProbeCache(now);
|
||||
const entry = this.probeCache.get(cacheKey);
|
||||
if (!entry || entry.expiresAt <= now) {
|
||||
return null;
|
||||
}
|
||||
return entry.result;
|
||||
}
|
||||
|
||||
storeProbeResult(cacheKey, result, now = Date.now()) {
|
||||
const ttl =
|
||||
Number.isFinite(this.probeCacheTtlMs) && this.probeCacheTtlMs > 0
|
||||
? this.probeCacheTtlMs
|
||||
: PROBE_CACHE_TTL_MS;
|
||||
this.probeCache.set(cacheKey, { result, expiresAt: now + ttl });
|
||||
}
|
||||
|
||||
async probeHostedUrl({ url, magnet, probeUrl } = {}) {
|
||||
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
|
||||
if (!sanitizedUrl) {
|
||||
return { outcome: "invalid" };
|
||||
}
|
||||
const cacheKey = this.getProbeCacheKey({
|
||||
url: sanitizedUrl,
|
||||
magnet,
|
||||
});
|
||||
const cachedResult = this.getCachedProbeResult(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
const inFlight = this.probeInFlight.get(cacheKey);
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const probePromise = (async () => {
|
||||
try {
|
||||
const result =
|
||||
typeof probeUrl === "function"
|
||||
? await probeUrl(sanitizedUrl)
|
||||
: { outcome: "error" };
|
||||
this.storeProbeResult(cacheKey, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result = { outcome: "error", error };
|
||||
this.storeProbeResult(cacheKey, result);
|
||||
return result;
|
||||
} finally {
|
||||
this.probeInFlight.delete(cacheKey);
|
||||
}
|
||||
})();
|
||||
|
||||
this.probeInFlight.set(cacheKey, probePromise);
|
||||
return probePromise;
|
||||
}
|
||||
|
||||
prepareVideoElement(videoElement) {
|
||||
if (!videoElement) {
|
||||
return;
|
||||
@@ -829,19 +602,10 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
throw new Error("No torrent playback handler provided.");
|
||||
}
|
||||
|
||||
let torrentInstance;
|
||||
try {
|
||||
torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
||||
fallbackMagnet: this.fallbackMagnet,
|
||||
urlList: webSeedCandidates,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = getTorrentErrorMessage(err);
|
||||
if (errorMessage) {
|
||||
this.emit("status", { message: errorMessage });
|
||||
}
|
||||
throw new Error(errorMessage, { cause: err });
|
||||
}
|
||||
const torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
||||
fallbackMagnet: this.fallbackMagnet,
|
||||
urlList: webSeedCandidates,
|
||||
});
|
||||
|
||||
this.service.handleAnalyticsEvent("sourcechange", { source: "torrent" });
|
||||
this.emit("sourcechange", { source: "torrent" });
|
||||
@@ -916,11 +680,8 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
const probeResult = await this.service.probeHostedUrl({
|
||||
url: httpsUrl,
|
||||
magnet: this.trimmedMagnet,
|
||||
probeUrl,
|
||||
});
|
||||
const probeResult =
|
||||
typeof probeUrl === "function" ? await probeUrl(httpsUrl) : null;
|
||||
const probeOutcome = probeResult?.outcome || "error";
|
||||
const probeStatus = probeResult?.status || "unknown";
|
||||
const shouldAttemptHosted =
|
||||
@@ -965,11 +726,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
attachWatchdogs({ stallMs: 0 });
|
||||
return;
|
||||
}
|
||||
const errorMessage = getHostedVideoErrorMessage(activeVideoEl);
|
||||
if (errorMessage) {
|
||||
this.emit("status", { message: errorMessage });
|
||||
}
|
||||
outcomeResolver({ status: "fallback", reason, errorMessage });
|
||||
outcomeResolver({ status: "fallback", reason });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1035,25 +792,12 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
|
||||
// Fallback triggered
|
||||
const fallbackReason = playbackOutcome?.reason || "watchdog-triggered";
|
||||
return {
|
||||
status: "fallback",
|
||||
reason: fallbackReason,
|
||||
errorMessage: playbackOutcome?.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const failureDetails = getHostedUrlFailureDetails(probeResult);
|
||||
if (failureDetails?.message) {
|
||||
this.emit("status", { message: failureDetails.message });
|
||||
return { status: "fallback", reason: fallbackReason };
|
||||
}
|
||||
|
||||
cleanupHostedUrlStatusListeners();
|
||||
cleanupDebugListeners();
|
||||
return {
|
||||
status: "fallback",
|
||||
reason: "probe-failed",
|
||||
errorMessage: failureDetails?.message,
|
||||
};
|
||||
return { status: "fallback", reason: "probe-failed" };
|
||||
};
|
||||
|
||||
// --- Timeout Wrapper ---
|
||||
@@ -1085,8 +829,6 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
|
||||
const effectiveTimeout = forcedSource ? 0 : playbackStartTimeout;
|
||||
|
||||
let hostedErrorMessage = "";
|
||||
|
||||
if (tryUrlFirst) {
|
||||
if (httpsUrl) {
|
||||
// Wrap URL attempt in timeout
|
||||
@@ -1100,10 +842,6 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return urlResult;
|
||||
}
|
||||
|
||||
if (typeof urlResult?.errorMessage === "string") {
|
||||
hostedErrorMessage = urlResult.errorMessage;
|
||||
}
|
||||
|
||||
// If timeout or failure, proceed to fallback
|
||||
if (urlResult?.reason === "timeout") {
|
||||
// Need to ensure URL probing/playback is effectively cancelled
|
||||
@@ -1167,18 +905,13 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
if (urlResult && urlResult.source === "url") {
|
||||
return urlResult;
|
||||
}
|
||||
|
||||
if (typeof urlResult?.errorMessage === "string") {
|
||||
hostedErrorMessage = urlResult.errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message =
|
||||
hostedErrorMessage ||
|
||||
(this.magnetProvided && !this.magnetForPlayback
|
||||
this.magnetProvided && !this.magnetForPlayback
|
||||
? unsupportedBtihMessage
|
||||
: "No playable source found.");
|
||||
: "No playable source found.";
|
||||
this.emit("status", { message });
|
||||
this.service.handleAnalyticsEvent("sourcechange", { source: null });
|
||||
this.emit("sourcechange", { source: null });
|
||||
|
||||
-293
@@ -1,293 +0,0 @@
|
||||
import { nostrClient } from "../nostrClientFacade.js";
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
import { relaySubscriptionService } from "./relaySubscriptionService.js";
|
||||
|
||||
const DEFAULT_PROFILE_IMAGE = "assets/svg/default-profile.svg";
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const inFlight = new Map();
|
||||
|
||||
function normalizePubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed || !HEX64_REGEX.test(trimmed)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function resolveRelays(relays, nostr) {
|
||||
if (Array.isArray(relays) && relays.length) {
|
||||
return relays.filter((relay) => typeof relay === "string" && relay.trim());
|
||||
}
|
||||
|
||||
if (Array.isArray(nostr?.readRelays) && nostr.readRelays.length) {
|
||||
return nostr.readRelays.filter((relay) => typeof relay === "string" && relay.trim());
|
||||
}
|
||||
|
||||
if (Array.isArray(nostr?.relays) && nostr.relays.length) {
|
||||
return nostr.relays.filter((relay) => typeof relay === "string" && relay.trim());
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function parseProfileEvent(event, defaultProfileImage) {
|
||||
if (!event || typeof event !== "object" || !event.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.content);
|
||||
const profile = {
|
||||
name: data?.display_name || data?.name || "Unknown",
|
||||
picture: data?.picture || defaultProfileImage,
|
||||
};
|
||||
|
||||
const about = typeof data?.about === "string" ? data.about : "";
|
||||
if (about) {
|
||||
profile.about = about;
|
||||
}
|
||||
|
||||
const website = typeof data?.website === "string" ? data.website : "";
|
||||
if (website) {
|
||||
profile.website = website;
|
||||
}
|
||||
|
||||
const banner = typeof data?.banner === "string" ? data.banner : "";
|
||||
if (banner) {
|
||||
profile.banner = banner;
|
||||
}
|
||||
|
||||
const lud16 = typeof data?.lud16 === "string" ? data.lud16 : "";
|
||||
if (lud16) {
|
||||
profile.lud16 = lud16;
|
||||
}
|
||||
|
||||
const lud06 = typeof data?.lud06 === "string" ? data.lud06 : "";
|
||||
if (lud06) {
|
||||
profile.lud06 = lud06;
|
||||
}
|
||||
|
||||
return profile;
|
||||
} catch (error) {
|
||||
devLogger.warn("[profileMetadata] Failed to parse profile event", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function performProfileFetch({
|
||||
pubkeys,
|
||||
nostr,
|
||||
relays,
|
||||
logger,
|
||||
defaultProfileImage,
|
||||
} = {}) {
|
||||
if (!nostr?.pool || typeof nostr.pool.list !== "function") {
|
||||
logger.warn("[profileMetadata] Relay pool is not ready; skipping profile fetch.");
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const relayUrls = resolveRelays(relays, nostr);
|
||||
if (!relayUrls.length) {
|
||||
logger.warn("[profileMetadata] No relays configured; skipping profile fetch.");
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const filter = {
|
||||
kinds: [0],
|
||||
authors: pubkeys,
|
||||
limit: pubkeys.length,
|
||||
};
|
||||
|
||||
const newestProfiles = new Map();
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
relayUrls.map((relayUrl) =>
|
||||
nostr.pool
|
||||
.list([relayUrl], [filter])
|
||||
.then((events) => ({ relayUrl, events }))
|
||||
)
|
||||
);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const relayUrl =
|
||||
result.status === "fulfilled"
|
||||
? result.value.relayUrl
|
||||
: relayUrls[index];
|
||||
|
||||
if (result.status !== "fulfilled") {
|
||||
logger.warn(
|
||||
`[profileMetadata] Failed to fetch profiles from relay ${relayUrl}:`,
|
||||
result.reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const events = Array.isArray(result.value.events)
|
||||
? result.value.events
|
||||
: [];
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const pubkey = normalizePubkey(event?.pubkey);
|
||||
if (!pubkey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdAt = Number.isFinite(event?.created_at)
|
||||
? event.created_at
|
||||
: 0;
|
||||
const previous = newestProfiles.get(pubkey);
|
||||
if (previous && previous.createdAt >= createdAt) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const profile = parseProfileEvent(event, defaultProfileImage);
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newestProfiles.set(pubkey, {
|
||||
createdAt,
|
||||
profile,
|
||||
event,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return newestProfiles;
|
||||
}
|
||||
|
||||
export async function fetchProfileMetadataBatch({
|
||||
pubkeys,
|
||||
nostr = nostrClient,
|
||||
relays = null,
|
||||
logger = devLogger,
|
||||
defaultProfileImage = DEFAULT_PROFILE_IMAGE,
|
||||
} = {}) {
|
||||
const normalizedPubkeys = Array.isArray(pubkeys)
|
||||
? pubkeys.map(normalizePubkey).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!normalizedPubkeys.length) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const uniquePubkeys = Array.from(new Set(normalizedPubkeys));
|
||||
const results = new Map();
|
||||
const waiters = [];
|
||||
const toFetch = [];
|
||||
|
||||
uniquePubkeys.forEach((pubkey) => {
|
||||
const existing = inFlight.get(pubkey);
|
||||
if (existing) {
|
||||
logger.debug?.("[profileMetadata] Reusing in-flight profile fetch", {
|
||||
pubkey,
|
||||
});
|
||||
waiters.push(
|
||||
existing.then((entry) => {
|
||||
if (entry) {
|
||||
results.set(pubkey, entry);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
toFetch.push(pubkey);
|
||||
});
|
||||
|
||||
let batchPromise = null;
|
||||
if (toFetch.length) {
|
||||
logger.debug?.("[profileMetadata] Fetching profile metadata", {
|
||||
count: toFetch.length,
|
||||
});
|
||||
const fetchPromise = performProfileFetch({
|
||||
pubkeys: toFetch,
|
||||
nostr,
|
||||
relays,
|
||||
logger,
|
||||
defaultProfileImage,
|
||||
});
|
||||
|
||||
toFetch.forEach((pubkey) => {
|
||||
const perPubkeyPromise = fetchPromise
|
||||
.then((map) => map.get(pubkey) || null)
|
||||
.finally(() => {
|
||||
if (inFlight.get(pubkey) === perPubkeyPromise) {
|
||||
inFlight.delete(pubkey);
|
||||
}
|
||||
});
|
||||
inFlight.set(pubkey, perPubkeyPromise);
|
||||
});
|
||||
|
||||
batchPromise = fetchPromise.then((map) => {
|
||||
map.forEach((value, pubkey) => results.set(pubkey, value));
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([...waiters, batchPromise].filter(Boolean));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function fetchProfileMetadata(pubkey, options = {}) {
|
||||
const normalized = normalizePubkey(pubkey);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = await fetchProfileMetadataBatch({
|
||||
pubkeys: [normalized],
|
||||
...options,
|
||||
});
|
||||
|
||||
return results.get(normalized) || null;
|
||||
}
|
||||
|
||||
export function ensureProfileMetadataSubscription({
|
||||
pubkey,
|
||||
nostr = nostrClient,
|
||||
relays = null,
|
||||
logger = devLogger,
|
||||
defaultProfileImage = DEFAULT_PROFILE_IMAGE,
|
||||
onProfile,
|
||||
} = {}) {
|
||||
const normalized = normalizePubkey(pubkey);
|
||||
if (!normalized || !nostr?.pool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relayUrls = resolveRelays(relays, nostr);
|
||||
const filters = [{ kinds: [0], authors: [normalized], limit: 1 }];
|
||||
const key = `profile:${normalized}`;
|
||||
|
||||
return relaySubscriptionService.ensureSubscription({
|
||||
key,
|
||||
pool: nostr.pool,
|
||||
relays: relayUrls,
|
||||
filters,
|
||||
label: "profile-metadata",
|
||||
onEvent: (event) => {
|
||||
if (!event || event.pubkey !== normalized) {
|
||||
return;
|
||||
}
|
||||
const profile = parseProfileEvent(event, defaultProfileImage);
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
if (typeof onProfile === "function") {
|
||||
try {
|
||||
onProfile({ profile, event });
|
||||
} catch (error) {
|
||||
logger.warn("[profileMetadata] onProfile callback threw", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
+2
-31
@@ -1,8 +1,6 @@
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
|
||||
const TELEMETRY_STORAGE_KEY = "bitvid:relay-health-telemetry-opt-in";
|
||||
const PERSISTENT_FAILURE_THRESHOLD = 3;
|
||||
const USER_LOG_COOLDOWN_MS = 5 * 60 * 1000;
|
||||
|
||||
function resolveLogger(logger) {
|
||||
if (logger && logger.dev && logger.user) {
|
||||
@@ -92,10 +90,8 @@ class RelayHealthService {
|
||||
connected: false,
|
||||
lastLatencyMs: null,
|
||||
errorCount: 0,
|
||||
consecutiveFailures: 0,
|
||||
lastCheckedAt: null,
|
||||
lastErrorAt: null,
|
||||
lastUserLogAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,47 +129,22 @@ class RelayHealthService {
|
||||
const state = this.ensureRelayState(relayUrl);
|
||||
state.connected = true;
|
||||
state.lastCheckedAt = Date.now();
|
||||
state.consecutiveFailures = 0;
|
||||
if (Number.isFinite(latencyMs)) {
|
||||
state.lastLatencyMs = Math.max(0, Math.round(latencyMs));
|
||||
}
|
||||
|
||||
if (this.nostrClient && typeof this.nostrClient.clearRelayBackoff === "function") {
|
||||
this.nostrClient.clearRelayBackoff(relayUrl);
|
||||
}
|
||||
}
|
||||
|
||||
recordRelayFailure(relayUrl, error) {
|
||||
const state = this.ensureRelayState(relayUrl);
|
||||
const now = Date.now();
|
||||
state.connected = false;
|
||||
state.errorCount += 1;
|
||||
state.consecutiveFailures += 1;
|
||||
state.lastCheckedAt = now;
|
||||
state.lastErrorAt = now;
|
||||
|
||||
if (this.nostrClient && typeof this.nostrClient.markRelayUnreachable === "function") {
|
||||
this.nostrClient.markRelayUnreachable(relayUrl, 60000, {
|
||||
reason: "relay-health-failed",
|
||||
});
|
||||
}
|
||||
state.lastCheckedAt = Date.now();
|
||||
state.lastErrorAt = Date.now();
|
||||
|
||||
this.logger.dev.warn("[relayHealth] Relay check failed.", {
|
||||
relayUrl,
|
||||
error,
|
||||
});
|
||||
|
||||
if (
|
||||
state.consecutiveFailures >= PERSISTENT_FAILURE_THRESHOLD &&
|
||||
(!state.lastUserLogAt || now - state.lastUserLogAt >= USER_LOG_COOLDOWN_MS)
|
||||
) {
|
||||
state.lastUserLogAt = now;
|
||||
this.logger.user.warn("[relayHealth] Relay unreachable after repeated failures.", {
|
||||
relayUrl,
|
||||
errorCount: state.consecutiveFailures,
|
||||
lastError: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkRelay(relayUrl) {
|
||||
|
||||
-250
@@ -1,250 +0,0 @@
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
|
||||
function normalizeRelayList(relays) {
|
||||
if (!Array.isArray(relays)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
relays
|
||||
.map((relay) => (typeof relay === "string" ? relay.trim() : ""))
|
||||
.filter(Boolean)
|
||||
.forEach((relay) => {
|
||||
if (!seen.has(relay)) {
|
||||
seen.add(relay);
|
||||
normalized.push(relay);
|
||||
}
|
||||
});
|
||||
|
||||
normalized.sort();
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeFilterValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
const normalizedArray = value.map((entry) => normalizeFilterValue(entry));
|
||||
const isPrimitiveArray = normalizedArray.every(
|
||||
(entry) => typeof entry === "string" || typeof entry === "number",
|
||||
);
|
||||
if (isPrimitiveArray) {
|
||||
return normalizedArray
|
||||
.slice()
|
||||
.sort((a, b) => String(a).localeCompare(String(b)));
|
||||
}
|
||||
return normalizedArray;
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
return normalizeFilter(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeFilter(filter) {
|
||||
if (!filter || typeof filter !== "object") {
|
||||
return {};
|
||||
}
|
||||
|
||||
const entries = Object.entries(filter)
|
||||
.filter(([key]) => typeof key === "string")
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
const normalized = {};
|
||||
entries.forEach(([key, value]) => {
|
||||
normalized[key] = normalizeFilterValue(value);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeFilters(filters) {
|
||||
if (!Array.isArray(filters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return filters.map((filter) => normalizeFilter(filter));
|
||||
}
|
||||
|
||||
function stableStringify(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
const keys = Object.keys(value).sort();
|
||||
return `{${keys
|
||||
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function buildSignature({ relays, filters }) {
|
||||
return stableStringify({
|
||||
relays: normalizeRelayList(relays),
|
||||
filters: normalizeFilters(filters),
|
||||
});
|
||||
}
|
||||
|
||||
class RelaySubscriptionService {
|
||||
constructor() {
|
||||
this.subscriptions = new Map();
|
||||
}
|
||||
|
||||
ensureSubscription({
|
||||
key,
|
||||
pool,
|
||||
relays,
|
||||
filters,
|
||||
label = "subscription",
|
||||
onEvent,
|
||||
onEose,
|
||||
onClose,
|
||||
} = {}) {
|
||||
if (!key || typeof key !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pool || typeof pool.sub !== "function") {
|
||||
devLogger.warn("[relaySubscriptions] Pool unavailable; skipping subscription", {
|
||||
key,
|
||||
label,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const signature = buildSignature({ relays, filters });
|
||||
const existing = this.subscriptions.get(key);
|
||||
|
||||
if (existing && existing.signature === signature && existing.subscription) {
|
||||
devLogger.log("[relaySubscriptions] Reusing existing subscription", {
|
||||
key,
|
||||
label,
|
||||
relays: existing.relays,
|
||||
});
|
||||
return existing.subscription;
|
||||
}
|
||||
|
||||
if (existing && existing.unsubscribe) {
|
||||
devLogger.warn("[relaySubscriptions] Replacing existing subscription", {
|
||||
key,
|
||||
label,
|
||||
});
|
||||
try {
|
||||
existing.unsubscribe();
|
||||
} catch (error) {
|
||||
userLogger.warn("[relaySubscriptions] Failed to unsubscribe existing subscription", error);
|
||||
}
|
||||
}
|
||||
|
||||
const relayList = normalizeRelayList(relays);
|
||||
const filterList = normalizeFilters(filters);
|
||||
|
||||
if (!relayList.length || !filterList.length) {
|
||||
devLogger.warn("[relaySubscriptions] Missing relays or filters; skipping subscription", {
|
||||
key,
|
||||
label,
|
||||
relays: relayList,
|
||||
filters: filterList,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
let subscription;
|
||||
try {
|
||||
subscription = pool.sub(relayList, filterList);
|
||||
} catch (error) {
|
||||
userLogger.warn("[relaySubscriptions] Failed to create relay subscription", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const unsubscribe = () => {
|
||||
try {
|
||||
if (typeof subscription?.unsub === "function") {
|
||||
subscription.unsub();
|
||||
}
|
||||
} catch (error) {
|
||||
userLogger.warn("[relaySubscriptions] Failed to unsubscribe relay subscription", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (subscription && typeof subscription.on === "function") {
|
||||
if (typeof onEvent === "function") {
|
||||
subscription.on("event", (event) => {
|
||||
try {
|
||||
onEvent(event);
|
||||
} catch (error) {
|
||||
userLogger.warn("[relaySubscriptions] Event handler threw", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof onEose === "function") {
|
||||
subscription.on("eose", () => {
|
||||
try {
|
||||
onEose();
|
||||
} catch (error) {
|
||||
userLogger.warn("[relaySubscriptions] EOSE handler threw", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof onClose === "function") {
|
||||
subscription.on("close", (reasons) => {
|
||||
try {
|
||||
onClose(reasons);
|
||||
} catch (error) {
|
||||
userLogger.warn("[relaySubscriptions] Close handler threw", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.subscriptions.set(key, {
|
||||
signature,
|
||||
subscription,
|
||||
unsubscribe,
|
||||
label,
|
||||
relays: relayList,
|
||||
filters: filterList,
|
||||
});
|
||||
|
||||
devLogger.log("[relaySubscriptions] Subscription activated", {
|
||||
key,
|
||||
label,
|
||||
relays: relayList,
|
||||
filters: filterList,
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
stopSubscription(key, reason = "") {
|
||||
if (!key || typeof key !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = this.subscriptions.get(key);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
existing.unsubscribe?.();
|
||||
} finally {
|
||||
this.subscriptions.delete(key);
|
||||
devLogger.log("[relaySubscriptions] Subscription stopped", {
|
||||
key,
|
||||
label: existing.label,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSignature({ relays, filters }) {
|
||||
return buildSignature({ relays, filters });
|
||||
}
|
||||
}
|
||||
|
||||
export const relaySubscriptionService = new RelaySubscriptionService();
|
||||
Vendored
+9
-39
@@ -166,39 +166,21 @@ class ProfileCache {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingEntry = this.getProfileData(normalizedPubkey, "profile");
|
||||
const existingProfile = existingEntry?.profile || {};
|
||||
|
||||
const existingName = sanitizeProfileString(existingProfile.name);
|
||||
const existingPicture = sanitizeProfileMediaUrl(existingProfile.picture);
|
||||
const existingAbout = sanitizeProfileString(existingProfile.about);
|
||||
const existingWebsite = sanitizeProfileString(existingProfile.website);
|
||||
const existingBanner = sanitizeProfileMediaUrl(existingProfile.banner);
|
||||
const existingLud16 = sanitizeProfileString(existingProfile.lud16);
|
||||
const existingLud06 = sanitizeProfileString(existingProfile.lud06);
|
||||
const existingLightningAddress = sanitizeProfileString(
|
||||
existingProfile.lightningAddress,
|
||||
);
|
||||
|
||||
const normalized = {
|
||||
name:
|
||||
sanitizeProfileString(profile.name || profile.display_name) ||
|
||||
existingName ||
|
||||
"Unknown",
|
||||
picture:
|
||||
sanitizeProfileMediaUrl(profile.picture || profile.image) ||
|
||||
existingPicture ||
|
||||
"assets/svg/default-profile.svg",
|
||||
};
|
||||
|
||||
const about =
|
||||
sanitizeProfileString(profile.about || profile.aboutMe) || existingAbout;
|
||||
const about = sanitizeProfileString(profile.about || profile.aboutMe);
|
||||
if (about) {
|
||||
normalized.about = about;
|
||||
}
|
||||
|
||||
const website =
|
||||
sanitizeProfileString(profile.website || profile.url) || existingWebsite;
|
||||
const website = sanitizeProfileString(profile.website || profile.url);
|
||||
if (website) {
|
||||
normalized.website = website;
|
||||
}
|
||||
@@ -209,19 +191,18 @@ class ProfileCache {
|
||||
profile.background ||
|
||||
profile.cover ||
|
||||
profile.cover_image ||
|
||||
profile.coverImage,
|
||||
profile.coverImage
|
||||
);
|
||||
const resolvedBanner = banner || existingBanner;
|
||||
if (resolvedBanner) {
|
||||
normalized.banner = resolvedBanner;
|
||||
if (banner) {
|
||||
normalized.banner = banner;
|
||||
}
|
||||
|
||||
const lud16 = sanitizeProfileString(profile.lud16) || existingLud16;
|
||||
const lud16 = sanitizeProfileString(profile.lud16);
|
||||
if (lud16) {
|
||||
normalized.lud16 = lud16;
|
||||
}
|
||||
|
||||
const lud06 = sanitizeProfileString(profile.lud06) || existingLud06;
|
||||
const lud06 = sanitizeProfileString(profile.lud06);
|
||||
if (lud06) {
|
||||
normalized.lud06 = lud06;
|
||||
}
|
||||
@@ -231,11 +212,8 @@ class ProfileCache {
|
||||
lud16,
|
||||
lud06,
|
||||
].filter(Boolean);
|
||||
const resolvedLightningAddress = lightningCandidates.length
|
||||
? lightningCandidates[0]
|
||||
: existingLightningAddress;
|
||||
if (resolvedLightningAddress) {
|
||||
normalized.lightningAddress = resolvedLightningAddress;
|
||||
if (lightningCandidates.length) {
|
||||
normalized.lightningAddress = lightningCandidates[0];
|
||||
}
|
||||
|
||||
const entry = {
|
||||
@@ -368,14 +346,6 @@ class ProfileCache {
|
||||
this.emit("runtimeCleared", { pubkey: normalized });
|
||||
}
|
||||
|
||||
reset() {
|
||||
const previousPubkey = this.activePubkey;
|
||||
this.activePubkey = null;
|
||||
this.memoryCache.clear();
|
||||
this.emit("profileChanged", { pubkey: null, previousPubkey: previousPubkey || null });
|
||||
this.emit("reset", { previousPubkey: previousPubkey || null });
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.listeners.add(callback);
|
||||
return () => this.listeners.delete(callback);
|
||||
|
||||
Vendored
+32
-177
@@ -5,7 +5,7 @@ import {
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostrClientFacade.js";
|
||||
import { convertEventToVideo as sharedConvertEventToVideo } from "./nostr/index.js";
|
||||
import { normalizeNostrPubkey, sanitizeRelayList } from "./nostr/nip46Client.js";
|
||||
import { normalizeNostrPubkey } from "./nostr/nip46Client.js";
|
||||
import {
|
||||
listVideoViewEvents,
|
||||
subscribeVideoViewEvents,
|
||||
@@ -31,16 +31,11 @@ import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import { profileCache } from "./state/profileCache.js";
|
||||
import {
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
runNip07WithRetry,
|
||||
NIP07_PRIORITY,
|
||||
} from "./nostr/nip07Permissions.js";
|
||||
import { relaySubscriptionService } from "./services/relaySubscriptionService.js";
|
||||
import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js";
|
||||
|
||||
const SUBSCRIPTION_SET_KIND =
|
||||
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
|
||||
const DECRYPT_TIMEOUT_MS = 15000;
|
||||
const DECRYPT_TIMEOUT_MS = 90000;
|
||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||
|
||||
function normalizeHexPubkey(value) {
|
||||
@@ -334,12 +329,10 @@ class SubscriptionsManager {
|
||||
this.currentUserPubkey = null;
|
||||
this.loaded = false;
|
||||
this.loadingPromise = null;
|
||||
this.backgroundLoading = false;
|
||||
this.subscriptionListView = null;
|
||||
this.lastRunOptions = null;
|
||||
this.lastResult = null;
|
||||
this.lastContainerId = null;
|
||||
this.lastLoadError = null;
|
||||
this.unsubscribeFromNostrUpdates = null;
|
||||
this.pendingRefreshPromise = null;
|
||||
this.scheduledRefreshDetail = null;
|
||||
@@ -348,7 +341,6 @@ class SubscriptionsManager {
|
||||
this.emitter = new TinyEventEmitter();
|
||||
this.decryptRetryTimeoutId = null;
|
||||
this.ensureNostrServiceListener();
|
||||
this.subscriptionKey = null;
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
@@ -358,9 +350,7 @@ class SubscriptionsManager {
|
||||
} else if (event === "runtimeCleared" && detail.pubkey === this.currentUserPubkey) {
|
||||
this.reset();
|
||||
if (this.currentUserPubkey) {
|
||||
this.loadSubscriptions(this.currentUserPubkey, {
|
||||
allowPermissionPrompt: false,
|
||||
});
|
||||
this.loadSubscriptions(this.currentUserPubkey);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -369,13 +359,12 @@ class SubscriptionsManager {
|
||||
/**
|
||||
* Decrypt the subscription list from kind=30000 (d="subscriptions").
|
||||
*/
|
||||
async loadSubscriptions(userPubkey, options = {}) {
|
||||
async loadSubscriptions(userPubkey) {
|
||||
if (!userPubkey) {
|
||||
userLogger.warn("[SubscriptionsManager] No pubkey => cannot load subs.");
|
||||
return;
|
||||
}
|
||||
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||
|
||||
// 1. Attempt to load from cache first
|
||||
@@ -390,25 +379,14 @@ class SubscriptionsManager {
|
||||
this.loaded = true;
|
||||
|
||||
// Trigger background update
|
||||
if (!allowPermissionPrompt && !this.backgroundLoading) {
|
||||
this.backgroundLoading = true;
|
||||
this.emitter.emit("change", {
|
||||
action: "background-loading",
|
||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
||||
background: true,
|
||||
});
|
||||
} else if (allowPermissionPrompt) {
|
||||
this.backgroundLoading = false;
|
||||
}
|
||||
|
||||
this.updateFromRelays(userPubkey, { allowPermissionPrompt }).catch((err) => {
|
||||
this.updateFromRelays(userPubkey).catch((err) => {
|
||||
devLogger.warn("[SubscriptionsManager] Background update failed:", err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If no cache, must wait for relays
|
||||
await this.updateFromRelays(userPubkey, { allowPermissionPrompt });
|
||||
await this.updateFromRelays(userPubkey);
|
||||
}
|
||||
|
||||
saveToCache(userPubkey) {
|
||||
@@ -424,7 +402,7 @@ class SubscriptionsManager {
|
||||
return this.emitter.on(eventName, handler);
|
||||
}
|
||||
|
||||
scheduleDecryptRetry(userPubkey, error, options = {}) {
|
||||
scheduleDecryptRetry(userPubkey, error) {
|
||||
const normalized = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||
if (!normalized) {
|
||||
return;
|
||||
@@ -437,7 +415,7 @@ class SubscriptionsManager {
|
||||
if (this.currentUserPubkey && this.currentUserPubkey !== normalized) {
|
||||
return;
|
||||
}
|
||||
this.updateFromRelays(normalized, options).catch((retryError) => {
|
||||
this.updateFromRelays(normalized).catch((retryError) => {
|
||||
userLogger.warn("[SubscriptionsManager] Decryption retry failed:", retryError);
|
||||
});
|
||||
}, DECRYPT_RETRY_DELAY_MS);
|
||||
@@ -447,14 +425,11 @@ class SubscriptionsManager {
|
||||
);
|
||||
}
|
||||
|
||||
async updateFromRelays(userPubkey, options = {}) {
|
||||
async updateFromRelays(userPubkey) {
|
||||
if (!userPubkey) return;
|
||||
|
||||
try {
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
const wasBackgroundLoading = this.backgroundLoading;
|
||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||
this.lastLoadError = null;
|
||||
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
@@ -462,26 +437,8 @@ class SubscriptionsManager {
|
||||
devLogger.warn(
|
||||
"[SubscriptionsManager] No relay URLs available while loading subscriptions.",
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitter.emit("change", {
|
||||
action: "background-loaded",
|
||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!nostrClient?.pool && typeof nostrClient?.ensurePool === "function") {
|
||||
try {
|
||||
await nostrClient.ensurePool();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[SubscriptionsManager] Failed to initialize relay pool for subscription",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.ensureSubscriptionListSubscription(normalizedUserPubkey, relayUrls);
|
||||
|
||||
// Use incremental fetch helper
|
||||
const cachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||
@@ -493,7 +450,7 @@ class SubscriptionsManager {
|
||||
pubkey: normalizedUserPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: shouldForceFullFetch ? 0 : (cachedSnapshot.createdAt || 0),
|
||||
since: shouldForceFullFetch ? 0 : undefined,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
|
||||
@@ -516,7 +473,7 @@ class SubscriptionsManager {
|
||||
pubkey: normalizedSessionActorPubkey,
|
||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||
relayUrls,
|
||||
since: shouldForceSessionFetch ? 0 : (sessionCachedSnapshot.createdAt || 0),
|
||||
since: shouldForceSessionFetch ? 0 : undefined,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
if (sessionEvents.length) {
|
||||
@@ -559,13 +516,6 @@ class SubscriptionsManager {
|
||||
// This means no updates. We keep what we have.
|
||||
devLogger.log("[SubscriptionsManager] No updates from relays.");
|
||||
}
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitter.emit("change", {
|
||||
action: "background-loaded",
|
||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -584,9 +534,7 @@ class SubscriptionsManager {
|
||||
|
||||
let decryptResult;
|
||||
try {
|
||||
const decryptPromise = this.decryptSubscriptionEvent(newest, userPubkey, {
|
||||
allowPermissionPrompt,
|
||||
});
|
||||
const decryptPromise = this.decryptSubscriptionEvent(newest, userPubkey);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => {
|
||||
@@ -605,33 +553,17 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
if (!decryptResult.ok) {
|
||||
this.lastLoadError = decryptResult.error || null;
|
||||
if (decryptResult.error?.code === "subscriptions-decrypt-timeout") {
|
||||
if (!this.loaded && !cachedSnapshot.hasSnapshot) {
|
||||
this.loaded = true;
|
||||
}
|
||||
this.scheduleDecryptRetry(normalizedUserPubkey, decryptResult.error, {
|
||||
allowPermissionPrompt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!allowPermissionPrompt &&
|
||||
decryptResult.error?.code === "subscriptions-permission-required"
|
||||
) {
|
||||
this.scheduleDecryptRetry(normalizedUserPubkey, decryptResult.error);
|
||||
return;
|
||||
}
|
||||
userLogger.error(
|
||||
"[SubscriptionsManager] Failed to decrypt subscription list:",
|
||||
decryptResult.error,
|
||||
);
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
this.emitter.emit("change", {
|
||||
action: "background-loaded",
|
||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
||||
});
|
||||
}
|
||||
if (!this.loaded) {
|
||||
this.subscribedPubkeys.clear();
|
||||
this.subsEventId = null;
|
||||
@@ -651,9 +583,6 @@ class SubscriptionsManager {
|
||||
this.subscribedPubkeys = newSet;
|
||||
this.currentUserPubkey = normalizedUserPubkey;
|
||||
this.loaded = true;
|
||||
if (wasBackgroundLoading) {
|
||||
this.backgroundLoading = false;
|
||||
}
|
||||
|
||||
// Update persistent cache
|
||||
this.saveToCache(normalizedUserPubkey);
|
||||
@@ -681,31 +610,22 @@ class SubscriptionsManager {
|
||||
error
|
||||
);
|
||||
});
|
||||
} else if (wasBackgroundLoading) {
|
||||
this.emitter.emit("change", {
|
||||
action: "background-loaded",
|
||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.lastLoadError = err || null;
|
||||
userLogger.error("[SubscriptionsManager] Failed to update subs from relays:", err);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.stopSubscriptionListSubscription();
|
||||
this.subscribedPubkeys.clear();
|
||||
this.subsEventId = null;
|
||||
this.subsEventCreatedAt = null;
|
||||
this.currentUserPubkey = null;
|
||||
this.loaded = false;
|
||||
this.backgroundLoading = false;
|
||||
this.lastRunOptions = null;
|
||||
this.lastResult = null;
|
||||
this.hasRenderedOnce = false;
|
||||
this.lastLoadError = null;
|
||||
if (this.decryptRetryTimeoutId) {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
@@ -713,56 +633,7 @@ class SubscriptionsManager {
|
||||
this.emitter.emit("change", { action: "reset", subscribedPubkeys: [] });
|
||||
}
|
||||
|
||||
stopSubscriptionListSubscription() {
|
||||
if (this.subscriptionKey) {
|
||||
relaySubscriptionService.stopSubscription(this.subscriptionKey, "reset");
|
||||
this.subscriptionKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
ensureSubscriptionListSubscription(userPubkey, relays) {
|
||||
const normalized = normalizeHexPubkey(userPubkey);
|
||||
if (!normalized || !nostrClient?.pool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = `subscriptions:${normalized}`;
|
||||
this.subscriptionKey = key;
|
||||
|
||||
const filters = [
|
||||
{
|
||||
kinds: [SUBSCRIPTION_SET_KIND],
|
||||
authors: [normalized],
|
||||
"#d": [SUBSCRIPTION_LIST_IDENTIFIER],
|
||||
},
|
||||
];
|
||||
|
||||
return relaySubscriptionService.ensureSubscription({
|
||||
key,
|
||||
pool: nostrClient.pool,
|
||||
relays,
|
||||
filters,
|
||||
label: "subscription-list",
|
||||
onEvent: (event) => this.handleSubscriptionListEvent(event),
|
||||
});
|
||||
}
|
||||
|
||||
handleSubscriptionListEvent(event) {
|
||||
const normalized = normalizeHexPubkey(event?.pubkey);
|
||||
if (!normalized || normalized !== this.currentUserPubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFromRelays(normalized, { allowPermissionPrompt: false })
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[SubscriptionsManager] Failed to refresh after subscription list event",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async ensureLoaded(actorHex, options = {}) {
|
||||
async ensureLoaded(actorHex) {
|
||||
devLogger.log("[SubscriptionsManager] ensureLoaded start", actorHex);
|
||||
const normalizedActor = normalizeHexPubkey(actorHex) || actorHex;
|
||||
if (!normalizedActor) {
|
||||
@@ -784,7 +655,7 @@ class SubscriptionsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = this.loadSubscriptions(normalizedActor, options);
|
||||
const loader = this.loadSubscriptions(normalizedActor);
|
||||
this.loadingPromise = loader;
|
||||
|
||||
try {
|
||||
@@ -838,7 +709,7 @@ class SubscriptionsManager {
|
||||
return Array.from(this.subscribedPubkeys);
|
||||
}
|
||||
|
||||
async decryptSubscriptionEvent(event, userPubkey, options = {}) {
|
||||
async decryptSubscriptionEvent(event, userPubkey) {
|
||||
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
||||
if (!ciphertext) {
|
||||
const error = new Error("Subscription event is missing ciphertext content.");
|
||||
@@ -846,7 +717,6 @@ class SubscriptionsManager {
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
||||
const hints = extractEncryptionHints(event);
|
||||
const requiresNip44 = hints.includes("nip44") || hints.includes("nip44_v2");
|
||||
const requiresNip04 = !hints.length || hints.includes("nip04") || !requiresNip44;
|
||||
@@ -861,8 +731,7 @@ class SubscriptionsManager {
|
||||
if (
|
||||
!signer ||
|
||||
(!signerHasRequiredDecryptors(signer) &&
|
||||
typeof nostrClient?.ensureActiveSignerForPubkey === "function" &&
|
||||
allowPermissionPrompt)
|
||||
typeof nostrClient?.ensureActiveSignerForPubkey === "function")
|
||||
) {
|
||||
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
|
||||
}
|
||||
@@ -870,11 +739,7 @@ class SubscriptionsManager {
|
||||
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
||||
const signerHasNip44 = typeof signer?.nip44Decrypt === "function";
|
||||
|
||||
const nostrApi = allowPermissionPrompt
|
||||
? typeof window !== "undefined"
|
||||
? window?.nostr
|
||||
: null
|
||||
: null;
|
||||
const nostrApi = typeof window !== "undefined" ? window?.nostr : null;
|
||||
const windowHasNip04 = typeof nostrApi?.nip04?.decrypt === "function";
|
||||
const windowHasNip44 =
|
||||
(nostrApi?.nip44 && typeof nostrApi.nip44.decrypt === "function") ||
|
||||
@@ -884,17 +749,8 @@ class SubscriptionsManager {
|
||||
(!signerHasNip44 && !windowHasNip44 && requiresNip44) ||
|
||||
(!signerHasNip04 && !windowHasNip04 && requiresNip04)
|
||||
) {
|
||||
if (!allowPermissionPrompt) {
|
||||
const error = new Error(
|
||||
"Decrypt permissions are required to read subscriptions."
|
||||
);
|
||||
error.code = "subscriptions-permission-required";
|
||||
return { ok: false, error };
|
||||
}
|
||||
try {
|
||||
const permissionResult = await requestDefaultExtensionPermissions(
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
const error =
|
||||
permissionResult?.error instanceof Error
|
||||
@@ -942,19 +798,13 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
if (nostrApi) {
|
||||
const decrypterOptions = {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
timeoutMs: 12000,
|
||||
retryMultiplier: 1,
|
||||
};
|
||||
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor("nip04", (payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip04.decrypt",
|
||||
...decrypterOptions,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -971,7 +821,7 @@ class SubscriptionsManager {
|
||||
() => nip44.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.decrypt",
|
||||
...decrypterOptions,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -985,7 +835,7 @@ class SubscriptionsManager {
|
||||
() => nip44v2.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.v2.decrypt",
|
||||
...decrypterOptions,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -995,7 +845,7 @@ class SubscriptionsManager {
|
||||
() => nip44v2.decrypt(userPubkey, payload),
|
||||
{
|
||||
label: "nip44.v2.decrypt",
|
||||
...decrypterOptions,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1129,9 +979,7 @@ class SubscriptionsManager {
|
||||
}
|
||||
|
||||
if (signer.type === "extension") {
|
||||
const permissionResult = await requestDefaultExtensionPermissions(
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
userLogger.warn(
|
||||
"[SubscriptionsManager] Signer permissions denied while updating subscriptions.",
|
||||
@@ -1240,6 +1088,13 @@ class SubscriptionsManager {
|
||||
throw signErr;
|
||||
}
|
||||
|
||||
const sanitizeRelayList = (candidate) =>
|
||||
Array.isArray(candidate)
|
||||
? candidate
|
||||
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const writeRelays = sanitizeRelayList(nostrClient.writeRelays);
|
||||
const fallbackRelays = writeRelays.length
|
||||
? writeRelays
|
||||
|
||||
Vendored
+6
-29
@@ -135,7 +135,7 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
let destroyed = false;
|
||||
let lastTime = 0;
|
||||
let tainted = false;
|
||||
let resizeObserver = null;
|
||||
let resizeBound = false;
|
||||
|
||||
const clearCanvas = () => {
|
||||
if (!canvasElement) {
|
||||
@@ -152,12 +152,9 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
cancelFrame(rafId);
|
||||
rafId = 0;
|
||||
}
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
if (win) {
|
||||
if (resizeBound && win) {
|
||||
win.removeEventListener("resize", handleResize);
|
||||
resizeBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,11 +176,7 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
lastTime = timestamp;
|
||||
|
||||
try {
|
||||
// Optimization: Rely on ResizeObserver for dimensions.
|
||||
// Do NOT call resizeCanvas() here to avoid layout thrashing.
|
||||
const width = canvasElement.width || 0;
|
||||
const height = canvasElement.height || 0;
|
||||
|
||||
const { width, height } = resizeCanvas(canvasElement, videoElement);
|
||||
if (!width || !height) {
|
||||
schedule();
|
||||
return;
|
||||
@@ -210,26 +203,10 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
||||
running = true;
|
||||
lastTime = 0;
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
|
||||
if (!resizeObserver && win && typeof win.ResizeObserver === "function") {
|
||||
try {
|
||||
resizeObserver = new win.ResizeObserver(() => {
|
||||
if (!destroyed) {
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
}
|
||||
});
|
||||
const host = canvasElement.parentElement || canvasElement;
|
||||
resizeObserver.observe(host);
|
||||
} catch (err) {
|
||||
if (logger && typeof logger.warn === "function") {
|
||||
logger.warn("[ambientBackground] Failed to initialize ResizeObserver", err);
|
||||
}
|
||||
}
|
||||
} else if (win && !resizeObserver) {
|
||||
// Fallback for browsers without ResizeObserver
|
||||
if (win && !resizeBound) {
|
||||
win.addEventListener("resize", handleResize);
|
||||
resizeBound = true;
|
||||
}
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
|
||||
Vendored
+1
-53
@@ -34,7 +34,6 @@ import ProfileModalController from "./profileModalController.js";
|
||||
import LoginModalController from "./loginModalController.js";
|
||||
import { VideoListView } from "./views/VideoListView.js";
|
||||
import MoreMenuController from "./moreMenuController.js";
|
||||
import VideoSettingsMenuController from "./videoSettingsMenuController.js";
|
||||
import AppChromeController from "./appChromeController.js";
|
||||
import { getSidebarLoadingMarkup } from "../sidebarLoading.js";
|
||||
import { isWatchHistoryDebugEnabled } from "../watchHistoryDebug.js";
|
||||
@@ -409,30 +408,6 @@ export default class ApplicationBootstrap {
|
||||
}
|
||||
}),
|
||||
);
|
||||
app.authEventUnsubscribes.push(
|
||||
app.authService.on("blocksLoaded", (detail) => {
|
||||
try {
|
||||
app.handleBlocksLoaded(detail);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"Failed to process blocks loaded event:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
app.authEventUnsubscribes.push(
|
||||
app.authService.on("relaysLoaded", (detail) => {
|
||||
try {
|
||||
app.handleRelaysLoaded(detail);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"Failed to process relays loaded event:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
app.commentThreadService =
|
||||
this.services.commentThreadService ||
|
||||
@@ -604,7 +579,6 @@ export default class ApplicationBootstrap {
|
||||
app.handleProfilePrivacyToggle(payload),
|
||||
onPublishDmRelayPreferences: (payload) =>
|
||||
app.handleProfilePublishDmRelayPreferences(payload),
|
||||
onRequestPermissionPrompt: () => app.handlePermissionPromptRequest(),
|
||||
};
|
||||
|
||||
app.profileController = new ProfileModalController({
|
||||
@@ -697,10 +671,7 @@ export default class ApplicationBootstrap {
|
||||
},
|
||||
});
|
||||
app.moreMenuController.setVideoModal(app.videoModal);
|
||||
app.videoSettingsMenuController = new VideoSettingsMenuController({
|
||||
designSystem: app.designSystemContext,
|
||||
isDevMode,
|
||||
});
|
||||
app.videoSettingsPopovers = new Map();
|
||||
app.tagPreferencePopovers = new Map();
|
||||
|
||||
app.subscriptionsLink = null;
|
||||
@@ -717,29 +688,6 @@ export default class ApplicationBootstrap {
|
||||
app.lastExperimentalWarningKey = null;
|
||||
app.lastExperimentalWarningAt = 0;
|
||||
|
||||
if (
|
||||
nostrClient &&
|
||||
typeof nostrClient.setExtensionPermissionStatusHandler === "function"
|
||||
) {
|
||||
nostrClient.setExtensionPermissionStatusHandler((status) => {
|
||||
if (!status || typeof status !== "object") {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
typeof status.message === "string" ? status.message.trim() : "";
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
const autoHideMs = Number.isFinite(status.autoHideMs)
|
||||
? status.autoHideMs
|
||||
: 12000;
|
||||
const showSpinner = status.showSpinner !== false;
|
||||
if (typeof app.showStatus === "function") {
|
||||
app.showStatus(message, { autoHideMs, showSpinner });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.pubkey = null;
|
||||
app.currentMagnetUri = null;
|
||||
app.currentVideo = null;
|
||||
|
||||
-9
@@ -985,15 +985,6 @@ export class SimilarContentCard {
|
||||
avatarImg.decoding = "async";
|
||||
avatarImg.loading = "lazy";
|
||||
avatarImg.alt = "";
|
||||
if (!avatarImg.dataset.avatarFallbackBound) {
|
||||
avatarImg.addEventListener("error", () => {
|
||||
if (avatarImg.getAttribute("src") !== DEFAULT_PROFILE_AVATAR) {
|
||||
avatarImg.src = DEFAULT_PROFILE_AVATAR;
|
||||
}
|
||||
avatarImg.dataset.avatarLoadFailed = "true";
|
||||
});
|
||||
avatarImg.dataset.avatarFallbackBound = "true";
|
||||
}
|
||||
avatarWrapper.appendChild(avatarImg);
|
||||
|
||||
// Append avatar then text
|
||||
|
||||
Vendored
-9
@@ -916,15 +916,6 @@ export class VideoCard {
|
||||
"rounded-full",
|
||||
"object-cover"
|
||||
);
|
||||
if (!avatar.dataset.avatarFallbackBound) {
|
||||
avatar.addEventListener("error", () => {
|
||||
if (avatar.getAttribute("src") !== DEFAULT_PROFILE_AVATAR) {
|
||||
avatar.src = DEFAULT_PROFILE_AVATAR;
|
||||
}
|
||||
avatar.dataset.avatarLoadFailed = "true";
|
||||
});
|
||||
avatar.dataset.avatarFallbackBound = "true";
|
||||
}
|
||||
if (this.video.pubkey) {
|
||||
avatar.dataset.pubkey = this.video.pubkey;
|
||||
}
|
||||
|
||||
Vendored
+4
-10
@@ -6201,16 +6201,11 @@ export class VideoModal {
|
||||
try {
|
||||
const token = subscribeToVideoViewCount(
|
||||
pointerInfo.pointer,
|
||||
({ total, status, partial }) => {
|
||||
({ total, status }) => {
|
||||
if (!viewEl || !viewEl.isConnected) {
|
||||
return;
|
||||
}
|
||||
viewEl.textContent = this.getViewCountLabel(total, status, partial);
|
||||
if (partial) {
|
||||
viewEl.dataset.viewCountState = "partial";
|
||||
} else {
|
||||
viewEl.dataset.viewCountState = status;
|
||||
}
|
||||
viewEl.textContent = this.getViewCountLabel(total, status);
|
||||
}
|
||||
);
|
||||
return { pointer: pointerInfo.pointer, token };
|
||||
@@ -6221,10 +6216,9 @@ export class VideoModal {
|
||||
}
|
||||
}
|
||||
|
||||
getViewCountLabel(total, status, partial) {
|
||||
getViewCountLabel(total, status) {
|
||||
if (Number.isFinite(total)) {
|
||||
const label = this.formatViewCountLabel(Number(total));
|
||||
return partial ? `${label} (partial)` : label;
|
||||
return this.formatViewCountLabel(Number(total));
|
||||
}
|
||||
if (status === "hydrating") {
|
||||
return "Loading…";
|
||||
|
||||
Vendored
+29
@@ -237,3 +237,32 @@ export function renderTagPillStrip({
|
||||
|
||||
return { root, buttons };
|
||||
}
|
||||
|
||||
export function updateTagPillStrip({
|
||||
root,
|
||||
tags = [],
|
||||
onTagActivate,
|
||||
getTagState,
|
||||
document: doc,
|
||||
} = {}) {
|
||||
if (!root) {
|
||||
throw new Error("A root element is required to update the tag pill strip.");
|
||||
}
|
||||
|
||||
const resolvedDocument = resolveDocument(doc ?? root?.ownerDocument);
|
||||
assertDocument(resolvedDocument);
|
||||
|
||||
cleanupExistingButtons(root);
|
||||
|
||||
const buttons = tags.map((tag) =>
|
||||
createTagButton({
|
||||
doc: resolvedDocument,
|
||||
tag,
|
||||
onTagActivate,
|
||||
getTagState,
|
||||
}),
|
||||
);
|
||||
root.append(...buttons);
|
||||
|
||||
return { root, buttons };
|
||||
}
|
||||
Vendored
+2
-12
@@ -2,7 +2,6 @@ import { Composer } from "./Composer.js";
|
||||
import { ConversationList } from "./ConversationList.js";
|
||||
import { MessageThread } from "./MessageThread.js";
|
||||
import { DMPrivacySettings } from "./DMPrivacySettings.js";
|
||||
import { DMRelaySettings } from "./DMRelaySettings.js";
|
||||
import { aggregateZapTotals } from "./zapHelpers.js";
|
||||
import { createZapInterface } from "./ZapInterface.js";
|
||||
|
||||
@@ -137,7 +136,6 @@ export class AppShell {
|
||||
onToggleTypingIndicators,
|
||||
onRefreshConversations,
|
||||
onBack,
|
||||
onOpenSettings,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("AppShell requires a document reference.");
|
||||
@@ -184,23 +182,15 @@ export class AppShell {
|
||||
popover.remove();
|
||||
return;
|
||||
}
|
||||
popover = createElement(doc, "div", "dm-settings-popover absolute z-50 right-4 top-12 flex flex-col gap-4");
|
||||
|
||||
const privacySettings = DMPrivacySettings({
|
||||
popover = DMPrivacySettings({
|
||||
document: doc,
|
||||
readReceiptsEnabled: dmPrivacySettings.readReceiptsEnabled,
|
||||
typingIndicatorsEnabled: dmPrivacySettings.typingIndicatorsEnabled,
|
||||
onToggleReadReceipts,
|
||||
onToggleTypingIndicators,
|
||||
});
|
||||
popover.appendChild(privacySettings);
|
||||
|
||||
const relaySettings = DMRelaySettings({ document: doc });
|
||||
popover.appendChild(relaySettings);
|
||||
popover.classList.add("dm-settings-popover", "absolute", "z-50", "right-4", "top-12", "bg-surface", "border", "border-border", "rounded-xl");
|
||||
sidebarHeader.appendChild(popover);
|
||||
if (typeof onOpenSettings === "function") {
|
||||
onOpenSettings(popover);
|
||||
}
|
||||
|
||||
const closeHandler = (event) => {
|
||||
if (!popover.contains(event.target) && event.target !== settingsBtn && !settingsBtn.contains(event.target)) {
|
||||
|
||||
Vendored
-80
@@ -1,80 +0,0 @@
|
||||
export function DMRelaySettings({ document: doc } = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("DMRelaySettings requires a document reference.");
|
||||
}
|
||||
|
||||
const root = doc.createElement("div");
|
||||
root.id = "profileMessagesRelayPanel";
|
||||
root.className = "dm-relay-settings card space-y-4 p-5";
|
||||
|
||||
const heading = doc.createElement("div");
|
||||
heading.className = "space-y-2";
|
||||
|
||||
const title = doc.createElement("h3");
|
||||
title.className = "text-base font-semibold text-text";
|
||||
title.textContent = "DM relay hints";
|
||||
heading.appendChild(title);
|
||||
|
||||
const description = doc.createElement("p");
|
||||
description.className = "text-sm text-muted";
|
||||
description.innerHTML =
|
||||
'Publish relay hints for direct messages as <span class="font-medium">kind:10050</span> events. Use WSS-only relay URLs so encrypted DM delivery stays secure.';
|
||||
heading.appendChild(description);
|
||||
root.appendChild(heading);
|
||||
|
||||
const addRow = doc.createElement("div");
|
||||
addRow.className = "flex flex-col gap-3 sm:flex-row sm:items-end";
|
||||
|
||||
const label = doc.createElement("label");
|
||||
label.className = "flex-1 text-sm text-muted-strong";
|
||||
label.setAttribute("for", "profileMessagesRelayInput");
|
||||
|
||||
const labelTitle = doc.createElement("span");
|
||||
labelTitle.className = "mb-2 block font-medium";
|
||||
labelTitle.textContent = "Add a DM relay";
|
||||
label.appendChild(labelTitle);
|
||||
|
||||
const input = doc.createElement("input");
|
||||
input.id = "profileMessagesRelayInput";
|
||||
input.type = "text";
|
||||
input.inputMode = "url";
|
||||
input.autocomplete = "off";
|
||||
input.className = "input";
|
||||
input.placeholder = "wss://relay.example.com";
|
||||
label.appendChild(input);
|
||||
addRow.appendChild(label);
|
||||
|
||||
const addButton = doc.createElement("button");
|
||||
addButton.id = "profileMessagesRelayAdd";
|
||||
addButton.type = "button";
|
||||
addButton.className = "btn focus-ring";
|
||||
addButton.textContent = "Add relay";
|
||||
addRow.appendChild(addButton);
|
||||
root.appendChild(addRow);
|
||||
|
||||
const publishRow = doc.createElement("div");
|
||||
publishRow.className = "flex flex-wrap items-center gap-2";
|
||||
|
||||
const publishButton = doc.createElement("button");
|
||||
publishButton.id = "profileMessagesRelayPublish";
|
||||
publishButton.type = "button";
|
||||
publishButton.className = "btn-ghost focus-ring text-xs";
|
||||
publishButton.textContent = "Publish relay hints";
|
||||
publishRow.appendChild(publishButton);
|
||||
|
||||
const status = doc.createElement("span");
|
||||
status.id = "profileMessagesRelayStatus";
|
||||
status.className = "text-xs text-muted";
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
publishRow.appendChild(status);
|
||||
root.appendChild(publishRow);
|
||||
|
||||
const list = doc.createElement("ul");
|
||||
list.id = "profileMessagesRelayList";
|
||||
list.className = "grid-stack";
|
||||
list.dataset.orientation = "vertical";
|
||||
root.appendChild(list);
|
||||
|
||||
return root;
|
||||
}
|
||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
||||
// js/ui/moderationCopy.js
|
||||
|
||||
function buildModerationReasonText(context) {
|
||||
export function buildModerationReasonText(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
@@ -31,7 +31,7 @@ function buildModerationReasonText(context) {
|
||||
return combined.charAt(0).toUpperCase() + combined.slice(1);
|
||||
}
|
||||
|
||||
function buildHiddenSummaryLabel(context) {
|
||||
export function buildHiddenSummaryLabel(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Vendored
-2
@@ -1520,8 +1520,6 @@ export default class MoreMenuController {
|
||||
const message =
|
||||
error?.code === "nip04-missing"
|
||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
||||
: error?.code === "extension-encryption-permission-denied"
|
||||
? "Your Nostr extension must allow encryption to update your block list."
|
||||
: "Failed to update your block list. Please try again.";
|
||||
this.callbacks.showError(message);
|
||||
}
|
||||
|
||||
Vendored
+50
-653
File diff suppressed because it is too large.
Load diff
+38
-92
@@ -1,4 +1,3 @@
|
||||
import { isVerboseDevMode } from "../config.js";
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
|
||||
export default class TorrentStatusController {
|
||||
@@ -8,36 +7,14 @@ export default class TorrentStatusController {
|
||||
}
|
||||
this.getVideoModal = getVideoModal;
|
||||
this.onRemovePoster = typeof onRemovePoster === "function" ? onRemovePoster : () => {};
|
||||
this.lastEmitted = {
|
||||
status: null,
|
||||
progress: null,
|
||||
peers: null,
|
||||
speed: null,
|
||||
downloaded: null,
|
||||
};
|
||||
this.lastVerboseLogAt = 0;
|
||||
this.lastVerboseSummaryKey = "";
|
||||
this.verboseLogIntervalMs = 3000;
|
||||
}
|
||||
|
||||
update(torrent) {
|
||||
if (!torrent) {
|
||||
if (isVerboseDevMode) {
|
||||
this.logVerboseSummary({ hasTorrent: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
devLogger.log("[DEBUG] TorrentStatusController.update called with torrent:", torrent);
|
||||
|
||||
if (isVerboseDevMode) {
|
||||
this.logVerboseSummary({
|
||||
hasTorrent: true,
|
||||
progress: torrent.progress,
|
||||
numPeers: torrent.numPeers,
|
||||
downloadSpeed: torrent.downloadSpeed,
|
||||
downloaded: torrent.downloaded,
|
||||
length: torrent.length,
|
||||
ready: torrent.ready,
|
||||
});
|
||||
if (!torrent) {
|
||||
devLogger.log("[DEBUG] torrent is null/undefined!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (torrent.ready || (typeof torrent.progress === "number" && torrent.progress > 0)) {
|
||||
@@ -49,79 +26,48 @@ export default class TorrentStatusController {
|
||||
);
|
||||
}
|
||||
|
||||
// Log only fields that actually exist on the torrent:
|
||||
devLogger.log("[DEBUG] torrent.progress =", torrent.progress);
|
||||
devLogger.log("[DEBUG] torrent.numPeers =", torrent.numPeers);
|
||||
devLogger.log("[DEBUG] torrent.downloadSpeed =", torrent.downloadSpeed);
|
||||
devLogger.log("[DEBUG] torrent.downloaded =", torrent.downloaded);
|
||||
devLogger.log("[DEBUG] torrent.length =", torrent.length);
|
||||
devLogger.log("[DEBUG] torrent.ready =", torrent.ready);
|
||||
|
||||
const videoModal = this.getVideoModal();
|
||||
|
||||
// Use "Complete" vs. "Downloading" as the textual status.
|
||||
if (videoModal) {
|
||||
const fullyDownloaded = Number(torrent.progress) >= 1;
|
||||
const status = torrent.ready
|
||||
? "Ready to play"
|
||||
: fullyDownloaded
|
||||
? "Complete"
|
||||
: "Downloading";
|
||||
this.updateIfChanged("status", status, () => {
|
||||
if (typeof videoModal.updateStatus === "function") {
|
||||
videoModal.updateStatus(status);
|
||||
}
|
||||
});
|
||||
const fullyDownloaded = torrent.progress >= 1;
|
||||
if (typeof videoModal.updateStatus === "function") {
|
||||
videoModal.updateStatus(fullyDownloaded ? "Complete" : "Downloading");
|
||||
}
|
||||
|
||||
const progressValue = Number.isFinite(torrent.progress)
|
||||
? `${(torrent.progress * 100).toFixed(2)}%`
|
||||
: "0.00%";
|
||||
this.updateIfChanged("progress", progressValue, () => {
|
||||
if (typeof videoModal.updateProgress === "function") {
|
||||
videoModal.updateProgress(progressValue);
|
||||
}
|
||||
});
|
||||
if (typeof videoModal.updateProgress === "function") {
|
||||
const percent = (torrent.progress * 100).toFixed(2);
|
||||
videoModal.updateProgress(`${percent}%`);
|
||||
}
|
||||
|
||||
const peersValue = `Peers: ${Number.isFinite(torrent.numPeers) ? torrent.numPeers : 0}`;
|
||||
this.updateIfChanged("peers", peersValue, () => {
|
||||
if (typeof videoModal.updatePeers === "function") {
|
||||
videoModal.updatePeers(peersValue);
|
||||
}
|
||||
});
|
||||
if (typeof videoModal.updatePeers === "function") {
|
||||
videoModal.updatePeers(`Peers: ${torrent.numPeers}`);
|
||||
}
|
||||
|
||||
const speedValue = Number.isFinite(torrent.downloadSpeed)
|
||||
? `${(torrent.downloadSpeed / 1024).toFixed(2)} KB/s`
|
||||
: "0.00 KB/s";
|
||||
this.updateIfChanged("speed", speedValue, () => {
|
||||
if (typeof videoModal.updateSpeed === "function") {
|
||||
videoModal.updateSpeed(speedValue);
|
||||
}
|
||||
});
|
||||
if (typeof videoModal.updateSpeed === "function") {
|
||||
const kb = (torrent.downloadSpeed / 1024).toFixed(2);
|
||||
videoModal.updateSpeed(`${kb} KB/s`);
|
||||
}
|
||||
|
||||
const downloadedMb = Number.isFinite(torrent.downloaded)
|
||||
? (torrent.downloaded / (1024 * 1024)).toFixed(2)
|
||||
: "0.00";
|
||||
const lengthMb = Number.isFinite(torrent.length)
|
||||
? (torrent.length / (1024 * 1024)).toFixed(2)
|
||||
: "0.00";
|
||||
const downloadedValue = `${downloadedMb} MB / ${lengthMb} MB`;
|
||||
this.updateIfChanged("downloaded", downloadedValue, () => {
|
||||
if (typeof videoModal.updateDownloaded === "function") {
|
||||
videoModal.updateDownloaded(downloadedValue);
|
||||
}
|
||||
});
|
||||
if (typeof videoModal.updateDownloaded === "function") {
|
||||
const downloadedMb = (torrent.downloaded / (1024 * 1024)).toFixed(2);
|
||||
const lengthMb = (torrent.length / (1024 * 1024)).toFixed(2);
|
||||
videoModal.updateDownloaded(
|
||||
`${downloadedMb} MB / ${lengthMb} MB`
|
||||
);
|
||||
}
|
||||
|
||||
if (torrent.ready && typeof videoModal.updateStatus === "function") {
|
||||
videoModal.updateStatus("Ready to play");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateIfChanged(key, nextValue, updater) {
|
||||
if (this.lastEmitted[key] === nextValue) {
|
||||
return;
|
||||
}
|
||||
this.lastEmitted[key] = nextValue;
|
||||
updater();
|
||||
}
|
||||
|
||||
logVerboseSummary(summary) {
|
||||
const summaryKey = JSON.stringify(summary);
|
||||
const now = Date.now();
|
||||
const shouldSample = now - this.lastVerboseLogAt >= this.verboseLogIntervalMs;
|
||||
if (summaryKey === this.lastVerboseSummaryKey && !shouldSample) {
|
||||
return;
|
||||
}
|
||||
this.lastVerboseSummaryKey = summaryKey;
|
||||
this.lastVerboseLogAt = now;
|
||||
devLogger.debug("[DEBUG] TorrentStatusController.update", summary);
|
||||
}
|
||||
}
|
||||
Vendored
+53
-75
@@ -11,10 +11,6 @@ export default class UrlHealthController {
|
||||
this.logger = logger;
|
||||
this.constants = constants;
|
||||
this.callbacks = callbacks;
|
||||
|
||||
this.activeVideoProbes = 0;
|
||||
this.videoProbeQueue = [];
|
||||
this.MAX_CONCURRENT_VIDEO_PROBES = 3;
|
||||
}
|
||||
|
||||
getUrlHealthPlaceholderMarkup(options = {}) {
|
||||
@@ -258,85 +254,67 @@ export default class UrlHealthController {
|
||||
return { outcome: "error" };
|
||||
}
|
||||
|
||||
if (this.activeVideoProbes >= this.MAX_CONCURRENT_VIDEO_PROBES) {
|
||||
await new Promise((resolve) => {
|
||||
this.videoProbeQueue.push(resolve);
|
||||
});
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
let settled = false;
|
||||
let timeoutId = null;
|
||||
|
||||
this.activeVideoProbes++;
|
||||
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
let settled = false;
|
||||
let timeoutId = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
video.removeEventListener("loadeddata", handleSuccess);
|
||||
video.removeEventListener("canplay", handleSuccess);
|
||||
video.removeEventListener("error", handleError);
|
||||
try {
|
||||
video.pause();
|
||||
} catch (err) {
|
||||
// ignore pause failures
|
||||
}
|
||||
try {
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
} catch (err) {
|
||||
// ignore cleanup failures
|
||||
}
|
||||
};
|
||||
|
||||
const settle = (result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
settle({ outcome: "ok" });
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
settle({ outcome: "error" });
|
||||
};
|
||||
|
||||
if (Number.isFinite(effectiveTimeout) && effectiveTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
settle({ outcome: "timeout" });
|
||||
}, effectiveTimeout);
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
video.removeEventListener("loadeddata", handleSuccess);
|
||||
video.removeEventListener("canplay", handleSuccess);
|
||||
video.removeEventListener("error", handleError);
|
||||
try {
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.addEventListener("loadeddata", handleSuccess, { once: true });
|
||||
video.addEventListener("canplay", handleSuccess, { once: true });
|
||||
video.addEventListener("error", handleError, { once: true });
|
||||
video.src = trimmed;
|
||||
video.pause();
|
||||
} catch (err) {
|
||||
// ignore pause failures
|
||||
}
|
||||
try {
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
} catch (err) {
|
||||
settle({ outcome: "error", error: err });
|
||||
// ignore cleanup failures
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.activeVideoProbes--;
|
||||
if (this.videoProbeQueue.length > 0) {
|
||||
const next = this.videoProbeQueue.shift();
|
||||
if (typeof next === "function") {
|
||||
next();
|
||||
};
|
||||
|
||||
const settle = (result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
settle({ outcome: "ok" });
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
settle({ outcome: "error" });
|
||||
};
|
||||
|
||||
if (Number.isFinite(effectiveTimeout) && effectiveTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
settle({ outcome: "timeout" });
|
||||
}, effectiveTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.addEventListener("loadeddata", handleSuccess, { once: true });
|
||||
video.addEventListener("canplay", handleSuccess, { once: true });
|
||||
video.addEventListener("error", handleError, { once: true });
|
||||
video.src = trimmed;
|
||||
video.load();
|
||||
} catch (err) {
|
||||
settle({ outcome: "error", error: err });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async probeUrl(url, options = {}) {
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ export default class VideoListViewController {
|
||||
}
|
||||
|
||||
const messageContext =
|
||||
reason === "login" && postLoginResult?.blocksLoaded === true
|
||||
reason === "login" && postLoginResult?.blocksLoaded !== false
|
||||
? "Applying your filters…"
|
||||
: "Refreshing videos…";
|
||||
|
||||
|
||||
Vendored
-202
@@ -1,202 +0,0 @@
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
|
||||
export default class VideoModalController {
|
||||
constructor({
|
||||
getVideoModal,
|
||||
callbacks: {
|
||||
showError,
|
||||
getLastModalTrigger,
|
||||
setLastModalTrigger,
|
||||
getCurrentVideo,
|
||||
} = {},
|
||||
}) {
|
||||
this.getVideoModal = getVideoModal;
|
||||
this.showError = showError;
|
||||
this.getLastModalTrigger = getLastModalTrigger;
|
||||
this.setLastModalTrigger = setLastModalTrigger;
|
||||
this.getCurrentVideo = getCurrentVideo;
|
||||
|
||||
this.videoModalReadyPromise = null;
|
||||
}
|
||||
|
||||
get videoModal() {
|
||||
return this.getVideoModal ? this.getVideoModal() : null;
|
||||
}
|
||||
|
||||
log(message, ...args) {
|
||||
devLogger.log("[VideoModalController]", message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the modal and set the "Please stand by" poster on the video.
|
||||
*/
|
||||
async showModalWithPoster(video = null, options = {}) {
|
||||
if (!this.videoModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(options || {}, "trigger")) {
|
||||
if (this.setLastModalTrigger) {
|
||||
this.setLastModalTrigger(options.trigger);
|
||||
}
|
||||
}
|
||||
|
||||
const targetVideo =
|
||||
video || (this.getCurrentVideo ? this.getCurrentVideo() : null);
|
||||
if (!targetVideo) {
|
||||
this.log("Skipping video modal open; no target video is available.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { root } = await this.ensureVideoModalReady({
|
||||
ensureVideoElement: true,
|
||||
});
|
||||
|
||||
if (!this.videoModal) {
|
||||
return root || null;
|
||||
}
|
||||
|
||||
const trigger = this.getLastModalTrigger
|
||||
? this.getLastModalTrigger()
|
||||
: null;
|
||||
this.videoModal.open(targetVideo, {
|
||||
triggerElement: trigger,
|
||||
});
|
||||
this.applyModalLoadingPoster();
|
||||
|
||||
return (
|
||||
root ||
|
||||
(typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null)
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.error(
|
||||
"[VideoModalController] Failed to open the video modal before playback:",
|
||||
error,
|
||||
);
|
||||
if (this.showError) {
|
||||
this.showError("Could not open the video player. Please try again.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
applyModalLoadingPoster() {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
}
|
||||
this.videoModal.applyLoadingPoster();
|
||||
}
|
||||
|
||||
forceRemoveModalPoster(reason = "manual-clear") {
|
||||
if (!this.videoModal) {
|
||||
return false;
|
||||
}
|
||||
return this.videoModal.forceRemovePoster(reason);
|
||||
}
|
||||
|
||||
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
|
||||
if (!this.videoModal) {
|
||||
throw new Error("Video modal instance is not available.");
|
||||
}
|
||||
|
||||
const getRoot = () =>
|
||||
typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null;
|
||||
const getVideoElement = () =>
|
||||
typeof this.videoModal.getVideoElement === "function"
|
||||
? this.videoModal.getVideoElement()
|
||||
: null;
|
||||
|
||||
const existingRoot = getRoot();
|
||||
const existingVideoElement = getVideoElement();
|
||||
const rootConnected = Boolean(existingRoot && existingRoot.isConnected);
|
||||
const hasVideoElement = Boolean(existingVideoElement);
|
||||
const videoConnected = Boolean(
|
||||
existingVideoElement && existingVideoElement.isConnected,
|
||||
);
|
||||
|
||||
const needsRehydrate =
|
||||
!rootConnected || !hasVideoElement || !videoConnected;
|
||||
|
||||
if (!needsRehydrate) {
|
||||
if (
|
||||
existingVideoElement &&
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
const currentModalVideo =
|
||||
typeof this.videoModal.getVideoElement === "function"
|
||||
? this.videoModal.getVideoElement()
|
||||
: null;
|
||||
if (currentModalVideo !== existingVideoElement) {
|
||||
this.videoModal.setVideoElement(existingVideoElement);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: existingRoot,
|
||||
videoElement: existingVideoElement,
|
||||
};
|
||||
}
|
||||
|
||||
if (!videoConnected) {
|
||||
if (
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
this.videoModal.setVideoElement(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.videoModalReadyPromise) {
|
||||
if (typeof this.videoModal.load !== "function") {
|
||||
throw new Error("Video modal does not expose a load() method.");
|
||||
}
|
||||
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
|
||||
}
|
||||
|
||||
try {
|
||||
await this.videoModalReadyPromise;
|
||||
} catch (error) {
|
||||
this.videoModalReadyPromise = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.videoModalReadyPromise = null;
|
||||
|
||||
const readyRoot = getRoot();
|
||||
const readyVideoElement = getVideoElement();
|
||||
const readyVideoConnected = Boolean(
|
||||
readyVideoElement && readyVideoElement.isConnected,
|
||||
);
|
||||
|
||||
if (readyVideoConnected) {
|
||||
if (
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
this.videoModal.setVideoElement(readyVideoElement);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
this.videoModal.setVideoElement(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (ensureVideoElement && !readyVideoConnected) {
|
||||
throw new Error("Video modal video element is missing after load().");
|
||||
}
|
||||
|
||||
return {
|
||||
root: readyRoot,
|
||||
videoElement: readyVideoConnected ? readyVideoElement : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
import createPopoverDefault from "./overlay/popoverEngine.js";
|
||||
import { createVideoSettingsMenuPanel as createVideoSettingsMenuPanelDefault } from "./components/videoMenuRenderers.js";
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { normalizeDesignSystemContext } from "../designSystem.js";
|
||||
|
||||
export default class VideoSettingsMenuController {
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
designSystem = null,
|
||||
isDevMode = false,
|
||||
createPopover = createPopoverDefault,
|
||||
createVideoSettingsMenuPanel = createVideoSettingsMenuPanelDefault,
|
||||
} = options;
|
||||
|
||||
this.createPopover = createPopover;
|
||||
this.createVideoSettingsMenuPanel = createVideoSettingsMenuPanel;
|
||||
this.designSystem = normalizeDesignSystemContext(designSystem);
|
||||
this.isDevMode = Boolean(isDevMode);
|
||||
this.popovers = new Map();
|
||||
}
|
||||
|
||||
ensurePopover(detail = {}) {
|
||||
const trigger = detail.trigger || null;
|
||||
if (!trigger) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let entry = this.popovers.get(trigger);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
trigger,
|
||||
context: {
|
||||
card: detail.card || null,
|
||||
video: detail.video || null,
|
||||
index: Number.isFinite(detail.index) ? Math.floor(detail.index) : 0,
|
||||
capabilities: detail.capabilities || {},
|
||||
restoreFocusOnClose: detail.restoreFocus !== false,
|
||||
},
|
||||
popover: null,
|
||||
};
|
||||
|
||||
const render = ({ document: documentRef, close }) => {
|
||||
const panel = this.createVideoSettingsMenuPanel({
|
||||
document: documentRef,
|
||||
video: entry.context.video,
|
||||
index: entry.context.index,
|
||||
capabilities: entry.context.capabilities,
|
||||
designSystem: this.designSystem,
|
||||
});
|
||||
|
||||
if (!panel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttons = panel.querySelectorAll("button[data-action]");
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const action = button.dataset.action || "";
|
||||
const handled = entry.context.card?.handleSettingsMenuAction?.(
|
||||
action,
|
||||
{ event },
|
||||
);
|
||||
|
||||
if (!handled && this.isDevMode) {
|
||||
userLogger.warn(`[SettingsMenu] Unhandled action: ${action}`);
|
||||
}
|
||||
|
||||
close();
|
||||
});
|
||||
});
|
||||
|
||||
return panel;
|
||||
};
|
||||
|
||||
const documentRef =
|
||||
trigger.ownerDocument ||
|
||||
(typeof document !== "undefined" ? document : null);
|
||||
|
||||
const popover = this.createPopover(trigger, render, {
|
||||
document: documentRef,
|
||||
placement: "bottom-end",
|
||||
});
|
||||
|
||||
const originalDestroy = popover.destroy?.bind(popover);
|
||||
if (typeof originalDestroy === "function") {
|
||||
popover.destroy = (...args) => {
|
||||
originalDestroy(...args);
|
||||
if (this.popovers.get(trigger) === entry) {
|
||||
this.popovers.delete(trigger);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
entry.popover = popover;
|
||||
this.popovers.set(trigger, entry);
|
||||
}
|
||||
|
||||
entry.context = {
|
||||
...entry.context,
|
||||
card: detail.card || entry.context.card,
|
||||
video: detail.video || entry.context.video,
|
||||
index: Number.isFinite(detail.index)
|
||||
? Math.floor(detail.index)
|
||||
: entry.context.index,
|
||||
capabilities: detail.capabilities || entry.context.capabilities,
|
||||
restoreFocusOnClose: detail.restoreFocus !== false,
|
||||
};
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
requestMenu(detail = {}) {
|
||||
const entry = this.ensurePopover(detail);
|
||||
if (!entry?.popover) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof entry.popover.isOpen === "function" && entry.popover.isOpen()) {
|
||||
entry.popover.close({
|
||||
restoreFocus: entry.context.restoreFocusOnClose !== false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
entry.popover
|
||||
.open()
|
||||
.catch((error) =>
|
||||
userLogger.error("[SettingsMenu] Failed to open popover:", error),
|
||||
);
|
||||
}
|
||||
|
||||
closeMenu(detail = {}) {
|
||||
const trigger = detail.trigger || null;
|
||||
const restoreFocus = detail.restoreFocus !== false;
|
||||
|
||||
if (trigger) {
|
||||
const entry = this.popovers.get(trigger);
|
||||
if (entry?.popover && typeof entry.popover.close === "function") {
|
||||
return entry.popover.close({ restoreFocus });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.closeAll({ restoreFocus });
|
||||
}
|
||||
|
||||
closeAll(options = {}) {
|
||||
let closed = false;
|
||||
const restoreFocus = options?.restoreFocus !== false;
|
||||
|
||||
this.popovers.forEach((entry) => {
|
||||
if (entry?.popover && typeof entry.popover.close === "function") {
|
||||
const result = entry.popover.close({ restoreFocus });
|
||||
closed = closed || result;
|
||||
}
|
||||
});
|
||||
return closed;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.popovers.forEach((entry) => {
|
||||
if (entry?.popover && typeof entry.popover.destroy === "function") {
|
||||
entry.popover.destroy();
|
||||
}
|
||||
});
|
||||
this.popovers.clear();
|
||||
}
|
||||
}
|
||||
Vendored
+1
-14
@@ -1134,20 +1134,16 @@ export class VideoListView {
|
||||
elements: new Set(),
|
||||
lastTotal: null,
|
||||
lastStatus: "idle",
|
||||
lastPartial: false,
|
||||
lastText: "– views",
|
||||
};
|
||||
|
||||
try {
|
||||
const token = subscribeToVideoViewCount(pointerInfo.pointer, ({ total, status, partial }) => {
|
||||
const token = subscribeToVideoViewCount(pointerInfo.pointer, ({ total, status }) => {
|
||||
let text;
|
||||
if (Number.isFinite(total)) {
|
||||
const numeric = Number(total);
|
||||
entry.lastTotal = numeric;
|
||||
text = this.formatters.formatViewCountLabel(numeric);
|
||||
if (partial) {
|
||||
text = `${text} (partial)`;
|
||||
}
|
||||
} else if (status === "hydrating") {
|
||||
text = "Loading views…";
|
||||
} else {
|
||||
@@ -1155,7 +1151,6 @@ export class VideoListView {
|
||||
}
|
||||
|
||||
entry.lastStatus = status;
|
||||
entry.lastPartial = Boolean(partial);
|
||||
entry.lastText = text;
|
||||
|
||||
for (const el of Array.from(entry.elements)) {
|
||||
@@ -1164,11 +1159,6 @@ export class VideoListView {
|
||||
continue;
|
||||
}
|
||||
el.textContent = text;
|
||||
if (entry.lastPartial) {
|
||||
el.dataset.viewCountState = "partial";
|
||||
} else {
|
||||
el.dataset.viewCountState = status;
|
||||
}
|
||||
}
|
||||
});
|
||||
entry.token = token;
|
||||
@@ -1203,9 +1193,6 @@ export class VideoListView {
|
||||
if (typeof pointerInfo.key === "string") {
|
||||
viewCountEl.dataset.viewPointer = pointerInfo.key;
|
||||
}
|
||||
viewCountEl.dataset.viewCountState = entry.lastPartial
|
||||
? "partial"
|
||||
: entry.lastStatus;
|
||||
viewCountEl.textContent = entry.lastText;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-19
@@ -398,7 +398,7 @@ export default class ZapController {
|
||||
|
||||
this.resetRetryState();
|
||||
this.modalZapCommentValue = "";
|
||||
this.videoModal?.setZapComment("");
|
||||
this.videoModal?.resetZapForm({ amount: "", comment: "" });
|
||||
this.videoModal?.setZapCompleted(true);
|
||||
this.applyDefaultAmount();
|
||||
} catch (error) {
|
||||
@@ -881,13 +881,6 @@ export default class ZapController {
|
||||
});
|
||||
const videoEvent = this.getActiveVideoEvent();
|
||||
|
||||
let previousOverride;
|
||||
const hasGlobal = typeof globalThis !== "undefined";
|
||||
if (hasGlobal && typeof overrideFee === "number" && Number.isFinite(overrideFee)) {
|
||||
previousOverride = globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
|
||||
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = overrideFee;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.splitAndZap(
|
||||
{
|
||||
@@ -895,6 +888,7 @@ export default class ZapController {
|
||||
amountSats: context.shares.total,
|
||||
comment: typeof comment === "string" ? comment : "",
|
||||
walletSettings: settings,
|
||||
overrideFee,
|
||||
},
|
||||
dependencies
|
||||
);
|
||||
@@ -937,17 +931,6 @@ export default class ZapController {
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
if (hasGlobal && typeof overrideFee === "number" && Number.isFinite(overrideFee)) {
|
||||
if (typeof previousOverride === "number") {
|
||||
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = previousOverride;
|
||||
} else if (
|
||||
globalThis &&
|
||||
Object.prototype.hasOwnProperty.call(globalThis, "__BITVID_PLATFORM_FEE_OVERRIDE__")
|
||||
) {
|
||||
delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+55
-324
@@ -20,12 +20,7 @@ import {
|
||||
import { profileCache } from "./state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
import {
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
runNip07WithRetry,
|
||||
NIP07_PRIORITY,
|
||||
} from "./nostr/nip07Permissions.js";
|
||||
import { relaySubscriptionService } from "./services/relaySubscriptionService.js";
|
||||
import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js";
|
||||
|
||||
class TinyEventEmitter {
|
||||
constructor() {
|
||||
@@ -79,10 +74,8 @@ export const USER_BLOCK_EVENTS = Object.freeze({
|
||||
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
|
||||
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
|
||||
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
|
||||
const DECRYPT_TIMEOUT_MS = 15000;
|
||||
const BACKGROUND_DECRYPT_TIMEOUT_MS = 2500;
|
||||
const DECRYPT_TIMEOUT_MS = 90000;
|
||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||
const MAX_BLOCKLIST_ENTRIES = 5000;
|
||||
|
||||
function sanitizeRelayList(candidate) {
|
||||
return Array.isArray(candidate)
|
||||
@@ -310,16 +303,13 @@ function serializeBlockListTagMatrix(values, ownerPubkey, options = {}) {
|
||||
return JSON.stringify(tags);
|
||||
}
|
||||
|
||||
function extractPubkeysFromTags(tags, ownerPubkey = null, options = {}) {
|
||||
function extractPubkeysFromTags(tags, ownerPubkey = null) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return [];
|
||||
}
|
||||
const collected = [];
|
||||
const seen = new Set();
|
||||
const owner = ownerPubkey ? normalizeHex(ownerPubkey) : null;
|
||||
const limit = Number.isFinite(options?.limit)
|
||||
? Math.max(0, Math.floor(options.limit))
|
||||
: null;
|
||||
|
||||
for (const entry of tags) {
|
||||
if (!Array.isArray(entry) || entry.length < 2) {
|
||||
@@ -335,14 +325,11 @@ function extractPubkeysFromTags(tags, ownerPubkey = null, options = {}) {
|
||||
}
|
||||
seen.add(normalized);
|
||||
collected.push(normalized);
|
||||
if (limit !== null && collected.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
function parseBlockListPlaintext(plaintext, ownerPubkey, options = {}) {
|
||||
function parseBlockListPlaintext(plaintext, ownerPubkey) {
|
||||
if (typeof plaintext !== "string" || !plaintext) {
|
||||
return [];
|
||||
}
|
||||
@@ -359,45 +346,20 @@ function parseBlockListPlaintext(plaintext, ownerPubkey, options = {}) {
|
||||
}
|
||||
|
||||
const owner = normalizeHex(ownerPubkey);
|
||||
const limit = Number.isFinite(options?.limit)
|
||||
? Math.max(0, Math.floor(options.limit))
|
||||
: null;
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
if (limit !== null && parsed.length > limit) {
|
||||
devLogger.warn(
|
||||
"[UserBlockList] Block list payload exceeds entry limit; truncating.",
|
||||
{ limit, payloadSize: parsed.length },
|
||||
);
|
||||
}
|
||||
return extractPubkeysFromTags(parsed, owner, { limit });
|
||||
return extractPubkeysFromTags(parsed, owner);
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === "object") {
|
||||
if (Array.isArray(parsed.tags) && parsed.tags.length) {
|
||||
if (limit !== null && parsed.tags.length > limit) {
|
||||
devLogger.warn(
|
||||
"[UserBlockList] Block list tag payload exceeds entry limit; truncating.",
|
||||
{ limit, payloadSize: parsed.tags.length },
|
||||
);
|
||||
}
|
||||
return extractPubkeysFromTags(parsed.tags, owner, { limit });
|
||||
return extractPubkeysFromTags(parsed.tags, owner);
|
||||
}
|
||||
|
||||
const legacy = Array.isArray(parsed.blockedPubkeys) ? parsed.blockedPubkeys : [];
|
||||
if (limit !== null && legacy.length > limit) {
|
||||
devLogger.warn(
|
||||
"[UserBlockList] Legacy block list payload exceeds entry limit; truncating.",
|
||||
{ limit, payloadSize: legacy.length },
|
||||
);
|
||||
}
|
||||
const normalizedLegacy = legacy
|
||||
return legacy
|
||||
.map((value) => normalizeHex(value))
|
||||
.filter((value) => value && value !== owner);
|
||||
if (limit === null) {
|
||||
return normalizedLegacy;
|
||||
}
|
||||
return normalizedLegacy.slice(0, limit);
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -686,9 +648,6 @@ class UserBlockListManager {
|
||||
this.emitter = new TinyEventEmitter();
|
||||
this.seedStateCache = new Map();
|
||||
this.decryptRetryTimeoutId = null;
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
this.blockListSubscriptionKey = null;
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
@@ -703,7 +662,6 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.stopBlockListSubscription();
|
||||
this.blockedPubkeys.clear();
|
||||
this.blockEventId = null;
|
||||
this.blockEventCreatedAt = null;
|
||||
@@ -715,56 +673,6 @@ class UserBlockListManager {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
this.decryptRetryTimeoutId = null;
|
||||
}
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
}
|
||||
|
||||
stopBlockListSubscription() {
|
||||
if (this.blockListSubscriptionKey) {
|
||||
relaySubscriptionService.stopSubscription(
|
||||
this.blockListSubscriptionKey,
|
||||
"reset",
|
||||
);
|
||||
this.blockListSubscriptionKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
ensureBlockListSubscription(userPubkey, relays) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
if (!normalized || !nostrClient?.pool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = `block-list:${normalized}`;
|
||||
this.blockListSubscriptionKey = key;
|
||||
|
||||
const filters = [
|
||||
{ kinds: [10000], authors: [normalized] },
|
||||
{ kinds: [30002], authors: [normalized], "#d": [BLOCK_LIST_IDENTIFIER] },
|
||||
];
|
||||
|
||||
return relaySubscriptionService.ensureSubscription({
|
||||
key,
|
||||
pool: nostrClient.pool,
|
||||
relays,
|
||||
filters,
|
||||
label: "block-list",
|
||||
onEvent: (event) => this.handleBlockListEvent(event),
|
||||
});
|
||||
}
|
||||
|
||||
handleBlockListEvent(event) {
|
||||
const normalized = normalizeHex(event?.pubkey);
|
||||
if (!normalized || normalized !== this.activePubkey) {
|
||||
return;
|
||||
}
|
||||
if (!isUserBlockListEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadBlocks(normalized).catch((error) => {
|
||||
devLogger.warn("[UserBlockList] Failed to refresh after block list event", error);
|
||||
});
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
@@ -775,32 +683,29 @@ class UserBlockListManager {
|
||||
return Array.from(this.blockedPubkeys);
|
||||
}
|
||||
|
||||
scheduleDecryptRetry(userPubkey, detail = {}) {
|
||||
scheduleDecryptRetry(userPubkey, error) {
|
||||
if (!userPubkey) {
|
||||
return;
|
||||
}
|
||||
if (this.decryptRetryTimeoutId) {
|
||||
clearTimeout(this.decryptRetryTimeoutId);
|
||||
}
|
||||
const retryMeta = detail && typeof detail === "object" ? detail : {};
|
||||
const signerStatus = retryMeta.signerStatus || "unknown";
|
||||
this.decryptRetryTimeoutId = setTimeout(() => {
|
||||
this.decryptRetryTimeoutId = null;
|
||||
if (this.activePubkey !== userPubkey) {
|
||||
return;
|
||||
}
|
||||
const sinceValue = Number.isFinite(this.blockEventCreatedAt)
|
||||
? Math.max(0, this.blockEventCreatedAt - 1)
|
||||
: undefined;
|
||||
this.loadBlocks(userPubkey, {
|
||||
since: sinceValue,
|
||||
since: Number.isFinite(this.blockEventCreatedAt)
|
||||
? this.blockEventCreatedAt
|
||||
: undefined,
|
||||
}).catch((retryError) => {
|
||||
userLogger.warn("[UserBlockList] Decryption retry failed:", retryError);
|
||||
});
|
||||
}, DECRYPT_RETRY_DELAY_MS);
|
||||
devLogger.log(
|
||||
`[UserBlockList] Decryption stalled (${signerStatus}); retrying in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
|
||||
retryMeta.error || retryMeta,
|
||||
`[UserBlockList] Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -827,38 +732,6 @@ class UserBlockListManager {
|
||||
|
||||
async loadBlocks(userPubkey, options = {}) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey === normalized) {
|
||||
devLogger.log("[UserBlockList] Reusing in-flight block list load.", {
|
||||
pubkey: normalized,
|
||||
});
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
if (this.loadPromise && this.loadingPubkey && this.loadingPubkey !== normalized) {
|
||||
devLogger.log("[UserBlockList] Waiting for existing block list load to settle.", {
|
||||
previous: this.loadingPubkey,
|
||||
next: normalized,
|
||||
});
|
||||
await this.loadPromise.catch(() => {});
|
||||
}
|
||||
|
||||
const loadPromise = this.loadBlocksInternal(normalized, options);
|
||||
this.loadPromise = loadPromise;
|
||||
this.loadingPubkey = normalized;
|
||||
|
||||
try {
|
||||
return await loadPromise;
|
||||
} finally {
|
||||
if (this.loadPromise === loadPromise) {
|
||||
this.loadPromise = null;
|
||||
this.loadingPubkey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadBlocksInternal(userPubkey, options = {}) {
|
||||
const normalized = normalizeHex(userPubkey);
|
||||
this.activePubkey = normalized;
|
||||
|
||||
if (!normalized) {
|
||||
@@ -872,15 +745,6 @@ class UserBlockListManager {
|
||||
: null;
|
||||
const statusCallback =
|
||||
typeof options?.statusCallback === "function" ? options.statusCallback : null;
|
||||
const loadMode = options?.mode === "background" ? "background" : "interactive";
|
||||
const allowPermissionPrompt =
|
||||
options?.allowPermissionPrompt !== false && loadMode !== "background";
|
||||
const decryptTimeoutMs = Number.isFinite(options?.decryptTimeoutMs)
|
||||
? Math.max(0, Math.floor(options.decryptTimeoutMs))
|
||||
: allowPermissionPrompt
|
||||
? DECRYPT_TIMEOUT_MS
|
||||
: BACKGROUND_DECRYPT_TIMEOUT_MS;
|
||||
const nip07DecryptTimeoutMs = allowPermissionPrompt ? 12000 : 3000;
|
||||
|
||||
const emitStatus = (detail) => {
|
||||
if (!detail || typeof detail !== "object") {
|
||||
@@ -909,23 +773,12 @@ class UserBlockListManager {
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
emitStatus({ status: "loading", relays });
|
||||
if (!nostrClient?.pool && typeof nostrClient?.ensurePool === "function") {
|
||||
try {
|
||||
await nostrClient.ensurePool();
|
||||
} catch (error) {
|
||||
devLogger.warn("[UserBlockList] Failed to initialize relay pool for subscription", error);
|
||||
}
|
||||
}
|
||||
this.ensureBlockListSubscription(normalized, relays);
|
||||
|
||||
const localData = this._loadLocal(normalized);
|
||||
const hasLocalData = !!localData;
|
||||
const localBlocks = this._loadLocal(normalized);
|
||||
const hasLocalData = !!localBlocks;
|
||||
|
||||
if (localData) {
|
||||
this.blockedPubkeys = localData.blockedPubkeys;
|
||||
if (Number.isFinite(localData.createdAt)) {
|
||||
this.blockEventCreatedAt = localData.createdAt;
|
||||
}
|
||||
if (localBlocks) {
|
||||
this.blockedPubkeys = localBlocks;
|
||||
this.loaded = true;
|
||||
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
|
||||
action: "sync",
|
||||
@@ -968,44 +821,21 @@ class UserBlockListManager {
|
||||
|
||||
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
||||
const signerHasNip44 = typeof signer?.nip44Decrypt === "function";
|
||||
const signerAvailable = Boolean(signer);
|
||||
const signerHasDecryptors = signerHasNip04 || signerHasNip44;
|
||||
const signerStatus = signerAvailable
|
||||
? signerHasDecryptors
|
||||
? "present"
|
||||
: "present-missing-decryptor"
|
||||
: "missing";
|
||||
|
||||
let permissionError = null;
|
||||
if ((!signerHasNip44 && requiresNip44) || (!signerHasNip04 && requiresNip04)) {
|
||||
if (!allowPermissionPrompt) {
|
||||
const error = new Error(
|
||||
"Encryption permissions are required to decrypt the block list.",
|
||||
);
|
||||
error.code = "user-blocklist-permission-required";
|
||||
error.permissionRequestDeferred = true;
|
||||
permissionError = error;
|
||||
} else {
|
||||
try {
|
||||
const permissionResult = await requestDefaultExtensionPermissions(
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
);
|
||||
if (!permissionResult?.ok) {
|
||||
const error =
|
||||
permissionResult?.error instanceof Error
|
||||
? permissionResult.error
|
||||
: new Error(
|
||||
"Extension encryption permissions are required to use the browser decryptor.",
|
||||
);
|
||||
error.code = "user-blocklist-permission-required";
|
||||
permissionError = error;
|
||||
}
|
||||
} catch (error) {
|
||||
const wrapped =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
wrapped.code = "user-blocklist-permission-required";
|
||||
permissionError = wrapped;
|
||||
try {
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
permissionError =
|
||||
permissionResult?.error instanceof Error
|
||||
? permissionResult.error
|
||||
: new Error(
|
||||
"Extension permissions are required to use the browser decryptor.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
permissionError = error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,12 +880,6 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
const nostrApi = typeof window !== "undefined" ? window?.nostr : null;
|
||||
const decrypterOptions = {
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
timeoutMs: nip07DecryptTimeoutMs,
|
||||
retryMultiplier: 1,
|
||||
};
|
||||
|
||||
if (nostrApi) {
|
||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||
registerDecryptor(
|
||||
@@ -1063,7 +887,7 @@ class UserBlockListManager {
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nostrApi.nip04.decrypt(normalized, payload),
|
||||
{ label: "nip04.decrypt", ...decrypterOptions },
|
||||
{ label: "nip04.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
@@ -1080,7 +904,7 @@ class UserBlockListManager {
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44.decrypt(normalized, payload), {
|
||||
label: "nip44.decrypt",
|
||||
...decrypterOptions,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
@@ -1094,7 +918,7 @@ class UserBlockListManager {
|
||||
(payload) =>
|
||||
runNip07WithRetry(() => nip44v2.decrypt(normalized, payload), {
|
||||
label: "nip44.v2.decrypt",
|
||||
...decrypterOptions,
|
||||
priority: NIP07_PRIORITY.HIGH,
|
||||
}),
|
||||
"extension",
|
||||
);
|
||||
@@ -1104,7 +928,7 @@ class UserBlockListManager {
|
||||
(payload) =>
|
||||
runNip07WithRetry(
|
||||
() => nip44v2.decrypt(normalized, payload),
|
||||
{ label: "nip44.v2.decrypt", ...decrypterOptions },
|
||||
{ label: "nip44.v2.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||
),
|
||||
"extension",
|
||||
);
|
||||
@@ -1113,16 +937,11 @@ class UserBlockListManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionError) {
|
||||
permissionError.signerStatus = signerStatus;
|
||||
}
|
||||
|
||||
return {
|
||||
decryptors,
|
||||
sources,
|
||||
permissionError,
|
||||
order: determineDecryptionOrder(event, Array.from(decryptors.keys())),
|
||||
signerStatus,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1227,35 +1046,26 @@ class UserBlockListManager {
|
||||
this.blockEventCreatedAt = newestCreatedAt;
|
||||
|
||||
// Helper to decrypt a single event
|
||||
const decryptEvent = async (ev, meta = {}) => {
|
||||
const decryptEvent = async (ev) => {
|
||||
if (!ev) return [];
|
||||
const isContentEmpty = !ev.content || !ev.content.trim();
|
||||
|
||||
if (isContentEmpty) {
|
||||
// Handle tag-only lists (e.g. Kind 10000 with empty content but populated p tags)
|
||||
if (Array.isArray(ev.tags) && ev.tags.length) {
|
||||
return extractPubkeysFromTags(ev.tags, normalized, {
|
||||
limit: MAX_BLOCKLIST_ENTRIES,
|
||||
});
|
||||
return extractPubkeysFromTags(ev.tags, normalized);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const { decryptors, order, sources, permissionError, signerStatus } =
|
||||
const { decryptors, order, sources, permissionError } =
|
||||
await resolveDecryptors(ev);
|
||||
meta.signerStatus = signerStatus;
|
||||
meta.decryptorSources = Array.from(sources.entries()).map(
|
||||
([scheme, source]) => ({ scheme, source }),
|
||||
);
|
||||
|
||||
if (!decryptors.size) {
|
||||
if (permissionError) {
|
||||
throw permissionError;
|
||||
}
|
||||
const error = new Error("No decryptor available for block list event.");
|
||||
error.code = "user-blocklist-no-decryptor";
|
||||
error.signerStatus = signerStatus;
|
||||
throw error;
|
||||
return [];
|
||||
}
|
||||
|
||||
let decryptedText = "";
|
||||
@@ -1279,48 +1089,27 @@ class UserBlockListManager {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parseBlockListPlaintext(decryptedText, normalized, {
|
||||
limit: MAX_BLOCKLIST_ENTRIES,
|
||||
});
|
||||
return parseBlockListPlaintext(decryptedText, normalized);
|
||||
};
|
||||
|
||||
const mergedPubkeys = new Set();
|
||||
let decryptionError = null;
|
||||
const decryptMeta = {
|
||||
mute: { eventId: newestMute?.id || null },
|
||||
block: { eventId: newestBlock?.id || null },
|
||||
};
|
||||
const resolveSignerStatus = () => {
|
||||
const statuses = [decryptMeta.mute.signerStatus, decryptMeta.block.signerStatus]
|
||||
.filter(Boolean);
|
||||
if (!statuses.length) {
|
||||
return "unknown";
|
||||
}
|
||||
if (statuses.every((status) => status === "missing")) {
|
||||
return "missing";
|
||||
}
|
||||
if (statuses.some((status) => status === "present-missing-decryptor")) {
|
||||
return "present-missing-decryptor";
|
||||
}
|
||||
return "present";
|
||||
};
|
||||
|
||||
try {
|
||||
const decryptPromise = Promise.all([
|
||||
decryptEvent(newestMute, decryptMeta.mute),
|
||||
decryptEvent(newestBlock, decryptMeta.block),
|
||||
decryptEvent(newestMute),
|
||||
decryptEvent(newestBlock),
|
||||
]);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => {
|
||||
const timeoutError = new Error(
|
||||
`Decryption timed out after ${decryptTimeoutMs / 1000}s`,
|
||||
`Decryption timed out after ${DECRYPT_TIMEOUT_MS / 1000}s`,
|
||||
);
|
||||
timeoutError.code = "user-blocklist-decrypt-timeout";
|
||||
timeoutError.signerStatus = resolveSignerStatus();
|
||||
reject(timeoutError);
|
||||
},
|
||||
decryptTimeoutMs,
|
||||
DECRYPT_TIMEOUT_MS,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1337,54 +1126,13 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
if (decryptionError) {
|
||||
if (decryptionError.code === "user-blocklist-permission-required") {
|
||||
const signerStatus =
|
||||
decryptionError.signerStatus || resolveSignerStatus();
|
||||
if (decryptionError.code === "user-blocklist-decrypt-timeout") {
|
||||
this.blockedPubkeys = previousState.blockedPubkeys;
|
||||
this.blockEventId = previousState.blockEventId;
|
||||
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
|
||||
this.loaded = true;
|
||||
emitStatus({
|
||||
status: "permission-required",
|
||||
signerStatus,
|
||||
source,
|
||||
reason: "decryptor-permission",
|
||||
allowPermissionPrompt,
|
||||
mode: loadMode,
|
||||
events: [newestMute?.id, newestBlock?.id].filter(Boolean),
|
||||
error: decryptionError,
|
||||
});
|
||||
emitStatus({ status: "settled" });
|
||||
return;
|
||||
}
|
||||
if (decryptionError.code === "user-blocklist-decrypt-timeout") {
|
||||
const signerStatus =
|
||||
decryptionError.signerStatus || resolveSignerStatus();
|
||||
applyBlockedPubkeys([], {
|
||||
source,
|
||||
reason: "decrypt-timeout",
|
||||
signerStatus,
|
||||
events: [newestMute?.id, newestBlock?.id].filter(Boolean),
|
||||
});
|
||||
this.loaded = true;
|
||||
emitStatus({
|
||||
status: "applied-empty",
|
||||
reason: "decrypt-timeout",
|
||||
signerStatus,
|
||||
source,
|
||||
});
|
||||
emitStatus({ status: "settled" });
|
||||
userLogger.warn(
|
||||
"[UserBlockList] Decryption timed out; applying empty list and retrying in background.",
|
||||
{
|
||||
signerStatus,
|
||||
events: [newestMute?.id, newestBlock?.id].filter(Boolean),
|
||||
},
|
||||
);
|
||||
this.scheduleDecryptRetry(normalized, {
|
||||
error: decryptionError,
|
||||
signerStatus,
|
||||
});
|
||||
this.scheduleDecryptRetry(normalized, decryptionError);
|
||||
return;
|
||||
}
|
||||
userLogger.warn(
|
||||
@@ -1395,13 +1143,10 @@ class UserBlockListManager {
|
||||
this.blockEventId = previousState.blockEventId;
|
||||
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
|
||||
this.loaded = true;
|
||||
const signerStatus =
|
||||
decryptionError.signerStatus || resolveSignerStatus();
|
||||
emitStatus({
|
||||
status: "error",
|
||||
error: decryptionError,
|
||||
decryptor: "mixed",
|
||||
signerStatus,
|
||||
});
|
||||
emitStatus({ status: "settled" });
|
||||
return;
|
||||
@@ -1437,13 +1182,9 @@ class UserBlockListManager {
|
||||
}
|
||||
};
|
||||
|
||||
// Anchor the fetch to the service's current state to prevent desync.
|
||||
// If we have cached data, we ask for updates since that timestamp.
|
||||
// If we have no data, we force a full fetch (since=0).
|
||||
const fetchSince =
|
||||
hasLocalData && Number.isFinite(this.blockEventCreatedAt)
|
||||
? this.blockEventCreatedAt
|
||||
: 0;
|
||||
// If we don't have local data, we must ignore the sync metadata and force a full fetch (since: 0).
|
||||
// Otherwise, we might miss data if the metadata store says we are up-to-date but the actual list is missing.
|
||||
const fetchSince = hasLocalData ? undefined : 0;
|
||||
|
||||
// Concurrent incremental fetch for both kinds
|
||||
const [muteEvents, blockEvents] = await Promise.all([
|
||||
@@ -1493,15 +1234,7 @@ class UserBlockListManager {
|
||||
}
|
||||
const cached = profileCache.getProfileData(normalizedActorHex, "blocks");
|
||||
if (Array.isArray(cached)) {
|
||||
return { blockedPubkeys: new Set(cached), createdAt: null };
|
||||
}
|
||||
if (cached && typeof cached === "object") {
|
||||
return {
|
||||
blockedPubkeys: new Set(
|
||||
Array.isArray(cached.blockedPubkeys) ? cached.blockedPubkeys : [],
|
||||
),
|
||||
createdAt: Number.isFinite(cached.createdAt) ? cached.createdAt : null,
|
||||
};
|
||||
return new Set(cached);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1511,11 +1244,11 @@ class UserBlockListManager {
|
||||
if (!normalizedActorHex) {
|
||||
return;
|
||||
}
|
||||
profileCache.setProfileData(normalizedActorHex, "blocks", {
|
||||
blockedPubkeys: Array.from(this.blockedPubkeys),
|
||||
createdAt: this.blockEventCreatedAt,
|
||||
version: 1,
|
||||
});
|
||||
profileCache.setProfileData(
|
||||
normalizedActorHex,
|
||||
"blocks",
|
||||
Array.from(this.blockedPubkeys),
|
||||
);
|
||||
}
|
||||
|
||||
_getSeedState(actorHex) {
|
||||
@@ -2056,18 +1789,16 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
if (signer.type === "extension") {
|
||||
const permissionResult = await requestDefaultExtensionPermissions(
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
);
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] Signer permissions denied while updating the block list.",
|
||||
permissionResult.error,
|
||||
);
|
||||
const err = new Error(
|
||||
"The active signer must allow encryption before updating the block list.",
|
||||
"The active signer must allow encryption and signing before updating the block list.",
|
||||
);
|
||||
err.code = "extension-encryption-permission-denied";
|
||||
err.code = "extension-permission-denied";
|
||||
err.cause = permissionResult.error;
|
||||
throw err;
|
||||
}
|
||||
|
||||
Vendored
+109
-11
@@ -2,7 +2,6 @@
|
||||
|
||||
import { nostrClient } from "../nostrClientFacade.js";
|
||||
import { devLogger } from "./logger.js";
|
||||
import { fetchProfileMetadataBatch } from "../services/profileMetadataService.js";
|
||||
|
||||
const DEFAULT_PROFILE_IMAGE = "assets/svg/default-profile.svg";
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
@@ -58,18 +57,117 @@ export async function batchFetchProfilesFromRelays({
|
||||
return;
|
||||
}
|
||||
|
||||
const profileResults = await fetchProfileMetadataBatch({
|
||||
pubkeys: toFetch,
|
||||
nostr,
|
||||
logger,
|
||||
defaultProfileImage,
|
||||
});
|
||||
if (!nostr?.pool || typeof nostr.pool.list !== "function") {
|
||||
logger.warn(
|
||||
"[batchFetchProfiles] Relay pool is not ready; skipping profile fetch.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
profileResults.forEach((entry, pubkey) => {
|
||||
if (!entry?.profile) {
|
||||
const filter = {
|
||||
kinds: [0],
|
||||
authors: toFetch,
|
||||
limit: toFetch.length,
|
||||
};
|
||||
|
||||
const newestProfiles = new Map();
|
||||
|
||||
const applyProfileEvent = (evt, relayUrl) => {
|
||||
if (!evt || typeof evt !== "object") {
|
||||
return;
|
||||
}
|
||||
setProfileCacheEntry(pubkey, entry.profile);
|
||||
updateProfileInDOM(pubkey, entry.profile);
|
||||
|
||||
const pubkey =
|
||||
typeof evt.pubkey === "string" && hex64Regex.test(evt.pubkey)
|
||||
? evt.pubkey
|
||||
: null;
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdAt = Number.isFinite(evt.created_at) ? evt.created_at : 0;
|
||||
const previous = newestProfiles.get(pubkey);
|
||||
if (previous && previous.createdAt >= createdAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(evt.content);
|
||||
const profile = {
|
||||
name: data?.name || data?.display_name || "Unknown",
|
||||
picture: data?.picture || defaultProfileImage,
|
||||
};
|
||||
|
||||
newestProfiles.set(pubkey, { createdAt, profile });
|
||||
setProfileCacheEntry(pubkey, profile);
|
||||
updateProfileInDOM(pubkey, profile);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[batchFetchProfiles] Profile parse error for ${pubkey} from ${relayUrl}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const relayPromises = (Array.isArray(nostr.relays) ? nostr.relays : []).map(
|
||||
(relayUrl) =>
|
||||
nostr.pool
|
||||
.list([relayUrl], [filter])
|
||||
.then((events) => ({ relayUrl, events })),
|
||||
);
|
||||
|
||||
if (!relayPromises.length) {
|
||||
logger.warn(
|
||||
"[batchFetchProfiles] No relays configured; skipping profile fetch.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(relayPromises);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const relayUrl =
|
||||
result.status === "fulfilled"
|
||||
? result.value.relayUrl
|
||||
: nostr.relays[index];
|
||||
|
||||
if (result.status === "fulfilled") {
|
||||
const events = Array.isArray(result.value.events)
|
||||
? result.value.events
|
||||
: [];
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newestByPubkey = new Map();
|
||||
for (const evt of events) {
|
||||
if (!evt || typeof evt !== "object") {
|
||||
continue;
|
||||
}
|
||||
const pubkey =
|
||||
typeof evt.pubkey === "string" && hex64Regex.test(evt.pubkey)
|
||||
? evt.pubkey
|
||||
: null;
|
||||
if (!pubkey) {
|
||||
continue;
|
||||
}
|
||||
const createdAt = Number.isFinite(evt.created_at)
|
||||
? evt.created_at
|
||||
: 0;
|
||||
const prior = newestByPubkey.get(pubkey);
|
||||
if (!prior || createdAt > prior.createdAt) {
|
||||
newestByPubkey.set(pubkey, { createdAt, event: evt });
|
||||
}
|
||||
}
|
||||
|
||||
newestByPubkey.forEach(({ event }) => {
|
||||
applyProfileEvent(event, relayUrl);
|
||||
});
|
||||
} else if (relayUrl) {
|
||||
logger.warn(
|
||||
`[batchFetchProfiles] Failed to fetch profiles from relay ${relayUrl}:`,
|
||||
result.reason,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Vendored
-4
@@ -46,10 +46,6 @@ export function sanitizeProfileMediaUrl(value) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (trimmed === "images/robot.png") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (/^ipfs:\/\//i.test(trimmed)) {
|
||||
return normalizeIpfsGatewayUrl(trimmed);
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -4,11 +4,11 @@ import { userLogger } from "./logger.js";
|
||||
const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:";
|
||||
const TORRENT_PROBE_STORAGE_PREFIX = "bitvid:torrentProbe:";
|
||||
|
||||
function getUrlHealthStorageKey(eventId) {
|
||||
export function getUrlHealthStorageKey(eventId) {
|
||||
return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`;
|
||||
}
|
||||
|
||||
function getTorrentProbeStorageKey(infoHash) {
|
||||
export function getTorrentProbeStorageKey(infoHash) {
|
||||
return `${TORRENT_PROBE_STORAGE_PREFIX}${infoHash}`;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -8,7 +8,7 @@ export function normalizeStoragePointer(value) {
|
||||
return safeTrim(value);
|
||||
}
|
||||
|
||||
function parseStoragePointer(value) {
|
||||
export function parseStoragePointer(value) {
|
||||
const normalized = normalizeStoragePointer(value);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
@@ -38,7 +38,7 @@ export function buildStoragePointerValue({ provider, prefix } = {}) {
|
||||
return `${normalizedProvider}:${normalizedPrefix}`;
|
||||
}
|
||||
|
||||
function deriveStoragePrefixFromUrl(url) {
|
||||
export function deriveStoragePrefixFromUrl(url) {
|
||||
const trimmed = safeTrim(url);
|
||||
if (!trimmed || !/^https?:\/\//i.test(trimmed)) {
|
||||
return "";
|
||||
|
||||
Vendored
+1
-1
@@ -44,7 +44,7 @@ function normalizeTagValue(rawValue) {
|
||||
* @returns {string} The tag prefixed with `#` when a tag exists, otherwise an
|
||||
* empty string.
|
||||
*/
|
||||
function formatTagDisplay(tag) {
|
||||
export function formatTagDisplay(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
Vendored
-15
@@ -50,7 +50,6 @@ restoreCacheSnapshot();
|
||||
* @property {Map<string, number>} dedupeBuckets
|
||||
* @property {number} lastSyncedAt
|
||||
* @property {"idle"|"hydrating"|"live"|"stale"} status
|
||||
* @property {boolean} partial
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -279,7 +278,6 @@ function restoreCacheSnapshot() {
|
||||
dedupeBuckets,
|
||||
lastSyncedAt,
|
||||
status: status === "hydrating" || status === "live" ? "idle" : status,
|
||||
partial: Boolean(state.partial),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -301,7 +299,6 @@ function persistCacheSnapshot() {
|
||||
dedupeBuckets: Array.from(state.dedupeBuckets.entries()),
|
||||
lastSyncedAt: state.lastSyncedAt,
|
||||
status: state.status,
|
||||
partial: Boolean(state.partial),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -324,7 +321,6 @@ function ensurePointerState(key) {
|
||||
dedupeBuckets: new Map(),
|
||||
lastSyncedAt: 0,
|
||||
status: "idle",
|
||||
partial: false,
|
||||
};
|
||||
pointerStates.set(key, state);
|
||||
} else if (!(state.dedupeBuckets instanceof Map)) {
|
||||
@@ -377,7 +373,6 @@ function notifyHandlers(key) {
|
||||
total: state.total,
|
||||
lastSyncedAt: state.lastSyncedAt,
|
||||
status: state.status,
|
||||
partial: Boolean(state.partial),
|
||||
};
|
||||
for (const handler of listeners.handlers.values()) {
|
||||
try {
|
||||
@@ -499,15 +494,6 @@ async function hydratePointer(key, listeners) {
|
||||
? Number(countResult.total)
|
||||
: null;
|
||||
|
||||
if (countResult && typeof countResult === "object") {
|
||||
const state = ensurePointerState(key);
|
||||
const nextPartial = Boolean(countResult.partial);
|
||||
if (state.partial !== nextPartial) {
|
||||
state.partial = nextPartial;
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCount !== null) {
|
||||
const state = ensurePointerState(key);
|
||||
const shouldUpdate = bestCount > state.total || state.total === 0;
|
||||
@@ -611,7 +597,6 @@ export function subscribeToVideoViewCount(pointerInput, handler, options = {}) {
|
||||
total: state.total,
|
||||
lastSyncedAt: state.lastSyncedAt,
|
||||
status: state.status,
|
||||
partial: Boolean(state.partial),
|
||||
});
|
||||
} catch (error) {
|
||||
userLogger.warn("[viewCounter] Initial handler invocation threw:", error);
|
||||
|
||||
Vendored
+23
-98
@@ -23,33 +23,9 @@ import { WSS_TRACKERS } from "./constants.js";
|
||||
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import { emit } from "./embedDiagnostics.js";
|
||||
import { infoHashFromMagnet, webSeedsFromMagnet } from "./magnets.js";
|
||||
|
||||
const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]);
|
||||
|
||||
function stripXsParameter(magnetURI) {
|
||||
const trimmed = typeof magnetURI === "string" ? magnetURI.trim() : "";
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
(window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1")
|
||||
) {
|
||||
try {
|
||||
const [prefix, queryString] = trimmed.split("?");
|
||||
if (queryString) {
|
||||
const params = new URLSearchParams(queryString);
|
||||
if (params.has("xs")) {
|
||||
params.delete("xs");
|
||||
return `${prefix}?${params.toString()}`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeTrackerList(trackers) {
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
@@ -185,8 +161,6 @@ export class TorrentClient {
|
||||
|
||||
// Timeout for SW operations
|
||||
this.TIMEOUT_DURATION = 60000;
|
||||
|
||||
this.probeInFlight = new Map();
|
||||
}
|
||||
|
||||
ensureClientForProbe() {
|
||||
@@ -205,10 +179,9 @@ export class TorrentClient {
|
||||
*/
|
||||
async probePeers(
|
||||
magnetURI,
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 2, urlList = [] } = {}
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {}
|
||||
) {
|
||||
const magnet = stripXsParameter(magnetURI);
|
||||
|
||||
const magnet = typeof magnetURI === "string" ? magnetURI.trim() : "";
|
||||
if (!magnet) {
|
||||
return {
|
||||
healthy: false,
|
||||
@@ -241,21 +214,14 @@ export class TorrentClient {
|
||||
};
|
||||
}
|
||||
|
||||
const infoHash = infoHashFromMagnet(magnet);
|
||||
const probeKey = infoHash || null;
|
||||
|
||||
if (probeKey && this.probeInFlight.has(probeKey)) {
|
||||
return this.probeInFlight.get(probeKey);
|
||||
}
|
||||
|
||||
const client = this.ensureClientForProbe();
|
||||
const safeTimeout = Math.max(0, normalizeNumber(timeoutMs, 8000));
|
||||
const safePolls = Math.max(0, Math.floor(normalizeNumber(polls, 2)));
|
||||
const safePolls = Math.max(1, Math.floor(normalizeNumber(polls, 3)));
|
||||
const safeMaxWebConns = Math.max(1, Math.floor(normalizeNumber(maxWebConns, 2)));
|
||||
const pollInterval =
|
||||
safePolls > 0
|
||||
? Math.max(1000, Math.floor(safeTimeout / Math.max(1, safePolls)))
|
||||
: 0;
|
||||
const pollInterval = Math.max(
|
||||
250,
|
||||
Math.floor(safeTimeout / Math.max(1, safePolls))
|
||||
);
|
||||
|
||||
const startedAt =
|
||||
typeof performance !== "undefined" && performance?.now
|
||||
@@ -264,12 +230,11 @@ export class TorrentClient {
|
||||
|
||||
emit("torrent-probe-start", { magnet: augmentedMagnet });
|
||||
|
||||
const probePromise = new Promise((resolve) => {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let torrent = null;
|
||||
let timeoutId = null;
|
||||
let pollId = null;
|
||||
let pollStartId = null;
|
||||
|
||||
const finalize = (overrides = {}) => {
|
||||
if (settled) {
|
||||
@@ -284,10 +249,6 @@ export class TorrentClient {
|
||||
clearInterval(pollId);
|
||||
pollId = null;
|
||||
}
|
||||
if (pollStartId) {
|
||||
clearTimeout(pollStartId);
|
||||
pollStartId = null;
|
||||
}
|
||||
if (torrent) {
|
||||
try {
|
||||
torrent.destroy({ destroyStore: true });
|
||||
@@ -367,34 +328,16 @@ export class TorrentClient {
|
||||
}, safeTimeout);
|
||||
}
|
||||
|
||||
if (safePolls > 0 && pollInterval > 0) {
|
||||
pollStartId = setTimeout(() => {
|
||||
if (!torrent || settled) {
|
||||
return;
|
||||
}
|
||||
pollId = setInterval(() => {
|
||||
if (!torrent || settled) {
|
||||
return;
|
||||
}
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent.numPeers, 0)));
|
||||
if (peers > 0) {
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
}
|
||||
}, pollInterval);
|
||||
}, pollInterval);
|
||||
}
|
||||
});
|
||||
|
||||
if (probeKey) {
|
||||
this.probeInFlight.set(probeKey, probePromise);
|
||||
probePromise.finally(() => {
|
||||
if (this.probeInFlight.get(probeKey) === probePromise) {
|
||||
this.probeInFlight.delete(probeKey);
|
||||
pollId = setInterval(() => {
|
||||
if (!torrent || settled) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return probePromise;
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent.numPeers, 0)));
|
||||
if (peers > 0) {
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
}
|
||||
}, pollInterval);
|
||||
});
|
||||
}
|
||||
|
||||
log(...args) {
|
||||
@@ -927,24 +870,9 @@ export class TorrentClient {
|
||||
*/
|
||||
async streamVideo(magnetURI, videoElement, opts = {}) {
|
||||
try {
|
||||
const effectiveMagnetURI = stripXsParameter(magnetURI);
|
||||
if (
|
||||
effectiveMagnetURI !==
|
||||
(typeof magnetURI === "string" ? magnetURI.trim() : "")
|
||||
) {
|
||||
this.log(
|
||||
"[WebTorrent] Stripped 'xs' parameter to prevent CORS errors on localhost.",
|
||||
);
|
||||
}
|
||||
|
||||
emit("torrent-stream-start", { magnet: effectiveMagnetURI });
|
||||
emit("torrent-stream-start", { magnet: magnetURI });
|
||||
// 1) Make sure we have a WebTorrent client and a valid SW registration.
|
||||
const initResult = await this.init();
|
||||
|
||||
if (!this.client) {
|
||||
throw new Error("Client destroyed during initialization");
|
||||
}
|
||||
|
||||
const serviceWorkerReady =
|
||||
!!(initResult?.serviceWorkerReady && this.swRegistration);
|
||||
|
||||
@@ -960,18 +888,15 @@ export class TorrentClient {
|
||||
}
|
||||
|
||||
const isFirefoxBrowser = this.isFirefox();
|
||||
const explicitUrls = Array.isArray(opts?.urlList)
|
||||
const candidateUrls = Array.isArray(opts?.urlList)
|
||||
? opts.urlList
|
||||
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
||||
.filter((entry) => /^https?:\/\//i.test(entry))
|
||||
: [];
|
||||
|
||||
const magnetUrls = webSeedsFromMagnet(effectiveMagnetURI);
|
||||
const combinedUrls = [...new Set([...explicitUrls, ...magnetUrls])];
|
||||
|
||||
const chromeOptions = { strategy: "sequential" };
|
||||
if (combinedUrls.length) {
|
||||
chromeOptions.urlList = combinedUrls;
|
||||
if (candidateUrls.length) {
|
||||
chromeOptions.urlList = candidateUrls;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -979,7 +904,7 @@ export class TorrentClient {
|
||||
if (isFirefoxBrowser) {
|
||||
this.log("Starting torrent download (Firefox path)");
|
||||
this.client.add(
|
||||
effectiveMagnetURI,
|
||||
magnetURI,
|
||||
{ ...chromeOptions, maxWebConns: 4 },
|
||||
(torrent) => {
|
||||
this.log("Torrent added (Firefox path):", torrent.name);
|
||||
@@ -988,7 +913,7 @@ export class TorrentClient {
|
||||
);
|
||||
} else {
|
||||
this.log("Starting torrent download (Chrome path)");
|
||||
this.client.add(effectiveMagnetURI, chromeOptions, (torrent) => {
|
||||
this.client.add(magnetURI, chromeOptions, (torrent) => {
|
||||
this.log("Torrent added (Chrome path):", torrent.name);
|
||||
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
|
||||
});
|
||||
|
||||
Vendored
-17
@@ -1,17 +0,0 @@
|
||||
# Release Draft - 2026-01-24
|
||||
|
||||
## Highlights
|
||||
|
||||
### S3 Multipart Upload Support
|
||||
We have introduced a new helper module `js/storage/s3-multipart.js` to facilitate reliable uploads of large files to S3-compatible storage providers. This module handles:
|
||||
- Multipart upload orchestration (initiate, upload parts, complete/abort).
|
||||
- Automatic part sizing calculation.
|
||||
- Bucket existence checks and creation (with region support).
|
||||
- CORS configuration helper.
|
||||
- Intelligent `Cache-Control` header generation based on file type.
|
||||
|
||||
This addition paves the way for more robust video upload capabilities directly to object storage.
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
No breaking changes or dependency bumps in this release.
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{IS_DEV_MODE as Le,IS_VERBOSE_DEV_MODE as Ae,IS_LOCKDOWN_MODE as Ce,LOCKDOWN_TITLE as yt,LOCKDOWN_MESSAGE as Et,ADMIN_SUPER_NPUB as Tt,ADMIN_DM_IMAGE_URL as gt,BITVID_WEBSITE_URL as _t,BLOG_URL as St,COMMUNITY_URL as wt,NOSTR_URL as Lt,GITHUB_URL as At,BETA_URL as Ct,DNS_URL as xt,TIP_JAR_URL as vt,MAX_WALLET_DEFAULT_ZAP as It,PLATFORM_FEE_PERCENT as Ot,PLATFORM_LUD16_OVERRIDE as Nt,DEFAULT_RELAY_URLS_OVERRIDE as Bt,ADMIN_WHITELIST_MODE_STORAGE_KEY as Mt,DEFAULT_WHITELIST_MODE_ENABLED as Dt,ALLOW_NSFW_CONTENT as Rt,DEFAULT_TRUST_SEED_NPUBS as Ut,DEFAULT_BLUR_THRESHOLD as Ht,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as kt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as Ft,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as Pt,WATCH_HISTORY_KIND as Vt,WATCH_HISTORY_LIST_IDENTIFIER as Wt,WATCH_HISTORY_MAX_ITEMS as qt,WATCH_HISTORY_BATCH_RESOLVE as jt,WATCH_HISTORY_BATCH_PAGE_SIZE as $t,WATCH_HISTORY_PAYLOAD_MAX_BYTES as Yt,WATCH_HISTORY_FETCH_EVENT_LIMIT as Kt,WATCH_HISTORY_CACHE_TTL_MS as Gt,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as zt,VIEW_COUNT_BACKFILL_MAX_DAYS as Xt,VIEW_COUNT_CACHE_TTL_MS as Zt,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as Jt,DEFAULT_PLAYBACK_SOURCE as Qt,DEFAULT_PLAYBACK_START_TIMEOUT as er}from"../config/instance-config.js";var U=!!Le;var xe=!!Ae;var ve=!!Ce;typeof window<"u"&&(window.__BITVID_DEV_MODE__=U,window.__BITVID_VERBOSE_DEV_MODE__=xe,window.__BITVID_LOCKDOWN_MODE__=ve);var k=()=>{};function Ie(){return typeof console>"u"||console===null?null:console}function Oe(){let e=Ie();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var _=Oe(),M="[bitvid]";function ee(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var S=Object.freeze({log:(...e)=>{U&&_.log(...e)},info:(...e)=>{U&&_.info(...e)},debug:(...e)=>{U&&_.debug(...e)},warn:(...e)=>{U&&_.warn(...e)},error:(...e)=>{U&&_.error(...e)}}),b=Object.freeze({log:(...e)=>{let{args:t,force:r}=ee(e);if(!r&&t.length===0)return;if(r){_.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.log(`${M} ${o}`);return}o!==void 0&&_.log(M,o)},info:(...e)=>{let{args:t,force:r}=ee(e);if(!r&&t.length===0)return;if(r){_.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.info(`${M} ${o}`);return}o!==void 0&&_.info(M,o)},warn:(...e)=>{_.warn(M,...e)},error:(...e)=>{_.error(M,...e)}}),te=Object.freeze({dev:S,user:b});function Ne(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Ne(window.bitvidLogger)||(window.bitvidLogger=te));Object.freeze(te);import{THEME_ACCENT_OVERRIDES as re}from"../config/instance-config.js";var z="bitvid:theme",v="dark",Be=new Set(["light","dark"]),ne={dark:"rgb(15, 23, 42)",light:"rgb(248, 250, 252)"},oe=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],ie=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),ae=new WeakSet,W=new Set,D=null,se=!1,F=null,I=()=>typeof window<"u"&&typeof document<"u",q=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?v:Be.has(t)?t:null},Me=()=>{if(!I())return null;try{let e=window.localStorage.getItem(z);return q(e)}catch(e){return b.warn("Unable to read stored theme preference:",e),null}},De=e=>{if(I())try{window.localStorage.setItem(z,e)}catch(t){b.warn("Unable to persist theme preference:",t)}},K=()=>{if(!I())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},Re=e=>typeof e=="string"?e.trim():"",Ue=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[u,d])=>(Object.prototype.hasOwnProperty.call(ie,u)&&(a[u]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},ce=e=>{F=Ue(e)};ce(typeof re<"u"?re:null);var le=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(ie).forEach(([n,c])=>{let a=r?r[n]:null,u=Re(a);u?(e.style.setProperty(c,u),o=!0):e.style.removeProperty(c)}),o?e.dataset.themeAccent="override":e.removeAttribute("data-theme-accent")},He=e=>{if(!I())return;let t=document.querySelector('meta[name="theme-color"]');if(!t)return;let r=K();if(!r)return;let o="";try{typeof window.getComputedStyle=="function"&&(o=getComputedStyle(r).getPropLine truncated
|
||||
import{IS_DEV_MODE as Le,IS_VERBOSE_DEV_MODE as Ce,IS_LOCKDOWN_MODE as Ae,LOCKDOWN_TITLE as bt,LOCKDOWN_MESSAGE as Et,ADMIN_SUPER_NPUB as Tt,ADMIN_DM_IMAGE_URL as gt,BITVID_WEBSITE_URL as _t,BLOG_URL as St,COMMUNITY_URL as wt,NOSTR_URL as Lt,GITHUB_URL as Ct,BETA_URL as At,DNS_URL as xt,TIP_JAR_URL as vt,MAX_WALLET_DEFAULT_ZAP as It,PLATFORM_FEE_PERCENT as Ot,PLATFORM_LUD16_OVERRIDE as Nt,DEFAULT_RELAY_URLS_OVERRIDE as Bt,ADMIN_WHITELIST_MODE_STORAGE_KEY as Mt,DEFAULT_WHITELIST_MODE_ENABLED as Dt,ALLOW_NSFW_CONTENT as Rt,DEFAULT_TRUST_SEED_NPUBS as Ht,DEFAULT_BLUR_THRESHOLD as Ut,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as kt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as Ft,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as Pt,WATCH_HISTORY_KIND as Vt,WATCH_HISTORY_LIST_IDENTIFIER as Wt,WATCH_HISTORY_MAX_ITEMS as qt,WATCH_HISTORY_BATCH_RESOLVE as jt,WATCH_HISTORY_BATCH_PAGE_SIZE as $t,WATCH_HISTORY_PAYLOAD_MAX_BYTES as Yt,WATCH_HISTORY_FETCH_EVENT_LIMIT as Gt,WATCH_HISTORY_CACHE_TTL_MS as zt,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as Kt,VIEW_COUNT_BACKFILL_MAX_DAYS as Xt,VIEW_COUNT_CACHE_TTL_MS as Zt,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as Jt}from"../config/instance-config.js";var H=!!Le;var xe=!!Ce;var ve=!!Ae;typeof window<"u"&&(window.__BITVID_DEV_MODE__=H,window.__BITVID_VERBOSE_DEV_MODE__=xe,window.__BITVID_LOCKDOWN_MODE__=ve);var k=()=>{};function Ie(){return typeof console>"u"||console===null?null:console}function Oe(){let e=Ie();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var _=Oe(),M="[bitvid]";function ee(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var S=Object.freeze({log:(...e)=>{H&&_.log(...e)},info:(...e)=>{H&&_.info(...e)},debug:(...e)=>{H&&_.debug(...e)},warn:(...e)=>{H&&_.warn(...e)},error:(...e)=>{H&&_.error(...e)}}),y=Object.freeze({log:(...e)=>{let{args:t,force:r}=ee(e);if(!r&&t.length===0)return;if(r){_.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.log(`${M} ${o}`);return}o!==void 0&&_.log(M,o)},info:(...e)=>{let{args:t,force:r}=ee(e);if(!r&&t.length===0)return;if(r){_.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.info(`${M} ${o}`);return}o!==void 0&&_.info(M,o)},warn:(...e)=>{_.warn(M,...e)},error:(...e)=>{_.error(M,...e)}}),te=Object.freeze({dev:S,user:y});function Ne(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Ne(window.bitvidLogger)||(window.bitvidLogger=te));Object.freeze(te);import{THEME_ACCENT_OVERRIDES as re}from"../config/instance-config.js";var K="bitvid:theme",v="dark",Be=new Set(["light","dark"]),ne={dark:"#0f172a",light:"#f8fafc"},oe=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],ie=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),ae=new WeakSet,W=new Set,D=null,se=!1,F=null,I=()=>typeof window<"u"&&typeof document<"u",q=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?v:Be.has(t)?t:null},Me=()=>{if(!I())return null;try{let e=window.localStorage.getItem(K);return q(e)}catch(e){return y.warn("Unable to read stored theme preference:",e),null}},De=e=>{if(I())try{window.localStorage.setItem(K,e)}catch(t){y.warn("Unable to persist theme preference:",t)}},G=()=>{if(!I())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},Re=e=>typeof e=="string"?e.trim():"",He=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[u,d])=>(Object.prototype.hasOwnProperty.call(ie,u)&&(a[u]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},ce=e=>{F=He(e)};ce(typeof re<"u"?re:null);var le=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(ie).forEach(([n,c])=>{let a=r?r[n]:null,u=Re(a);u?(e.style.setProperty(c,u),o=!0):e.style.removeProperty(c)}),o?e.dataset.themeAccent="override":e.removeAttribute("data-theme-accent")},Ue=e=>{if(!I())return;let t=document.querySelector('meta[name="theme-color"]');if(!t)return;let r=G();if(!r)return;let o="";try{typeof window.getComputedStyle=="function"&&(o=getComputedStyle(r).getPropertyValue("--color-page").trim())}catch(a){y.warn("Unable to compute theme color tokenLine truncated
|
||||
+1
-1
@@ -1 +1 @@
|
||||
var $=Math.min,B=Math.max,ot=Math.round,it=Math.floor,E=t=>({x:t,y:t}),qt={left:"right",right:"left",bottom:"top",top:"bottom"},Ut={start:"end",end:"start"};function lt(t,e,n){return B(t,$(e,n))}function U(t,e){return typeof t=="function"?t(e):t}function V(t){return t.split("-")[0]}function K(t){return t.split("-")[1]}function pt(t){return t==="x"?"y":"x"}function at(t){return t==="y"?"height":"width"}var Kt=new Set(["top","bottom"]);function W(t){return Kt.has(V(t))?"y":"x"}function ft(t){return pt(W(t))}function At(t,e,n){n===void 0&&(n=!1);let o=K(t),i=ft(t),r=at(i),s=i==="x"?o===(n?"end":"start")?"right":"left":o==="start"?"bottom":"top";return e.reference[r]>e.floating[r]&&(s=nt(s)),[s,nt(s)]}function Ot(t){let e=nt(t);return[ct(t),e,ct(e)]}function ct(t){return t.replace(/start|end/g,e=>Ut[e])}var vt=["left","right"],bt=["right","left"],Gt=["top","bottom"],Jt=["bottom","top"];function Qt(t,e,n){switch(t){case"top":case"bottom":return n?e?bt:vt:e?vt:bt;case"left":case"right":return e?Gt:Jt;default:return[]}}function Rt(t,e,n,o){let i=K(t),r=Qt(V(t),n==="start",o);return i&&(r=r.map(s=>s+"-"+i),e&&(r=r.concat(r.map(ct)))),r}function nt(t){return t.replace(/left|right|bottom|top/g,e=>qt[e])}function Zt(t){return{top:0,right:0,bottom:0,left:0,...t}}function wt(t){return typeof t!="number"?Zt(t):{top:t,right:t,bottom:t,left:t}}function X(t){let{x:e,y:n,width:o,height:i}=t;return{width:o,height:i,top:n,left:e,right:e+o,bottom:n+i,x:e,y:n}}function St(t,e,n){let{reference:o,floating:i}=t,r=W(e),s=ft(e),c=at(s),l=V(e),a=r==="y",d=o.x+o.width/2-i.width/2,u=o.y+o.height/2-i.height/2,m=o[c]/2-i[c]/2,f;switch(l){case"top":f={x:d,y:o.y-i.height};break;case"bottom":f={x:d,y:o.y+o.height};break;case"right":f={x:o.x+o.width,y:u};break;case"left":f={x:o.x-i.width,y:u};break;default:f={x:o.x,y:o.y}}switch(K(e)){case"start":f[s]-=m*(n&&a?-1:1);break;case"end":f[s]+=m*(n&&a?-1:1);break}return f}async function Ct(t,e){var n;e===void 0&&(e={});let{x:o,y:i,platform:r,rects:s,elements:c,strategy:l}=t,{boundary:a="clippingAncestors",rootBoundary:d="viewport",elementContext:u="floating",altBoundary:m=!1,padding:f=0}=U(e,t),g=wt(f),w=c[m?u==="floating"?"reference":"floating":u],h=X(await r.getClippingRect({element:(n=await(r.isElement==null?void 0:r.isElement(w)))==null||n?w:w.contextElement||await(r.getDocumentElement==null?void 0:r.getDocumentElement(c.floating)),boundary:a,rootBoundary:d,strategy:l})),x=u==="floating"?{x:o,y:i,width:s.floating.width,height:s.floating.height}:s.reference,y=await(r.getOffsetParent==null?void 0:r.getOffsetParent(c.floating)),v=await(r.isElement==null?void 0:r.isElement(y))?await(r.getScale==null?void 0:r.getScale(y))||{x:1,y:1}:{x:1,y:1},O=X(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:c,rect:x,offsetParent:y,strategy:l}):x);return{top:(h.top-O.top+g.top)/v.y,bottom:(O.bottom-h.bottom+g.bottom)/v.y,left:(h.left-O.left+g.left)/v.x,right:(O.right-h.right+g.right)/v.x}}var Pt=async(t,e,n)=>{let{placement:o="bottom",strategy:i="absolute",middleware:r=[],platform:s}=n,c=r.filter(Boolean),l=await(s.isRTL==null?void 0:s.isRTL(e)),a=await s.getElementRects({reference:t,floating:e,strategy:i}),{x:d,y:u}=St(a,o,l),m=o,f={},g=0;for(let w=0;w<c.length;w++){var p;let{name:h,fn:x}=c[w],{x:y,y:v,data:O,reset:b}=await x({x:d,y:u,initialPlacement:o,placement:m,strategy:i,middlewareData:f,rects:a,platform:{...s,detectOverflow:(p=s.detectOverflow)!=null?p:Ct},elements:{reference:t,floating:e}});d=y??d,u=v??u,f={...f,[h]:{...f[h],...O}},b&&g<=50&&(g++,typeof b=="object"&&(b.placement&&(m=b.placement),b.rects&&(a=b.rects===!0?await s.getElementRects({reference:t,floating:e,strategy:i}):b.rects),{x:d,y:u}=St(a,m,l)),w=-1)}return{x:d,y:u,placement:m,strategy:i,middlewareData:f}},Tt=t=>({name:"arrow",options:t,async fn(e){let{x:n,y:o,placement:i,rects:r,platform:s,elements:c,middlewareData:l}=e,{element:a,padding:d=0}=U(t,e)||{};if(a==null)return{};let u=wt(d),m={x:n,y:o},f=ft(i),g=at(f),p=await s.getDimensions(a),w=f==="y",h=w?"top":"left",x=w?"bottom":"right",y=w?"clientHeight":"clientWidth",v=r.reference[g]+r.reference[f]-m[f]-r.floating[g],O=m[f]-r.reference[f],b=await(s.getOffsetParent==null?void 0:s.getOffsetParent(a)),S=b?b[y]:0;(!S||!await(s.isElement==null?void 0:s.isElement(b)))&&(S=c.floating[y]||r.floating[g]);let Z=v/2-O/2,z=S/2-p[g]/2-1,F=$(u[h],z),tt=$(u[x],z),_=F,et=S-p[g]-tt,A=S/2-p[g]/2+Z,I=lt(_,A,et),k=!l.arrow&&K(i)!=null&&A!==I&&r.reference[g]/2-(A<_?F:tt)-p[g]/2<0,T=k?A<_?A-_:A-et:0;return{[f]:m[f]+T,data:{[f]:I,centerOffset:A-I-T,...k&&{alignmentOffset:T}},reset:k}}});var Lt=function(t){return t===void 0&&(t={}),{name:"flip",options:t,async fn(e){var n,o;let{placement:i,middlewareData:r,rects:s,initialPlacement:c,platform:l,elements:a}=e,{mainAxis:d=!0,crossAxis:u=!0,fallbackPlacements:m,fallbackStrategy:f="bestFit",fallbackAxisSideDirection:g="none",flipAlignment:p=!0,...w}=U(t,e);if((n=r.arrow)!=null&&n.alignmeLine truncated
|
||||
var H=Math.min,B=Math.max,ot=Math.round,it=Math.floor,L=t=>({x:t,y:t}),qt={left:"right",right:"left",bottom:"top",top:"bottom"},Ut={start:"end",end:"start"};function lt(t,e,n){return B(t,H(e,n))}function U(t,e){return typeof t=="function"?t(e):t}function V(t){return t.split("-")[0]}function K(t){return t.split("-")[1]}function pt(t){return t==="x"?"y":"x"}function at(t){return t==="y"?"height":"width"}var Kt=new Set(["top","bottom"]);function W(t){return Kt.has(V(t))?"y":"x"}function ft(t){return pt(W(t))}function Ot(t,e,n){n===void 0&&(n=!1);let o=K(t),i=ft(t),r=at(i),s=i==="x"?o===(n?"end":"start")?"right":"left":o==="start"?"bottom":"top";return e.reference[r]>e.floating[r]&&(s=nt(s)),[s,nt(s)]}function Rt(t){let e=nt(t);return[ct(t),e,ct(e)]}function ct(t){return t.replace(/start|end/g,e=>Ut[e])}var bt=["left","right"],At=["right","left"],Gt=["top","bottom"],Jt=["bottom","top"];function Qt(t,e,n){switch(t){case"top":case"bottom":return n?e?At:bt:e?bt:At;case"left":case"right":return e?Gt:Jt;default:return[]}}function St(t,e,n,o){let i=K(t),r=Qt(V(t),n==="start",o);return i&&(r=r.map(s=>s+"-"+i),e&&(r=r.concat(r.map(ct)))),r}function nt(t){return t.replace(/left|right|bottom|top/g,e=>qt[e])}function Zt(t){return{top:0,right:0,bottom:0,left:0,...t}}function wt(t){return typeof t!="number"?Zt(t):{top:t,right:t,bottom:t,left:t}}function X(t){let{x:e,y:n,width:o,height:i}=t;return{width:o,height:i,top:n,left:e,right:e+o,bottom:n+i,x:e,y:n}}function Ct(t,e,n){let{reference:o,floating:i}=t,r=W(e),s=ft(e),c=at(s),a=V(e),l=r==="y",d=o.x+o.width/2-i.width/2,u=o.y+o.height/2-i.height/2,g=o[c]/2-i[c]/2,f;switch(a){case"top":f={x:d,y:o.y-i.height};break;case"bottom":f={x:d,y:o.y+o.height};break;case"right":f={x:o.x+o.width,y:u};break;case"left":f={x:o.x-i.width,y:u};break;default:f={x:o.x,y:o.y}}switch(K(e)){case"start":f[s]-=g*(n&&l?-1:1);break;case"end":f[s]+=g*(n&&l?-1:1);break}return f}var Pt=async(t,e,n)=>{let{placement:o="bottom",strategy:i="absolute",middleware:r=[],platform:s}=n,c=r.filter(Boolean),a=await(s.isRTL==null?void 0:s.isRTL(e)),l=await s.getElementRects({reference:t,floating:e,strategy:i}),{x:d,y:u}=Ct(l,o,a),g=o,f={},m=0;for(let h=0;h<c.length;h++){let{name:w,fn:p}=c[h],{x,y,data:b,reset:v}=await p({x:d,y:u,initialPlacement:o,placement:g,strategy:i,middlewareData:f,rects:l,platform:s,elements:{reference:t,floating:e}});d=x??d,u=y??u,f={...f,[w]:{...f[w],...b}},v&&m<=50&&(m++,typeof v=="object"&&(v.placement&&(g=v.placement),v.rects&&(l=v.rects===!0?await s.getElementRects({reference:t,floating:e,strategy:i}):v.rects),{x:d,y:u}=Ct(l,g,a)),h=-1)}return{x:d,y:u,placement:g,strategy:i,middlewareData:f}};async function xt(t,e){var n;e===void 0&&(e={});let{x:o,y:i,platform:r,rects:s,elements:c,strategy:a}=t,{boundary:l="clippingAncestors",rootBoundary:d="viewport",elementContext:u="floating",altBoundary:g=!1,padding:f=0}=U(e,t),m=wt(f),w=c[g?u==="floating"?"reference":"floating":u],p=X(await r.getClippingRect({element:(n=await(r.isElement==null?void 0:r.isElement(w)))==null||n?w:w.contextElement||await(r.getDocumentElement==null?void 0:r.getDocumentElement(c.floating)),boundary:l,rootBoundary:d,strategy:a})),x=u==="floating"?{x:o,y:i,width:s.floating.width,height:s.floating.height}:s.reference,y=await(r.getOffsetParent==null?void 0:r.getOffsetParent(c.floating)),b=await(r.isElement==null?void 0:r.isElement(y))?await(r.getScale==null?void 0:r.getScale(y))||{x:1,y:1}:{x:1,y:1},v=X(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:c,rect:x,offsetParent:y,strategy:a}):x);return{top:(p.top-v.top+m.top)/b.y,bottom:(v.bottom-p.bottom+m.bottom)/b.y,left:(p.left-v.left+m.left)/b.x,right:(v.right-p.right+m.right)/b.x}}var Tt=t=>({name:"arrow",options:t,async fn(e){let{x:n,y:o,placement:i,rects:r,platform:s,elements:c,middlewareData:a}=e,{element:l,padding:d=0}=U(t,e)||{};if(l==null)return{};let u=wt(d),g={x:n,y:o},f=ft(i),m=at(f),h=await s.getDimensions(l),w=f==="y",p=w?"top":"left",x=w?"bottom":"right",y=w?"clientHeight":"clientWidth",b=r.reference[m]+r.reference[f]-g[f]-r.floating[m],v=g[f]-r.reference[f],M=await(s.getOffsetParent==null?void 0:s.getOffsetParent(l)),R=M?M[y]:0;(!R||!await(s.isElement==null?void 0:s.isElement(M)))&&(R=c.floating[y]||r.floating[m]);let Z=b/2-v/2,_=R/2-h[m]/2-1,F=H(u[p],_),tt=H(u[x],_),z=F,et=R-h[m]-tt,A=R/2-h[m]/2+Z,I=lt(z,A,et),k=!a.arrow&&K(i)!=null&&A!==I&&r.reference[m]/2-(A<z?F:tt)-h[m]/2<0,P=k?A<z?A-z:A-et:0;return{[f]:g[f]+P,data:{[f]:I,centerOffset:A-I-P,...k&&{alignmentOffset:P}},reset:k}}});var Lt=function(t){return t===void 0&&(t={}),{name:"flip",options:t,async fn(e){var n,o;let{placement:i,middlewareData:r,rects:s,initialPlacement:c,platform:a,elements:l}=e,{mainAxis:d=!0,crossAxis:u=!0,fallbackPlacements:g,fallbackStrategy:f="bestFit",fallbackAxisSideDirection:m="none",flipAlignment:h=!0,...w}=U(t,e);if((n=r.arrow)!=null&&n.alignmentOffset)return{};let p=V(i),x=W(c),y=V(c)===c,b=await(a.iLine truncated
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "."
|
||||
publish = "dist"
|
||||
Generated
+6
-116
@@ -11,27 +11,27 @@
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"esbuild": "^0.25.12",
|
||||
"jsdom": "^27.4.0",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@playwright/test": "^1.58.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.12",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"nostr-tools": "^2.19.4",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"playwright": "^1.58.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.41",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"serve": "^14.2.5",
|
||||
"stylelint": "^16.26.1",
|
||||
"tailwindcss": "^3.4.19"
|
||||
"stylelint": "^16.26.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@acemir/cssom": {
|
||||
@@ -44,7 +44,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -302,7 +301,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -319,7 +317,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -336,7 +333,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -353,7 +349,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -370,7 +365,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -387,7 +381,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -404,7 +397,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -421,7 +413,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -438,7 +429,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -455,7 +445,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -472,7 +461,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -489,7 +477,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -506,7 +493,6 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -523,7 +509,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -540,7 +525,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -557,7 +541,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -574,7 +557,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -591,7 +573,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -608,7 +589,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -625,7 +605,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -642,7 +621,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -659,7 +637,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -676,7 +653,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -693,7 +669,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -710,7 +685,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -727,7 +701,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -783,7 +756,6 @@
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -794,7 +766,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -804,14 +775,12 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -895,7 +864,6 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -909,7 +877,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -919,7 +886,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -1043,7 +1009,6 @@
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
@@ -1173,14 +1138,12 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -1215,7 +1178,6 @@
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -1249,7 +1211,6 @@
|
||||
"version": "10.4.23",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1293,7 +1254,6 @@
|
||||
"version": "2.9.18",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
||||
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -1312,7 +1272,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1372,7 +1331,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -1385,7 +1343,6 @@
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1466,7 +1423,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -1476,7 +1432,6 @@
|
||||
"version": "1.0.30001766",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1530,7 +1485,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
@@ -1613,7 +1567,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -1794,7 +1747,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
@@ -1877,7 +1829,6 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
@@ -1897,7 +1848,6 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
@@ -1911,7 +1861,6 @@
|
||||
"version": "1.5.279",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
|
||||
"integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -1957,7 +1906,6 @@
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1999,7 +1947,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -2057,7 +2004,6 @@
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -2084,7 +2030,6 @@
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -2104,7 +2049,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -2136,7 +2080,6 @@
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -2150,7 +2093,6 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -2165,7 +2107,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -2188,7 +2129,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -2303,7 +2243,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -2433,7 +2372,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
@@ -2446,7 +2384,6 @@
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
@@ -2478,7 +2415,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -2498,7 +2434,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -2511,7 +2446,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -2583,7 +2517,6 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -2705,7 +2638,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -2718,7 +2650,6 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.truncate": {
|
||||
@@ -2778,7 +2709,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -2788,7 +2718,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -2874,7 +2803,6 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
@@ -2886,7 +2814,6 @@
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2915,14 +2842,12 @@
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -2989,7 +2914,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -2999,7 +2923,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3096,7 +3019,6 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
@@ -3120,14 +3042,12 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -3140,7 +3060,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3150,7 +3069,6 @@
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3225,7 +3143,6 @@
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3254,7 +3171,6 @@
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
@@ -3272,7 +3188,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
||||
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3298,7 +3213,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3341,7 +3255,6 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3367,7 +3280,6 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -3415,7 +3327,6 @@
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -3429,7 +3340,6 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
@@ -3561,7 +3471,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3608,7 +3517,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
@@ -3618,7 +3526,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
@@ -3664,7 +3571,6 @@
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.1",
|
||||
@@ -3695,7 +3601,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@@ -3706,7 +3611,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4094,7 +3998,6 @@
|
||||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
@@ -4147,7 +4050,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4234,7 +4136,6 @@
|
||||
"version": "3.4.19",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -4272,7 +4173,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
@@ -4285,7 +4185,6 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -4299,7 +4198,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
@@ -4309,7 +4207,6 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
@@ -4322,7 +4219,6 @@
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4339,7 +4235,6 @@
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -4357,7 +4252,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4388,7 +4282,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -4425,7 +4318,6 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
@@ -4445,7 +4337,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4497,7 +4388,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vary": {
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "npm run build && npx serve",
|
||||
"start": "npm run build && npx serve dist",
|
||||
"serve": "npx serve",
|
||||
"build": "npm run build:css",
|
||||
"build": "node scripts/build-dist.mjs",
|
||||
"build:css": "tailwindcss -c tailwind.config.cjs -i css/tailwind.source.css -o css/tailwind.generated.css --minify",
|
||||
"build:beacon:bundle": "node scripts/build-beacon.mjs",
|
||||
"build:beacon": "npm run build:beacon:bundle && npm run lint:inline-styles",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const FILES_TO_COPY = [
|
||||
'index.html',
|
||||
'embed.html',
|
||||
'site.webmanifest',
|
||||
'sw.min.js',
|
||||
'_headers',
|
||||
'_redirects',
|
||||
'assets',
|
||||
'bitvid_logo',
|
||||
'components',
|
||||
'config',
|
||||
'content',
|
||||
'css',
|
||||
'docs',
|
||||
'js',
|
||||
'torrent',
|
||||
'vendor',
|
||||
'views'
|
||||
];
|
||||
|
||||
async function build() {
|
||||
const distDir = 'dist';
|
||||
|
||||
console.log(`[build] Cleaning ${distDir}...`);
|
||||
await fs.rm(distDir, { recursive: true, force: true });
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
|
||||
console.log('[build] Building CSS...');
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync('npm run build:css');
|
||||
if (stdout) console.log(stdout);
|
||||
if (stderr) console.error(stderr);
|
||||
} catch (error) {
|
||||
console.error('[build] CSS build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('[build] Copying files...');
|
||||
for (const item of FILES_TO_COPY) {
|
||||
const source = item;
|
||||
const destination = path.join(distDir, item);
|
||||
|
||||
try {
|
||||
// Check if source exists
|
||||
await fs.access(source);
|
||||
|
||||
console.log(` -> Copying ${source}`);
|
||||
await fs.cp(source, destination, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.warn(` [warn] Source file/directory not found: ${source}`);
|
||||
} else {
|
||||
console.error(` [error] Failed to copy ${source}:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[build] Build complete!');
|
||||
}
|
||||
|
||||
build();
|
||||
Reference in new issue
Block a user