mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 19:35:28 +00:00
Compare commits
26
Commits
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
/* /index.html 200
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
/* /index.html 200!
|
/* /index.html 200
|
||||||
Vendored
+16
-156
@@ -381,24 +381,6 @@
|
|||||||
data-profile-mobile-pane
|
data-profile-mobile-pane
|
||||||
aria-hidden="true"
|
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">
|
<div id="profileModalPanes" class="profile-modal__panes">
|
||||||
<section
|
<section
|
||||||
id="profilePaneAccount"
|
id="profilePaneAccount"
|
||||||
@@ -705,18 +687,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<ul id="relayList" class="grid-stack" data-orientation="vertical"></ul>
|
||||||
<div id="relayHealthPanel" class="card space-y-4 p-5">
|
<div id="relayHealthPanel" class="card space-y-4 p-5">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
@@ -734,18 +704,6 @@
|
|||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<label
|
||||||
for="relayHealthTelemetryOptIn"
|
for="relayHealthTelemetryOptIn"
|
||||||
class="inline-flex items-center gap-2 text-xs font-medium text-muted-strong"
|
class="inline-flex items-center gap-2 text-xs font-medium text-muted-strong"
|
||||||
@@ -962,22 +920,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card space-y-4 p-5">
|
<div class="card space-y-4 p-5">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="space-y-1">
|
||||||
<div class="space-y-1">
|
<h3 class="text-base font-semibold text-text">Interests</h3>
|
||||||
<h3 class="text-base font-semibold text-text">Interests</h3>
|
<p class="text-sm text-muted">
|
||||||
<p class="text-sm text-muted">
|
Videos tagged with these hashtags will be highlighted for you.
|
||||||
Videos tagged with these hashtags will be highlighted for you.
|
</p>
|
||||||
</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>
|
</div>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||||
<label
|
<label
|
||||||
@@ -1016,22 +963,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card space-y-4 p-5">
|
<div class="card space-y-4 p-5">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="space-y-1">
|
||||||
<div class="space-y-1">
|
<h3 class="text-base font-semibold text-text">Disinterests</h3>
|
||||||
<h3 class="text-base font-semibold text-text">Disinterests</h3>
|
<p class="text-sm text-muted">
|
||||||
<p class="text-sm text-muted">
|
We’ll de-emphasize or hide videos using these hashtags.
|
||||||
We’ll de-emphasize or hide videos using these hashtags.
|
</p>
|
||||||
</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>
|
</div>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||||
<label
|
<label
|
||||||
@@ -1079,24 +1015,6 @@
|
|||||||
<p class="text-sm text-muted">
|
<p class="text-sm text-muted">
|
||||||
These are the creators you’ve subscribed to directly in bitvid.
|
These are the creators you’ve subscribed to directly in bitvid.
|
||||||
</p>
|
</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
|
<div
|
||||||
id="subscriptionsEmpty"
|
id="subscriptionsEmpty"
|
||||||
class="card border border-dashed border-border/60 p-6 text-center text-sm text-muted"
|
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">
|
<p class="text-sm text-muted">
|
||||||
Add friends to spotlight creators you want to keep up with across bitvid.
|
Add friends to spotlight creators you want to keep up with across bitvid.
|
||||||
</p>
|
</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">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||||
<label class="flex-1 text-sm text-muted-strong" for="friendsInput">
|
<label class="flex-1 text-sm text-muted-strong" for="friendsInput">
|
||||||
<span class="mb-2 block font-medium">Creator npub</span>
|
<span class="mb-2 block font-medium">Creator npub</span>
|
||||||
@@ -1345,18 +1251,6 @@
|
|||||||
Creators you block are hidden from your bitvid experience. This
|
Creators you block are hidden from your bitvid experience. This
|
||||||
list is private to your account.
|
list is private to your account.
|
||||||
</p>
|
</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">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||||
<label class="flex-1 text-sm text-muted-strong" for="blockedInput">
|
<label class="flex-1 text-sm text-muted-strong" for="blockedInput">
|
||||||
<span class="mb-2 block font-medium">Creator npub</span>
|
<span class="mb-2 block font-medium">Creator npub</span>
|
||||||
@@ -1387,7 +1281,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
id="profilePaneMessages"
|
id="profilePaneMessages"
|
||||||
class="profile-pane hidden space-y-4"
|
class="profile-pane hidden"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
aria-labelledby="profileNavMessages"
|
aria-labelledby="profileNavMessages"
|
||||||
>
|
>
|
||||||
@@ -1426,11 +1320,10 @@
|
|||||||
<button
|
<button
|
||||||
id="profileHistoryRefresh"
|
id="profileHistoryRefresh"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-ghost btn-icon focus-ring"
|
class="btn-ghost focus-ring inline-flex items-center gap-2"
|
||||||
data-size="sm"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1526,18 +1419,7 @@
|
|||||||
|
|
||||||
<div id="adminModeratorsSection" class="space-y-4 hidden" aria-hidden="true">
|
<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 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>
|
||||||
<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>
|
|
||||||
<p class="text-xs uppercase tracking-wide text-muted">
|
<p class="text-xs uppercase tracking-wide text-muted">
|
||||||
Super Admin only
|
Super Admin only
|
||||||
</p>
|
</p>
|
||||||
@@ -1576,18 +1458,7 @@
|
|||||||
|
|
||||||
<div id="adminWhitelistSection" class="space-y-4">
|
<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 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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted">
|
<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.
|
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 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 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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted">
|
<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.
|
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.
|
* Whether the current deployment should enable developer-centric behavior.
|
||||||
*
|
*
|
||||||
* When `true`, bitvid surfaces extra logging, debug helpers, and experimental
|
* When `true`, bitvid surfaces extra logging, debug helpers, and experimental
|
||||||
* UI affordances intended for development environments. Leave the value
|
* UI affordances intended for development environments. Set the value to
|
||||||
* `false` for production so end users receive the stable experience; use the
|
* `false` before deploying to production so that end users receive the stable
|
||||||
* runtime overrides documented in `js/config.js` when you need to toggle dev
|
* experience. Downstream modules read this flag via `js/config.js`.
|
||||||
* mode without editing this file. 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.
|
* 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
|
* 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
|
* request failures) visible in the console. Flip it to `false` when you want
|
||||||
* a quieter dev mode without disabling other development helpers entirely.
|
* 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.
|
* Whether the current deployment should operate in lockdown mode.
|
||||||
|
|||||||
Vendored
+1
-2
@@ -45,8 +45,7 @@
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition:
|
transition: background-color var(--motion-duration-fast) ease,
|
||||||
background-color var(--motion-duration-fast) ease,
|
|
||||||
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
|
debug affordances. The value is re-exported as `isDevMode` in
|
||||||
`js/config.js` and controls `window.__BITVID_DEV_MODE__` as well as
|
`js/config.js` and controls `window.__BITVID_DEV_MODE__` as well as
|
||||||
logger routing.
|
logger routing.
|
||||||
- **Why change it:** Defaults to `false` for production builds. Set to `true`
|
- **Why change it:** Set to `false` for production builds so only the stable
|
||||||
for local development, or use the runtime override described below to
|
UX and user-facing logging remain.
|
||||||
toggle dev mode without editing this file.
|
|
||||||
|
|
||||||
- **`IS_VERBOSE_DEV_MODE`**
|
- **`IS_VERBOSE_DEV_MODE`**
|
||||||
- **What it does:** Enables high-volume diagnostics inside dev mode (for
|
- **What it does:** Enables high-volume diagnostics inside dev mode (for
|
||||||
example, repeated COUNT failures).
|
example, repeated COUNT failures).
|
||||||
- **Why change it:** Keep `true` while debugging noisy workflows and set to
|
- **Why change it:** Keep `true` while debugging noisy workflows and set to
|
||||||
`false` when you want a quieter dev console without disabling dev mode.
|
`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`**
|
- **`IS_LOCKDOWN_MODE`**
|
||||||
- **What it does:** Toggles a lockdown posture that limits access and switches
|
- **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
|
- Set `IS_DEV_MODE = true` while developing locally so the dev logger and other
|
||||||
diagnostics are available.
|
diagnostics are available.
|
||||||
- Keep `IS_DEV_MODE = false` for production builds. This value propagates to
|
- Flip `IS_DEV_MODE = false` before shipping a production build. This change
|
||||||
`isDevMode` (the module export), `window.__BITVID_DEV_MODE__`, and
|
propagates to `isDevMode` (the module export), `window.__BITVID_DEV_MODE__`,
|
||||||
`logger.dev`, silencing development-only messages.
|
and `logger.dev`, silencing development-only messages.
|
||||||
|
|
||||||
Always commit the flag change alongside deployment-ready configuration tweaks.
|
Always commit the flag change alongside deployment-ready configuration tweaks.
|
||||||
Forgetting to update the flag leaves verbose traces in user consoles and can
|
Forgetting to update the flag leaves verbose traces in user consoles and can
|
||||||
expose experimental behavior intended only for QA environments.
|
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
|
## Core Responsibilities
|
||||||
|
|
||||||
1. **Connection Management**: Maintains a persistent connection pool (`SimplePool`) to multiple relays.
|
1. **Connection Management**: Maintains a pool of WebSocket connections to relays via `nostr-tools/SimplePool`.
|
||||||
2. **State Management**: Tracks the global state of video events, including history, active versions, and deletions.
|
2. **Event Processing**: Subscribes to event streams, buffers incoming bursts, validates schemas, and converts raw events into domain objects.
|
||||||
3. **Event Ingestion**: Handles high-volume event streams with buffering and deduplication.
|
3. **State Management**:
|
||||||
4. **Caching**: Implements a persistent IndexedDB cache to support "Stale-While-Revalidate" loading (instant UI render).
|
* **`allEvents`**: A complete history of every video version fetched.
|
||||||
5. **Signer Orchestration**: Manages NIP-07 (Browser Extension), NIP-46 (Remote Signer), and Local (nsec) signers seamlessly.
|
* **`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>`):
|
* **`allEvents` (`Map<EventID, Video>`)**:
|
||||||
* Stores *every* valid video event fetched from relays, indexed by Event ID.
|
* Stores every valid video event received.
|
||||||
* Used for history preservation and resolving direct links to specific versions.
|
* 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>`):
|
## Caching Strategy
|
||||||
* 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.
|
|
||||||
|
|
||||||
* **`tombstones`** (`Map<string, timestamp>`):
|
The client uses a multi-layered approach to ensure fast boot times:
|
||||||
* 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.
|
|
||||||
|
|
||||||
## 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.
|
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.
|
||||||
2. **Buffering**: Incoming events are pushed to an `eventBuffer` array immediately.
|
|
||||||
3. **Debouncing**: A timer flushes the buffer every ~75ms (or when full).
|
### Flow
|
||||||
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.
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
sequenceDiagram
|
||||||
Relay[Relays] -->|Event Stream| Buffer[Event Buffer]
|
participant Relay
|
||||||
Buffer -->|Debounce Timer| Processor[Processor]
|
participant ClientBuffer
|
||||||
Processor -->|Check| Tombstones{Is Deleted?}
|
participant Processor
|
||||||
Tombstones -->|Yes| Discard[Discard]
|
participant UI
|
||||||
Tombstones -->|No| Compare{Is Newer?}
|
|
||||||
Compare -->|No| Discard
|
Relay->>ClientBuffer: Event 1
|
||||||
Compare -->|Yes| ActiveMap[Update ActiveMap]
|
Relay->>ClientBuffer: Event 2
|
||||||
ActiveMap --> UI[Update UI]
|
Relay->>ClientBuffer: ... Event 50
|
||||||
ActiveMap --> IDB[(IndexedDB Cache)]
|
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.
|
1. **`videoRootId` (V3)**: The canonical identifier for a video series.
|
||||||
* **Incremental Persistence**: Only changed events (where fingerprint differs) are written to IDB during a snapshot.
|
2. **`d` Tag (NIP-33)**: The addressable identifier.
|
||||||
* **Stale-While-Revalidate**: On startup (`init()`), the client immediately loads data from IDB to render the UI, then connects to relays to fetch updates.
|
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).
|
* **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).
|
||||||
2. **NIP-46 (Remote/Bunker)**: Uses a partial `NostrClient` instance to communicate with a remote signer via a relay.
|
* **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.
|
||||||
3. **Local (NIP-01)**: Uses a local private key (nsec) stored in `sessionStorage` (encrypted).
|
* **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
|
```bash
|
||||||
# Run the validation script
|
# Run the validation script
|
||||||
node scripts/agent/validate-events.mjs
|
node scripts/agent/validate-events.test.mjs
|
||||||
|
|
||||||
# Run the unit test suite
|
# Run the unit test suite
|
||||||
npm run test:unit
|
npm run test:unit
|
||||||
```
|
```
|
||||||
|
|
||||||
When modifying schemas or builders, use `scripts/agent/validate-events.mjs` to
|
When modifying schemas or builders, use `scripts/agent/validate-events.test.mjs` to
|
||||||
verify your changes locally. This tool also scans the codebase to ensure all
|
verify your changes locally.
|
||||||
active event builders are covered by tests.
|
|
||||||
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 { ALLOW_NSFW_CONTENT } from "./config.js";
|
||||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||||
import moderationService from "./services/moderationService.js";
|
import moderationService from "./services/moderationService.js";
|
||||||
import {
|
|
||||||
fetchProfileMetadata,
|
|
||||||
ensureProfileMetadataSubscription,
|
|
||||||
} from "./services/profileMetadataService.js";
|
|
||||||
import { createModerationStage } from "./feedEngine/stages.js";
|
import { createModerationStage } from "./feedEngine/stages.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
||||||
@@ -2799,23 +2795,13 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
|
|||||||
});
|
});
|
||||||
const videoEvent = getZapVideoEvent();
|
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 {
|
try {
|
||||||
const result = await splitAndZap(
|
const result = await splitAndZap(
|
||||||
{
|
{
|
||||||
videoEvent,
|
videoEvent,
|
||||||
amountSats: context.shares.total,
|
amountSats: context.shares.total,
|
||||||
walletSettings: settings
|
walletSettings: settings,
|
||||||
|
overrideFee
|
||||||
},
|
},
|
||||||
dependencies
|
dependencies
|
||||||
);
|
);
|
||||||
@@ -2836,21 +2822,6 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
|
|||||||
error
|
error
|
||||||
);
|
);
|
||||||
throw 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 aggregatedReceipts = [];
|
||||||
const aggregatedTracker = [];
|
const aggregatedTracker = [];
|
||||||
|
const nextRetryShares = [];
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
for (const share of shares) {
|
const results = await Promise.allSettled(
|
||||||
const overrideFee = share.type === "platform" ? 100 : 0;
|
shares.map((share) => {
|
||||||
try {
|
const overrideFee = share.type === "platform" ? 100 : 0;
|
||||||
const attempt = await runZapAttempt({
|
return runZapAttempt({
|
||||||
amount: share.amount,
|
amount: share.amount,
|
||||||
overrideFee,
|
overrideFee,
|
||||||
walletSettings
|
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) {
|
if (!attempt) {
|
||||||
setZapStatus("", "neutral");
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (attempt?.result?.receipts) {
|
if (attempt?.result?.receipts) {
|
||||||
aggregatedReceipts.push(...attempt.result.receipts);
|
aggregatedReceipts.push(...attempt.result.receipts);
|
||||||
} else if (Array.isArray(attempt?.shareTracker)) {
|
} else if (Array.isArray(attempt?.shareTracker)) {
|
||||||
aggregatedTracker.push(...attempt.shareTracker);
|
aggregatedTracker.push(...attempt.shareTracker);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
|
const error = result.reason;
|
||||||
|
lastError = error;
|
||||||
const tracker = Array.isArray(error?.__zapShareTracker)
|
const tracker = Array.isArray(error?.__zapShareTracker)
|
||||||
? error.__zapShareTracker
|
? error.__zapShareTracker
|
||||||
: [];
|
: [];
|
||||||
@@ -2916,20 +2899,28 @@ async function executePendingRetry({ walletSettings }) {
|
|||||||
const failureShares = tracker.filter(
|
const failureShares = tracker.filter(
|
||||||
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
||||||
);
|
);
|
||||||
markZapRetryPending(failureShares.length ? failureShares : [share]);
|
if (failureShares.length) {
|
||||||
const message = error?.message || "Retry failed.";
|
nextRetryShares.push(...failureShares);
|
||||||
setZapStatus(message, "error");
|
} else {
|
||||||
app?.showError?.(message);
|
nextRetryShares.push(share);
|
||||||
renderZapReceipts(
|
}
|
||||||
aggregatedTracker.length ? aggregatedTracker : tracker,
|
|
||||||
{
|
|
||||||
partial: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
renderZapReceipts(
|
||||||
aggregatedReceipts.length ? aggregatedReceipts : aggregatedTracker,
|
aggregatedReceipts.length ? aggregatedReceipts : aggregatedTracker,
|
||||||
{ partial: false }
|
{ partial: false }
|
||||||
@@ -5103,20 +5094,31 @@ async function fetchChannelProfileFromRelays(pubkey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const profileEntry = await fetchProfileMetadata(pubkey, {
|
const events = await pool.list(relayUrls, [
|
||||||
nostr: nostrClient,
|
{ kinds: [0], authors: [pubkey], limit: 1 }
|
||||||
relays: relayUrls,
|
]);
|
||||||
logger: devLogger,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!profileEntry) {
|
let newestEvent = null;
|
||||||
return { event: null, profile: {} };
|
for (const event of Array.isArray(events) ? events : []) {
|
||||||
|
if (!event || !event.content) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!newestEvent || event.created_at > newestEvent.created_at) {
|
||||||
|
newestEvent = event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (newestEvent?.content) {
|
||||||
event: profileEntry.event || null,
|
try {
|
||||||
profile: profileEntry.profile || {},
|
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) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -5206,23 +5208,6 @@ async function loadUserProfile(pubkey) {
|
|||||||
npub: currentChannelNpub,
|
npub: currentChannelNpub,
|
||||||
loadToken
|
loadToken
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureProfileMetadataSubscription({
|
|
||||||
pubkey,
|
|
||||||
nostr: nostrClient,
|
|
||||||
onProfile: ({ profile, event }) => {
|
|
||||||
if (loadToken !== currentProfileLoadToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyChannelProfileMetadata({
|
|
||||||
profile,
|
|
||||||
event,
|
|
||||||
pubkey,
|
|
||||||
npub: currentChannelNpub,
|
|
||||||
loadToken,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (loadToken === currentProfileLoadToken) {
|
if (loadToken === currentProfileLoadToken) {
|
||||||
userLogger.error("Failed to fetch user profile data:", error);
|
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";
|
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
|
||||||
|
|
||||||
const runtimeScope = typeof globalThis === "undefined" ? null : globalThis;
|
export const isDevMode = Boolean(IS_DEV_MODE);
|
||||||
|
|
||||||
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 { IS_DEV_MODE };
|
export { IS_DEV_MODE };
|
||||||
|
|
||||||
export const isVerboseDevMode =
|
export const isVerboseDevMode = Boolean(IS_VERBOSE_DEV_MODE);
|
||||||
verboseDevModeOverride === null
|
|
||||||
? Boolean(IS_VERBOSE_DEV_MODE)
|
|
||||||
: verboseDevModeOverride;
|
|
||||||
export { IS_VERBOSE_DEV_MODE };
|
export { IS_VERBOSE_DEV_MODE };
|
||||||
|
|
||||||
export const isLockdownMode = Boolean(IS_LOCKDOWN_MODE);
|
export const isLockdownMode = Boolean(IS_LOCKDOWN_MODE);
|
||||||
|
|||||||
Vendored
+7
-2
@@ -1,7 +1,6 @@
|
|||||||
import Application from "./app.js";
|
import Application from "./app.js";
|
||||||
import { nostrClient } from "./nostrClientFacade.js";
|
import { nostrClient } from "./nostrClientFacade.js";
|
||||||
import { convertEventToVideo, getDTagValueFromTags } from "./nostr/index.js";
|
import { convertEventToVideo, getDTagValueFromTags } from "./nostr/index.js";
|
||||||
import { sanitizeRelayList } from "./nostr/nip46Client.js";
|
|
||||||
import { resolveVideoPointer } from "./utils/videoPointer.js";
|
import { resolveVideoPointer } from "./utils/videoPointer.js";
|
||||||
import { devLogger, userLogger } from "./utils/logger.js";
|
import { devLogger, userLogger } from "./utils/logger.js";
|
||||||
import { nostrToolsReady } from "./nostrToolsBootstrap.js";
|
import { nostrToolsReady } from "./nostrToolsBootstrap.js";
|
||||||
@@ -181,7 +180,13 @@ const buildRelayList = (relays) => {
|
|||||||
...(Array.isArray(nostrClient.relays) ? nostrClient.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 });
|
diag?.emit("relay-list", { relays: result });
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+4
-5
@@ -261,11 +261,10 @@ export function createResolvePostedAtStage({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const task of tasks) {
|
const results = await Promise.allSettled(tasks.map((task) => task()));
|
||||||
try {
|
for (const result of results) {
|
||||||
await task();
|
if (result.status === "rejected") {
|
||||||
} catch (error) {
|
context?.log?.(`[${stageName}] resolution task failed`, result.reason);
|
||||||
context?.log?.(`[${stageName}] resolution task failed`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
-30
@@ -49,36 +49,14 @@ export function trackersFromMagnet(magnet) {
|
|||||||
return Array.from(deduped);
|
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) {
|
function parseMagnetLite(magnet) {
|
||||||
if (typeof magnet !== "string") {
|
if (typeof magnet !== "string") {
|
||||||
return { infoHash: "", announce: [], ws: [] };
|
return { infoHash: "", announce: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmed = magnet.trim();
|
const trimmed = magnet.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return { infoHash: "", announce: [], ws: [] };
|
return { infoHash: "", announce: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const lower = trimmed.toLowerCase();
|
const lower = trimmed.toLowerCase();
|
||||||
@@ -86,7 +64,6 @@ function parseMagnetLite(magnet) {
|
|||||||
const paramsPart = queryIndex >= 0 ? trimmed.slice(queryIndex + 1) : "";
|
const paramsPart = queryIndex >= 0 ? trimmed.slice(queryIndex + 1) : "";
|
||||||
const params = paramsPart.split("&");
|
const params = paramsPart.split("&");
|
||||||
const announce = [];
|
const announce = [];
|
||||||
const ws = [];
|
|
||||||
let infoHash = "";
|
let infoHash = "";
|
||||||
|
|
||||||
for (const param of params) {
|
for (const param of params) {
|
||||||
@@ -112,12 +89,8 @@ function parseMagnetLite(magnet) {
|
|||||||
if (value) {
|
if (value) {
|
||||||
announce.push(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;
|
return nip44;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createNip07Adapter(initialExtension, { preloadedPubkey } = {}) {
|
export async function createNip07Adapter(initialExtension) {
|
||||||
const getExtension = () => {
|
const getExtension = () => {
|
||||||
if (typeof window !== "undefined" && window.nostr) {
|
if (typeof window !== "undefined" && window.nostr) {
|
||||||
return window.nostr;
|
return window.nostr;
|
||||||
@@ -86,10 +86,15 @@ export async function createNip07Adapter(initialExtension, { preloadedPubkey } =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pubkey =
|
const pubkey =
|
||||||
preloadedPubkey ||
|
typeof bootstrapExtension.getPublicKey === "function"
|
||||||
(typeof bootstrapExtension.getPublicKey === "function"
|
|
||||||
? normalizeActorKey(await getPublicKey())
|
? 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
|
// Wrapper builders to ensure dynamic lookup at call time
|
||||||
const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => {
|
const createNip04Encrypt = () => (pubkey, plaintext, options = {}) => {
|
||||||
@@ -201,29 +206,30 @@ export async function createNip07Adapter(initialExtension, { preloadedPubkey } =
|
|||||||
requestPermissions,
|
requestPermissions,
|
||||||
destroy: async () => {},
|
destroy: async () => {},
|
||||||
canSign: () => typeof getExtension()?.signEvent === "function",
|
canSign: () => typeof getExtension()?.signEvent === "function",
|
||||||
// Always expose these methods; they perform their own existence checks at runtime.
|
capabilities: {
|
||||||
// This handles race conditions where NIP-04/44 APIs are injected lazily.
|
sign: typeof bootstrapExtension.signEvent === "function",
|
||||||
nip04Encrypt: createNip04Encrypt(),
|
nip44: hasNip44,
|
||||||
nip04Decrypt: createNip04Decrypt(),
|
nip04: hasNip04,
|
||||||
nip44Encrypt: createNip44Encrypt(),
|
},
|
||||||
nip44Decrypt: createNip44Decrypt(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define capabilities as dynamic properties to reflect current extension state
|
if (hasNip04) {
|
||||||
Object.defineProperty(signer, "capabilities", {
|
if (typeof bootstrapExtension.nip04.encrypt === "function") {
|
||||||
get: () => {
|
signer.nip04Encrypt = createNip04Encrypt();
|
||||||
const ext = getExtension();
|
}
|
||||||
const hasNip04 = !!(ext?.nip04 && typeof ext.nip04 === "object");
|
if (typeof bootstrapExtension.nip04.decrypt === "function") {
|
||||||
const hasNip44 = !!resolveNip44Module(ext);
|
signer.nip04Decrypt = createNip04Decrypt();
|
||||||
return {
|
}
|
||||||
sign: typeof ext?.signEvent === "function",
|
}
|
||||||
nip44: hasNip44,
|
|
||||||
nip04: hasNip04,
|
if (hasNip44) {
|
||||||
};
|
if (typeof bootstrapNip44.encrypt === "function") {
|
||||||
},
|
signer.nip44Encrypt = createNip44Encrypt();
|
||||||
enumerable: true,
|
}
|
||||||
configurable: true,
|
if (typeof bootstrapNip44.decrypt === "function") {
|
||||||
});
|
signer.nip44Decrypt = createNip44Decrypt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return signer;
|
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 { isSessionActor } from "./sessionActor.js";
|
||||||
import { queueSignEvent } from "./signRequestQueue.js";
|
import { queueSignEvent } from "./signRequestQueue.js";
|
||||||
import { getActiveSigner } from "../nostrClientRegistry.js";
|
import { getActiveSigner } from "../nostrClientRegistry.js";
|
||||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
|
||||||
|
|
||||||
const COMMENT_EVENT_SCHEMA = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
|
const COMMENT_EVENT_SCHEMA = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
|
||||||
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_COMMENT];
|
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_COMMENT];
|
||||||
@@ -33,15 +32,13 @@ function getAllowedCommentKinds() {
|
|||||||
return ALLOWED_COMMENT_KINDS.slice();
|
return ALLOWED_COMMENT_KINDS.slice();
|
||||||
}
|
}
|
||||||
function sanitizeRelayList(primary, fallback) {
|
function sanitizeRelayList(primary, fallback) {
|
||||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
if (Array.isArray(primary) && primary.length) {
|
||||||
if (primaryList.length) {
|
return primary;
|
||||||
return primaryList;
|
|
||||||
}
|
}
|
||||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
if (Array.isArray(fallback) && fallback.length) {
|
||||||
if (fallbackList.length) {
|
return fallback;
|
||||||
return fallbackList;
|
|
||||||
}
|
}
|
||||||
return sanitizeRelayUrls(RELAY_URLS);
|
return RELAY_URLS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRelay(candidate) {
|
function normalizeRelay(candidate) {
|
||||||
|
|||||||
Vendored
-2
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { NostrClient } from "./client.js";
|
import { NostrClient } from "./client.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_NIP07_CORE_METHODS,
|
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||||
} from "./nip07Permissions.js";
|
} from "./nip07Permissions.js";
|
||||||
@@ -27,7 +26,6 @@ export function requestDefaultExtensionPermissions(
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
NostrClient,
|
NostrClient,
|
||||||
DEFAULT_NIP07_CORE_METHODS,
|
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||||
DEFAULT_NIP07_PERMISSION_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 { queueSignEvent } from "./signRequestQueue.js";
|
||||||
import { getActiveSigner } from "../nostrClientRegistry.js";
|
import { getActiveSigner } from "../nostrClientRegistry.js";
|
||||||
import { isSessionActor } from "./sessionActor.js";
|
import { isSessionActor } from "./sessionActor.js";
|
||||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
|
||||||
|
|
||||||
const DEFAULT_TYPING_EXPIRY_SECONDS = 15;
|
const DEFAULT_TYPING_EXPIRY_SECONDS = 15;
|
||||||
|
|
||||||
function sanitizeRelayList(primary, fallback) {
|
function sanitizeRelayList(primary, fallback) {
|
||||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
if (Array.isArray(primary) && primary.length) {
|
||||||
if (primaryList.length) {
|
return primary;
|
||||||
return primaryList;
|
|
||||||
}
|
}
|
||||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
if (Array.isArray(fallback) && fallback.length) {
|
||||||
if (fallbackList.length) {
|
return fallback;
|
||||||
return fallbackList;
|
|
||||||
}
|
}
|
||||||
return sanitizeRelayUrls(RELAY_URLS);
|
return RELAY_URLS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeString(value) {
|
function normalizeString(value) {
|
||||||
|
|||||||
Vendored
+1
-5
@@ -21,16 +21,12 @@ export const DEFAULT_NIP07_ENCRYPTION_METHODS = Object.freeze([
|
|||||||
"nip44.v2.decrypt",
|
"nip44.v2.decrypt",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DEFAULT_NIP07_CORE_METHODS = Object.freeze([
|
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
||||||
// Core auth + relay metadata
|
// Core auth + relay metadata
|
||||||
"get_public_key",
|
"get_public_key",
|
||||||
"sign_event",
|
"sign_event",
|
||||||
"read_relays",
|
"read_relays",
|
||||||
"write_relays",
|
"write_relays",
|
||||||
]);
|
|
||||||
|
|
||||||
export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([
|
|
||||||
...DEFAULT_NIP07_CORE_METHODS,
|
|
||||||
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
...DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-37
@@ -43,28 +43,6 @@ export const NIP46_ENCRYPTION_ALGORITHMS = Object.freeze([
|
|||||||
"nip04",
|
"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.
|
* resolves the appropriate storage mechanism (localStorage or polyfill) for NIP-46 sessions.
|
||||||
* @returns {Storage|null} The storage interface or null if unavailable.
|
* @returns {Storage|null} The storage interface or null if unavailable.
|
||||||
@@ -94,7 +72,6 @@ export function sanitizeRelayList(list) {
|
|||||||
if (!Array.isArray(list)) {
|
if (!Array.isArray(list)) {
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
const enforceTls = isSecureRelayContext();
|
|
||||||
|
|
||||||
list.forEach((value) => {
|
list.forEach((value) => {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
@@ -113,29 +90,16 @@ export function sanitizeRelayList(list) {
|
|||||||
|
|
||||||
let normalized = trimmed.replace(/\/+$/, "");
|
let normalized = trimmed.replace(/\/+$/, "");
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(normalized);
|
const parsed = new URL(trimmed);
|
||||||
if (!parsed.hostname) {
|
if (!parsed.hostname) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resolvedProtocol = resolveRelayProtocol(parsed.protocol, {
|
|
||||||
enforceTls,
|
|
||||||
});
|
|
||||||
if (!resolvedProtocol) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (parsed.protocol !== resolvedProtocol) {
|
|
||||||
parsed.protocol = resolvedProtocol;
|
|
||||||
}
|
|
||||||
const pathname = parsed.pathname.replace(/\/+$/, "");
|
const pathname = parsed.pathname.replace(/\/+$/, "");
|
||||||
normalized = `${parsed.protocol}//${parsed.host}${pathname}${parsed.search || ""}`;
|
normalized = `${parsed.protocol}//${parsed.host}${pathname}${parsed.search || ""}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore URL parsing failures and fall back to the trimmed string.
|
// Ignore URL parsing failures and fall back to the trimmed string.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enforceTls && normalized.startsWith("ws://")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seen.has(normalized)) {
|
if (seen.has(normalized)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-12
@@ -6,10 +6,6 @@ import {
|
|||||||
resolveInfoJsonUrl,
|
resolveInfoJsonUrl,
|
||||||
} from "../utils/storagePointer.js";
|
} from "../utils/storagePointer.js";
|
||||||
import { getCachedNostrTools } from "./toolkit.js";
|
import { getCachedNostrTools } from "./toolkit.js";
|
||||||
import {
|
|
||||||
validateEventAgainstSchema,
|
|
||||||
NOTE_TYPES,
|
|
||||||
} from "../nostrEventSchemas.js";
|
|
||||||
|
|
||||||
function stringFromInput(value) {
|
function stringFromInput(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
@@ -938,20 +934,13 @@ export function buildNip71VideoEvent({
|
|||||||
tags.push(...pointerTags);
|
tags.push(...pointerTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = {
|
return {
|
||||||
kind: normalizeNip71Kind(metadata.kind),
|
kind: normalizeNip71Kind(metadata.kind),
|
||||||
pubkey: normalizedPubkey,
|
pubkey: normalizedPubkey,
|
||||||
created_at: timestamp,
|
created_at: timestamp,
|
||||||
tags,
|
tags,
|
||||||
content: summary,
|
content: summary,
|
||||||
};
|
};
|
||||||
|
|
||||||
validateEventAgainstSchema(
|
|
||||||
event.kind === 22 ? NOTE_TYPES.NIP71_SHORT_VIDEO : NOTE_TYPES.NIP71_VIDEO,
|
|
||||||
event,
|
|
||||||
);
|
|
||||||
|
|
||||||
return event;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectNip71PointerRequests(videos = []) {
|
export function collectNip71PointerRequests(videos = []) {
|
||||||
|
|||||||
Vendored
+5
-8
@@ -7,21 +7,18 @@ import { LRUCache } from "../utils/lruCache.js";
|
|||||||
import { CACHE_POLICIES } from "./cachePolicies.js";
|
import { CACHE_POLICIES } from "./cachePolicies.js";
|
||||||
import { NOTE_TYPES } from "../nostrEventSchemas.js";
|
import { NOTE_TYPES } from "../nostrEventSchemas.js";
|
||||||
import { queueSignEvent } from "./signRequestQueue.js";
|
import { queueSignEvent } from "./signRequestQueue.js";
|
||||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
|
||||||
|
|
||||||
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_REACTION];
|
const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_REACTION];
|
||||||
const reactionCache = new LRUCache({ maxSize: 100 });
|
const reactionCache = new LRUCache({ maxSize: 100 });
|
||||||
|
|
||||||
function sanitizeRelayList(primary, fallback) {
|
function sanitizeRelayList(primary, fallback) {
|
||||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
if (Array.isArray(primary) && primary.length) {
|
||||||
if (primaryList.length) {
|
return primary;
|
||||||
return primaryList;
|
|
||||||
}
|
}
|
||||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
if (Array.isArray(fallback) && fallback.length) {
|
||||||
if (fallbackList.length) {
|
return fallback;
|
||||||
return fallbackList;
|
|
||||||
}
|
}
|
||||||
return sanitizeRelayUrls(RELAY_URLS);
|
return RELAY_URLS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeString(value) {
|
function normalizeString(value) {
|
||||||
|
|||||||
Vendored
-11
@@ -487,21 +487,10 @@ export function isSessionActor(nostrClient) {
|
|||||||
return !!sa && sa.source !== "nsec";
|
return !!sa && sa.source !== "nsec";
|
||||||
}
|
}
|
||||||
|
|
||||||
function _closeSessionActorDb() {
|
|
||||||
if (sessionActorDbPromise) {
|
|
||||||
return sessionActorDbPromise.then((db) => {
|
|
||||||
db.close();
|
|
||||||
sessionActorDbPromise = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const __testExports = {
|
export const __testExports = {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
base64ToUint8Array,
|
base64ToUint8Array,
|
||||||
deriveSessionEncryptionKey,
|
deriveSessionEncryptionKey,
|
||||||
generateRandomBytes,
|
generateRandomBytes,
|
||||||
isSubtleCryptoAvailable,
|
isSubtleCryptoAvailable,
|
||||||
_closeSessionActorDb,
|
|
||||||
};
|
};
|
||||||
Vendored
+6
-18
@@ -14,7 +14,6 @@ import { normalizePointerInput } from "./watchHistory.js";
|
|||||||
import { devLogger, userLogger } from "../utils/logger.js";
|
import { devLogger, userLogger } from "../utils/logger.js";
|
||||||
import { logViewCountFailure } from "./countDiagnostics.js";
|
import { logViewCountFailure } from "./countDiagnostics.js";
|
||||||
import { queueSignEvent } from "./signRequestQueue.js";
|
import { queueSignEvent } from "./signRequestQueue.js";
|
||||||
import { sanitizeRelayList as sanitizeRelayUrls } from "./nip46Client.js";
|
|
||||||
|
|
||||||
const VIEW_EVENT_GUARD_PREFIX = "bitvid:viewed";
|
const VIEW_EVENT_GUARD_PREFIX = "bitvid:viewed";
|
||||||
|
|
||||||
@@ -341,15 +340,13 @@ function flattenListResults(input) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeRelayList(primary, fallback) {
|
function sanitizeRelayList(primary, fallback) {
|
||||||
const primaryList = sanitizeRelayUrls(Array.isArray(primary) ? primary : []);
|
if (Array.isArray(primary) && primary.length) {
|
||||||
if (primaryList.length) {
|
return primary;
|
||||||
return primaryList;
|
|
||||||
}
|
}
|
||||||
const fallbackList = sanitizeRelayUrls(Array.isArray(fallback) ? fallback : []);
|
if (Array.isArray(fallback) && fallback.length) {
|
||||||
if (fallbackList.length) {
|
return fallback;
|
||||||
return fallbackList;
|
|
||||||
}
|
}
|
||||||
return sanitizeRelayUrls(RELAY_URLS);
|
return RELAY_URLS;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listVideoViewEvents(client, pointer, options = {}) {
|
export async function listVideoViewEvents(client, pointer, options = {}) {
|
||||||
@@ -566,7 +563,6 @@ export async function countVideoViewEvents(client, pointer, options = {}) {
|
|||||||
perRelay: [],
|
perRelay: [],
|
||||||
best: null,
|
best: null,
|
||||||
fallback: true,
|
fallback: true,
|
||||||
partial: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,26 +588,19 @@ export async function countVideoViewEvents(client, pointer, options = {}) {
|
|||||||
throw normalizeAbortError();
|
throw normalizeAbortError();
|
||||||
}
|
}
|
||||||
|
|
||||||
let countAttemptPartial = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await client.countEventsAcrossRelays([pointerFilter], {
|
const result = await client.countEventsAcrossRelays([pointerFilter], {
|
||||||
relays: sanitizeRelayList(relayList, client.relays),
|
relays: sanitizeRelayList(relayList, client.relays),
|
||||||
timeoutMs: options?.timeoutMs,
|
timeoutMs: options?.timeoutMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
countAttemptPartial = Boolean(result?.partial);
|
|
||||||
|
|
||||||
if (result?.perRelay?.some((entry) => entry && entry.ok)) {
|
if (result?.perRelay?.some((entry) => entry && entry.ok)) {
|
||||||
return { ...result, fallback: false, partial: countAttemptPartial };
|
return { ...result, fallback: false };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code !== "count-unsupported") {
|
if (error?.code !== "count-unsupported") {
|
||||||
logViewCountFailure(error);
|
logViewCountFailure(error);
|
||||||
}
|
}
|
||||||
if (error?.code === "count-timeout" || error?.code === "timeout") {
|
|
||||||
countAttemptPartial = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
@@ -658,7 +647,6 @@ export async function countVideoViewEvents(client, pointer, options = {}) {
|
|||||||
perRelay: [],
|
perRelay: [],
|
||||||
best: null,
|
best: null,
|
||||||
fallback: true,
|
fallback: true,
|
||||||
partial: countAttemptPartial,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+12
-2
@@ -520,7 +520,13 @@ async function processShare({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function splitAndZap(
|
export async function splitAndZap(
|
||||||
{ videoEvent, amountSats, comment = "", walletSettings } = {},
|
{
|
||||||
|
videoEvent,
|
||||||
|
amountSats,
|
||||||
|
comment = "",
|
||||||
|
walletSettings,
|
||||||
|
overrideFee = null,
|
||||||
|
} = {},
|
||||||
dependencies = {}
|
dependencies = {}
|
||||||
) {
|
) {
|
||||||
if (!videoEvent || typeof videoEvent !== "object") {
|
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.");
|
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 platformShare = Math.floor((amount * platformFee) / 100);
|
||||||
const creatorShare = amount - platformShare;
|
const creatorShare = amount - platformShare;
|
||||||
|
|
||||||
|
|||||||
Vendored
-16
@@ -39,10 +39,6 @@ function normalizeRelayUrl(input) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enforceTls =
|
|
||||||
typeof window !== "undefined" &&
|
|
||||||
window?.location?.protocol === "https:";
|
|
||||||
|
|
||||||
let candidate = input.trim();
|
let candidate = input.trim();
|
||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
return null;
|
return null;
|
||||||
@@ -63,18 +59,6 @@ function normalizeRelayUrl(input) {
|
|||||||
return null;
|
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 hostname = parsed.hostname.toLowerCase();
|
||||||
const port = parsed.port ? `:${parsed.port}` : "";
|
const port = parsed.port ? `:${parsed.port}` : "";
|
||||||
let pathname = parsed.pathname || "";
|
let pathname = parsed.pathname || "";
|
||||||
|
|||||||
Vendored
+84
-145
@@ -26,7 +26,6 @@ import { profileCache } from "../state/profileCache.js";
|
|||||||
import getDefaultAuthProvider, {
|
import getDefaultAuthProvider, {
|
||||||
providers as defaultAuthProviders,
|
providers as defaultAuthProviders,
|
||||||
} from "./authProviders/index.js";
|
} from "./authProviders/index.js";
|
||||||
import { fetchProfileMetadata } from "./profileMetadataService.js";
|
|
||||||
|
|
||||||
class SimpleEventEmitter {
|
class SimpleEventEmitter {
|
||||||
constructor(logger = null) {
|
constructor(logger = null) {
|
||||||
@@ -623,12 +622,8 @@ export default class AuthService {
|
|||||||
postLoginError: null,
|
postLoginError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { detail: postLoginDetail, completionPromise } = this.applyPostLoginState({
|
const postLoginPromise = Promise.resolve()
|
||||||
deferBlocks: true,
|
.then(() => this.applyPostLoginState())
|
||||||
});
|
|
||||||
detail.postLogin = postLoginDetail;
|
|
||||||
|
|
||||||
const postLoginPromise = Promise.resolve(completionPromise)
|
|
||||||
.then((postLogin) => {
|
.then((postLogin) => {
|
||||||
detail.postLogin = postLogin;
|
detail.postLogin = postLogin;
|
||||||
detail.postLoginError = null;
|
detail.postLoginError = null;
|
||||||
@@ -682,151 +677,75 @@ export default class AuthService {
|
|||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPostLoginState({ deferBlocks = false } = {}) {
|
async applyPostLoginState() {
|
||||||
const activePubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
|
const activePubkey = this.normalizeHexPubkey(getPubkey()) || getPubkey();
|
||||||
const cachedEntry = activePubkey ? this.getProfileCacheEntry(activePubkey) : null;
|
|
||||||
const cachedProfile = cachedEntry?.profile || FALLBACK_PROFILE;
|
|
||||||
const detail = {
|
const detail = {
|
||||||
pubkey: activePubkey || null,
|
pubkey: activePubkey || null,
|
||||||
blocksLoaded: deferBlocks ? null : false,
|
blocksLoaded: false,
|
||||||
relaysLoaded: false,
|
relaysLoaded: false,
|
||||||
profile: cachedProfile,
|
profile: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activePubkey) {
|
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 schedule = (callback) => Promise.resolve().then(() => callback());
|
||||||
|
|
||||||
const completionPromise = (async () => {
|
const operations = [];
|
||||||
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 concurrentOps = [];
|
if (this.userBlocks && typeof this.userBlocks.loadBlocks === "function") {
|
||||||
|
operations.push({
|
||||||
if (!deferBlocks) {
|
name: "blocksLoaded",
|
||||||
const blocksOperation = this.createBlocksLoadOperation(activePubkey, schedule);
|
promise: schedule(() => this.userBlocks.loadBlocks(activePubkey)),
|
||||||
if (blocksOperation) {
|
onFulfilled: () => true,
|
||||||
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;
|
|
||||||
},
|
|
||||||
onRejected: (error) => {
|
onRejected: (error) => {
|
||||||
this.log("[AuthService] Failed to load own profile", error);
|
this.log("[AuthService] Failed to load block list", error);
|
||||||
const fallbackProfile = cachedProfile || FALLBACK_PROFILE;
|
return false;
|
||||||
this.setProfileCacheEntry(activePubkey, fallbackProfile, {
|
|
||||||
persist: true,
|
|
||||||
reason: "post-login-fallback",
|
|
||||||
});
|
|
||||||
return fallbackProfile;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
if (this.relayManager && typeof this.relayManager.loadRelayList === "function") {
|
||||||
name: "blocksLoaded",
|
operations.push({
|
||||||
promise: schedule(() => this.userBlocks.loadBlocks(activePubkey, options)),
|
name: "relaysLoaded",
|
||||||
onFulfilled: () => true,
|
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) => {
|
onRejected: (error) => {
|
||||||
this.log("[AuthService] Failed to load block list", error);
|
this.log("[AuthService] Failed to load own profile", error);
|
||||||
return false;
|
return null;
|
||||||
},
|
},
|
||||||
emit: (value, error) => {
|
});
|
||||||
this.emit("blocksLoaded", {
|
|
||||||
pubkey: activePubkey,
|
|
||||||
blocksLoaded: value,
|
|
||||||
error: error ?? null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadBlocksForPubkey(pubkey, options = {}) {
|
const settled = await Promise.allSettled(
|
||||||
const normalized = this.normalizeHexPubkey(pubkey) ||
|
operations.map((operation) => operation.promise)
|
||||||
(typeof pubkey === "string" ? pubkey.trim() : "");
|
);
|
||||||
if (!normalized) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const operation = this.createBlocksLoadOperation(normalized, undefined, options);
|
settled.forEach((result, index) => {
|
||||||
if (!operation) {
|
const operation = operations[index];
|
||||||
return false;
|
if (!operation) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
if (result.status === "fulfilled") {
|
||||||
const value = await operation.promise;
|
detail[operation.name] = operation.onFulfilled(result.value);
|
||||||
const resolved = operation.onFulfilled(value);
|
} else {
|
||||||
operation.emit?.(resolved, null);
|
detail[operation.name] = operation.onRejected(result.reason);
|
||||||
return resolved;
|
}
|
||||||
} catch (error) {
|
});
|
||||||
const resolved = operation.onRejected(error);
|
|
||||||
operation.emit?.(resolved, error);
|
return detail;
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
@@ -844,14 +763,6 @@ export default class AuthService {
|
|||||||
setCurrentUserNpub(null);
|
setCurrentUserNpub(null);
|
||||||
setActiveProfilePubkey(null, { persist: true });
|
setActiveProfilePubkey(null, { persist: true });
|
||||||
|
|
||||||
try {
|
|
||||||
if (profileCache && typeof profileCache.reset === "function") {
|
|
||||||
profileCache.reset();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.log("[AuthService] profileCache.reset threw", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resetModerationSettings({ persist: true });
|
resetModerationSettings({ persist: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1212,16 +1123,44 @@ export default class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fetchProfileMetadata(normalized, {
|
const results = await Promise.all(
|
||||||
nostr: this.nostrClient,
|
this.nostrClient.relays.map((relayUrl) =>
|
||||||
});
|
this.nostrClient.pool.list([relayUrl], [
|
||||||
|
{ kinds: [0], authors: [normalized], limit: 1 },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (result?.event && typeof this.nostrClient?.handleEvent === "function") {
|
const events = results.flat();
|
||||||
this.nostrClient.handleEvent(result.event);
|
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) {
|
if (newest) {
|
||||||
return result.profile;
|
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) {
|
} catch (error) {
|
||||||
this.log("[AuthService] Failed to fetch profile", 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 DEFAULT_MAX_DISCUSSION_COUNT_VIDEOS = 24;
|
||||||
const COUNT_UNSUPPORTED_TITLE = "Relay does not support NIP-45 COUNT queries.";
|
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) {
|
function toPositiveInteger(value, fallback) {
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
@@ -101,11 +99,10 @@ export default class DiscussionCountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const total = Number(result?.total);
|
const total = Number(result?.total);
|
||||||
const isPartial = Boolean(result?.partial);
|
|
||||||
const normalized =
|
const normalized =
|
||||||
Number.isFinite(total) && total >= 0 ? Math.floor(total) : 0;
|
Number.isFinite(total) && total >= 0 ? Math.floor(total) : 0;
|
||||||
this.videoDiscussionCountCache.set(video.id, normalized);
|
this.videoDiscussionCountCache.set(video.id, normalized);
|
||||||
this.updateDiscussionCountElement(container, normalized, { partial: isPartial });
|
this.updateDiscussionCountElement(container, normalized);
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -207,7 +204,7 @@ export default class DiscussionCountService {
|
|||||||
element.removeAttribute("title");
|
element.removeAttribute("title");
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDiscussionCountElement(element, count, { partial = false } = {}) {
|
updateDiscussionCountElement(element, count) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -220,13 +217,9 @@ export default class DiscussionCountService {
|
|||||||
const numeric = Number(count);
|
const numeric = Number(count);
|
||||||
const safeValue =
|
const safeValue =
|
||||||
Number.isFinite(numeric) && numeric >= 0 ? Math.floor(numeric) : 0;
|
Number.isFinite(numeric) && numeric >= 0 ? Math.floor(numeric) : 0;
|
||||||
element.dataset.countState = partial ? "partial" : "ready";
|
element.dataset.countState = "ready";
|
||||||
valueEl.textContent = safeValue.toLocaleString();
|
valueEl.textContent = safeValue.toLocaleString();
|
||||||
if (partial) {
|
element.removeAttribute("title");
|
||||||
element.title = COUNT_PARTIAL_TITLE;
|
|
||||||
} else {
|
|
||||||
element.removeAttribute("title");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
markDiscussionCountError(element, { unsupported = false } = {}) {
|
markDiscussionCountError(element, { unsupported = false } = {}) {
|
||||||
|
|||||||
Vendored
+5
-48
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { ensureNostrTools, resolveSimplePoolConstructor, shimLegacySimplePoolMethods } from "../nostr/toolkit.js";
|
||||||
DEFAULT_RELAY_URLS,
|
|
||||||
ensureNostrTools,
|
|
||||||
resolveSimplePoolConstructor,
|
|
||||||
shimLegacySimplePoolMethods,
|
|
||||||
} from "../nostr/toolkit.js";
|
|
||||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||||
import { publishEventToRelay } from "../nostrPublish.js";
|
import { publishEventToRelay } from "../nostrPublish.js";
|
||||||
import logger from "../utils/logger.js";
|
import logger from "../utils/logger.js";
|
||||||
@@ -15,7 +10,6 @@ const DEFAULT_BACKOFF = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SEEN_EVENT_IDS = 5000;
|
const MAX_SEEN_EVENT_IDS = 5000;
|
||||||
const DM_RELAY_LIST_TIMEOUT_MS = 5000;
|
|
||||||
export const DM_RELAY_WARNING_FALLBACK = "dm-relays-fallback";
|
export const DM_RELAY_WARNING_FALLBACK = "dm-relays-fallback";
|
||||||
|
|
||||||
function normalizeRelayList(relays) {
|
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({
|
export async function resolveDmRelaySelection({
|
||||||
pubkey,
|
pubkey,
|
||||||
relayHints = [],
|
relayHints = [],
|
||||||
@@ -103,14 +74,11 @@ export async function resolveDmRelaySelection({
|
|||||||
const discoveryList = sanitizeRelayList(
|
const discoveryList = sanitizeRelayList(
|
||||||
Array.isArray(discoveryRelays) ? discoveryRelays : [],
|
Array.isArray(discoveryRelays) ? discoveryRelays : [],
|
||||||
);
|
);
|
||||||
const candidateFallbackList = sanitizeRelayList(
|
const fallbackList = sanitizeRelayList(
|
||||||
Array.isArray(fallbackRelays) && fallbackRelays.length
|
Array.isArray(fallbackRelays) && fallbackRelays.length
|
||||||
? fallbackRelays
|
? fallbackRelays
|
||||||
: discoveryRelays,
|
: discoveryRelays,
|
||||||
);
|
);
|
||||||
const defaultFallbackList = sanitizeRelayList(Array.from(DEFAULT_RELAY_URLS));
|
|
||||||
const fallbackList =
|
|
||||||
candidateFallbackList.length > 0 ? candidateFallbackList : defaultFallbackList;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
normalizedPubkey &&
|
normalizedPubkey &&
|
||||||
@@ -119,13 +87,9 @@ export async function resolveDmRelaySelection({
|
|||||||
discoveryList.length
|
discoveryList.length
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const events = await withTimeout(
|
const events = await pool.list(discoveryList, [
|
||||||
pool.list(discoveryList, [
|
{ kinds: [10050], authors: [normalizedPubkey], limit: 1 },
|
||||||
{ kinds: [10050], authors: [normalizedPubkey], limit: 1 },
|
]);
|
||||||
]),
|
|
||||||
DM_RELAY_LIST_TIMEOUT_MS,
|
|
||||||
"dm-relay-discovery-timeout",
|
|
||||||
);
|
|
||||||
const sorted = Array.isArray(events)
|
const sorted = Array.isArray(events)
|
||||||
? events
|
? events
|
||||||
.filter((entry) => entry && entry.pubkey === normalizedPubkey)
|
.filter((entry) => entry && entry.pubkey === normalizedPubkey)
|
||||||
@@ -139,13 +103,6 @@ export async function resolveDmRelaySelection({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resolvedLogger.dev.warn("[dmNostrService] Failed to fetch DM relay hints.", 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;
|
return this.focusedConversationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.conversationCache.clear();
|
|
||||||
this.focusedConversationId = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
setFocusedConversation(conversationId, isFocused = true) {
|
setFocusedConversation(conversationId, isFocused = true) {
|
||||||
if (typeof conversationId !== "string") {
|
if (typeof conversationId !== "string") {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+9
-25
@@ -6,9 +6,6 @@ import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
|||||||
const DEFAULT_IDF_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
const DEFAULT_IDF_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||||
const DEFAULT_HISTORY_REFRESH_INTERVAL_MS = 60 * 1000;
|
const DEFAULT_HISTORY_REFRESH_INTERVAL_MS = 60 * 1000;
|
||||||
const DEFAULT_REFRESH_DEBOUNCE_MS = 200;
|
const DEFAULT_REFRESH_DEBOUNCE_MS = 200;
|
||||||
const YIELD_CHUNK_SIZE = 500;
|
|
||||||
|
|
||||||
const yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
|
|
||||||
function normalizeAddressKey(value) {
|
function normalizeAddressKey(value) {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
@@ -102,8 +99,7 @@ export async function buildWatchHistoryTagCounts({
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (const entry of items) {
|
||||||
const entry = items[i];
|
|
||||||
const pointer = normalizePointerInput(entry?.pointer || entry);
|
const pointer = normalizePointerInput(entry?.pointer || entry);
|
||||||
let video = entry?.video || entry?.metadata?.video || null;
|
let video = entry?.video || entry?.metadata?.video || null;
|
||||||
|
|
||||||
@@ -127,16 +123,12 @@ export async function buildWatchHistoryTagCounts({
|
|||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
counts.set(tag, (counts.get(tag) || 0) + 1);
|
counts.set(tag, (counts.get(tag) || 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i > 0 && i % YIELD_CHUNK_SIZE === 0) {
|
|
||||||
await yieldToMain();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildTagIdf({ videos } = {}) {
|
export function buildTagIdf({ videos } = {}) {
|
||||||
const idf = new Map();
|
const idf = new Map();
|
||||||
const list = Array.isArray(videos) ? videos : [];
|
const list = Array.isArray(videos) ? videos : [];
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
@@ -146,31 +138,23 @@ export async function buildTagIdf({ videos } = {}) {
|
|||||||
const docFrequency = new Map();
|
const docFrequency = new Map();
|
||||||
const totalDocs = list.length;
|
const totalDocs = list.length;
|
||||||
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (const video of list) {
|
||||||
const video = list[i];
|
|
||||||
const tags = collectVideoTags(video);
|
const tags = collectVideoTags(video);
|
||||||
if (tags.size) {
|
if (!tags.size) {
|
||||||
for (const tag of tags) {
|
continue;
|
||||||
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i > 0 && i % YIELD_CHUNK_SIZE === 0) {
|
for (const tag of tags) {
|
||||||
await yieldToMain();
|
docFrequency.set(tag, (docFrequency.get(tag) || 0) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let processedCount = 0;
|
|
||||||
for (const [tag, df] of docFrequency.entries()) {
|
for (const [tag, df] of docFrequency.entries()) {
|
||||||
const ratio = (totalDocs + 1) / (df + 1);
|
const ratio = (totalDocs + 1) / (df + 1);
|
||||||
const value = 1 + Math.log(ratio);
|
const value = 1 + Math.log(ratio);
|
||||||
if (Number.isFinite(value) && value > 0) {
|
if (Number.isFinite(value) && value > 0) {
|
||||||
idf.set(tag, value);
|
idf.set(tag, value);
|
||||||
}
|
}
|
||||||
processedCount++;
|
|
||||||
if (processedCount % YIELD_CHUNK_SIZE === 0) {
|
|
||||||
await yieldToMain();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return idf;
|
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();
|
const now = Date.now();
|
||||||
if (!force && this.tagIdfUpdatedAt) {
|
if (!force && this.tagIdfUpdatedAt) {
|
||||||
const elapsed = now - this.tagIdfUpdatedAt;
|
const elapsed = now - this.tagIdfUpdatedAt;
|
||||||
@@ -342,7 +326,7 @@ export default class ExploreDataService {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.tagIdf = await buildTagIdf({ videos: sourceVideos });
|
this.tagIdf = buildTagIdf({ videos: sourceVideos });
|
||||||
this.tagIdfUpdatedAt = now;
|
this.tagIdfUpdatedAt = now;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.logger?.warn) {
|
if (this.logger?.warn) {
|
||||||
|
|||||||
+22
-229
@@ -15,17 +15,11 @@ import {
|
|||||||
assertAnyRelayAccepted,
|
assertAnyRelayAccepted,
|
||||||
} from "../nostrPublish.js";
|
} from "../nostrPublish.js";
|
||||||
import { userLogger, devLogger } from "../utils/logger.js";
|
import { userLogger, devLogger } from "../utils/logger.js";
|
||||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
|
||||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||||
import { profileCache } from "../state/profileCache.js";
|
import { profileCache } from "../state/profileCache.js";
|
||||||
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
||||||
import { relayManager } from "../relayManager.js";
|
import { relayManager } from "../relayManager.js";
|
||||||
import {
|
import { runNip07WithRetry, NIP07_PRIORITY } from "../nostr/nip07Permissions.js";
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
runNip07WithRetry,
|
|
||||||
NIP07_PRIORITY,
|
|
||||||
} from "../nostr/nip07Permissions.js";
|
|
||||||
import { relaySubscriptionService } from "./relaySubscriptionService.js";
|
|
||||||
|
|
||||||
const LOG_PREFIX = "[HashtagPreferences]";
|
const LOG_PREFIX = "[HashtagPreferences]";
|
||||||
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
const HASHTAG_IDENTIFIER = "bitvid:tag-preferences";
|
||||||
@@ -99,6 +93,14 @@ function normalizeHexPubkey(pubkey) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeRelayList(candidate) {
|
||||||
|
return Array.isArray(candidate)
|
||||||
|
? candidate
|
||||||
|
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeEncryptionToken(value) {
|
function normalizeEncryptionToken(value) {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return "";
|
return "";
|
||||||
@@ -236,24 +238,19 @@ class HashtagPreferencesService {
|
|||||||
this.eventId = null;
|
this.eventId = null;
|
||||||
this.eventCreatedAt = null;
|
this.eventCreatedAt = null;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.preferencesVersion = DEFAULT_VERSION;
|
this.preferencesVersion = DEFAULT_VERSION;
|
||||||
this.decryptRetryTimeoutId = null;
|
this.decryptRetryTimeoutId = null;
|
||||||
this.loadPromise = null;
|
|
||||||
this.loadingPubkey = null;
|
|
||||||
this.subscriptionKey = null;
|
|
||||||
this.lastLoadError = null;
|
|
||||||
|
|
||||||
profileCache.subscribe((event, detail) => {
|
profileCache.subscribe((event, detail) => {
|
||||||
if (event === "profileChanged") {
|
if (event === "profileChanged") {
|
||||||
this.reset();
|
this.reset();
|
||||||
if (detail.pubkey) {
|
if (detail.pubkey) {
|
||||||
this.load(detail.pubkey, { allowPermissionPrompt: false });
|
this.load(detail.pubkey);
|
||||||
}
|
}
|
||||||
} else if (event === "runtimeCleared" && detail.pubkey === this.activePubkey) {
|
} else if (event === "runtimeCleared" && detail.pubkey === this.activePubkey) {
|
||||||
this.reset();
|
this.reset();
|
||||||
if (this.activePubkey) {
|
if (this.activePubkey) {
|
||||||
this.load(this.activePubkey, { allowPermissionPrompt: false });
|
this.load(this.activePubkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -264,74 +261,19 @@ class HashtagPreferencesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.stopHashtagPreferencesSubscription();
|
|
||||||
this.interests = new Set();
|
this.interests = new Set();
|
||||||
this.disinterests = new Set();
|
this.disinterests = new Set();
|
||||||
this.eventId = null;
|
this.eventId = null;
|
||||||
this.eventCreatedAt = null;
|
this.eventCreatedAt = null;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.preferencesVersion = DEFAULT_VERSION;
|
this.preferencesVersion = DEFAULT_VERSION;
|
||||||
this.lastLoadError = null;
|
|
||||||
if (this.decryptRetryTimeoutId) {
|
if (this.decryptRetryTimeoutId) {
|
||||||
clearTimeout(this.decryptRetryTimeoutId);
|
clearTimeout(this.decryptRetryTimeoutId);
|
||||||
this.decryptRetryTimeoutId = null;
|
this.decryptRetryTimeoutId = null;
|
||||||
}
|
}
|
||||||
this.loadPromise = null;
|
|
||||||
this.loadingPubkey = null;
|
|
||||||
this.emitChange("reset");
|
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() {
|
saveToCache() {
|
||||||
const interests = this.getInterests();
|
const interests = this.getInterests();
|
||||||
const disinterests = this.getDisinterests();
|
const disinterests = this.getDisinterests();
|
||||||
@@ -483,7 +425,7 @@ class HashtagPreferencesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleDecryptRetry(pubkey, error, options = {}) {
|
scheduleDecryptRetry(pubkey, error) {
|
||||||
const normalized = normalizeHexPubkey(pubkey);
|
const normalized = normalizeHexPubkey(pubkey);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return;
|
return;
|
||||||
@@ -496,7 +438,7 @@ class HashtagPreferencesService {
|
|||||||
if (this.activePubkey && this.activePubkey !== normalized) {
|
if (this.activePubkey && this.activePubkey !== normalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.load(normalized, options).catch((retryError) => {
|
this.load(normalized).catch((retryError) => {
|
||||||
userLogger.warn(`${LOG_PREFIX} Decryption retry failed`, retryError);
|
userLogger.warn(`${LOG_PREFIX} Decryption retry failed`, retryError);
|
||||||
});
|
});
|
||||||
}, DECRYPT_RETRY_DELAY_MS);
|
}, DECRYPT_RETRY_DELAY_MS);
|
||||||
@@ -506,51 +448,14 @@ class HashtagPreferencesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(pubkey, options = {}) {
|
async load(pubkey) {
|
||||||
const normalized = normalizeHexPubkey(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 =
|
let wasLoadedForUser =
|
||||||
this.activePubkey &&
|
this.activePubkey &&
|
||||||
this.activePubkey === normalized &&
|
this.activePubkey === normalized &&
|
||||||
(this.interests.size > 0 || this.disinterests.size > 0 || this.loaded);
|
(this.interests.size > 0 || this.disinterests.size > 0 || this.loaded);
|
||||||
|
|
||||||
this.activePubkey = normalized;
|
this.activePubkey = normalized;
|
||||||
if (allowPermissionPrompt) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -561,12 +466,7 @@ class HashtagPreferencesService {
|
|||||||
// Try cache first
|
// Try cache first
|
||||||
if (this.loadFromCache(normalized)) {
|
if (this.loadFromCache(normalized)) {
|
||||||
wasLoadedForUser = true;
|
wasLoadedForUser = true;
|
||||||
if (!allowPermissionPrompt && !this.backgroundLoading) {
|
|
||||||
this.backgroundLoading = true;
|
|
||||||
this.emitChange("background-loading", { background: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const wasBackgroundLoading = this.backgroundLoading;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!nostrClient ||
|
!nostrClient ||
|
||||||
@@ -581,18 +481,10 @@ class HashtagPreferencesService {
|
|||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
`${LOG_PREFIX} Keeping existing preferences despite client unavailability.`,
|
`${LOG_PREFIX} Keeping existing preferences despite client unavailability.`,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.reset();
|
this.reset();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,31 +495,12 @@ class HashtagPreferencesService {
|
|||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
`${LOG_PREFIX} Keeping existing preferences; no relays available for refresh.`,
|
`${LOG_PREFIX} Keeping existing preferences; no relays available for refresh.`,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.reset();
|
this.reset();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
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 schema = getNostrEventSchema(NOTE_TYPES.HASHTAG_PREFERENCES);
|
||||||
const canonicalKind = schema?.kind ?? 30015;
|
const canonicalKind = schema?.kind ?? 30015;
|
||||||
@@ -680,10 +553,6 @@ class HashtagPreferencesService {
|
|||||||
`${LOG_PREFIX} Failed to load hashtag preferences (keeping local state if any).`,
|
`${LOG_PREFIX} Failed to load hashtag preferences (keeping local state if any).`,
|
||||||
fetchError,
|
fetchError,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,19 +563,11 @@ class HashtagPreferencesService {
|
|||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
`${LOG_PREFIX} Keeping existing preferences despite empty relay response.`,
|
`${LOG_PREFIX} Keeping existing preferences despite empty relay response.`,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,18 +600,10 @@ class HashtagPreferencesService {
|
|||||||
|
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
if (wasLoadedForUser) {
|
if (wasLoadedForUser) {
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.reset();
|
this.reset();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,18 +616,12 @@ class HashtagPreferencesService {
|
|||||||
devLogger.log(
|
devLogger.log(
|
||||||
`${LOG_PREFIX} Ignoring stale/concurrent preferences event (remote: ${latestCreatedAt}, local: ${currentCreatedAt}).`,
|
`${LOG_PREFIX} Ignoring stale/concurrent preferences event (remote: ${latestCreatedAt}, local: ${currentCreatedAt}).`,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let decryptResult;
|
let decryptResult;
|
||||||
try {
|
try {
|
||||||
const decryptPromise = this.decryptEvent(latest, normalized, {
|
const decryptPromise = this.decryptEvent(latest, normalized);
|
||||||
allowPermissionPrompt,
|
|
||||||
});
|
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
@@ -793,7 +640,6 @@ class HashtagPreferencesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!decryptResult.ok) {
|
if (!decryptResult.ok) {
|
||||||
this.lastLoadError = decryptResult.error || null;
|
|
||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
`${LOG_PREFIX} Failed to decrypt hashtag preferences`,
|
`${LOG_PREFIX} Failed to decrypt hashtag preferences`,
|
||||||
decryptResult.error,
|
decryptResult.error,
|
||||||
@@ -803,15 +649,7 @@ class HashtagPreferencesService {
|
|||||||
if (!wasLoadedForUser && !this.loaded) {
|
if (!wasLoadedForUser && !this.loaded) {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
this.scheduleDecryptRetry(normalized, decryptResult.error, {
|
this.scheduleDecryptRetry(normalized, decryptResult.error);
|
||||||
allowPermissionPrompt,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!allowPermissionPrompt &&
|
|
||||||
decryptResult.error?.code === "hashtag-preferences-permission-required"
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,19 +659,11 @@ class HashtagPreferencesService {
|
|||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
`${LOG_PREFIX} Preserving cached preferences despite decryption failure.`,
|
`${LOG_PREFIX} Preserving cached preferences despite decryption failure.`,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitChange("background-loaded", { background: false });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,9 +681,6 @@ class HashtagPreferencesService {
|
|||||||
? latest.created_at
|
? latest.created_at
|
||||||
: null;
|
: null;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (this.backgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
}
|
|
||||||
this.saveToCache();
|
this.saveToCache();
|
||||||
this.emitChange("sync", { scheme: decryptResult.scheme });
|
this.emitChange("sync", { scheme: decryptResult.scheme });
|
||||||
} catch (error) {
|
} 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 : "";
|
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
||||||
if (!ciphertext) {
|
if (!ciphertext) {
|
||||||
const error = new Error("Preference event missing ciphertext content.");
|
const error = new Error("Preference event missing ciphertext content.");
|
||||||
@@ -874,13 +701,8 @@ class HashtagPreferencesService {
|
|||||||
return { ok: false, error };
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
|
||||||
let signer = getActiveSigner();
|
let signer = getActiveSigner();
|
||||||
if (
|
if (!signer && typeof nostrClient?.ensureActiveSignerForPubkey === "function") {
|
||||||
!signer &&
|
|
||||||
typeof nostrClient?.ensureActiveSignerForPubkey === "function" &&
|
|
||||||
allowPermissionPrompt
|
|
||||||
) {
|
|
||||||
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
|
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
|
||||||
}
|
}
|
||||||
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
||||||
@@ -894,36 +716,13 @@ class HashtagPreferencesService {
|
|||||||
(!signerHasNip44 && requiresNip44) ||
|
(!signerHasNip44 && requiresNip44) ||
|
||||||
(!signerHasNip04 && requiresNip04)
|
(!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 {
|
try {
|
||||||
const permissionResult = await requestDefaultExtensionPermissions(
|
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 };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
`${LOG_PREFIX} Extension permissions request failed while loading preferences`,
|
`${LOG_PREFIX} Extension permissions request failed while loading preferences`,
|
||||||
error,
|
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
|
const nostrApi = typeof window !== "undefined" ? window?.nostr : null;
|
||||||
? typeof window !== "undefined"
|
|
||||||
? window?.nostr
|
|
||||||
: null
|
|
||||||
: null;
|
|
||||||
if (nostrApi) {
|
if (nostrApi) {
|
||||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||||
registerDecryptor(
|
registerDecryptor(
|
||||||
@@ -1125,12 +920,10 @@ class HashtagPreferencesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (signer.type === "extension") {
|
if (signer.type === "extension") {
|
||||||
const permissionResult = await requestDefaultExtensionPermissions(
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
);
|
|
||||||
if (!permissionResult?.ok) {
|
if (!permissionResult?.ok) {
|
||||||
const error = new Error(
|
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.code = "hashtag-preferences-extension-denied";
|
||||||
error.cause = permissionResult?.error || null;
|
error.cause = permissionResult?.error || null;
|
||||||
|
|||||||
Vendored
+52
-168
@@ -176,30 +176,6 @@ function sanitizeSnapshotTimestamp(value) {
|
|||||||
return floored;
|
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;
|
const SNAPSHOT_PREVIEW_LIMIT = 160;
|
||||||
|
|
||||||
function sanitizeSnapshotPreview(value) {
|
function sanitizeSnapshotPreview(value) {
|
||||||
@@ -264,7 +240,11 @@ function resolvePreviewCacheKey(message) {
|
|||||||
: typeof message?.remotePubkey === "string"
|
: typeof message?.remotePubkey === "string"
|
||||||
? message.remotePubkey.trim()
|
? 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) {
|
if (remote && timestamp) {
|
||||||
return `snapshot:${remote}:${timestamp}`;
|
return `snapshot:${remote}:${timestamp}`;
|
||||||
@@ -421,7 +401,11 @@ function normalizeDirectMessageRecord(message, actorPubkey = "") {
|
|||||||
receiverPubkey = senderPubkey === actor ? remote : actor;
|
receiverPubkey = senderPubkey === actor ? remote : actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdAt = resolveDirectMessageCreatedAt(message);
|
const createdAt = sanitizeSnapshotTimestamp(
|
||||||
|
message?.message?.created_at ??
|
||||||
|
message?.event?.created_at ??
|
||||||
|
message?.timestamp,
|
||||||
|
);
|
||||||
|
|
||||||
const kind =
|
const kind =
|
||||||
Number.isFinite(message?.message?.kind)
|
Number.isFinite(message?.message?.kind)
|
||||||
@@ -475,7 +459,7 @@ function buildSnapshotFromMessages(messages, actorPubkey = "", resolvePreview =
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = resolveDirectMessageSortValue(message);
|
const timestamp = sanitizeSnapshotTimestamp(message.timestamp);
|
||||||
const preview =
|
const preview =
|
||||||
typeof resolvePreview === "function"
|
typeof resolvePreview === "function"
|
||||||
? resolvePreview(message)
|
? 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 = "") {
|
function hydrateMessagesFromSnapshot(snapshot, actorPubkey = "") {
|
||||||
const normalizedActor = normalizeHexPubkey(actorPubkey);
|
const normalizedActor = normalizeHexPubkey(actorPubkey);
|
||||||
const hydrated = [];
|
const hydrated = [];
|
||||||
@@ -629,7 +559,6 @@ export class NostrService {
|
|||||||
this.dmMessageIndex = new Map();
|
this.dmMessageIndex = new Map();
|
||||||
this.dmSubscription = null;
|
this.dmSubscription = null;
|
||||||
this.dmActorPubkey = null;
|
this.dmActorPubkey = null;
|
||||||
this.dmSubscriptionRelays = null;
|
|
||||||
this.dmHydratedFromSnapshot = false;
|
this.dmHydratedFromSnapshot = false;
|
||||||
this.dmNotificationManager = new DmNotificationManager({
|
this.dmNotificationManager = new DmNotificationManager({
|
||||||
logger: userLogger,
|
logger: userLogger,
|
||||||
@@ -770,13 +699,6 @@ export class NostrService {
|
|||||||
if (this.dmPreviewCache) {
|
if (this.dmPreviewCache) {
|
||||||
this.dmPreviewCache.clear();
|
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(
|
const activeActor = normalizeHexPubkey(
|
||||||
typeof actorPubkey === "string" && actorPubkey
|
typeof actorPubkey === "string" && actorPubkey
|
||||||
? actorPubkey
|
? actorPubkey
|
||||||
@@ -871,10 +793,17 @@ export class NostrService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = normalizeDirectMessagePayload(message);
|
const normalized = {
|
||||||
if (!normalized) {
|
...message,
|
||||||
return;
|
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);
|
this.dmMessageIndex.set(eventId, normalized);
|
||||||
|
|
||||||
@@ -899,9 +828,7 @@ export class NostrService {
|
|||||||
this.dmMessages.push(normalized);
|
this.dmMessages.push(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dmMessages.sort(
|
this.dmMessages.sort((a, b) => (b?.timestamp || 0) - (a?.timestamp || 0));
|
||||||
(a, b) => resolveDirectMessageSortValue(b) - resolveDirectMessageSortValue(a),
|
|
||||||
);
|
|
||||||
if (actor) {
|
if (actor) {
|
||||||
this.dmActorPubkey = actor;
|
this.dmActorPubkey = actor;
|
||||||
}
|
}
|
||||||
@@ -964,14 +891,8 @@ export class NostrService {
|
|||||||
return this.getDirectMessages();
|
return this.getDirectMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
this.dmMessages = hydrated;
|
||||||
this.dmMessages = [];
|
this.dmMessageIndex = new Map();
|
||||||
this.dmMessageIndex = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
const merged = mergeDirectMessageCache(this.dmMessages, hydrated);
|
|
||||||
this.dmMessageIndex = merged.index;
|
|
||||||
this.dmMessages = merged.messages;
|
|
||||||
this.dmHydratedFromSnapshot = hydrated.length > 0;
|
this.dmHydratedFromSnapshot = hydrated.length > 0;
|
||||||
|
|
||||||
const snapshot = this.getDirectMessages();
|
const snapshot = this.getDirectMessages();
|
||||||
@@ -1006,8 +927,6 @@ export class NostrService {
|
|||||||
: Number.isFinite(decryptLimitCandidate) && decryptLimitCandidate > 0
|
: Number.isFinite(decryptLimitCandidate) && decryptLimitCandidate > 0
|
||||||
? Math.floor(decryptLimitCandidate)
|
? Math.floor(decryptLimitCandidate)
|
||||||
: null;
|
: null;
|
||||||
const sinceCandidate = Number(options.since);
|
|
||||||
const untilCandidate = Number(options.until);
|
|
||||||
|
|
||||||
if (resolvedDecryptLimit && resolvedDecryptLimit > resolvedLimit) {
|
if (resolvedDecryptLimit && resolvedDecryptLimit > resolvedLimit) {
|
||||||
resolvedDecryptLimit = resolvedLimit;
|
resolvedDecryptLimit = resolvedLimit;
|
||||||
@@ -1032,27 +951,6 @@ export class NostrService {
|
|||||||
|
|
||||||
this.dmActorPubkey = normalizedActor;
|
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 = [];
|
let messages = [];
|
||||||
try {
|
try {
|
||||||
messages = await this.nostrClient.listDirectMessages(normalizedActor, {
|
messages = await this.nostrClient.listDirectMessages(normalizedActor, {
|
||||||
@@ -1060,8 +958,6 @@ export class NostrService {
|
|||||||
...options,
|
...options,
|
||||||
limit: resolvedLimit,
|
limit: resolvedLimit,
|
||||||
decryptLimit: resolvedDecryptLimit,
|
decryptLimit: resolvedDecryptLimit,
|
||||||
...(resolvedSince !== null ? { since: resolvedSince } : {}),
|
|
||||||
...(resolvedUntil !== null ? { until: resolvedUntil } : {}),
|
|
||||||
onMessage: (message) => {
|
onMessage: (message) => {
|
||||||
this.applyDirectMessage(message, { reason: "load-incremental" });
|
this.applyDirectMessage(message, { reason: "load-incremental" });
|
||||||
},
|
},
|
||||||
@@ -1072,19 +968,37 @@ export class NostrService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const collected = [];
|
const collected = [];
|
||||||
|
const index = new Map();
|
||||||
|
|
||||||
for (const message of Array.isArray(messages) ? messages : []) {
|
for (const message of Array.isArray(messages) ? messages : []) {
|
||||||
const normalized = normalizeDirectMessagePayload(message);
|
if (!message || message.ok !== true) {
|
||||||
if (!normalized) {
|
|
||||||
continue;
|
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);
|
collected.push(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
const merged = mergeDirectMessageCache(this.dmMessages, collected);
|
collected.sort((a, b) => (b?.timestamp || 0) - (a?.timestamp || 0));
|
||||||
this.dmMessageIndex = merged.index;
|
this.dmMessageIndex = index;
|
||||||
this.dmMessages = merged.messages;
|
this.dmMessages = collected;
|
||||||
|
|
||||||
const snapshot = this.getDirectMessages();
|
const snapshot = this.getDirectMessages();
|
||||||
|
|
||||||
@@ -1118,38 +1032,16 @@ export class NostrService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ensureDirectMessageSubscription({ actorPubkey, relays, ...handlers } = {}) {
|
async ensureDirectMessageSubscription({ actorPubkey, relays, ...handlers } = {}) {
|
||||||
|
if (this.dmSubscription) {
|
||||||
|
return this.dmSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
const activeActor = actorPubkey || this.resolveActiveDmActor();
|
const activeActor = actorPubkey || this.resolveActiveDmActor();
|
||||||
const normalizedActor = normalizeHexPubkey(activeActor);
|
const normalizedActor = normalizeHexPubkey(activeActor);
|
||||||
if (!normalizedActor) {
|
if (!normalizedActor) {
|
||||||
return null;
|
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 resolvedRelays = Array.isArray(relays) ? relays : null;
|
||||||
let relaySelection = null;
|
let relaySelection = null;
|
||||||
|
|
||||||
@@ -1250,13 +1142,6 @@ export class NostrService {
|
|||||||
|
|
||||||
this.dmSubscription = subscription;
|
this.dmSubscription = subscription;
|
||||||
this.dmActorPubkey = normalizedActor;
|
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 });
|
this.emit("directMessages:subscribed", { subscription });
|
||||||
if (relaySelection?.warning === DM_RELAY_WARNING_FALLBACK) {
|
if (relaySelection?.warning === DM_RELAY_WARNING_FALLBACK) {
|
||||||
this.emit("directMessages:relayWarning", {
|
this.emit("directMessages:relayWarning", {
|
||||||
@@ -1288,7 +1173,6 @@ export class NostrService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.dmSubscription = null;
|
this.dmSubscription = null;
|
||||||
this.dmSubscriptionRelays = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async persistDirectMessageSnapshot(actorPubkey = null, messages = 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 HOSTED_URL_SUCCESS_MESSAGE = "✅ Streaming from hosted URL";
|
||||||
const DEFAULT_UNSUPPORTED_BTITH_MESSAGE =
|
const DEFAULT_UNSUPPORTED_BTITH_MESSAGE =
|
||||||
"This magnet link is missing a compatible BitTorrent v1 info hash.";
|
"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 {
|
export class PlaybackService {
|
||||||
constructor({
|
constructor({
|
||||||
@@ -242,9 +88,6 @@ export class PlaybackService {
|
|||||||
this.analyticsCallbacks = analyticsCallbacks || {};
|
this.analyticsCallbacks = analyticsCallbacks || {};
|
||||||
this.playbackStartTimeout = playbackStartTimeout;
|
this.playbackStartTimeout = playbackStartTimeout;
|
||||||
this.currentSession = null;
|
this.currentSession = null;
|
||||||
this.probeCache = new Map();
|
|
||||||
this.probeInFlight = new Map();
|
|
||||||
this.probeCacheTtlMs = PROBE_CACHE_TTL_MS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args) {
|
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) {
|
prepareVideoElement(videoElement) {
|
||||||
if (!videoElement) {
|
if (!videoElement) {
|
||||||
return;
|
return;
|
||||||
@@ -829,19 +602,10 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
throw new Error("No torrent playback handler provided.");
|
throw new Error("No torrent playback handler provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let torrentInstance;
|
const torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
||||||
try {
|
fallbackMagnet: this.fallbackMagnet,
|
||||||
torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
urlList: webSeedCandidates,
|
||||||
fallbackMagnet: this.fallbackMagnet,
|
});
|
||||||
urlList: webSeedCandidates,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = getTorrentErrorMessage(err);
|
|
||||||
if (errorMessage) {
|
|
||||||
this.emit("status", { message: errorMessage });
|
|
||||||
}
|
|
||||||
throw new Error(errorMessage, { cause: err });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.service.handleAnalyticsEvent("sourcechange", { source: "torrent" });
|
this.service.handleAnalyticsEvent("sourcechange", { source: "torrent" });
|
||||||
this.emit("sourcechange", { source: "torrent" });
|
this.emit("sourcechange", { source: "torrent" });
|
||||||
@@ -916,11 +680,8 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const probeResult = await this.service.probeHostedUrl({
|
const probeResult =
|
||||||
url: httpsUrl,
|
typeof probeUrl === "function" ? await probeUrl(httpsUrl) : null;
|
||||||
magnet: this.trimmedMagnet,
|
|
||||||
probeUrl,
|
|
||||||
});
|
|
||||||
const probeOutcome = probeResult?.outcome || "error";
|
const probeOutcome = probeResult?.outcome || "error";
|
||||||
const probeStatus = probeResult?.status || "unknown";
|
const probeStatus = probeResult?.status || "unknown";
|
||||||
const shouldAttemptHosted =
|
const shouldAttemptHosted =
|
||||||
@@ -965,11 +726,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
attachWatchdogs({ stallMs: 0 });
|
attachWatchdogs({ stallMs: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const errorMessage = getHostedVideoErrorMessage(activeVideoEl);
|
outcomeResolver({ status: "fallback", reason });
|
||||||
if (errorMessage) {
|
|
||||||
this.emit("status", { message: errorMessage });
|
|
||||||
}
|
|
||||||
outcomeResolver({ status: "fallback", reason, errorMessage });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1035,25 +792,12 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
|
|
||||||
// Fallback triggered
|
// Fallback triggered
|
||||||
const fallbackReason = playbackOutcome?.reason || "watchdog-triggered";
|
const fallbackReason = playbackOutcome?.reason || "watchdog-triggered";
|
||||||
return {
|
return { status: "fallback", reason: fallbackReason };
|
||||||
status: "fallback",
|
|
||||||
reason: fallbackReason,
|
|
||||||
errorMessage: playbackOutcome?.errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const failureDetails = getHostedUrlFailureDetails(probeResult);
|
|
||||||
if (failureDetails?.message) {
|
|
||||||
this.emit("status", { message: failureDetails.message });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupHostedUrlStatusListeners();
|
cleanupHostedUrlStatusListeners();
|
||||||
cleanupDebugListeners();
|
cleanupDebugListeners();
|
||||||
return {
|
return { status: "fallback", reason: "probe-failed" };
|
||||||
status: "fallback",
|
|
||||||
reason: "probe-failed",
|
|
||||||
errorMessage: failureDetails?.message,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Timeout Wrapper ---
|
// --- Timeout Wrapper ---
|
||||||
@@ -1085,8 +829,6 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
|
|
||||||
const effectiveTimeout = forcedSource ? 0 : playbackStartTimeout;
|
const effectiveTimeout = forcedSource ? 0 : playbackStartTimeout;
|
||||||
|
|
||||||
let hostedErrorMessage = "";
|
|
||||||
|
|
||||||
if (tryUrlFirst) {
|
if (tryUrlFirst) {
|
||||||
if (httpsUrl) {
|
if (httpsUrl) {
|
||||||
// Wrap URL attempt in timeout
|
// Wrap URL attempt in timeout
|
||||||
@@ -1100,10 +842,6 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
return urlResult;
|
return urlResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof urlResult?.errorMessage === "string") {
|
|
||||||
hostedErrorMessage = urlResult.errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If timeout or failure, proceed to fallback
|
// If timeout or failure, proceed to fallback
|
||||||
if (urlResult?.reason === "timeout") {
|
if (urlResult?.reason === "timeout") {
|
||||||
// Need to ensure URL probing/playback is effectively cancelled
|
// Need to ensure URL probing/playback is effectively cancelled
|
||||||
@@ -1167,18 +905,13 @@ class PlaybackSession extends SimpleEventEmitter {
|
|||||||
if (urlResult && urlResult.source === "url") {
|
if (urlResult && urlResult.source === "url") {
|
||||||
return urlResult;
|
return urlResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof urlResult?.errorMessage === "string") {
|
|
||||||
hostedErrorMessage = urlResult.errorMessage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
hostedErrorMessage ||
|
this.magnetProvided && !this.magnetForPlayback
|
||||||
(this.magnetProvided && !this.magnetForPlayback
|
|
||||||
? unsupportedBtihMessage
|
? unsupportedBtihMessage
|
||||||
: "No playable source found.");
|
: "No playable source found.";
|
||||||
this.emit("status", { message });
|
this.emit("status", { message });
|
||||||
this.service.handleAnalyticsEvent("sourcechange", { source: null });
|
this.service.handleAnalyticsEvent("sourcechange", { source: null });
|
||||||
this.emit("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";
|
import { devLogger, userLogger } from "../utils/logger.js";
|
||||||
|
|
||||||
const TELEMETRY_STORAGE_KEY = "bitvid:relay-health-telemetry-opt-in";
|
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) {
|
function resolveLogger(logger) {
|
||||||
if (logger && logger.dev && logger.user) {
|
if (logger && logger.dev && logger.user) {
|
||||||
@@ -92,10 +90,8 @@ class RelayHealthService {
|
|||||||
connected: false,
|
connected: false,
|
||||||
lastLatencyMs: null,
|
lastLatencyMs: null,
|
||||||
errorCount: 0,
|
errorCount: 0,
|
||||||
consecutiveFailures: 0,
|
|
||||||
lastCheckedAt: null,
|
lastCheckedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
lastUserLogAt: null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,47 +129,22 @@ class RelayHealthService {
|
|||||||
const state = this.ensureRelayState(relayUrl);
|
const state = this.ensureRelayState(relayUrl);
|
||||||
state.connected = true;
|
state.connected = true;
|
||||||
state.lastCheckedAt = Date.now();
|
state.lastCheckedAt = Date.now();
|
||||||
state.consecutiveFailures = 0;
|
|
||||||
if (Number.isFinite(latencyMs)) {
|
if (Number.isFinite(latencyMs)) {
|
||||||
state.lastLatencyMs = Math.max(0, Math.round(latencyMs));
|
state.lastLatencyMs = Math.max(0, Math.round(latencyMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.nostrClient && typeof this.nostrClient.clearRelayBackoff === "function") {
|
|
||||||
this.nostrClient.clearRelayBackoff(relayUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recordRelayFailure(relayUrl, error) {
|
recordRelayFailure(relayUrl, error) {
|
||||||
const state = this.ensureRelayState(relayUrl);
|
const state = this.ensureRelayState(relayUrl);
|
||||||
const now = Date.now();
|
|
||||||
state.connected = false;
|
state.connected = false;
|
||||||
state.errorCount += 1;
|
state.errorCount += 1;
|
||||||
state.consecutiveFailures += 1;
|
state.lastCheckedAt = Date.now();
|
||||||
state.lastCheckedAt = now;
|
state.lastErrorAt = Date.now();
|
||||||
state.lastErrorAt = now;
|
|
||||||
|
|
||||||
if (this.nostrClient && typeof this.nostrClient.markRelayUnreachable === "function") {
|
|
||||||
this.nostrClient.markRelayUnreachable(relayUrl, 60000, {
|
|
||||||
reason: "relay-health-failed",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.dev.warn("[relayHealth] Relay check failed.", {
|
this.logger.dev.warn("[relayHealth] Relay check failed.", {
|
||||||
relayUrl,
|
relayUrl,
|
||||||
error,
|
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) {
|
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;
|
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 = {
|
const normalized = {
|
||||||
name:
|
name:
|
||||||
sanitizeProfileString(profile.name || profile.display_name) ||
|
sanitizeProfileString(profile.name || profile.display_name) ||
|
||||||
existingName ||
|
|
||||||
"Unknown",
|
"Unknown",
|
||||||
picture:
|
picture:
|
||||||
sanitizeProfileMediaUrl(profile.picture || profile.image) ||
|
sanitizeProfileMediaUrl(profile.picture || profile.image) ||
|
||||||
existingPicture ||
|
|
||||||
"assets/svg/default-profile.svg",
|
"assets/svg/default-profile.svg",
|
||||||
};
|
};
|
||||||
|
|
||||||
const about =
|
const about = sanitizeProfileString(profile.about || profile.aboutMe);
|
||||||
sanitizeProfileString(profile.about || profile.aboutMe) || existingAbout;
|
|
||||||
if (about) {
|
if (about) {
|
||||||
normalized.about = about;
|
normalized.about = about;
|
||||||
}
|
}
|
||||||
|
|
||||||
const website =
|
const website = sanitizeProfileString(profile.website || profile.url);
|
||||||
sanitizeProfileString(profile.website || profile.url) || existingWebsite;
|
|
||||||
if (website) {
|
if (website) {
|
||||||
normalized.website = website;
|
normalized.website = website;
|
||||||
}
|
}
|
||||||
@@ -209,19 +191,18 @@ class ProfileCache {
|
|||||||
profile.background ||
|
profile.background ||
|
||||||
profile.cover ||
|
profile.cover ||
|
||||||
profile.cover_image ||
|
profile.cover_image ||
|
||||||
profile.coverImage,
|
profile.coverImage
|
||||||
);
|
);
|
||||||
const resolvedBanner = banner || existingBanner;
|
if (banner) {
|
||||||
if (resolvedBanner) {
|
normalized.banner = banner;
|
||||||
normalized.banner = resolvedBanner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lud16 = sanitizeProfileString(profile.lud16) || existingLud16;
|
const lud16 = sanitizeProfileString(profile.lud16);
|
||||||
if (lud16) {
|
if (lud16) {
|
||||||
normalized.lud16 = lud16;
|
normalized.lud16 = lud16;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lud06 = sanitizeProfileString(profile.lud06) || existingLud06;
|
const lud06 = sanitizeProfileString(profile.lud06);
|
||||||
if (lud06) {
|
if (lud06) {
|
||||||
normalized.lud06 = lud06;
|
normalized.lud06 = lud06;
|
||||||
}
|
}
|
||||||
@@ -231,11 +212,8 @@ class ProfileCache {
|
|||||||
lud16,
|
lud16,
|
||||||
lud06,
|
lud06,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const resolvedLightningAddress = lightningCandidates.length
|
if (lightningCandidates.length) {
|
||||||
? lightningCandidates[0]
|
normalized.lightningAddress = lightningCandidates[0];
|
||||||
: existingLightningAddress;
|
|
||||||
if (resolvedLightningAddress) {
|
|
||||||
normalized.lightningAddress = resolvedLightningAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = {
|
const entry = {
|
||||||
@@ -368,14 +346,6 @@ class ProfileCache {
|
|||||||
this.emit("runtimeCleared", { pubkey: normalized });
|
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) {
|
subscribe(callback) {
|
||||||
this.listeners.add(callback);
|
this.listeners.add(callback);
|
||||||
return () => this.listeners.delete(callback);
|
return () => this.listeners.delete(callback);
|
||||||
|
|||||||
Vendored
+32
-177
@@ -5,7 +5,7 @@ import {
|
|||||||
requestDefaultExtensionPermissions,
|
requestDefaultExtensionPermissions,
|
||||||
} from "./nostrClientFacade.js";
|
} from "./nostrClientFacade.js";
|
||||||
import { convertEventToVideo as sharedConvertEventToVideo } from "./nostr/index.js";
|
import { convertEventToVideo as sharedConvertEventToVideo } from "./nostr/index.js";
|
||||||
import { normalizeNostrPubkey, sanitizeRelayList } from "./nostr/nip46Client.js";
|
import { normalizeNostrPubkey } from "./nostr/nip46Client.js";
|
||||||
import {
|
import {
|
||||||
listVideoViewEvents,
|
listVideoViewEvents,
|
||||||
subscribeVideoViewEvents,
|
subscribeVideoViewEvents,
|
||||||
@@ -31,16 +31,11 @@ import { devLogger, userLogger } from "./utils/logger.js";
|
|||||||
import moderationService from "./services/moderationService.js";
|
import moderationService from "./services/moderationService.js";
|
||||||
import nostrService from "./services/nostrService.js";
|
import nostrService from "./services/nostrService.js";
|
||||||
import { profileCache } from "./state/profileCache.js";
|
import { profileCache } from "./state/profileCache.js";
|
||||||
import {
|
import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js";
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
runNip07WithRetry,
|
|
||||||
NIP07_PRIORITY,
|
|
||||||
} from "./nostr/nip07Permissions.js";
|
|
||||||
import { relaySubscriptionService } from "./services/relaySubscriptionService.js";
|
|
||||||
|
|
||||||
const SUBSCRIPTION_SET_KIND =
|
const SUBSCRIPTION_SET_KIND =
|
||||||
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
|
getNostrEventSchema(NOTE_TYPES.SUBSCRIPTION_LIST)?.kind ?? 30000;
|
||||||
const DECRYPT_TIMEOUT_MS = 15000;
|
const DECRYPT_TIMEOUT_MS = 90000;
|
||||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||||
|
|
||||||
function normalizeHexPubkey(value) {
|
function normalizeHexPubkey(value) {
|
||||||
@@ -334,12 +329,10 @@ class SubscriptionsManager {
|
|||||||
this.currentUserPubkey = null;
|
this.currentUserPubkey = null;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.loadingPromise = null;
|
this.loadingPromise = null;
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.subscriptionListView = null;
|
this.subscriptionListView = null;
|
||||||
this.lastRunOptions = null;
|
this.lastRunOptions = null;
|
||||||
this.lastResult = null;
|
this.lastResult = null;
|
||||||
this.lastContainerId = null;
|
this.lastContainerId = null;
|
||||||
this.lastLoadError = null;
|
|
||||||
this.unsubscribeFromNostrUpdates = null;
|
this.unsubscribeFromNostrUpdates = null;
|
||||||
this.pendingRefreshPromise = null;
|
this.pendingRefreshPromise = null;
|
||||||
this.scheduledRefreshDetail = null;
|
this.scheduledRefreshDetail = null;
|
||||||
@@ -348,7 +341,6 @@ class SubscriptionsManager {
|
|||||||
this.emitter = new TinyEventEmitter();
|
this.emitter = new TinyEventEmitter();
|
||||||
this.decryptRetryTimeoutId = null;
|
this.decryptRetryTimeoutId = null;
|
||||||
this.ensureNostrServiceListener();
|
this.ensureNostrServiceListener();
|
||||||
this.subscriptionKey = null;
|
|
||||||
|
|
||||||
profileCache.subscribe((event, detail) => {
|
profileCache.subscribe((event, detail) => {
|
||||||
if (event === "profileChanged") {
|
if (event === "profileChanged") {
|
||||||
@@ -358,9 +350,7 @@ class SubscriptionsManager {
|
|||||||
} else if (event === "runtimeCleared" && detail.pubkey === this.currentUserPubkey) {
|
} else if (event === "runtimeCleared" && detail.pubkey === this.currentUserPubkey) {
|
||||||
this.reset();
|
this.reset();
|
||||||
if (this.currentUserPubkey) {
|
if (this.currentUserPubkey) {
|
||||||
this.loadSubscriptions(this.currentUserPubkey, {
|
this.loadSubscriptions(this.currentUserPubkey);
|
||||||
allowPermissionPrompt: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -369,13 +359,12 @@ class SubscriptionsManager {
|
|||||||
/**
|
/**
|
||||||
* Decrypt the subscription list from kind=30000 (d="subscriptions").
|
* Decrypt the subscription list from kind=30000 (d="subscriptions").
|
||||||
*/
|
*/
|
||||||
async loadSubscriptions(userPubkey, options = {}) {
|
async loadSubscriptions(userPubkey) {
|
||||||
if (!userPubkey) {
|
if (!userPubkey) {
|
||||||
userLogger.warn("[SubscriptionsManager] No pubkey => cannot load subs.");
|
userLogger.warn("[SubscriptionsManager] No pubkey => cannot load subs.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
|
||||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||||
|
|
||||||
// 1. Attempt to load from cache first
|
// 1. Attempt to load from cache first
|
||||||
@@ -390,25 +379,14 @@ class SubscriptionsManager {
|
|||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
// Trigger background update
|
// Trigger background update
|
||||||
if (!allowPermissionPrompt && !this.backgroundLoading) {
|
this.updateFromRelays(userPubkey).catch((err) => {
|
||||||
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) => {
|
|
||||||
devLogger.warn("[SubscriptionsManager] Background update failed:", err);
|
devLogger.warn("[SubscriptionsManager] Background update failed:", err);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If no cache, must wait for relays
|
// 2. If no cache, must wait for relays
|
||||||
await this.updateFromRelays(userPubkey, { allowPermissionPrompt });
|
await this.updateFromRelays(userPubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveToCache(userPubkey) {
|
saveToCache(userPubkey) {
|
||||||
@@ -424,7 +402,7 @@ class SubscriptionsManager {
|
|||||||
return this.emitter.on(eventName, handler);
|
return this.emitter.on(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleDecryptRetry(userPubkey, error, options = {}) {
|
scheduleDecryptRetry(userPubkey, error) {
|
||||||
const normalized = normalizeHexPubkey(userPubkey) || userPubkey;
|
const normalized = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return;
|
return;
|
||||||
@@ -437,7 +415,7 @@ class SubscriptionsManager {
|
|||||||
if (this.currentUserPubkey && this.currentUserPubkey !== normalized) {
|
if (this.currentUserPubkey && this.currentUserPubkey !== normalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.updateFromRelays(normalized, options).catch((retryError) => {
|
this.updateFromRelays(normalized).catch((retryError) => {
|
||||||
userLogger.warn("[SubscriptionsManager] Decryption retry failed:", retryError);
|
userLogger.warn("[SubscriptionsManager] Decryption retry failed:", retryError);
|
||||||
});
|
});
|
||||||
}, DECRYPT_RETRY_DELAY_MS);
|
}, DECRYPT_RETRY_DELAY_MS);
|
||||||
@@ -447,14 +425,11 @@ class SubscriptionsManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFromRelays(userPubkey, options = {}) {
|
async updateFromRelays(userPubkey) {
|
||||||
if (!userPubkey) return;
|
if (!userPubkey) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
|
||||||
const wasBackgroundLoading = this.backgroundLoading;
|
|
||||||
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
const normalizedUserPubkey = normalizeHexPubkey(userPubkey) || userPubkey;
|
||||||
this.lastLoadError = null;
|
|
||||||
|
|
||||||
const readRelays = relayManager.getReadRelayUrls();
|
const readRelays = relayManager.getReadRelayUrls();
|
||||||
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
const relayUrls = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||||
@@ -462,26 +437,8 @@ class SubscriptionsManager {
|
|||||||
devLogger.warn(
|
devLogger.warn(
|
||||||
"[SubscriptionsManager] No relay URLs available while loading subscriptions.",
|
"[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;
|
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
|
// Use incremental fetch helper
|
||||||
const cachedSnapshot = parseCachedSubscriptionSnapshot(
|
const cachedSnapshot = parseCachedSubscriptionSnapshot(
|
||||||
@@ -493,7 +450,7 @@ class SubscriptionsManager {
|
|||||||
pubkey: normalizedUserPubkey,
|
pubkey: normalizedUserPubkey,
|
||||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||||
relayUrls,
|
relayUrls,
|
||||||
since: shouldForceFullFetch ? 0 : (cachedSnapshot.createdAt || 0),
|
since: shouldForceFullFetch ? 0 : undefined,
|
||||||
timeoutMs: 12000,
|
timeoutMs: 12000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -516,7 +473,7 @@ class SubscriptionsManager {
|
|||||||
pubkey: normalizedSessionActorPubkey,
|
pubkey: normalizedSessionActorPubkey,
|
||||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||||
relayUrls,
|
relayUrls,
|
||||||
since: shouldForceSessionFetch ? 0 : (sessionCachedSnapshot.createdAt || 0),
|
since: shouldForceSessionFetch ? 0 : undefined,
|
||||||
timeoutMs: 12000,
|
timeoutMs: 12000,
|
||||||
});
|
});
|
||||||
if (sessionEvents.length) {
|
if (sessionEvents.length) {
|
||||||
@@ -559,13 +516,6 @@ class SubscriptionsManager {
|
|||||||
// This means no updates. We keep what we have.
|
// This means no updates. We keep what we have.
|
||||||
devLogger.log("[SubscriptionsManager] No updates from relays.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,9 +534,7 @@ class SubscriptionsManager {
|
|||||||
|
|
||||||
let decryptResult;
|
let decryptResult;
|
||||||
try {
|
try {
|
||||||
const decryptPromise = this.decryptSubscriptionEvent(newest, userPubkey, {
|
const decryptPromise = this.decryptSubscriptionEvent(newest, userPubkey);
|
||||||
allowPermissionPrompt,
|
|
||||||
});
|
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
@@ -605,33 +553,17 @@ class SubscriptionsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!decryptResult.ok) {
|
if (!decryptResult.ok) {
|
||||||
this.lastLoadError = decryptResult.error || null;
|
|
||||||
if (decryptResult.error?.code === "subscriptions-decrypt-timeout") {
|
if (decryptResult.error?.code === "subscriptions-decrypt-timeout") {
|
||||||
if (!this.loaded && !cachedSnapshot.hasSnapshot) {
|
if (!this.loaded && !cachedSnapshot.hasSnapshot) {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
this.scheduleDecryptRetry(normalizedUserPubkey, decryptResult.error, {
|
this.scheduleDecryptRetry(normalizedUserPubkey, decryptResult.error);
|
||||||
allowPermissionPrompt,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!allowPermissionPrompt &&
|
|
||||||
decryptResult.error?.code === "subscriptions-permission-required"
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
userLogger.error(
|
userLogger.error(
|
||||||
"[SubscriptionsManager] Failed to decrypt subscription list:",
|
"[SubscriptionsManager] Failed to decrypt subscription list:",
|
||||||
decryptResult.error,
|
decryptResult.error,
|
||||||
);
|
);
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.emitter.emit("change", {
|
|
||||||
action: "background-loaded",
|
|
||||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!this.loaded) {
|
if (!this.loaded) {
|
||||||
this.subscribedPubkeys.clear();
|
this.subscribedPubkeys.clear();
|
||||||
this.subsEventId = null;
|
this.subsEventId = null;
|
||||||
@@ -651,9 +583,6 @@ class SubscriptionsManager {
|
|||||||
this.subscribedPubkeys = newSet;
|
this.subscribedPubkeys = newSet;
|
||||||
this.currentUserPubkey = normalizedUserPubkey;
|
this.currentUserPubkey = normalizedUserPubkey;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
if (wasBackgroundLoading) {
|
|
||||||
this.backgroundLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update persistent cache
|
// Update persistent cache
|
||||||
this.saveToCache(normalizedUserPubkey);
|
this.saveToCache(normalizedUserPubkey);
|
||||||
@@ -681,31 +610,22 @@ class SubscriptionsManager {
|
|||||||
error
|
error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (wasBackgroundLoading) {
|
|
||||||
this.emitter.emit("change", {
|
|
||||||
action: "background-loaded",
|
|
||||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.lastLoadError = err || null;
|
|
||||||
userLogger.error("[SubscriptionsManager] Failed to update subs from relays:", err);
|
userLogger.error("[SubscriptionsManager] Failed to update subs from relays:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.stopSubscriptionListSubscription();
|
|
||||||
this.subscribedPubkeys.clear();
|
this.subscribedPubkeys.clear();
|
||||||
this.subsEventId = null;
|
this.subsEventId = null;
|
||||||
this.subsEventCreatedAt = null;
|
this.subsEventCreatedAt = null;
|
||||||
this.currentUserPubkey = null;
|
this.currentUserPubkey = null;
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.backgroundLoading = false;
|
|
||||||
this.lastRunOptions = null;
|
this.lastRunOptions = null;
|
||||||
this.lastResult = null;
|
this.lastResult = null;
|
||||||
this.hasRenderedOnce = false;
|
this.hasRenderedOnce = false;
|
||||||
this.lastLoadError = null;
|
|
||||||
if (this.decryptRetryTimeoutId) {
|
if (this.decryptRetryTimeoutId) {
|
||||||
clearTimeout(this.decryptRetryTimeoutId);
|
clearTimeout(this.decryptRetryTimeoutId);
|
||||||
this.decryptRetryTimeoutId = null;
|
this.decryptRetryTimeoutId = null;
|
||||||
@@ -713,56 +633,7 @@ class SubscriptionsManager {
|
|||||||
this.emitter.emit("change", { action: "reset", subscribedPubkeys: [] });
|
this.emitter.emit("change", { action: "reset", subscribedPubkeys: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
stopSubscriptionListSubscription() {
|
async ensureLoaded(actorHex) {
|
||||||
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 = {}) {
|
|
||||||
devLogger.log("[SubscriptionsManager] ensureLoaded start", actorHex);
|
devLogger.log("[SubscriptionsManager] ensureLoaded start", actorHex);
|
||||||
const normalizedActor = normalizeHexPubkey(actorHex) || actorHex;
|
const normalizedActor = normalizeHexPubkey(actorHex) || actorHex;
|
||||||
if (!normalizedActor) {
|
if (!normalizedActor) {
|
||||||
@@ -784,7 +655,7 @@ class SubscriptionsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = this.loadSubscriptions(normalizedActor, options);
|
const loader = this.loadSubscriptions(normalizedActor);
|
||||||
this.loadingPromise = loader;
|
this.loadingPromise = loader;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -838,7 +709,7 @@ class SubscriptionsManager {
|
|||||||
return Array.from(this.subscribedPubkeys);
|
return Array.from(this.subscribedPubkeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptSubscriptionEvent(event, userPubkey, options = {}) {
|
async decryptSubscriptionEvent(event, userPubkey) {
|
||||||
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
||||||
if (!ciphertext) {
|
if (!ciphertext) {
|
||||||
const error = new Error("Subscription event is missing ciphertext content.");
|
const error = new Error("Subscription event is missing ciphertext content.");
|
||||||
@@ -846,7 +717,6 @@ class SubscriptionsManager {
|
|||||||
return { ok: false, error };
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowPermissionPrompt = options?.allowPermissionPrompt !== false;
|
|
||||||
const hints = extractEncryptionHints(event);
|
const hints = extractEncryptionHints(event);
|
||||||
const requiresNip44 = hints.includes("nip44") || hints.includes("nip44_v2");
|
const requiresNip44 = hints.includes("nip44") || hints.includes("nip44_v2");
|
||||||
const requiresNip04 = !hints.length || hints.includes("nip04") || !requiresNip44;
|
const requiresNip04 = !hints.length || hints.includes("nip04") || !requiresNip44;
|
||||||
@@ -861,8 +731,7 @@ class SubscriptionsManager {
|
|||||||
if (
|
if (
|
||||||
!signer ||
|
!signer ||
|
||||||
(!signerHasRequiredDecryptors(signer) &&
|
(!signerHasRequiredDecryptors(signer) &&
|
||||||
typeof nostrClient?.ensureActiveSignerForPubkey === "function" &&
|
typeof nostrClient?.ensureActiveSignerForPubkey === "function")
|
||||||
allowPermissionPrompt)
|
|
||||||
) {
|
) {
|
||||||
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
|
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
|
||||||
}
|
}
|
||||||
@@ -870,11 +739,7 @@ class SubscriptionsManager {
|
|||||||
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
||||||
const signerHasNip44 = typeof signer?.nip44Decrypt === "function";
|
const signerHasNip44 = typeof signer?.nip44Decrypt === "function";
|
||||||
|
|
||||||
const nostrApi = allowPermissionPrompt
|
const nostrApi = typeof window !== "undefined" ? window?.nostr : null;
|
||||||
? typeof window !== "undefined"
|
|
||||||
? window?.nostr
|
|
||||||
: null
|
|
||||||
: null;
|
|
||||||
const windowHasNip04 = typeof nostrApi?.nip04?.decrypt === "function";
|
const windowHasNip04 = typeof nostrApi?.nip04?.decrypt === "function";
|
||||||
const windowHasNip44 =
|
const windowHasNip44 =
|
||||||
(nostrApi?.nip44 && typeof nostrApi.nip44.decrypt === "function") ||
|
(nostrApi?.nip44 && typeof nostrApi.nip44.decrypt === "function") ||
|
||||||
@@ -884,17 +749,8 @@ class SubscriptionsManager {
|
|||||||
(!signerHasNip44 && !windowHasNip44 && requiresNip44) ||
|
(!signerHasNip44 && !windowHasNip44 && requiresNip44) ||
|
||||||
(!signerHasNip04 && !windowHasNip04 && requiresNip04)
|
(!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 {
|
try {
|
||||||
const permissionResult = await requestDefaultExtensionPermissions(
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
);
|
|
||||||
if (!permissionResult?.ok) {
|
if (!permissionResult?.ok) {
|
||||||
const error =
|
const error =
|
||||||
permissionResult?.error instanceof Error
|
permissionResult?.error instanceof Error
|
||||||
@@ -942,19 +798,13 @@ class SubscriptionsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nostrApi) {
|
if (nostrApi) {
|
||||||
const decrypterOptions = {
|
|
||||||
priority: NIP07_PRIORITY.HIGH,
|
|
||||||
timeoutMs: 12000,
|
|
||||||
retryMultiplier: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||||
registerDecryptor("nip04", (payload) =>
|
registerDecryptor("nip04", (payload) =>
|
||||||
runNip07WithRetry(
|
runNip07WithRetry(
|
||||||
() => nostrApi.nip04.decrypt(userPubkey, payload),
|
() => nostrApi.nip04.decrypt(userPubkey, payload),
|
||||||
{
|
{
|
||||||
label: "nip04.decrypt",
|
label: "nip04.decrypt",
|
||||||
...decrypterOptions,
|
priority: NIP07_PRIORITY.HIGH,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -971,7 +821,7 @@ class SubscriptionsManager {
|
|||||||
() => nip44.decrypt(userPubkey, payload),
|
() => nip44.decrypt(userPubkey, payload),
|
||||||
{
|
{
|
||||||
label: "nip44.decrypt",
|
label: "nip44.decrypt",
|
||||||
...decrypterOptions,
|
priority: NIP07_PRIORITY.HIGH,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -985,7 +835,7 @@ class SubscriptionsManager {
|
|||||||
() => nip44v2.decrypt(userPubkey, payload),
|
() => nip44v2.decrypt(userPubkey, payload),
|
||||||
{
|
{
|
||||||
label: "nip44.v2.decrypt",
|
label: "nip44.v2.decrypt",
|
||||||
...decrypterOptions,
|
priority: NIP07_PRIORITY.HIGH,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -995,7 +845,7 @@ class SubscriptionsManager {
|
|||||||
() => nip44v2.decrypt(userPubkey, payload),
|
() => nip44v2.decrypt(userPubkey, payload),
|
||||||
{
|
{
|
||||||
label: "nip44.v2.decrypt",
|
label: "nip44.v2.decrypt",
|
||||||
...decrypterOptions,
|
priority: NIP07_PRIORITY.HIGH,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1129,9 +979,7 @@ class SubscriptionsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (signer.type === "extension") {
|
if (signer.type === "extension") {
|
||||||
const permissionResult = await requestDefaultExtensionPermissions(
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
);
|
|
||||||
if (!permissionResult.ok) {
|
if (!permissionResult.ok) {
|
||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
"[SubscriptionsManager] Signer permissions denied while updating subscriptions.",
|
"[SubscriptionsManager] Signer permissions denied while updating subscriptions.",
|
||||||
@@ -1240,6 +1088,13 @@ class SubscriptionsManager {
|
|||||||
throw signErr;
|
throw signErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sanitizeRelayList = (candidate) =>
|
||||||
|
Array.isArray(candidate)
|
||||||
|
? candidate
|
||||||
|
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
const writeRelays = sanitizeRelayList(nostrClient.writeRelays);
|
const writeRelays = sanitizeRelayList(nostrClient.writeRelays);
|
||||||
const fallbackRelays = writeRelays.length
|
const fallbackRelays = writeRelays.length
|
||||||
? writeRelays
|
? writeRelays
|
||||||
|
|||||||
Vendored
+6
-29
@@ -135,7 +135,7 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
|||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
let tainted = false;
|
let tainted = false;
|
||||||
let resizeObserver = null;
|
let resizeBound = false;
|
||||||
|
|
||||||
const clearCanvas = () => {
|
const clearCanvas = () => {
|
||||||
if (!canvasElement) {
|
if (!canvasElement) {
|
||||||
@@ -152,12 +152,9 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
|||||||
cancelFrame(rafId);
|
cancelFrame(rafId);
|
||||||
rafId = 0;
|
rafId = 0;
|
||||||
}
|
}
|
||||||
if (resizeObserver) {
|
if (resizeBound && win) {
|
||||||
resizeObserver.disconnect();
|
|
||||||
resizeObserver = null;
|
|
||||||
}
|
|
||||||
if (win) {
|
|
||||||
win.removeEventListener("resize", handleResize);
|
win.removeEventListener("resize", handleResize);
|
||||||
|
resizeBound = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,11 +176,7 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
|||||||
lastTime = timestamp;
|
lastTime = timestamp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimization: Rely on ResizeObserver for dimensions.
|
const { width, height } = resizeCanvas(canvasElement, videoElement);
|
||||||
// Do NOT call resizeCanvas() here to avoid layout thrashing.
|
|
||||||
const width = canvasElement.width || 0;
|
|
||||||
const height = canvasElement.height || 0;
|
|
||||||
|
|
||||||
if (!width || !height) {
|
if (!width || !height) {
|
||||||
schedule();
|
schedule();
|
||||||
return;
|
return;
|
||||||
@@ -210,26 +203,10 @@ export function attachAmbientBackground(videoElement, canvasElement, options = {
|
|||||||
running = true;
|
running = true;
|
||||||
lastTime = 0;
|
lastTime = 0;
|
||||||
resizeCanvas(canvasElement, videoElement);
|
resizeCanvas(canvasElement, videoElement);
|
||||||
|
if (win && !resizeBound) {
|
||||||
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
|
|
||||||
win.addEventListener("resize", handleResize);
|
win.addEventListener("resize", handleResize);
|
||||||
|
resizeBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
step();
|
step();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-53
@@ -34,7 +34,6 @@ import ProfileModalController from "./profileModalController.js";
|
|||||||
import LoginModalController from "./loginModalController.js";
|
import LoginModalController from "./loginModalController.js";
|
||||||
import { VideoListView } from "./views/VideoListView.js";
|
import { VideoListView } from "./views/VideoListView.js";
|
||||||
import MoreMenuController from "./moreMenuController.js";
|
import MoreMenuController from "./moreMenuController.js";
|
||||||
import VideoSettingsMenuController from "./videoSettingsMenuController.js";
|
|
||||||
import AppChromeController from "./appChromeController.js";
|
import AppChromeController from "./appChromeController.js";
|
||||||
import { getSidebarLoadingMarkup } from "../sidebarLoading.js";
|
import { getSidebarLoadingMarkup } from "../sidebarLoading.js";
|
||||||
import { isWatchHistoryDebugEnabled } from "../watchHistoryDebug.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 =
|
app.commentThreadService =
|
||||||
this.services.commentThreadService ||
|
this.services.commentThreadService ||
|
||||||
@@ -604,7 +579,6 @@ export default class ApplicationBootstrap {
|
|||||||
app.handleProfilePrivacyToggle(payload),
|
app.handleProfilePrivacyToggle(payload),
|
||||||
onPublishDmRelayPreferences: (payload) =>
|
onPublishDmRelayPreferences: (payload) =>
|
||||||
app.handleProfilePublishDmRelayPreferences(payload),
|
app.handleProfilePublishDmRelayPreferences(payload),
|
||||||
onRequestPermissionPrompt: () => app.handlePermissionPromptRequest(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
app.profileController = new ProfileModalController({
|
app.profileController = new ProfileModalController({
|
||||||
@@ -697,10 +671,7 @@ export default class ApplicationBootstrap {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
app.moreMenuController.setVideoModal(app.videoModal);
|
app.moreMenuController.setVideoModal(app.videoModal);
|
||||||
app.videoSettingsMenuController = new VideoSettingsMenuController({
|
app.videoSettingsPopovers = new Map();
|
||||||
designSystem: app.designSystemContext,
|
|
||||||
isDevMode,
|
|
||||||
});
|
|
||||||
app.tagPreferencePopovers = new Map();
|
app.tagPreferencePopovers = new Map();
|
||||||
|
|
||||||
app.subscriptionsLink = null;
|
app.subscriptionsLink = null;
|
||||||
@@ -717,29 +688,6 @@ export default class ApplicationBootstrap {
|
|||||||
app.lastExperimentalWarningKey = null;
|
app.lastExperimentalWarningKey = null;
|
||||||
app.lastExperimentalWarningAt = 0;
|
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.pubkey = null;
|
||||||
app.currentMagnetUri = null;
|
app.currentMagnetUri = null;
|
||||||
app.currentVideo = null;
|
app.currentVideo = null;
|
||||||
|
|||||||
-9
@@ -985,15 +985,6 @@ export class SimilarContentCard {
|
|||||||
avatarImg.decoding = "async";
|
avatarImg.decoding = "async";
|
||||||
avatarImg.loading = "lazy";
|
avatarImg.loading = "lazy";
|
||||||
avatarImg.alt = "";
|
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);
|
avatarWrapper.appendChild(avatarImg);
|
||||||
|
|
||||||
// Append avatar then text
|
// Append avatar then text
|
||||||
|
|||||||
Vendored
-9
@@ -916,15 +916,6 @@ export class VideoCard {
|
|||||||
"rounded-full",
|
"rounded-full",
|
||||||
"object-cover"
|
"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) {
|
if (this.video.pubkey) {
|
||||||
avatar.dataset.pubkey = this.video.pubkey;
|
avatar.dataset.pubkey = this.video.pubkey;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-10
@@ -6201,16 +6201,11 @@ export class VideoModal {
|
|||||||
try {
|
try {
|
||||||
const token = subscribeToVideoViewCount(
|
const token = subscribeToVideoViewCount(
|
||||||
pointerInfo.pointer,
|
pointerInfo.pointer,
|
||||||
({ total, status, partial }) => {
|
({ total, status }) => {
|
||||||
if (!viewEl || !viewEl.isConnected) {
|
if (!viewEl || !viewEl.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
viewEl.textContent = this.getViewCountLabel(total, status, partial);
|
viewEl.textContent = this.getViewCountLabel(total, status);
|
||||||
if (partial) {
|
|
||||||
viewEl.dataset.viewCountState = "partial";
|
|
||||||
} else {
|
|
||||||
viewEl.dataset.viewCountState = status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return { pointer: pointerInfo.pointer, token };
|
return { pointer: pointerInfo.pointer, token };
|
||||||
@@ -6221,10 +6216,9 @@ export class VideoModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewCountLabel(total, status, partial) {
|
getViewCountLabel(total, status) {
|
||||||
if (Number.isFinite(total)) {
|
if (Number.isFinite(total)) {
|
||||||
const label = this.formatViewCountLabel(Number(total));
|
return this.formatViewCountLabel(Number(total));
|
||||||
return partial ? `${label} (partial)` : label;
|
|
||||||
}
|
}
|
||||||
if (status === "hydrating") {
|
if (status === "hydrating") {
|
||||||
return "Loading…";
|
return "Loading…";
|
||||||
|
|||||||
Vendored
+29
@@ -237,3 +237,32 @@ export function renderTagPillStrip({
|
|||||||
|
|
||||||
return { root, buttons };
|
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 { ConversationList } from "./ConversationList.js";
|
||||||
import { MessageThread } from "./MessageThread.js";
|
import { MessageThread } from "./MessageThread.js";
|
||||||
import { DMPrivacySettings } from "./DMPrivacySettings.js";
|
import { DMPrivacySettings } from "./DMPrivacySettings.js";
|
||||||
import { DMRelaySettings } from "./DMRelaySettings.js";
|
|
||||||
import { aggregateZapTotals } from "./zapHelpers.js";
|
import { aggregateZapTotals } from "./zapHelpers.js";
|
||||||
import { createZapInterface } from "./ZapInterface.js";
|
import { createZapInterface } from "./ZapInterface.js";
|
||||||
|
|
||||||
@@ -137,7 +136,6 @@ export class AppShell {
|
|||||||
onToggleTypingIndicators,
|
onToggleTypingIndicators,
|
||||||
onRefreshConversations,
|
onRefreshConversations,
|
||||||
onBack,
|
onBack,
|
||||||
onOpenSettings,
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new Error("AppShell requires a document reference.");
|
throw new Error("AppShell requires a document reference.");
|
||||||
@@ -184,23 +182,15 @@ export class AppShell {
|
|||||||
popover.remove();
|
popover.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
popover = createElement(doc, "div", "dm-settings-popover absolute z-50 right-4 top-12 flex flex-col gap-4");
|
popover = DMPrivacySettings({
|
||||||
|
|
||||||
const privacySettings = DMPrivacySettings({
|
|
||||||
document: doc,
|
document: doc,
|
||||||
readReceiptsEnabled: dmPrivacySettings.readReceiptsEnabled,
|
readReceiptsEnabled: dmPrivacySettings.readReceiptsEnabled,
|
||||||
typingIndicatorsEnabled: dmPrivacySettings.typingIndicatorsEnabled,
|
typingIndicatorsEnabled: dmPrivacySettings.typingIndicatorsEnabled,
|
||||||
onToggleReadReceipts,
|
onToggleReadReceipts,
|
||||||
onToggleTypingIndicators,
|
onToggleTypingIndicators,
|
||||||
});
|
});
|
||||||
popover.appendChild(privacySettings);
|
popover.classList.add("dm-settings-popover", "absolute", "z-50", "right-4", "top-12", "bg-surface", "border", "border-border", "rounded-xl");
|
||||||
|
|
||||||
const relaySettings = DMRelaySettings({ document: doc });
|
|
||||||
popover.appendChild(relaySettings);
|
|
||||||
sidebarHeader.appendChild(popover);
|
sidebarHeader.appendChild(popover);
|
||||||
if (typeof onOpenSettings === "function") {
|
|
||||||
onOpenSettings(popover);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeHandler = (event) => {
|
const closeHandler = (event) => {
|
||||||
if (!popover.contains(event.target) && event.target !== settingsBtn && !settingsBtn.contains(event.target)) {
|
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
|
// js/ui/moderationCopy.js
|
||||||
|
|
||||||
function buildModerationReasonText(context) {
|
export function buildModerationReasonText(context) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ function buildModerationReasonText(context) {
|
|||||||
return combined.charAt(0).toUpperCase() + combined.slice(1);
|
return combined.charAt(0).toUpperCase() + combined.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHiddenSummaryLabel(context) {
|
export function buildHiddenSummaryLabel(context) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-2
@@ -1520,8 +1520,6 @@ export default class MoreMenuController {
|
|||||||
const message =
|
const message =
|
||||||
error?.code === "nip04-missing"
|
error?.code === "nip04-missing"
|
||||||
? "Your Nostr extension must support NIP-04 to manage private lists."
|
? "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.";
|
: "Failed to update your block list. Please try again.";
|
||||||
this.callbacks.showError(message);
|
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";
|
import { devLogger } from "../utils/logger.js";
|
||||||
|
|
||||||
export default class TorrentStatusController {
|
export default class TorrentStatusController {
|
||||||
@@ -8,36 +7,14 @@ export default class TorrentStatusController {
|
|||||||
}
|
}
|
||||||
this.getVideoModal = getVideoModal;
|
this.getVideoModal = getVideoModal;
|
||||||
this.onRemovePoster = typeof onRemovePoster === "function" ? onRemovePoster : () => {};
|
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) {
|
update(torrent) {
|
||||||
if (!torrent) {
|
devLogger.log("[DEBUG] TorrentStatusController.update called with torrent:", torrent);
|
||||||
if (isVerboseDevMode) {
|
|
||||||
this.logVerboseSummary({ hasTorrent: false });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isVerboseDevMode) {
|
if (!torrent) {
|
||||||
this.logVerboseSummary({
|
devLogger.log("[DEBUG] torrent is null/undefined!");
|
||||||
hasTorrent: true,
|
return;
|
||||||
progress: torrent.progress,
|
|
||||||
numPeers: torrent.numPeers,
|
|
||||||
downloadSpeed: torrent.downloadSpeed,
|
|
||||||
downloaded: torrent.downloaded,
|
|
||||||
length: torrent.length,
|
|
||||||
ready: torrent.ready,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrent.ready || (typeof torrent.progress === "number" && torrent.progress > 0)) {
|
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();
|
const videoModal = this.getVideoModal();
|
||||||
|
|
||||||
// Use "Complete" vs. "Downloading" as the textual status.
|
// Use "Complete" vs. "Downloading" as the textual status.
|
||||||
if (videoModal) {
|
if (videoModal) {
|
||||||
const fullyDownloaded = Number(torrent.progress) >= 1;
|
const fullyDownloaded = torrent.progress >= 1;
|
||||||
const status = torrent.ready
|
if (typeof videoModal.updateStatus === "function") {
|
||||||
? "Ready to play"
|
videoModal.updateStatus(fullyDownloaded ? "Complete" : "Downloading");
|
||||||
: fullyDownloaded
|
}
|
||||||
? "Complete"
|
|
||||||
: "Downloading";
|
|
||||||
this.updateIfChanged("status", status, () => {
|
|
||||||
if (typeof videoModal.updateStatus === "function") {
|
|
||||||
videoModal.updateStatus(status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const progressValue = Number.isFinite(torrent.progress)
|
if (typeof videoModal.updateProgress === "function") {
|
||||||
? `${(torrent.progress * 100).toFixed(2)}%`
|
const percent = (torrent.progress * 100).toFixed(2);
|
||||||
: "0.00%";
|
videoModal.updateProgress(`${percent}%`);
|
||||||
this.updateIfChanged("progress", progressValue, () => {
|
}
|
||||||
if (typeof videoModal.updateProgress === "function") {
|
|
||||||
videoModal.updateProgress(progressValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const peersValue = `Peers: ${Number.isFinite(torrent.numPeers) ? torrent.numPeers : 0}`;
|
if (typeof videoModal.updatePeers === "function") {
|
||||||
this.updateIfChanged("peers", peersValue, () => {
|
videoModal.updatePeers(`Peers: ${torrent.numPeers}`);
|
||||||
if (typeof videoModal.updatePeers === "function") {
|
}
|
||||||
videoModal.updatePeers(peersValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const speedValue = Number.isFinite(torrent.downloadSpeed)
|
if (typeof videoModal.updateSpeed === "function") {
|
||||||
? `${(torrent.downloadSpeed / 1024).toFixed(2)} KB/s`
|
const kb = (torrent.downloadSpeed / 1024).toFixed(2);
|
||||||
: "0.00 KB/s";
|
videoModal.updateSpeed(`${kb} KB/s`);
|
||||||
this.updateIfChanged("speed", speedValue, () => {
|
}
|
||||||
if (typeof videoModal.updateSpeed === "function") {
|
|
||||||
videoModal.updateSpeed(speedValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadedMb = Number.isFinite(torrent.downloaded)
|
if (typeof videoModal.updateDownloaded === "function") {
|
||||||
? (torrent.downloaded / (1024 * 1024)).toFixed(2)
|
const downloadedMb = (torrent.downloaded / (1024 * 1024)).toFixed(2);
|
||||||
: "0.00";
|
const lengthMb = (torrent.length / (1024 * 1024)).toFixed(2);
|
||||||
const lengthMb = Number.isFinite(torrent.length)
|
videoModal.updateDownloaded(
|
||||||
? (torrent.length / (1024 * 1024)).toFixed(2)
|
`${downloadedMb} MB / ${lengthMb} MB`
|
||||||
: "0.00";
|
);
|
||||||
const downloadedValue = `${downloadedMb} MB / ${lengthMb} MB`;
|
}
|
||||||
this.updateIfChanged("downloaded", downloadedValue, () => {
|
|
||||||
if (typeof videoModal.updateDownloaded === "function") {
|
if (torrent.ready && typeof videoModal.updateStatus === "function") {
|
||||||
videoModal.updateDownloaded(downloadedValue);
|
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.logger = logger;
|
||||||
this.constants = constants;
|
this.constants = constants;
|
||||||
this.callbacks = callbacks;
|
this.callbacks = callbacks;
|
||||||
|
|
||||||
this.activeVideoProbes = 0;
|
|
||||||
this.videoProbeQueue = [];
|
|
||||||
this.MAX_CONCURRENT_VIDEO_PROBES = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrlHealthPlaceholderMarkup(options = {}) {
|
getUrlHealthPlaceholderMarkup(options = {}) {
|
||||||
@@ -258,85 +254,67 @@ export default class UrlHealthController {
|
|||||||
return { outcome: "error" };
|
return { outcome: "error" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activeVideoProbes >= this.MAX_CONCURRENT_VIDEO_PROBES) {
|
return new Promise((resolve) => {
|
||||||
await new Promise((resolve) => {
|
const video = document.createElement("video");
|
||||||
this.videoProbeQueue.push(resolve);
|
let settled = false;
|
||||||
});
|
let timeoutId = null;
|
||||||
}
|
|
||||||
|
|
||||||
this.activeVideoProbes++;
|
const cleanup = () => {
|
||||||
|
if (timeoutId) {
|
||||||
try {
|
clearTimeout(timeoutId);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
video.removeEventListener("loadeddata", handleSuccess);
|
||||||
|
video.removeEventListener("canplay", handleSuccess);
|
||||||
|
video.removeEventListener("error", handleError);
|
||||||
try {
|
try {
|
||||||
video.preload = "metadata";
|
video.pause();
|
||||||
video.muted = true;
|
} catch (err) {
|
||||||
video.playsInline = true;
|
// ignore pause failures
|
||||||
video.addEventListener("loadeddata", handleSuccess, { once: true });
|
}
|
||||||
video.addEventListener("canplay", handleSuccess, { once: true });
|
try {
|
||||||
video.addEventListener("error", handleError, { once: true });
|
video.removeAttribute("src");
|
||||||
video.src = trimmed;
|
|
||||||
video.load();
|
video.load();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
settle({ outcome: "error", error: err });
|
// ignore cleanup failures
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
} finally {
|
|
||||||
this.activeVideoProbes--;
|
const settle = (result) => {
|
||||||
if (this.videoProbeQueue.length > 0) {
|
if (settled) {
|
||||||
const next = this.videoProbeQueue.shift();
|
return;
|
||||||
if (typeof next === "function") {
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
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 = {}) {
|
async probeUrl(url, options = {}) {
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ export default class VideoListViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageContext =
|
const messageContext =
|
||||||
reason === "login" && postLoginResult?.blocksLoaded === true
|
reason === "login" && postLoginResult?.blocksLoaded !== false
|
||||||
? "Applying your filters…"
|
? "Applying your filters…"
|
||||||
: "Refreshing videos…";
|
: "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(),
|
elements: new Set(),
|
||||||
lastTotal: null,
|
lastTotal: null,
|
||||||
lastStatus: "idle",
|
lastStatus: "idle",
|
||||||
lastPartial: false,
|
|
||||||
lastText: "– views",
|
lastText: "– views",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = subscribeToVideoViewCount(pointerInfo.pointer, ({ total, status, partial }) => {
|
const token = subscribeToVideoViewCount(pointerInfo.pointer, ({ total, status }) => {
|
||||||
let text;
|
let text;
|
||||||
if (Number.isFinite(total)) {
|
if (Number.isFinite(total)) {
|
||||||
const numeric = Number(total);
|
const numeric = Number(total);
|
||||||
entry.lastTotal = numeric;
|
entry.lastTotal = numeric;
|
||||||
text = this.formatters.formatViewCountLabel(numeric);
|
text = this.formatters.formatViewCountLabel(numeric);
|
||||||
if (partial) {
|
|
||||||
text = `${text} (partial)`;
|
|
||||||
}
|
|
||||||
} else if (status === "hydrating") {
|
} else if (status === "hydrating") {
|
||||||
text = "Loading views…";
|
text = "Loading views…";
|
||||||
} else {
|
} else {
|
||||||
@@ -1155,7 +1151,6 @@ export class VideoListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry.lastStatus = status;
|
entry.lastStatus = status;
|
||||||
entry.lastPartial = Boolean(partial);
|
|
||||||
entry.lastText = text;
|
entry.lastText = text;
|
||||||
|
|
||||||
for (const el of Array.from(entry.elements)) {
|
for (const el of Array.from(entry.elements)) {
|
||||||
@@ -1164,11 +1159,6 @@ export class VideoListView {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
el.textContent = text;
|
el.textContent = text;
|
||||||
if (entry.lastPartial) {
|
|
||||||
el.dataset.viewCountState = "partial";
|
|
||||||
} else {
|
|
||||||
el.dataset.viewCountState = status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
entry.token = token;
|
entry.token = token;
|
||||||
@@ -1203,9 +1193,6 @@ export class VideoListView {
|
|||||||
if (typeof pointerInfo.key === "string") {
|
if (typeof pointerInfo.key === "string") {
|
||||||
viewCountEl.dataset.viewPointer = pointerInfo.key;
|
viewCountEl.dataset.viewPointer = pointerInfo.key;
|
||||||
}
|
}
|
||||||
viewCountEl.dataset.viewCountState = entry.lastPartial
|
|
||||||
? "partial"
|
|
||||||
: entry.lastStatus;
|
|
||||||
viewCountEl.textContent = entry.lastText;
|
viewCountEl.textContent = entry.lastText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-19
@@ -398,7 +398,7 @@ export default class ZapController {
|
|||||||
|
|
||||||
this.resetRetryState();
|
this.resetRetryState();
|
||||||
this.modalZapCommentValue = "";
|
this.modalZapCommentValue = "";
|
||||||
this.videoModal?.setZapComment("");
|
this.videoModal?.resetZapForm({ amount: "", comment: "" });
|
||||||
this.videoModal?.setZapCompleted(true);
|
this.videoModal?.setZapCompleted(true);
|
||||||
this.applyDefaultAmount();
|
this.applyDefaultAmount();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -881,13 +881,6 @@ export default class ZapController {
|
|||||||
});
|
});
|
||||||
const videoEvent = this.getActiveVideoEvent();
|
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 {
|
try {
|
||||||
const result = await this.splitAndZap(
|
const result = await this.splitAndZap(
|
||||||
{
|
{
|
||||||
@@ -895,6 +888,7 @@ export default class ZapController {
|
|||||||
amountSats: context.shares.total,
|
amountSats: context.shares.total,
|
||||||
comment: typeof comment === "string" ? comment : "",
|
comment: typeof comment === "string" ? comment : "",
|
||||||
walletSettings: settings,
|
walletSettings: settings,
|
||||||
|
overrideFee,
|
||||||
},
|
},
|
||||||
dependencies
|
dependencies
|
||||||
);
|
);
|
||||||
@@ -937,17 +931,6 @@ export default class ZapController {
|
|||||||
error
|
error
|
||||||
);
|
);
|
||||||
throw 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 { profileCache } from "./state/profileCache.js";
|
||||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||||
import { relayManager } from "./relayManager.js";
|
import { relayManager } from "./relayManager.js";
|
||||||
import {
|
import { runNip07WithRetry, NIP07_PRIORITY } from "./nostr/nip07Permissions.js";
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
runNip07WithRetry,
|
|
||||||
NIP07_PRIORITY,
|
|
||||||
} from "./nostr/nip07Permissions.js";
|
|
||||||
import { relaySubscriptionService } from "./services/relaySubscriptionService.js";
|
|
||||||
|
|
||||||
class TinyEventEmitter {
|
class TinyEventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -79,10 +74,8 @@ export const USER_BLOCK_EVENTS = Object.freeze({
|
|||||||
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
|
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
|
||||||
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
|
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
|
||||||
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
|
const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000;
|
||||||
const DECRYPT_TIMEOUT_MS = 15000;
|
const DECRYPT_TIMEOUT_MS = 90000;
|
||||||
const BACKGROUND_DECRYPT_TIMEOUT_MS = 2500;
|
|
||||||
const DECRYPT_RETRY_DELAY_MS = 10000;
|
const DECRYPT_RETRY_DELAY_MS = 10000;
|
||||||
const MAX_BLOCKLIST_ENTRIES = 5000;
|
|
||||||
|
|
||||||
function sanitizeRelayList(candidate) {
|
function sanitizeRelayList(candidate) {
|
||||||
return Array.isArray(candidate)
|
return Array.isArray(candidate)
|
||||||
@@ -310,16 +303,13 @@ function serializeBlockListTagMatrix(values, ownerPubkey, options = {}) {
|
|||||||
return JSON.stringify(tags);
|
return JSON.stringify(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPubkeysFromTags(tags, ownerPubkey = null, options = {}) {
|
function extractPubkeysFromTags(tags, ownerPubkey = null) {
|
||||||
if (!Array.isArray(tags)) {
|
if (!Array.isArray(tags)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const collected = [];
|
const collected = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const owner = ownerPubkey ? normalizeHex(ownerPubkey) : null;
|
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) {
|
for (const entry of tags) {
|
||||||
if (!Array.isArray(entry) || entry.length < 2) {
|
if (!Array.isArray(entry) || entry.length < 2) {
|
||||||
@@ -335,14 +325,11 @@ function extractPubkeysFromTags(tags, ownerPubkey = null, options = {}) {
|
|||||||
}
|
}
|
||||||
seen.add(normalized);
|
seen.add(normalized);
|
||||||
collected.push(normalized);
|
collected.push(normalized);
|
||||||
if (limit !== null && collected.length >= limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBlockListPlaintext(plaintext, ownerPubkey, options = {}) {
|
function parseBlockListPlaintext(plaintext, ownerPubkey) {
|
||||||
if (typeof plaintext !== "string" || !plaintext) {
|
if (typeof plaintext !== "string" || !plaintext) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -359,45 +346,20 @@ function parseBlockListPlaintext(plaintext, ownerPubkey, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const owner = normalizeHex(ownerPubkey);
|
const owner = normalizeHex(ownerPubkey);
|
||||||
const limit = Number.isFinite(options?.limit)
|
|
||||||
? Math.max(0, Math.floor(options.limit))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
if (limit !== null && parsed.length > limit) {
|
return extractPubkeysFromTags(parsed, owner);
|
||||||
devLogger.warn(
|
|
||||||
"[UserBlockList] Block list payload exceeds entry limit; truncating.",
|
|
||||||
{ limit, payloadSize: parsed.length },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return extractPubkeysFromTags(parsed, owner, { limit });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed && typeof parsed === "object") {
|
if (parsed && typeof parsed === "object") {
|
||||||
if (Array.isArray(parsed.tags) && parsed.tags.length) {
|
if (Array.isArray(parsed.tags) && parsed.tags.length) {
|
||||||
if (limit !== null && parsed.tags.length > limit) {
|
return extractPubkeysFromTags(parsed.tags, owner);
|
||||||
devLogger.warn(
|
|
||||||
"[UserBlockList] Block list tag payload exceeds entry limit; truncating.",
|
|
||||||
{ limit, payloadSize: parsed.tags.length },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return extractPubkeysFromTags(parsed.tags, owner, { limit });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacy = Array.isArray(parsed.blockedPubkeys) ? parsed.blockedPubkeys : [];
|
const legacy = Array.isArray(parsed.blockedPubkeys) ? parsed.blockedPubkeys : [];
|
||||||
if (limit !== null && legacy.length > limit) {
|
return legacy
|
||||||
devLogger.warn(
|
|
||||||
"[UserBlockList] Legacy block list payload exceeds entry limit; truncating.",
|
|
||||||
{ limit, payloadSize: legacy.length },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const normalizedLegacy = legacy
|
|
||||||
.map((value) => normalizeHex(value))
|
.map((value) => normalizeHex(value))
|
||||||
.filter((value) => value && value !== owner);
|
.filter((value) => value && value !== owner);
|
||||||
if (limit === null) {
|
|
||||||
return normalizedLegacy;
|
|
||||||
}
|
|
||||||
return normalizedLegacy.slice(0, limit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@@ -686,9 +648,6 @@ class UserBlockListManager {
|
|||||||
this.emitter = new TinyEventEmitter();
|
this.emitter = new TinyEventEmitter();
|
||||||
this.seedStateCache = new Map();
|
this.seedStateCache = new Map();
|
||||||
this.decryptRetryTimeoutId = null;
|
this.decryptRetryTimeoutId = null;
|
||||||
this.loadPromise = null;
|
|
||||||
this.loadingPubkey = null;
|
|
||||||
this.blockListSubscriptionKey = null;
|
|
||||||
|
|
||||||
profileCache.subscribe((event, detail) => {
|
profileCache.subscribe((event, detail) => {
|
||||||
if (event === "profileChanged") {
|
if (event === "profileChanged") {
|
||||||
@@ -703,7 +662,6 @@ class UserBlockListManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.stopBlockListSubscription();
|
|
||||||
this.blockedPubkeys.clear();
|
this.blockedPubkeys.clear();
|
||||||
this.blockEventId = null;
|
this.blockEventId = null;
|
||||||
this.blockEventCreatedAt = null;
|
this.blockEventCreatedAt = null;
|
||||||
@@ -715,56 +673,6 @@ class UserBlockListManager {
|
|||||||
clearTimeout(this.decryptRetryTimeoutId);
|
clearTimeout(this.decryptRetryTimeoutId);
|
||||||
this.decryptRetryTimeoutId = null;
|
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) {
|
on(eventName, handler) {
|
||||||
@@ -775,32 +683,29 @@ class UserBlockListManager {
|
|||||||
return Array.from(this.blockedPubkeys);
|
return Array.from(this.blockedPubkeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleDecryptRetry(userPubkey, detail = {}) {
|
scheduleDecryptRetry(userPubkey, error) {
|
||||||
if (!userPubkey) {
|
if (!userPubkey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.decryptRetryTimeoutId) {
|
if (this.decryptRetryTimeoutId) {
|
||||||
clearTimeout(this.decryptRetryTimeoutId);
|
clearTimeout(this.decryptRetryTimeoutId);
|
||||||
}
|
}
|
||||||
const retryMeta = detail && typeof detail === "object" ? detail : {};
|
|
||||||
const signerStatus = retryMeta.signerStatus || "unknown";
|
|
||||||
this.decryptRetryTimeoutId = setTimeout(() => {
|
this.decryptRetryTimeoutId = setTimeout(() => {
|
||||||
this.decryptRetryTimeoutId = null;
|
this.decryptRetryTimeoutId = null;
|
||||||
if (this.activePubkey !== userPubkey) {
|
if (this.activePubkey !== userPubkey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sinceValue = Number.isFinite(this.blockEventCreatedAt)
|
|
||||||
? Math.max(0, this.blockEventCreatedAt - 1)
|
|
||||||
: undefined;
|
|
||||||
this.loadBlocks(userPubkey, {
|
this.loadBlocks(userPubkey, {
|
||||||
since: sinceValue,
|
since: Number.isFinite(this.blockEventCreatedAt)
|
||||||
|
? this.blockEventCreatedAt
|
||||||
|
: undefined,
|
||||||
}).catch((retryError) => {
|
}).catch((retryError) => {
|
||||||
userLogger.warn("[UserBlockList] Decryption retry failed:", retryError);
|
userLogger.warn("[UserBlockList] Decryption retry failed:", retryError);
|
||||||
});
|
});
|
||||||
}, DECRYPT_RETRY_DELAY_MS);
|
}, DECRYPT_RETRY_DELAY_MS);
|
||||||
devLogger.log(
|
devLogger.log(
|
||||||
`[UserBlockList] Decryption stalled (${signerStatus}); retrying in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
|
`[UserBlockList] Decryption timed out; scheduling retry in ${DECRYPT_RETRY_DELAY_MS / 1000}s.`,
|
||||||
retryMeta.error || retryMeta,
|
error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,38 +732,6 @@ class UserBlockListManager {
|
|||||||
|
|
||||||
async loadBlocks(userPubkey, options = {}) {
|
async loadBlocks(userPubkey, options = {}) {
|
||||||
const normalized = normalizeHex(userPubkey);
|
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;
|
this.activePubkey = normalized;
|
||||||
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -872,15 +745,6 @@ class UserBlockListManager {
|
|||||||
: null;
|
: null;
|
||||||
const statusCallback =
|
const statusCallback =
|
||||||
typeof options?.statusCallback === "function" ? options.statusCallback : null;
|
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) => {
|
const emitStatus = (detail) => {
|
||||||
if (!detail || typeof detail !== "object") {
|
if (!detail || typeof detail !== "object") {
|
||||||
@@ -909,23 +773,12 @@ class UserBlockListManager {
|
|||||||
const readRelays = relayManager.getReadRelayUrls();
|
const readRelays = relayManager.getReadRelayUrls();
|
||||||
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||||
emitStatus({ status: "loading", relays });
|
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 localBlocks = this._loadLocal(normalized);
|
||||||
const hasLocalData = !!localData;
|
const hasLocalData = !!localBlocks;
|
||||||
|
|
||||||
if (localData) {
|
if (localBlocks) {
|
||||||
this.blockedPubkeys = localData.blockedPubkeys;
|
this.blockedPubkeys = localBlocks;
|
||||||
if (Number.isFinite(localData.createdAt)) {
|
|
||||||
this.blockEventCreatedAt = localData.createdAt;
|
|
||||||
}
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
|
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
|
||||||
action: "sync",
|
action: "sync",
|
||||||
@@ -968,44 +821,21 @@ class UserBlockListManager {
|
|||||||
|
|
||||||
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
|
||||||
const signerHasNip44 = typeof signer?.nip44Decrypt === "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;
|
let permissionError = null;
|
||||||
if ((!signerHasNip44 && requiresNip44) || (!signerHasNip04 && requiresNip04)) {
|
if ((!signerHasNip44 && requiresNip44) || (!signerHasNip04 && requiresNip04)) {
|
||||||
if (!allowPermissionPrompt) {
|
try {
|
||||||
const error = new Error(
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
"Encryption permissions are required to decrypt the block list.",
|
if (!permissionResult?.ok) {
|
||||||
);
|
permissionError =
|
||||||
error.code = "user-blocklist-permission-required";
|
permissionResult?.error instanceof Error
|
||||||
error.permissionRequestDeferred = true;
|
? permissionResult.error
|
||||||
permissionError = error;
|
: new Error(
|
||||||
} else {
|
"Extension permissions are required to use the browser decryptor.",
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
} 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 nostrApi = typeof window !== "undefined" ? window?.nostr : null;
|
||||||
const decrypterOptions = {
|
|
||||||
priority: NIP07_PRIORITY.HIGH,
|
|
||||||
timeoutMs: nip07DecryptTimeoutMs,
|
|
||||||
retryMultiplier: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (nostrApi) {
|
if (nostrApi) {
|
||||||
if (typeof nostrApi.nip04?.decrypt === "function") {
|
if (typeof nostrApi.nip04?.decrypt === "function") {
|
||||||
registerDecryptor(
|
registerDecryptor(
|
||||||
@@ -1063,7 +887,7 @@ class UserBlockListManager {
|
|||||||
(payload) =>
|
(payload) =>
|
||||||
runNip07WithRetry(
|
runNip07WithRetry(
|
||||||
() => nostrApi.nip04.decrypt(normalized, payload),
|
() => nostrApi.nip04.decrypt(normalized, payload),
|
||||||
{ label: "nip04.decrypt", ...decrypterOptions },
|
{ label: "nip04.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||||
),
|
),
|
||||||
"extension",
|
"extension",
|
||||||
);
|
);
|
||||||
@@ -1080,7 +904,7 @@ class UserBlockListManager {
|
|||||||
(payload) =>
|
(payload) =>
|
||||||
runNip07WithRetry(() => nip44.decrypt(normalized, payload), {
|
runNip07WithRetry(() => nip44.decrypt(normalized, payload), {
|
||||||
label: "nip44.decrypt",
|
label: "nip44.decrypt",
|
||||||
...decrypterOptions,
|
priority: NIP07_PRIORITY.HIGH,
|
||||||
}),
|
}),
|
||||||
"extension",
|
"extension",
|
||||||
);
|
);
|
||||||
@@ -1094,7 +918,7 @@ class UserBlockListManager {
|
|||||||
(payload) =>
|
(payload) =>
|
||||||
runNip07WithRetry(() => nip44v2.decrypt(normalized, payload), {
|
runNip07WithRetry(() => nip44v2.decrypt(normalized, payload), {
|
||||||
label: "nip44.v2.decrypt",
|
label: "nip44.v2.decrypt",
|
||||||
...decrypterOptions,
|
priority: NIP07_PRIORITY.HIGH,
|
||||||
}),
|
}),
|
||||||
"extension",
|
"extension",
|
||||||
);
|
);
|
||||||
@@ -1104,7 +928,7 @@ class UserBlockListManager {
|
|||||||
(payload) =>
|
(payload) =>
|
||||||
runNip07WithRetry(
|
runNip07WithRetry(
|
||||||
() => nip44v2.decrypt(normalized, payload),
|
() => nip44v2.decrypt(normalized, payload),
|
||||||
{ label: "nip44.v2.decrypt", ...decrypterOptions },
|
{ label: "nip44.v2.decrypt", priority: NIP07_PRIORITY.HIGH },
|
||||||
),
|
),
|
||||||
"extension",
|
"extension",
|
||||||
);
|
);
|
||||||
@@ -1113,16 +937,11 @@ class UserBlockListManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permissionError) {
|
|
||||||
permissionError.signerStatus = signerStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
decryptors,
|
decryptors,
|
||||||
sources,
|
sources,
|
||||||
permissionError,
|
permissionError,
|
||||||
order: determineDecryptionOrder(event, Array.from(decryptors.keys())),
|
order: determineDecryptionOrder(event, Array.from(decryptors.keys())),
|
||||||
signerStatus,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1227,35 +1046,26 @@ class UserBlockListManager {
|
|||||||
this.blockEventCreatedAt = newestCreatedAt;
|
this.blockEventCreatedAt = newestCreatedAt;
|
||||||
|
|
||||||
// Helper to decrypt a single event
|
// Helper to decrypt a single event
|
||||||
const decryptEvent = async (ev, meta = {}) => {
|
const decryptEvent = async (ev) => {
|
||||||
if (!ev) return [];
|
if (!ev) return [];
|
||||||
const isContentEmpty = !ev.content || !ev.content.trim();
|
const isContentEmpty = !ev.content || !ev.content.trim();
|
||||||
|
|
||||||
if (isContentEmpty) {
|
if (isContentEmpty) {
|
||||||
// Handle tag-only lists (e.g. Kind 10000 with empty content but populated p tags)
|
// Handle tag-only lists (e.g. Kind 10000 with empty content but populated p tags)
|
||||||
if (Array.isArray(ev.tags) && ev.tags.length) {
|
if (Array.isArray(ev.tags) && ev.tags.length) {
|
||||||
return extractPubkeysFromTags(ev.tags, normalized, {
|
return extractPubkeysFromTags(ev.tags, normalized);
|
||||||
limit: MAX_BLOCKLIST_ENTRIES,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { decryptors, order, sources, permissionError, signerStatus } =
|
const { decryptors, order, sources, permissionError } =
|
||||||
await resolveDecryptors(ev);
|
await resolveDecryptors(ev);
|
||||||
meta.signerStatus = signerStatus;
|
|
||||||
meta.decryptorSources = Array.from(sources.entries()).map(
|
|
||||||
([scheme, source]) => ({ scheme, source }),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!decryptors.size) {
|
if (!decryptors.size) {
|
||||||
if (permissionError) {
|
if (permissionError) {
|
||||||
throw permissionError;
|
throw permissionError;
|
||||||
}
|
}
|
||||||
const error = new Error("No decryptor available for block list event.");
|
return [];
|
||||||
error.code = "user-blocklist-no-decryptor";
|
|
||||||
error.signerStatus = signerStatus;
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let decryptedText = "";
|
let decryptedText = "";
|
||||||
@@ -1279,48 +1089,27 @@ class UserBlockListManager {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseBlockListPlaintext(decryptedText, normalized, {
|
return parseBlockListPlaintext(decryptedText, normalized);
|
||||||
limit: MAX_BLOCKLIST_ENTRIES,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mergedPubkeys = new Set();
|
const mergedPubkeys = new Set();
|
||||||
let decryptionError = null;
|
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 {
|
try {
|
||||||
const decryptPromise = Promise.all([
|
const decryptPromise = Promise.all([
|
||||||
decryptEvent(newestMute, decryptMeta.mute),
|
decryptEvent(newestMute),
|
||||||
decryptEvent(newestBlock, decryptMeta.block),
|
decryptEvent(newestBlock),
|
||||||
]);
|
]);
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
const timeoutError = new Error(
|
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.code = "user-blocklist-decrypt-timeout";
|
||||||
timeoutError.signerStatus = resolveSignerStatus();
|
|
||||||
reject(timeoutError);
|
reject(timeoutError);
|
||||||
},
|
},
|
||||||
decryptTimeoutMs,
|
DECRYPT_TIMEOUT_MS,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1337,54 +1126,13 @@ class UserBlockListManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (decryptionError) {
|
if (decryptionError) {
|
||||||
if (decryptionError.code === "user-blocklist-permission-required") {
|
if (decryptionError.code === "user-blocklist-decrypt-timeout") {
|
||||||
const signerStatus =
|
|
||||||
decryptionError.signerStatus || resolveSignerStatus();
|
|
||||||
this.blockedPubkeys = previousState.blockedPubkeys;
|
this.blockedPubkeys = previousState.blockedPubkeys;
|
||||||
this.blockEventId = previousState.blockEventId;
|
this.blockEventId = previousState.blockEventId;
|
||||||
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
|
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
|
||||||
this.loaded = true;
|
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" });
|
emitStatus({ status: "settled" });
|
||||||
return;
|
this.scheduleDecryptRetry(normalized, decryptionError);
|
||||||
}
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
@@ -1395,13 +1143,10 @@ class UserBlockListManager {
|
|||||||
this.blockEventId = previousState.blockEventId;
|
this.blockEventId = previousState.blockEventId;
|
||||||
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
|
this.blockEventCreatedAt = previousState.blockEventCreatedAt;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
const signerStatus =
|
|
||||||
decryptionError.signerStatus || resolveSignerStatus();
|
|
||||||
emitStatus({
|
emitStatus({
|
||||||
status: "error",
|
status: "error",
|
||||||
error: decryptionError,
|
error: decryptionError,
|
||||||
decryptor: "mixed",
|
decryptor: "mixed",
|
||||||
signerStatus,
|
|
||||||
});
|
});
|
||||||
emitStatus({ status: "settled" });
|
emitStatus({ status: "settled" });
|
||||||
return;
|
return;
|
||||||
@@ -1437,13 +1182,9 @@ class UserBlockListManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Anchor the fetch to the service's current state to prevent desync.
|
// If we don't have local data, we must ignore the sync metadata and force a full fetch (since: 0).
|
||||||
// If we have cached data, we ask for updates since that timestamp.
|
// Otherwise, we might miss data if the metadata store says we are up-to-date but the actual list is missing.
|
||||||
// If we have no data, we force a full fetch (since=0).
|
const fetchSince = hasLocalData ? undefined : 0;
|
||||||
const fetchSince =
|
|
||||||
hasLocalData && Number.isFinite(this.blockEventCreatedAt)
|
|
||||||
? this.blockEventCreatedAt
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Concurrent incremental fetch for both kinds
|
// Concurrent incremental fetch for both kinds
|
||||||
const [muteEvents, blockEvents] = await Promise.all([
|
const [muteEvents, blockEvents] = await Promise.all([
|
||||||
@@ -1493,15 +1234,7 @@ class UserBlockListManager {
|
|||||||
}
|
}
|
||||||
const cached = profileCache.getProfileData(normalizedActorHex, "blocks");
|
const cached = profileCache.getProfileData(normalizedActorHex, "blocks");
|
||||||
if (Array.isArray(cached)) {
|
if (Array.isArray(cached)) {
|
||||||
return { blockedPubkeys: new Set(cached), createdAt: null };
|
return new Set(cached);
|
||||||
}
|
|
||||||
if (cached && typeof cached === "object") {
|
|
||||||
return {
|
|
||||||
blockedPubkeys: new Set(
|
|
||||||
Array.isArray(cached.blockedPubkeys) ? cached.blockedPubkeys : [],
|
|
||||||
),
|
|
||||||
createdAt: Number.isFinite(cached.createdAt) ? cached.createdAt : null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1511,11 +1244,11 @@ class UserBlockListManager {
|
|||||||
if (!normalizedActorHex) {
|
if (!normalizedActorHex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
profileCache.setProfileData(normalizedActorHex, "blocks", {
|
profileCache.setProfileData(
|
||||||
blockedPubkeys: Array.from(this.blockedPubkeys),
|
normalizedActorHex,
|
||||||
createdAt: this.blockEventCreatedAt,
|
"blocks",
|
||||||
version: 1,
|
Array.from(this.blockedPubkeys),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSeedState(actorHex) {
|
_getSeedState(actorHex) {
|
||||||
@@ -2056,18 +1789,16 @@ class UserBlockListManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (signer.type === "extension") {
|
if (signer.type === "extension") {
|
||||||
const permissionResult = await requestDefaultExtensionPermissions(
|
const permissionResult = await requestDefaultExtensionPermissions();
|
||||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
|
||||||
);
|
|
||||||
if (!permissionResult.ok) {
|
if (!permissionResult.ok) {
|
||||||
userLogger.warn(
|
userLogger.warn(
|
||||||
"[UserBlockList] Signer permissions denied while updating the block list.",
|
"[UserBlockList] Signer permissions denied while updating the block list.",
|
||||||
permissionResult.error,
|
permissionResult.error,
|
||||||
);
|
);
|
||||||
const err = new 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;
|
err.cause = permissionResult.error;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+109
-11
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { nostrClient } from "../nostrClientFacade.js";
|
import { nostrClient } from "../nostrClientFacade.js";
|
||||||
import { devLogger } from "./logger.js";
|
import { devLogger } from "./logger.js";
|
||||||
import { fetchProfileMetadataBatch } from "../services/profileMetadataService.js";
|
|
||||||
|
|
||||||
const DEFAULT_PROFILE_IMAGE = "assets/svg/default-profile.svg";
|
const DEFAULT_PROFILE_IMAGE = "assets/svg/default-profile.svg";
|
||||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||||
@@ -58,18 +57,117 @@ export async function batchFetchProfilesFromRelays({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileResults = await fetchProfileMetadataBatch({
|
if (!nostr?.pool || typeof nostr.pool.list !== "function") {
|
||||||
pubkeys: toFetch,
|
logger.warn(
|
||||||
nostr,
|
"[batchFetchProfiles] Relay pool is not ready; skipping profile fetch.",
|
||||||
logger,
|
);
|
||||||
defaultProfileImage,
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
profileResults.forEach((entry, pubkey) => {
|
const filter = {
|
||||||
if (!entry?.profile) {
|
kinds: [0],
|
||||||
|
authors: toFetch,
|
||||||
|
limit: toFetch.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newestProfiles = new Map();
|
||||||
|
|
||||||
|
const applyProfileEvent = (evt, relayUrl) => {
|
||||||
|
if (!evt || typeof evt !== "object") {
|
||||||
return;
|
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;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmed === "images/robot.png") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^ipfs:\/\//i.test(trimmed)) {
|
if (/^ipfs:\/\//i.test(trimmed)) {
|
||||||
return normalizeIpfsGatewayUrl(trimmed);
|
return normalizeIpfsGatewayUrl(trimmed);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-2
@@ -4,11 +4,11 @@ import { userLogger } from "./logger.js";
|
|||||||
const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:";
|
const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:";
|
||||||
const TORRENT_PROBE_STORAGE_PREFIX = "bitvid:torrentProbe:";
|
const TORRENT_PROBE_STORAGE_PREFIX = "bitvid:torrentProbe:";
|
||||||
|
|
||||||
function getUrlHealthStorageKey(eventId) {
|
export function getUrlHealthStorageKey(eventId) {
|
||||||
return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`;
|
return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTorrentProbeStorageKey(infoHash) {
|
export function getTorrentProbeStorageKey(infoHash) {
|
||||||
return `${TORRENT_PROBE_STORAGE_PREFIX}${infoHash}`;
|
return `${TORRENT_PROBE_STORAGE_PREFIX}${infoHash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -8,7 +8,7 @@ export function normalizeStoragePointer(value) {
|
|||||||
return safeTrim(value);
|
return safeTrim(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStoragePointer(value) {
|
export function parseStoragePointer(value) {
|
||||||
const normalized = normalizeStoragePointer(value);
|
const normalized = normalizeStoragePointer(value);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return null;
|
return null;
|
||||||
@@ -38,7 +38,7 @@ export function buildStoragePointerValue({ provider, prefix } = {}) {
|
|||||||
return `${normalizedProvider}:${normalizedPrefix}`;
|
return `${normalizedProvider}:${normalizedPrefix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deriveStoragePrefixFromUrl(url) {
|
export function deriveStoragePrefixFromUrl(url) {
|
||||||
const trimmed = safeTrim(url);
|
const trimmed = safeTrim(url);
|
||||||
if (!trimmed || !/^https?:\/\//i.test(trimmed)) {
|
if (!trimmed || !/^https?:\/\//i.test(trimmed)) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
Vendored
+1
-1
@@ -44,7 +44,7 @@ function normalizeTagValue(rawValue) {
|
|||||||
* @returns {string} The tag prefixed with `#` when a tag exists, otherwise an
|
* @returns {string} The tag prefixed with `#` when a tag exists, otherwise an
|
||||||
* empty string.
|
* empty string.
|
||||||
*/
|
*/
|
||||||
function formatTagDisplay(tag) {
|
export function formatTagDisplay(tag) {
|
||||||
if (typeof tag !== "string") {
|
if (typeof tag !== "string") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-15
@@ -50,7 +50,6 @@ restoreCacheSnapshot();
|
|||||||
* @property {Map<string, number>} dedupeBuckets
|
* @property {Map<string, number>} dedupeBuckets
|
||||||
* @property {number} lastSyncedAt
|
* @property {number} lastSyncedAt
|
||||||
* @property {"idle"|"hydrating"|"live"|"stale"} status
|
* @property {"idle"|"hydrating"|"live"|"stale"} status
|
||||||
* @property {boolean} partial
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -279,7 +278,6 @@ function restoreCacheSnapshot() {
|
|||||||
dedupeBuckets,
|
dedupeBuckets,
|
||||||
lastSyncedAt,
|
lastSyncedAt,
|
||||||
status: status === "hydrating" || status === "live" ? "idle" : status,
|
status: status === "hydrating" || status === "live" ? "idle" : status,
|
||||||
partial: Boolean(state.partial),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -301,7 +299,6 @@ function persistCacheSnapshot() {
|
|||||||
dedupeBuckets: Array.from(state.dedupeBuckets.entries()),
|
dedupeBuckets: Array.from(state.dedupeBuckets.entries()),
|
||||||
lastSyncedAt: state.lastSyncedAt,
|
lastSyncedAt: state.lastSyncedAt,
|
||||||
status: state.status,
|
status: state.status,
|
||||||
partial: Boolean(state.partial),
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -324,7 +321,6 @@ function ensurePointerState(key) {
|
|||||||
dedupeBuckets: new Map(),
|
dedupeBuckets: new Map(),
|
||||||
lastSyncedAt: 0,
|
lastSyncedAt: 0,
|
||||||
status: "idle",
|
status: "idle",
|
||||||
partial: false,
|
|
||||||
};
|
};
|
||||||
pointerStates.set(key, state);
|
pointerStates.set(key, state);
|
||||||
} else if (!(state.dedupeBuckets instanceof Map)) {
|
} else if (!(state.dedupeBuckets instanceof Map)) {
|
||||||
@@ -377,7 +373,6 @@ function notifyHandlers(key) {
|
|||||||
total: state.total,
|
total: state.total,
|
||||||
lastSyncedAt: state.lastSyncedAt,
|
lastSyncedAt: state.lastSyncedAt,
|
||||||
status: state.status,
|
status: state.status,
|
||||||
partial: Boolean(state.partial),
|
|
||||||
};
|
};
|
||||||
for (const handler of listeners.handlers.values()) {
|
for (const handler of listeners.handlers.values()) {
|
||||||
try {
|
try {
|
||||||
@@ -499,15 +494,6 @@ async function hydratePointer(key, listeners) {
|
|||||||
? Number(countResult.total)
|
? Number(countResult.total)
|
||||||
: null;
|
: 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) {
|
if (bestCount !== null) {
|
||||||
const state = ensurePointerState(key);
|
const state = ensurePointerState(key);
|
||||||
const shouldUpdate = bestCount > state.total || state.total === 0;
|
const shouldUpdate = bestCount > state.total || state.total === 0;
|
||||||
@@ -611,7 +597,6 @@ export function subscribeToVideoViewCount(pointerInput, handler, options = {}) {
|
|||||||
total: state.total,
|
total: state.total,
|
||||||
lastSyncedAt: state.lastSyncedAt,
|
lastSyncedAt: state.lastSyncedAt,
|
||||||
status: state.status,
|
status: state.status,
|
||||||
partial: Boolean(state.partial),
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
userLogger.warn("[viewCounter] Initial handler invocation threw:", 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 { safeDecodeURIComponent } from "./utils/safeDecode.js";
|
||||||
import { devLogger, userLogger } from "./utils/logger.js";
|
import { devLogger, userLogger } from "./utils/logger.js";
|
||||||
import { emit } from "./embedDiagnostics.js";
|
import { emit } from "./embedDiagnostics.js";
|
||||||
import { infoHashFromMagnet, webSeedsFromMagnet } from "./magnets.js";
|
|
||||||
|
|
||||||
const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]);
|
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) {
|
function normalizeTrackerList(trackers) {
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -185,8 +161,6 @@ export class TorrentClient {
|
|||||||
|
|
||||||
// Timeout for SW operations
|
// Timeout for SW operations
|
||||||
this.TIMEOUT_DURATION = 60000;
|
this.TIMEOUT_DURATION = 60000;
|
||||||
|
|
||||||
this.probeInFlight = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureClientForProbe() {
|
ensureClientForProbe() {
|
||||||
@@ -205,10 +179,9 @@ export class TorrentClient {
|
|||||||
*/
|
*/
|
||||||
async probePeers(
|
async probePeers(
|
||||||
magnetURI,
|
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) {
|
if (!magnet) {
|
||||||
return {
|
return {
|
||||||
healthy: false,
|
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 client = this.ensureClientForProbe();
|
||||||
const safeTimeout = Math.max(0, normalizeNumber(timeoutMs, 8000));
|
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 safeMaxWebConns = Math.max(1, Math.floor(normalizeNumber(maxWebConns, 2)));
|
||||||
const pollInterval =
|
const pollInterval = Math.max(
|
||||||
safePolls > 0
|
250,
|
||||||
? Math.max(1000, Math.floor(safeTimeout / Math.max(1, safePolls)))
|
Math.floor(safeTimeout / Math.max(1, safePolls))
|
||||||
: 0;
|
);
|
||||||
|
|
||||||
const startedAt =
|
const startedAt =
|
||||||
typeof performance !== "undefined" && performance?.now
|
typeof performance !== "undefined" && performance?.now
|
||||||
@@ -264,12 +230,11 @@ export class TorrentClient {
|
|||||||
|
|
||||||
emit("torrent-probe-start", { magnet: augmentedMagnet });
|
emit("torrent-probe-start", { magnet: augmentedMagnet });
|
||||||
|
|
||||||
const probePromise = new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
let torrent = null;
|
let torrent = null;
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
let pollId = null;
|
let pollId = null;
|
||||||
let pollStartId = null;
|
|
||||||
|
|
||||||
const finalize = (overrides = {}) => {
|
const finalize = (overrides = {}) => {
|
||||||
if (settled) {
|
if (settled) {
|
||||||
@@ -284,10 +249,6 @@ export class TorrentClient {
|
|||||||
clearInterval(pollId);
|
clearInterval(pollId);
|
||||||
pollId = null;
|
pollId = null;
|
||||||
}
|
}
|
||||||
if (pollStartId) {
|
|
||||||
clearTimeout(pollStartId);
|
|
||||||
pollStartId = null;
|
|
||||||
}
|
|
||||||
if (torrent) {
|
if (torrent) {
|
||||||
try {
|
try {
|
||||||
torrent.destroy({ destroyStore: true });
|
torrent.destroy({ destroyStore: true });
|
||||||
@@ -367,34 +328,16 @@ export class TorrentClient {
|
|||||||
}, safeTimeout);
|
}, safeTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safePolls > 0 && pollInterval > 0) {
|
pollId = setInterval(() => {
|
||||||
pollStartId = setTimeout(() => {
|
if (!torrent || settled) {
|
||||||
if (!torrent || settled) {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
const peers = Math.max(0, Math.floor(normalizeNumber(torrent.numPeers, 0)));
|
||||||
}
|
if (peers > 0) {
|
||||||
|
finalize({ healthy: true, peers, reason: "peer" });
|
||||||
return probePromise;
|
}
|
||||||
|
}, pollInterval);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args) {
|
log(...args) {
|
||||||
@@ -927,24 +870,9 @@ export class TorrentClient {
|
|||||||
*/
|
*/
|
||||||
async streamVideo(magnetURI, videoElement, opts = {}) {
|
async streamVideo(magnetURI, videoElement, opts = {}) {
|
||||||
try {
|
try {
|
||||||
const effectiveMagnetURI = stripXsParameter(magnetURI);
|
emit("torrent-stream-start", { magnet: 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 });
|
|
||||||
// 1) Make sure we have a WebTorrent client and a valid SW registration.
|
// 1) Make sure we have a WebTorrent client and a valid SW registration.
|
||||||
const initResult = await this.init();
|
const initResult = await this.init();
|
||||||
|
|
||||||
if (!this.client) {
|
|
||||||
throw new Error("Client destroyed during initialization");
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceWorkerReady =
|
const serviceWorkerReady =
|
||||||
!!(initResult?.serviceWorkerReady && this.swRegistration);
|
!!(initResult?.serviceWorkerReady && this.swRegistration);
|
||||||
|
|
||||||
@@ -960,18 +888,15 @@ export class TorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isFirefoxBrowser = this.isFirefox();
|
const isFirefoxBrowser = this.isFirefox();
|
||||||
const explicitUrls = Array.isArray(opts?.urlList)
|
const candidateUrls = Array.isArray(opts?.urlList)
|
||||||
? opts.urlList
|
? opts.urlList
|
||||||
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
||||||
.filter((entry) => /^https?:\/\//i.test(entry))
|
.filter((entry) => /^https?:\/\//i.test(entry))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const magnetUrls = webSeedsFromMagnet(effectiveMagnetURI);
|
|
||||||
const combinedUrls = [...new Set([...explicitUrls, ...magnetUrls])];
|
|
||||||
|
|
||||||
const chromeOptions = { strategy: "sequential" };
|
const chromeOptions = { strategy: "sequential" };
|
||||||
if (combinedUrls.length) {
|
if (candidateUrls.length) {
|
||||||
chromeOptions.urlList = combinedUrls;
|
chromeOptions.urlList = candidateUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -979,7 +904,7 @@ export class TorrentClient {
|
|||||||
if (isFirefoxBrowser) {
|
if (isFirefoxBrowser) {
|
||||||
this.log("Starting torrent download (Firefox path)");
|
this.log("Starting torrent download (Firefox path)");
|
||||||
this.client.add(
|
this.client.add(
|
||||||
effectiveMagnetURI,
|
magnetURI,
|
||||||
{ ...chromeOptions, maxWebConns: 4 },
|
{ ...chromeOptions, maxWebConns: 4 },
|
||||||
(torrent) => {
|
(torrent) => {
|
||||||
this.log("Torrent added (Firefox path):", torrent.name);
|
this.log("Torrent added (Firefox path):", torrent.name);
|
||||||
@@ -988,7 +913,7 @@ export class TorrentClient {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.log("Starting torrent download (Chrome path)");
|
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.log("Torrent added (Chrome path):", torrent.name);
|
||||||
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
|
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
|
||||||
+40
-45
@@ -2795,23 +2795,13 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
|
|||||||
});
|
});
|
||||||
const videoEvent = getZapVideoEvent();
|
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 {
|
try {
|
||||||
const result = await splitAndZap(
|
const result = await splitAndZap(
|
||||||
{
|
{
|
||||||
videoEvent,
|
videoEvent,
|
||||||
amountSats: context.shares.total,
|
amountSats: context.shares.total,
|
||||||
walletSettings: settings
|
walletSettings: settings,
|
||||||
|
overrideFee
|
||||||
},
|
},
|
||||||
dependencies
|
dependencies
|
||||||
);
|
);
|
||||||
@@ -2832,21 +2822,6 @@ async function runZapAttempt({ amount, overrideFee = null, walletSettings }) {
|
|||||||
error
|
error
|
||||||
);
|
);
|
||||||
throw 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__;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2873,25 +2848,37 @@ async function executePendingRetry({ walletSettings }) {
|
|||||||
|
|
||||||
const aggregatedReceipts = [];
|
const aggregatedReceipts = [];
|
||||||
const aggregatedTracker = [];
|
const aggregatedTracker = [];
|
||||||
|
const nextRetryShares = [];
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
for (const share of shares) {
|
const results = await Promise.allSettled(
|
||||||
const overrideFee = share.type === "platform" ? 100 : 0;
|
shares.map((share) => {
|
||||||
try {
|
const overrideFee = share.type === "platform" ? 100 : 0;
|
||||||
const attempt = await runZapAttempt({
|
return runZapAttempt({
|
||||||
amount: share.amount,
|
amount: share.amount,
|
||||||
overrideFee,
|
overrideFee,
|
||||||
walletSettings
|
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) {
|
if (!attempt) {
|
||||||
setZapStatus("", "neutral");
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (attempt?.result?.receipts) {
|
if (attempt?.result?.receipts) {
|
||||||
aggregatedReceipts.push(...attempt.result.receipts);
|
aggregatedReceipts.push(...attempt.result.receipts);
|
||||||
} else if (Array.isArray(attempt?.shareTracker)) {
|
} else if (Array.isArray(attempt?.shareTracker)) {
|
||||||
aggregatedTracker.push(...attempt.shareTracker);
|
aggregatedTracker.push(...attempt.shareTracker);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
|
const error = result.reason;
|
||||||
|
lastError = error;
|
||||||
const tracker = Array.isArray(error?.__zapShareTracker)
|
const tracker = Array.isArray(error?.__zapShareTracker)
|
||||||
? error.__zapShareTracker
|
? error.__zapShareTracker
|
||||||
: [];
|
: [];
|
||||||
@@ -2912,20 +2899,28 @@ async function executePendingRetry({ walletSettings }) {
|
|||||||
const failureShares = tracker.filter(
|
const failureShares = tracker.filter(
|
||||||
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
(entry) => entry && entry.status !== "success" && entry.amount > 0
|
||||||
);
|
);
|
||||||
markZapRetryPending(failureShares.length ? failureShares : [share]);
|
if (failureShares.length) {
|
||||||
const message = error?.message || "Retry failed.";
|
nextRetryShares.push(...failureShares);
|
||||||
setZapStatus(message, "error");
|
} else {
|
||||||
app?.showError?.(message);
|
nextRetryShares.push(share);
|
||||||
renderZapReceipts(
|
}
|
||||||
aggregatedTracker.length ? aggregatedTracker : tracker,
|
|
||||||
{
|
|
||||||
partial: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
renderZapReceipts(
|
||||||
aggregatedReceipts.length ? aggregatedReceipts : aggregatedTracker,
|
aggregatedReceipts.length ? aggregatedReceipts : aggregatedTracker,
|
||||||
{ partial: false }
|
{ partial: false }
|
||||||
|
|||||||
@@ -261,11 +261,10 @@ export function createResolvePostedAtStage({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const task of tasks) {
|
const results = await Promise.allSettled(tasks.map((task) => task()));
|
||||||
try {
|
for (const result of results) {
|
||||||
await task();
|
if (result.status === "rejected") {
|
||||||
} catch (error) {
|
context?.log?.(`[${stageName}] resolution task failed`, result.reason);
|
||||||
context?.log?.(`[${stageName}] resolution task failed`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+54
-40
@@ -6737,7 +6737,13 @@ export class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chunkSize = 100;
|
const chunkSize = 100;
|
||||||
for (let index = 0; index < identifierRecords.length; index += chunkSize) {
|
const publishPromises = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
let index = 0;
|
||||||
|
index < identifierRecords.length;
|
||||||
|
index += chunkSize
|
||||||
|
) {
|
||||||
const chunk = identifierRecords.slice(index, index + chunkSize);
|
const chunk = identifierRecords.slice(index, index + chunkSize);
|
||||||
const eventIds = chunk
|
const eventIds = chunk
|
||||||
.filter((record) => record.type === "e")
|
.filter((record) => record.type === "e")
|
||||||
@@ -6757,50 +6763,58 @@ export class NostrClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const signedDelete = await queueSignEvent(signer, deleteEvent);
|
const signedDelete = await queueSignEvent(signer, deleteEvent);
|
||||||
const publishResults = await publishEventToRelays(
|
|
||||||
this.pool,
|
|
||||||
this.relays,
|
|
||||||
signedDelete,
|
|
||||||
);
|
|
||||||
const publishSummary = summarizePublishResults(publishResults);
|
|
||||||
|
|
||||||
publishSummary.accepted.forEach(({ url }) =>
|
const publishPromise = (async () => {
|
||||||
devLogger.log(`Delete event published to ${url}`),
|
const publishResults = await publishEventToRelays(
|
||||||
);
|
this.pool,
|
||||||
|
this.relays,
|
||||||
|
signedDelete,
|
||||||
|
);
|
||||||
|
const publishSummary = summarizePublishResults(publishResults);
|
||||||
|
|
||||||
if (publishSummary.failed.length) {
|
publishSummary.accepted.forEach(({ url }) =>
|
||||||
publishSummary.failed.forEach(({ url, error: relayError }) => {
|
devLogger.log(`Delete event published to ${url}`)
|
||||||
const reason =
|
);
|
||||||
relayError instanceof Error
|
|
||||||
? relayError.message
|
|
||||||
: relayError
|
|
||||||
? String(relayError)
|
|
||||||
: "publish failed";
|
|
||||||
userLogger.warn(
|
|
||||||
`[nostr] Delete event not accepted by ${url}: ${reason}`,
|
|
||||||
relayError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signedDelete?.id) {
|
if (publishSummary.failed.length) {
|
||||||
this.rawEvents.set(signedDelete.id, signedDelete);
|
publishSummary.failed.forEach(({ url, error: relayError }) => {
|
||||||
}
|
const reason =
|
||||||
|
relayError instanceof Error
|
||||||
|
? relayError.message
|
||||||
|
: relayError
|
||||||
|
? String(relayError)
|
||||||
|
: "publish failed";
|
||||||
|
userLogger.warn(
|
||||||
|
`[nostr] Delete event not accepted by ${url}: ${reason}`,
|
||||||
|
relayError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deleteSummaries.push({
|
if (signedDelete?.id) {
|
||||||
event: signedDelete,
|
this.rawEvents.set(signedDelete.id, signedDelete);
|
||||||
publishResults,
|
}
|
||||||
summary: publishSummary,
|
|
||||||
identifiers: {
|
return {
|
||||||
events: chunk
|
event: signedDelete,
|
||||||
.filter((record) => record.type === "e")
|
publishResults,
|
||||||
.map((record) => record.value),
|
summary: publishSummary,
|
||||||
addresses: chunk
|
identifiers: {
|
||||||
.filter((record) => record.type === "a")
|
events: chunk
|
||||||
.map((record) => record.value),
|
.filter((record) => record.type === "e")
|
||||||
},
|
.map((record) => record.value),
|
||||||
});
|
addresses: chunk
|
||||||
|
.filter((record) => record.type === "a")
|
||||||
|
.map((record) => record.value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
publishPromises.push(publishPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chunkResults = await Promise.all(publishPromises);
|
||||||
|
deleteSummaries.push(...chunkResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveLocalData("delete-events", { immediate: true });
|
this.saveLocalData("delete-events", { immediate: true });
|
||||||
|
|||||||
+12
-2
@@ -520,7 +520,13 @@ async function processShare({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function splitAndZap(
|
export async function splitAndZap(
|
||||||
{ videoEvent, amountSats, comment = "", walletSettings } = {},
|
{
|
||||||
|
videoEvent,
|
||||||
|
amountSats,
|
||||||
|
comment = "",
|
||||||
|
walletSettings,
|
||||||
|
overrideFee = null,
|
||||||
|
} = {},
|
||||||
dependencies = {}
|
dependencies = {}
|
||||||
) {
|
) {
|
||||||
if (!videoEvent || typeof videoEvent !== "object") {
|
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.");
|
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 platformShare = Math.floor((amount * platformFee) / 100);
|
||||||
const creatorShare = amount - platformShare;
|
const creatorShare = amount - platformShare;
|
||||||
|
|
||||||
|
|||||||
+2
-19
@@ -398,7 +398,7 @@ export default class ZapController {
|
|||||||
|
|
||||||
this.resetRetryState();
|
this.resetRetryState();
|
||||||
this.modalZapCommentValue = "";
|
this.modalZapCommentValue = "";
|
||||||
this.videoModal?.setZapComment("");
|
this.videoModal?.resetZapForm({ amount: "", comment: "" });
|
||||||
this.videoModal?.setZapCompleted(true);
|
this.videoModal?.setZapCompleted(true);
|
||||||
this.applyDefaultAmount();
|
this.applyDefaultAmount();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -881,13 +881,6 @@ export default class ZapController {
|
|||||||
});
|
});
|
||||||
const videoEvent = this.getActiveVideoEvent();
|
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 {
|
try {
|
||||||
const result = await this.splitAndZap(
|
const result = await this.splitAndZap(
|
||||||
{
|
{
|
||||||
@@ -895,6 +888,7 @@ export default class ZapController {
|
|||||||
amountSats: context.shares.total,
|
amountSats: context.shares.total,
|
||||||
comment: typeof comment === "string" ? comment : "",
|
comment: typeof comment === "string" ? comment : "",
|
||||||
walletSettings: settings,
|
walletSettings: settings,
|
||||||
|
overrideFee,
|
||||||
},
|
},
|
||||||
dependencies
|
dependencies
|
||||||
);
|
);
|
||||||
@@ -937,17 +931,6 @@ export default class ZapController {
|
|||||||
error
|
error
|
||||||
);
|
);
|
||||||
throw 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__;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[build]
|
[build]
|
||||||
command = "npm run build"
|
command = "npm run build"
|
||||||
publish = "."
|
publish = "dist"
|
||||||
Generated
+6
-116
@@ -11,27 +11,27 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.7.4",
|
"@floating-ui/dom": "^1.7.4",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
|
"esbuild": "^0.25.12",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^27.4.0",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@playwright/test": "^1.58.0",
|
"@playwright/test": "^1.58.0",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
|
||||||
"autoprefixer": "^10.4.23",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.25.12",
|
|
||||||
"fake-indexeddb": "^6.2.5",
|
"fake-indexeddb": "^6.2.5",
|
||||||
"nostr-tools": "^2.19.4",
|
"nostr-tools": "^2.19.4",
|
||||||
"pixelmatch": "^5.3.0",
|
"pixelmatch": "^5.3.0",
|
||||||
"playwright": "^1.58.0",
|
"playwright": "^1.58.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postcss": "^8.4.41",
|
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"serve": "^14.2.5",
|
"serve": "^14.2.5",
|
||||||
"stylelint": "^16.26.1",
|
"stylelint": "^16.26.1"
|
||||||
"tailwindcss": "^3.4.19"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@acemir/cssom": {
|
"node_modules/@acemir/cssom": {
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -302,7 +301,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -319,7 +317,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -336,7 +333,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -353,7 +349,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -370,7 +365,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -387,7 +381,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -404,7 +397,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -421,7 +413,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -438,7 +429,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -455,7 +445,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -472,7 +461,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -489,7 +477,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -506,7 +493,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -523,7 +509,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -540,7 +525,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -557,7 +541,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -574,7 +557,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -591,7 +573,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -608,7 +589,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -625,7 +605,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -642,7 +621,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -659,7 +637,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -676,7 +653,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -693,7 +669,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -710,7 +685,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -727,7 +701,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -783,7 +756,6 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -794,7 +766,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -804,14 +775,12 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -895,7 +864,6 @@
|
|||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
@@ -909,7 +877,6 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
@@ -919,7 +886,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
@@ -1043,7 +1009,6 @@
|
|||||||
"version": "0.5.19",
|
"version": "0.5.19",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||||
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-selector-parser": "6.0.10"
|
"postcss-selector-parser": "6.0.10"
|
||||||
@@ -1173,14 +1138,12 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -1215,7 +1178,6 @@
|
|||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
@@ -1249,7 +1211,6 @@
|
|||||||
"version": "10.4.23",
|
"version": "10.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||||
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1293,7 +1254,6 @@
|
|||||||
"version": "2.9.18",
|
"version": "2.9.18",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
||||||
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
|
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
@@ -1312,7 +1272,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -1372,7 +1331,6 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.1.1"
|
"fill-range": "^7.1.1"
|
||||||
@@ -1385,7 +1343,6 @@
|
|||||||
"version": "4.28.1",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1466,7 +1423,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -1476,7 +1432,6 @@
|
|||||||
"version": "1.0.30001766",
|
"version": "1.0.30001766",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||||
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1530,7 +1485,6 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anymatch": "~3.1.2",
|
"anymatch": "~3.1.2",
|
||||||
@@ -1613,7 +1567,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -1794,7 +1747,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"cssesc": "bin/cssesc"
|
"cssesc": "bin/cssesc"
|
||||||
@@ -1877,7 +1829,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/dir-glob": {
|
"node_modules/dir-glob": {
|
||||||
@@ -1897,7 +1848,6 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
@@ -1911,7 +1861,6 @@
|
|||||||
"version": "1.5.279",
|
"version": "1.5.279",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
|
||||||
"integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==",
|
"integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
@@ -1957,7 +1906,6 @@
|
|||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1999,7 +1947,6 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -2057,7 +2004,6 @@
|
|||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
@@ -2084,7 +2030,6 @@
|
|||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
@@ -2104,7 +2049,6 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
@@ -2136,7 +2080,6 @@
|
|||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
@@ -2150,7 +2093,6 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -2165,7 +2107,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -2188,7 +2129,6 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
@@ -2303,7 +2243,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -2433,7 +2372,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
@@ -2446,7 +2384,6 @@
|
|||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.2"
|
"hasown": "^2.0.2"
|
||||||
@@ -2478,7 +2415,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -2498,7 +2434,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
@@ -2511,7 +2446,6 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
@@ -2583,7 +2517,6 @@
|
|||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
@@ -2705,7 +2638,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -2718,7 +2650,6 @@
|
|||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.truncate": {
|
"node_modules/lodash.truncate": {
|
||||||
@@ -2778,7 +2709,6 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
@@ -2788,7 +2718,6 @@
|
|||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
@@ -2874,7 +2803,6 @@
|
|||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"any-promise": "^1.0.0",
|
"any-promise": "^1.0.0",
|
||||||
@@ -2886,7 +2814,6 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2915,14 +2842,12 @@
|
|||||||
"version": "2.0.27",
|
"version": "2.0.27",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -2989,7 +2914,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -2999,7 +2923,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -3096,7 +3019,6 @@
|
|||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
@@ -3120,14 +3042,12 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -3140,7 +3060,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -3150,7 +3069,6 @@
|
|||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||||
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -3225,7 +3143,6 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3254,7 +3171,6 @@
|
|||||||
"version": "15.1.0",
|
"version": "15.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.0.0",
|
"postcss-value-parser": "^4.0.0",
|
||||||
@@ -3272,7 +3188,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
||||||
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3298,7 +3213,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3341,7 +3255,6 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3367,7 +3280,6 @@
|
|||||||
"version": "6.1.2",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
@@ -3415,7 +3327,6 @@
|
|||||||
"version": "6.0.10",
|
"version": "6.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
@@ -3429,7 +3340,6 @@
|
|||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
@@ -3561,7 +3471,6 @@
|
|||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3608,7 +3517,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
@@ -3618,7 +3526,6 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
@@ -3664,7 +3571,6 @@
|
|||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-core-module": "^2.16.1",
|
"is-core-module": "^2.16.1",
|
||||||
@@ -3695,7 +3601,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"iojs": ">=1.0.0",
|
"iojs": ">=1.0.0",
|
||||||
@@ -3706,7 +3611,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4094,7 +3998,6 @@
|
|||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||||
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
@@ -4147,7 +4050,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -4234,7 +4136,6 @@
|
|||||||
"version": "3.4.19",
|
"version": "3.4.19",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
@@ -4272,7 +4173,6 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
@@ -4285,7 +4185,6 @@
|
|||||||
"version": "6.1.2",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
@@ -4299,7 +4198,6 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"any-promise": "^1.0.0"
|
"any-promise": "^1.0.0"
|
||||||
@@ -4309,7 +4207,6 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"thenify": ">= 3.1.0 < 4"
|
"thenify": ">= 3.1.0 < 4"
|
||||||
@@ -4322,7 +4219,6 @@
|
|||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -4339,7 +4235,6 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -4357,7 +4252,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -4388,7 +4282,6 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
@@ -4425,7 +4318,6 @@
|
|||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
@@ -4445,7 +4337,6 @@
|
|||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -4497,7 +4388,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
|
|||||||
+9
-9
@@ -4,10 +4,10 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run build && npx serve",
|
"start": "npm run build && npx serve dist",
|
||||||
"serve": "npx serve",
|
"serve": "npx serve",
|
||||||
"build": "npm run build:css",
|
"build": "node scripts/build-dist.mjs",
|
||||||
"build:css": "npx tailwindcss -c tailwind.config.cjs -i css/tailwind.source.css -o css/tailwind.generated.css --minify",
|
"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:bundle": "node scripts/build-beacon.mjs",
|
||||||
"build:beacon": "npm run build:beacon:bundle && npm run lint:inline-styles",
|
"build:beacon": "npm run build:beacon:bundle && npm run lint:inline-styles",
|
||||||
"ci:beacon": "npm run build:beacon",
|
"ci:beacon": "npm run build:beacon",
|
||||||
@@ -34,27 +34,27 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@playwright/test": "^1.58.0",
|
"@playwright/test": "^1.58.0",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
|
||||||
"autoprefixer": "^10.4.23",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.25.12",
|
|
||||||
"fake-indexeddb": "^6.2.5",
|
"fake-indexeddb": "^6.2.5",
|
||||||
"nostr-tools": "^2.19.4",
|
"nostr-tools": "^2.19.4",
|
||||||
"pixelmatch": "^5.3.0",
|
"pixelmatch": "^5.3.0",
|
||||||
"playwright": "^1.58.0",
|
"playwright": "^1.58.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postcss": "^8.4.41",
|
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"serve": "^14.2.5",
|
"serve": "^14.2.5",
|
||||||
"stylelint": "^16.26.1",
|
"stylelint": "^16.26.1"
|
||||||
"tailwindcss": "^3.4.19"
|
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.7.4",
|
"@floating-ui/dom": "^1.7.4",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
|
"esbuild": "^0.25.12",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^27.4.0",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
@@ -313,7 +313,7 @@ test("NIP-07 login falls back when structured permissions fail", async () => {
|
|||||||
const pubkey = result.pubkey;
|
const pubkey = result.pubkey;
|
||||||
assert.equal(pubkey, HEX_PUBKEY);
|
assert.equal(pubkey, HEX_PUBKEY);
|
||||||
assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads");
|
assert.ok(env.enableCalls.length >= 2, "should retry with alternate payloads");
|
||||||
const [objectCall, stringCall] = env.enableCalls;
|
const [objectCall, stringCall, plainCall] = env.enableCalls;
|
||||||
assert.ok(Array.isArray(objectCall?.permissions));
|
assert.ok(Array.isArray(objectCall?.permissions));
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof objectCall.permissions[0],
|
typeof objectCall.permissions[0],
|
||||||
|
|||||||
@@ -143,7 +143,15 @@ if (typeof globalThis.WebSocket === "undefined") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalThis.document = documentStub;
|
globalThis.document = documentStub;
|
||||||
globalThis.navigator = windowStub.navigator;
|
try {
|
||||||
|
globalThis.navigator = windowStub.navigator;
|
||||||
|
} catch (error) {
|
||||||
|
Object.defineProperty(globalThis, "navigator", {
|
||||||
|
value: windowStub.navigator,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
globalThis.location = windowStub.location;
|
globalThis.location = windowStub.location;
|
||||||
|
|
||||||
if (typeof globalThis.fetch === "undefined") {
|
if (typeof globalThis.fetch === "undefined") {
|
||||||
@@ -628,8 +636,8 @@ await (async () => {
|
|||||||
).length;
|
).length;
|
||||||
assert.equal(
|
assert.equal(
|
||||||
warningsAfter,
|
warningsAfter,
|
||||||
initialWarningCount,
|
initialWarningCount + 1,
|
||||||
"skipped validation should not emit new warnings",
|
"skipped validation should not emit new warnings (except for the initial 'Sending' state)",
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastStatus = modalStub.statusMessages[modalStub.statusMessages.length - 1];
|
const lastStatus = modalStub.statusMessages[modalStub.statusMessages.length - 1];
|
||||||
|
|||||||
Reference in new issue
Block a user