mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Align video modal reactions row
This commit is contained in:
1 parent
7a20a32bf1
commit
4e108f3e43
5 files changed
+1010
-162
No files matched your search
+254
-161
@@ -85,183 +85,104 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-modal__actions-row flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
id="copyMagnetBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-label="Copy magnet"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.374 20.235c2.444-2.981 6.626-8.157 6.626-8.157l-3.846-3.092s-2.857 3.523-6.571 8.097c-4.312 5.312-11.881-2.41-6.671-6.671 4.561-3.729 8.097-6.57 8.097-6.57l-3.092-3.842s-5.173 4.181-8.157 6.621c-2.662 2.175-3.76 4.749-3.76 7.24 0 5.254 4.867 10.139 10.121 10.139 2.487 0 5.064-1.095 7.253-3.765zm4.724-7.953l-1.699 2.111-1.74-1.397 1.701-2.114 1.738 1.4zm-10.386-10.385l1.4 1.738-2.113 1.701-1.397-1.74 2.11-1.699z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">Copy magnet</span>
|
||||
</button>
|
||||
<div class="popover" data-modal-zap-wrapper>
|
||||
<button
|
||||
id="modalZapBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-label="Send a zap"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded="false"
|
||||
aria-controls="modalZapDialog"
|
||||
>
|
||||
<div class="video-modal__actions-row flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="video-modal__reactions flex flex-wrap items-center gap-3 sm:gap-4">
|
||||
<div class="video-modal__reaction-buttons inline-flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
id="modalLikeBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button video-modal__reaction-button focus-ring accent-action-button"
|
||||
data-reaction="+"
|
||||
aria-pressed="false"
|
||||
aria-label="Like this video"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<polygon
|
||||
points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7 10v12H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2Z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7 10 11.34 2.67A2 2 0 0 1 13.14 2H14a2 2 0 0 1 2 2v4h3.37a2 2 0 0 1 1.97 2.5l-1.1 5A2 2 0 0 1 18.3 18H11"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">Zap</span>
|
||||
</button>
|
||||
<span class="video-modal__action-label">Like</span>
|
||||
<span class="video-modal__reaction-count" data-reaction-like-count>0</span>
|
||||
</button>
|
||||
<button
|
||||
id="modalDislikeBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button video-modal__reaction-button focus-ring accent-action-button"
|
||||
data-reaction="-"
|
||||
aria-pressed="false"
|
||||
aria-label="Dislike this video"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17 14V2h3a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17 14 12.66 21.33A2 2 0 0 1 10.86 22H10a2 2 0 0 1-2-2v-4H4.63a2 2 0 0 1-1.97-2.5l1.1-5A2 2 0 0 1 4.73 8H13"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">Dislike</span>
|
||||
<span class="video-modal__reaction-count" data-reaction-dislike-count>0</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="modalZapDialog"
|
||||
class="popover__panel card w-72 max-w-popover-safe px-5 py-4"
|
||||
data-variant="zap"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
data-state="closed"
|
||||
hidden
|
||||
class="video-modal__reaction-meter"
|
||||
data-reaction-meter
|
||||
role="meter"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="0"
|
||||
aria-valuetext="No reactions yet"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div class="mb-4 flex items-center justify-between gap-4">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Send a zap
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
id="modalZapCloseBtn"
|
||||
class="focus-ring inline-flex h-8 w-8 items-center justify-center rounded-full text-muted transition hover:text-text"
|
||||
aria-label="Close zap dialog"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
id="modalZapWalletPrompt"
|
||||
class="mb-3 text-sm text-muted"
|
||||
aria-hidden="true"
|
||||
hidden
|
||||
>
|
||||
Connect a wallet in
|
||||
<button
|
||||
type="button"
|
||||
id="modalZapWalletLink"
|
||||
class="focus-ring inline-flex items-center gap-1 text-sm font-semibold text-info underline decoration-dotted underline-offset-2 hover:text-info-strong"
|
||||
>
|
||||
Wallet Connect settings
|
||||
</button>
|
||||
to send zaps.
|
||||
</p>
|
||||
<form id="modalZapForm" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
for="modalZapAmountInput"
|
||||
class="text-xs font-semibold uppercase tracking-wide text-muted"
|
||||
>
|
||||
Zap amount (sats)
|
||||
</label>
|
||||
<input
|
||||
id="modalZapAmountInput"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
class="input"
|
||||
placeholder="Enter sats"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
for="modalZapCommentInput"
|
||||
class="text-xs font-semibold uppercase tracking-wide text-muted"
|
||||
>
|
||||
Comment (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="modalZapCommentInput"
|
||||
rows="2"
|
||||
maxlength="280"
|
||||
class="input resize-y"
|
||||
placeholder="Include a note with your zap"
|
||||
></textarea>
|
||||
</div>
|
||||
<p
|
||||
id="modalZapSplitSummary"
|
||||
class="text-sm text-muted"
|
||||
aria-live="polite"
|
||||
>
|
||||
Enter an amount to view the split.
|
||||
</p>
|
||||
<p
|
||||
id="modalZapStatus"
|
||||
class="text-xs text-muted"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></p>
|
||||
<ul
|
||||
id="modalZapReceipts"
|
||||
class="space-y-2 text-xs text-muted"
|
||||
></ul>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
id="modalZapSendBtn"
|
||||
class="btn"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<span class="video-modal__reaction-meter-label" data-reaction-meter-label>No reactions yet</span>
|
||||
<div class="video-modal__reaction-meter-track" aria-hidden="true">
|
||||
<div
|
||||
class="video-modal__reaction-meter-fill"
|
||||
data-reaction-meter-fill
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="sr-only" data-reaction-meter-sr>No reactions yet</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="shareBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-label="Share video"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm11.122 12.065A4 4 0 1 1 24 20a4 4 0 0 1-7.878-.935l-5.488-2.927a6.009 6.009 0 0 1-.943 1.764l5.488 2.927ZM24 4a4 4 0 1 1-7.878.935l-5.488 2.927a6.01 6.01 0 0 1-.943-1.764l5.488-2.927A4 4 0 0 1 24 4Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">Share</span>
|
||||
</button>
|
||||
<div class="relative" data-more-menu-wrapper="true">
|
||||
<div class="video-modal__actions-group flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
id="modalMoreBtn"
|
||||
id="copyMagnetBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-label="More actions"
|
||||
data-more-dropdown="modal"
|
||||
aria-label="Copy magnet"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
@@ -269,12 +190,184 @@
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<circle cx="5" cy="12" r="2" fill="currentColor" />
|
||||
<circle cx="12" cy="12" r="2" fill="currentColor" />
|
||||
<circle cx="19" cy="12" r="2" fill="currentColor" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.374 20.235c2.444-2.981 6.626-8.157 6.626-8.157l-3.846-3.092s-2.857 3.523-6.571 8.097c-4.312 5.312-11.881-2.41-6.671-6.671 4.561-3.729 8.097-6.57 8.097-6.57l-3.092-3.842s-5.173 4.181-8.157 6.621c-2.662 2.175-3.76 4.749-3.76 7.24 0 5.254 4.867 10.139 10.121 10.139 2.487 0 5.064-1.095 7.253-3.765zm4.724-7.953l-1.699 2.111-1.74-1.397 1.701-2.114 1.738 1.4zm-10.386-10.385l1.4 1.738-2.113 1.701-1.397-1.74 2.11-1.699z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">More</span>
|
||||
<span class="video-modal__action-label">Copy magnet</span>
|
||||
</button>
|
||||
<div class="popover" data-modal-zap-wrapper>
|
||||
<button
|
||||
id="modalZapBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-label="Send a zap"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded="false"
|
||||
aria-controls="modalZapDialog"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<polygon
|
||||
points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">Zap</span>
|
||||
</button>
|
||||
<div
|
||||
id="modalZapDialog"
|
||||
class="popover__panel card w-72 max-w-popover-safe px-5 py-4"
|
||||
data-variant="zap"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
data-state="closed"
|
||||
hidden
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div class="mb-4 flex items-center justify-between gap-4">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Send a zap
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
id="modalZapCloseBtn"
|
||||
class="focus-ring inline-flex h-8 w-8 items-center justify-center rounded-full text-muted transition hover:text-text"
|
||||
aria-label="Close zap dialog"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
id="modalZapWalletPrompt"
|
||||
class="mb-3 text-sm text-muted"
|
||||
aria-hidden="true"
|
||||
hidden
|
||||
>
|
||||
Connect a wallet in
|
||||
<button
|
||||
type="button"
|
||||
id="modalZapWalletLink"
|
||||
class="focus-ring inline-flex items-center gap-1 text-sm font-semibold text-info underline decoration-dotted underline-offset-2 hover:text-info-strong"
|
||||
>
|
||||
Wallet Connect settings
|
||||
</button>
|
||||
to send zaps.
|
||||
</p>
|
||||
<form id="modalZapForm" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
for="modalZapAmountInput"
|
||||
class="text-xs font-semibold uppercase tracking-wide text-muted"
|
||||
>
|
||||
Zap amount (sats)
|
||||
</label>
|
||||
<input
|
||||
id="modalZapAmountInput"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
class="input"
|
||||
placeholder="Enter sats"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
for="modalZapCommentInput"
|
||||
class="text-xs font-semibold uppercase tracking-wide text-muted"
|
||||
>
|
||||
Comment (optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="modalZapCommentInput"
|
||||
rows="2"
|
||||
maxlength="280"
|
||||
class="input resize-y"
|
||||
placeholder="Include a note with your zap"
|
||||
></textarea>
|
||||
</div>
|
||||
<p
|
||||
id="modalZapSplitSummary"
|
||||
class="text-sm text-muted"
|
||||
aria-live="polite"
|
||||
>
|
||||
Enter an amount to view the split.
|
||||
</p>
|
||||
<p
|
||||
id="modalZapStatus"
|
||||
class="text-xs text-muted"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></p>
|
||||
<ul
|
||||
id="modalZapReceipts"
|
||||
class="space-y-2 text-xs text-muted"
|
||||
></ul>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
id="modalZapSendBtn"
|
||||
class="btn"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="shareBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-label="Share video"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm11.122 12.065A4 4 0 1 1 24 20a4 4 0 0 1-7.878-.935l-5.488-2.927a6.009 6.009 0 0 1-.943 1.764l5.488 2.927ZM24 4a4 4 0 1 1-7.878.935l-5.488 2.927a6.01 6.01 0 0 1-.943-1.764l5.488-2.927A4 4 0 0 1 24 4Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="video-modal__action-label">Share</span>
|
||||
</button>
|
||||
<div class="relative" data-more-menu-wrapper="true">
|
||||
<button
|
||||
id="modalMoreBtn"
|
||||
type="button"
|
||||
class="video-modal__action-button focus-ring accent-action-button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-label="More actions"
|
||||
data-more-dropdown="modal"
|
||||
>
|
||||
<svg
|
||||
class="video-modal__action-icon icon-image"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<circle cx="5" cy="12" r="2" fill="currentColor" />
|
||||
<circle cx="12" cy="12" r="2" fill="currentColor" />
|
||||
<circle cx="19" cy="12" r="2" fill="currentColor" />
|
||||
</svg>
|
||||
<span class="video-modal__action-label">More</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+81
-1
@@ -1725,7 +1725,87 @@
|
||||
}
|
||||
|
||||
.video-modal__actions-row {
|
||||
@apply flex flex-wrap items-center gap-3;
|
||||
@apply flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between;
|
||||
}
|
||||
|
||||
.video-modal__reactions {
|
||||
@apply flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4;
|
||||
}
|
||||
|
||||
.video-modal__reaction-buttons {
|
||||
@apply inline-flex flex-wrap items-center gap-2;
|
||||
}
|
||||
|
||||
.video-modal__reaction-button {
|
||||
--video-modal-reaction-active-bg: color-mix(
|
||||
in srgb,
|
||||
var(--color-accent) 22%,
|
||||
var(--surface-overlay-panel)
|
||||
);
|
||||
--video-modal-reaction-active-border: color-mix(
|
||||
in srgb,
|
||||
var(--color-accent) 38%,
|
||||
var(--border-overlay)
|
||||
);
|
||||
}
|
||||
|
||||
.video-modal__reaction-button[aria-pressed="true"] {
|
||||
color: var(--color-accent-strong);
|
||||
background-color: var(--video-modal-reaction-active-bg);
|
||||
border-color: var(--video-modal-reaction-active-border);
|
||||
}
|
||||
|
||||
.video-modal__reaction-button[aria-pressed="true"]
|
||||
.video-modal__reaction-count {
|
||||
color: var(--color-accent-strong);
|
||||
}
|
||||
|
||||
.video-modal__reaction-count {
|
||||
@apply text-sm font-semibold text-muted;
|
||||
}
|
||||
|
||||
.video-modal__reaction-meter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 var(--menu-min-width);
|
||||
max-inline-size: 100%;
|
||||
}
|
||||
|
||||
.video-modal__reaction-meter-label {
|
||||
@apply text-sm font-medium text-muted;
|
||||
min-inline-size: 6rem;
|
||||
}
|
||||
|
||||
.video-modal__reaction-meter-track {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
inline-size: 100%;
|
||||
block-size: var(--space-xs);
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--color-border-muted);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-modal__reaction-meter-fill {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 0%;
|
||||
max-inline-size: 100%;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
var(--color-accent) 0%,
|
||||
var(--color-accent-strong) 100%
|
||||
);
|
||||
transition: inline-size var(--motion-duration-base)
|
||||
var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.video-modal__actions-group {
|
||||
@apply flex flex-wrap items-center gap-3 justify-end;
|
||||
}
|
||||
|
||||
.video-modal__action-button {
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
truncateMiddle,
|
||||
formatShortNpub,
|
||||
} from "./utils/formatters.js";
|
||||
import reactionCounter from "./reactionCounter.js";
|
||||
import {
|
||||
escapeHTML as escapeHtml,
|
||||
removeTrackingScripts,
|
||||
@@ -722,6 +723,14 @@ class Application {
|
||||
"video:share",
|
||||
this.boundVideoModalShareHandler
|
||||
);
|
||||
this.boundVideoModalReactionHandler = (event) => {
|
||||
const detail = event?.detail || {};
|
||||
this.handleVideoReaction(detail);
|
||||
};
|
||||
this.videoModal.addEventListener(
|
||||
"video:reaction",
|
||||
this.boundVideoModalReactionHandler
|
||||
);
|
||||
this.boundVideoModalContextActionHandler = (event) => {
|
||||
const detail = event?.detail || {};
|
||||
const action = typeof detail.action === "string" ? detail.action : "";
|
||||
@@ -878,6 +887,13 @@ class Application {
|
||||
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
|
||||
this.videoList = null;
|
||||
this.modalViewCountUnsub = null;
|
||||
this.modalReactionUnsub = null;
|
||||
this.modalReactionPointerKey = null;
|
||||
this.modalReactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
|
||||
// Videos stored as a Map (key=event.id)
|
||||
this.videosMap = this.nostrService.getVideosMap();
|
||||
@@ -1747,6 +1763,123 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
resetModalReactionState() {
|
||||
this.modalReactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
if (this.videoModal?.updateReactionSummary) {
|
||||
this.videoModal.updateReactionSummary({
|
||||
total: 0,
|
||||
counts: { "+": 0, "-": 0 },
|
||||
userReaction: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
teardownModalReactionSubscription() {
|
||||
if (typeof this.modalReactionUnsub === "function") {
|
||||
try {
|
||||
this.modalReactionUnsub();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[reaction] Failed to tear down modal subscription:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.modalReactionUnsub = null;
|
||||
this.modalReactionPointerKey = null;
|
||||
this.resetModalReactionState();
|
||||
}
|
||||
|
||||
subscribeModalReactions(pointer, pointerKey) {
|
||||
if (!this.videoModal?.updateReactionSummary) {
|
||||
this.teardownModalReactionSubscription();
|
||||
return;
|
||||
}
|
||||
|
||||
this.teardownModalReactionSubscription();
|
||||
|
||||
if (!pointer || !pointerKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedUser = this.normalizeHexPubkey(this.pubkey);
|
||||
const unsubscribe = reactionCounter.subscribe(pointer, (snapshot) => {
|
||||
const counts = { ...this.modalReactionState.counts };
|
||||
if (snapshot?.counts && typeof snapshot.counts === "object") {
|
||||
for (const [key, value] of Object.entries(snapshot.counts)) {
|
||||
counts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "+")) {
|
||||
counts["+"] = 0;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "-")) {
|
||||
counts["-"] = 0;
|
||||
}
|
||||
|
||||
let total = Number.isFinite(snapshot?.total)
|
||||
? Math.max(0, Number(snapshot.total))
|
||||
: 0;
|
||||
if (!Number.isFinite(total) || total === 0) {
|
||||
total = 0;
|
||||
for (const value of Object.values(counts)) {
|
||||
total += this.normalizeReactionCount(value);
|
||||
}
|
||||
}
|
||||
|
||||
let userReaction = "";
|
||||
if (normalizedUser && snapshot?.reactions) {
|
||||
const record = snapshot.reactions[normalizedUser] || null;
|
||||
if (record && typeof record.content === "string") {
|
||||
userReaction =
|
||||
record.content === "+"
|
||||
? "+"
|
||||
: record.content === "-"
|
||||
? "-"
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
this.modalReactionState = {
|
||||
counts,
|
||||
total,
|
||||
userReaction,
|
||||
};
|
||||
this.videoModal.updateReactionSummary({
|
||||
total,
|
||||
counts,
|
||||
userReaction,
|
||||
});
|
||||
});
|
||||
|
||||
this.modalReactionPointerKey = pointerKey;
|
||||
this.modalReactionUnsub = () => {
|
||||
try {
|
||||
unsubscribe?.();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[reaction] Failed to tear down modal subscription:",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
this.modalReactionUnsub = null;
|
||||
this.modalReactionPointerKey = null;
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[reaction] Failed to subscribe modal reaction counter:",
|
||||
error,
|
||||
);
|
||||
this.resetModalReactionState();
|
||||
}
|
||||
}
|
||||
|
||||
subscribeModalViewCount(pointer, pointerKey) {
|
||||
const viewEl = this.videoModal?.getViewCountElement() || null;
|
||||
if (!viewEl) {
|
||||
@@ -1812,6 +1945,203 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
applyModalReactionOptimisticUpdate(nextReaction) {
|
||||
if (nextReaction !== "+" && nextReaction !== "-") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousCounts = {
|
||||
...(this.modalReactionState?.counts || {}),
|
||||
};
|
||||
const previousTotalValue = Number.isFinite(this.modalReactionState?.total)
|
||||
? Math.max(0, Number(this.modalReactionState.total))
|
||||
: null;
|
||||
const previousReaction = this.modalReactionState?.userReaction || "";
|
||||
|
||||
const likeBefore = this.normalizeReactionCount(previousCounts["+"]);
|
||||
const dislikeBefore = this.normalizeReactionCount(previousCounts["-"]);
|
||||
const otherCounts = {};
|
||||
for (const [key, value] of Object.entries(previousCounts)) {
|
||||
if (key === "+" || key === "-") {
|
||||
continue;
|
||||
}
|
||||
otherCounts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
|
||||
let likeCount = likeBefore;
|
||||
let dislikeCount = dislikeBefore;
|
||||
|
||||
if (previousReaction === "+") {
|
||||
likeCount = Math.max(0, likeCount - 1);
|
||||
} else if (previousReaction === "-") {
|
||||
dislikeCount = Math.max(0, dislikeCount - 1);
|
||||
}
|
||||
|
||||
if (nextReaction === "+") {
|
||||
likeCount += 1;
|
||||
} else if (nextReaction === "-") {
|
||||
dislikeCount += 1;
|
||||
}
|
||||
|
||||
const updatedCounts = {
|
||||
...previousCounts,
|
||||
"+": likeCount,
|
||||
"-": dislikeCount,
|
||||
};
|
||||
|
||||
let updatedTotal = likeCount + dislikeCount;
|
||||
for (const value of Object.values(otherCounts)) {
|
||||
updatedTotal += this.normalizeReactionCount(value);
|
||||
}
|
||||
|
||||
this.modalReactionState = {
|
||||
counts: updatedCounts,
|
||||
total: updatedTotal,
|
||||
userReaction: nextReaction,
|
||||
};
|
||||
|
||||
if (this.videoModal?.updateReactionSummary) {
|
||||
this.videoModal.updateReactionSummary({
|
||||
total: updatedTotal,
|
||||
counts: updatedCounts,
|
||||
userReaction: nextReaction,
|
||||
});
|
||||
}
|
||||
|
||||
const fallbackPreviousTotal = Number.isFinite(previousTotalValue)
|
||||
? previousTotalValue
|
||||
: likeBefore +
|
||||
dislikeBefore +
|
||||
Object.values(otherCounts).reduce(
|
||||
(sum, value) => sum + this.normalizeReactionCount(value),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
counts: previousCounts,
|
||||
total: fallbackPreviousTotal,
|
||||
userReaction: previousReaction,
|
||||
};
|
||||
}
|
||||
|
||||
restoreModalReactionSnapshot(snapshot) {
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const countsInput =
|
||||
snapshot.counts && typeof snapshot.counts === "object"
|
||||
? snapshot.counts
|
||||
: {};
|
||||
const counts = { ...countsInput };
|
||||
for (const [key, value] of Object.entries(counts)) {
|
||||
counts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "+")) {
|
||||
counts["+"] = 0;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, "-")) {
|
||||
counts["-"] = 0;
|
||||
}
|
||||
|
||||
let total = Number.isFinite(snapshot.total)
|
||||
? Math.max(0, Number(snapshot.total))
|
||||
: 0;
|
||||
if (!Number.isFinite(total) || total === 0) {
|
||||
total = 0;
|
||||
for (const value of Object.values(counts)) {
|
||||
total += this.normalizeReactionCount(value);
|
||||
}
|
||||
}
|
||||
|
||||
const userReaction =
|
||||
snapshot.userReaction === "+"
|
||||
? "+"
|
||||
: snapshot.userReaction === "-"
|
||||
? "-"
|
||||
: "";
|
||||
|
||||
this.modalReactionState = {
|
||||
counts,
|
||||
total,
|
||||
userReaction,
|
||||
};
|
||||
|
||||
if (this.videoModal?.updateReactionSummary) {
|
||||
this.videoModal.updateReactionSummary({
|
||||
total,
|
||||
counts,
|
||||
userReaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleVideoReaction(detail = {}) {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointer = this.currentVideoPointer;
|
||||
if (!pointer) {
|
||||
this.showError("Reactions are unavailable for this video.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestedReaction =
|
||||
typeof detail.reaction === "string" ? detail.reaction : "";
|
||||
const normalizedReaction =
|
||||
requestedReaction === "+"
|
||||
? "+"
|
||||
: requestedReaction === "-"
|
||||
? "-"
|
||||
: "";
|
||||
|
||||
if (!normalizedReaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousReaction = this.modalReactionState?.userReaction || "";
|
||||
if (normalizedReaction === previousReaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isUserLoggedIn()) {
|
||||
this.showError("Please login to react to videos.");
|
||||
this.videoModal.setUserReaction(previousReaction);
|
||||
return;
|
||||
}
|
||||
|
||||
let rollbackSnapshot = null;
|
||||
try {
|
||||
rollbackSnapshot = this.applyModalReactionOptimisticUpdate(
|
||||
normalizedReaction
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[reaction] Failed to apply optimistic reaction state:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await reactionCounter.publish(pointer, {
|
||||
content: normalizedReaction,
|
||||
video: this.currentVideo,
|
||||
currentVideoPubkey: this.currentVideo?.pubkey,
|
||||
});
|
||||
|
||||
if (!result?.ok) {
|
||||
if (rollbackSnapshot) {
|
||||
this.restoreModalReactionSnapshot(rollbackSnapshot);
|
||||
}
|
||||
this.showError("Failed to send reaction. Please try again.");
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn("[reaction] Failed to publish reaction:", error);
|
||||
if (rollbackSnapshot) {
|
||||
this.restoreModalReactionSnapshot(rollbackSnapshot);
|
||||
}
|
||||
this.showError("Failed to send reaction. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
syncProfileModalState({
|
||||
includeSavedProfiles = true,
|
||||
includeActivePubkey = true,
|
||||
@@ -3872,6 +4202,7 @@ class Application {
|
||||
this.playbackService.cleanupWatchdog();
|
||||
}
|
||||
this.teardownModalViewCountSubscription();
|
||||
this.teardownModalReactionSubscription();
|
||||
|
||||
if (!preserveObservers && this.mediaLoader) {
|
||||
this.mediaLoader.disconnect();
|
||||
@@ -4438,6 +4769,7 @@ class Application {
|
||||
this.cancelPendingViewLogging();
|
||||
this.clearActiveIntervals();
|
||||
this.teardownModalViewCountSubscription();
|
||||
this.teardownModalReactionSubscription();
|
||||
|
||||
// 2) Close the modal UI right away so the user gets instant feedback
|
||||
const modalVideoElement =
|
||||
@@ -7461,6 +7793,10 @@ class Application {
|
||||
this.currentVideoPointer,
|
||||
this.currentVideoPointerKey
|
||||
);
|
||||
this.subscribeModalReactions(
|
||||
this.currentVideoPointer,
|
||||
this.currentVideoPointerKey
|
||||
);
|
||||
|
||||
this.syncModalMoreMenuData();
|
||||
|
||||
@@ -7767,6 +8103,7 @@ class Application {
|
||||
this.currentVideoPointer = null;
|
||||
this.currentVideoPointerKey = null;
|
||||
this.subscribeModalViewCount(null, null);
|
||||
this.subscribeModalReactions(null, null);
|
||||
|
||||
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
|
||||
const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : "";
|
||||
@@ -7881,6 +8218,14 @@ class Application {
|
||||
return null;
|
||||
}
|
||||
|
||||
normalizeReactionCount(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.round(Number(value)));
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? Math.max(0, Math.round(parsed)) : 0;
|
||||
}
|
||||
|
||||
normalizeHexPubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
|
||||
@@ -218,6 +218,75 @@ function notifyHandlers(key) {
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToPointer(pointerInput, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const canonical = canonicalizePointerForState(pointerInput);
|
||||
if (!canonical) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const { key, pointer } = canonical;
|
||||
const state = ensurePointerState(key, pointer);
|
||||
let handlers = pointerHandlers.get(key);
|
||||
if (!handlers) {
|
||||
handlers = new Set();
|
||||
pointerHandlers.set(key, handlers);
|
||||
}
|
||||
handlers.add(handler);
|
||||
|
||||
if (state) {
|
||||
try {
|
||||
handler(snapshotState(state));
|
||||
} catch (error) {
|
||||
userLogger.warn("[reactionCounter] Reaction handler threw:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribeFromPointer(pointer, handler);
|
||||
};
|
||||
}
|
||||
|
||||
function unsubscribeFromPointer(pointerInput, handler) {
|
||||
const canonical = canonicalizePointerForState(pointerInput);
|
||||
if (!canonical) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = canonical;
|
||||
const handlers = pointerHandlers.get(key);
|
||||
if (!handlers) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
handlers.delete(handler);
|
||||
} else {
|
||||
handlers.clear();
|
||||
}
|
||||
|
||||
if (handlers.size === 0) {
|
||||
pointerHandlers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function getPointerSnapshot(pointerInput) {
|
||||
const canonical = canonicalizePointerForState(pointerInput);
|
||||
if (!canonical) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = pointerStates.get(canonical.key);
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return snapshotState(state);
|
||||
}
|
||||
|
||||
export function ingestLocalReaction({ event, pointer }) {
|
||||
if (!event || !pointer) {
|
||||
return;
|
||||
@@ -353,5 +422,13 @@ export const reactionCounter = {
|
||||
};
|
||||
|
||||
reactionCounter.ingestLocalReaction = ingestLocalReaction;
|
||||
reactionCounter.subscribe = subscribeToPointer;
|
||||
reactionCounter.unsubscribe = unsubscribeFromPointer;
|
||||
reactionCounter.getSnapshot = getPointerSnapshot;
|
||||
|
||||
export default reactionCounter;
|
||||
export {
|
||||
subscribeToPointer as subscribeToReactions,
|
||||
unsubscribeFromPointer as unsubscribeFromReactions,
|
||||
getPointerSnapshot as getReactionSnapshot,
|
||||
};
|
||||
@@ -61,6 +61,24 @@ export class VideoModal {
|
||||
this.shareBtn = null;
|
||||
this.modalZapBtn = null;
|
||||
this.modalMoreBtn = null;
|
||||
this.reactionButtons = {
|
||||
"+": null,
|
||||
"-": null,
|
||||
};
|
||||
this.reactionMeter = null;
|
||||
this.reactionMeterFill = null;
|
||||
this.reactionMeterLabel = null;
|
||||
this.reactionMeterAssistive = null;
|
||||
this.reactionCountLabels = {
|
||||
"+": null,
|
||||
"-": null,
|
||||
};
|
||||
this.reactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
this.reactionNumberFormatter = null;
|
||||
|
||||
this.modalAccessibility = null;
|
||||
this.modalNavScrollHandler = null;
|
||||
@@ -104,6 +122,7 @@ export class VideoModal {
|
||||
this.handleShareRequest = this.handleShareRequest.bind(this);
|
||||
this.handleCreatorNavigation = this.handleCreatorNavigation.bind(this);
|
||||
this.handleModalMoreButtonClick = this.handleModalMoreButtonClick.bind(this);
|
||||
this.handleReactionClick = this.handleReactionClick.bind(this);
|
||||
|
||||
this.MODAL_LOADING_POSTER = "assets/gif/please-stand-by.gif";
|
||||
}
|
||||
@@ -295,6 +314,22 @@ export class VideoModal {
|
||||
this.shareBtn = playerModal.querySelector("#shareBtn") || null;
|
||||
this.modalZapBtn = playerModal.querySelector("#modalZapBtn") || null;
|
||||
this.modalMoreBtn = playerModal.querySelector("#modalMoreBtn") || null;
|
||||
this.reactionButtons["+"] =
|
||||
playerModal.querySelector("#modalLikeBtn") || null;
|
||||
this.reactionButtons["-"] =
|
||||
playerModal.querySelector("#modalDislikeBtn") || null;
|
||||
this.reactionMeter =
|
||||
playerModal.querySelector("[data-reaction-meter]") || null;
|
||||
this.reactionMeterFill =
|
||||
playerModal.querySelector("[data-reaction-meter-fill]") || null;
|
||||
this.reactionMeterLabel =
|
||||
playerModal.querySelector("[data-reaction-meter-label]") || null;
|
||||
this.reactionMeterAssistive =
|
||||
playerModal.querySelector("[data-reaction-meter-sr]") || null;
|
||||
this.reactionCountLabels["+"] =
|
||||
playerModal.querySelector("[data-reaction-like-count]") || null;
|
||||
this.reactionCountLabels["-"] =
|
||||
playerModal.querySelector("[data-reaction-dislike-count]") || null;
|
||||
|
||||
this.ambientCanvas = playerModal.querySelector("#ambientCanvas") || null;
|
||||
|
||||
@@ -322,6 +357,14 @@ export class VideoModal {
|
||||
this.setupModalZapPopover();
|
||||
this.setupModalMorePopover();
|
||||
|
||||
Object.values(this.reactionButtons).forEach((button) => {
|
||||
if (button) {
|
||||
button.addEventListener("click", this.handleReactionClick);
|
||||
}
|
||||
});
|
||||
this.syncReactionButtons();
|
||||
this.updateReactionMeterDisplay();
|
||||
|
||||
const closeButton = playerModal.querySelector("#closeModal");
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener("click", () => {
|
||||
@@ -912,12 +955,54 @@ export class VideoModal {
|
||||
}
|
||||
}
|
||||
|
||||
handleReactionClick(event) {
|
||||
event?.preventDefault?.();
|
||||
event?.stopPropagation?.();
|
||||
|
||||
const button = event?.currentTarget;
|
||||
if (!button || typeof button !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.disabled || button.getAttribute("aria-disabled") === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionValue =
|
||||
typeof button.dataset?.reaction === "string"
|
||||
? button.dataset.reaction.trim()
|
||||
: "";
|
||||
const normalized =
|
||||
reactionValue === "+"
|
||||
? "+"
|
||||
: reactionValue === "-"
|
||||
? "-"
|
||||
: "";
|
||||
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentReaction = this.reactionState?.userReaction || "";
|
||||
if (normalized === currentReaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatch("video:reaction", {
|
||||
video: this.activeVideo,
|
||||
reaction: normalized,
|
||||
previousReaction: currentReaction,
|
||||
});
|
||||
}
|
||||
|
||||
open(video, options = {}) {
|
||||
this.activeVideo = video || null;
|
||||
if (!this.playerModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetReactions();
|
||||
|
||||
this.playerModal.classList.remove("hidden");
|
||||
this.playerModal.removeAttribute("hidden");
|
||||
this.document.body.classList.add("modal-open");
|
||||
@@ -949,6 +1034,174 @@ export class VideoModal {
|
||||
this.forceRemovePoster("close");
|
||||
}
|
||||
|
||||
resetReactions() {
|
||||
this.reactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
this.syncReactionButtons();
|
||||
this.updateReactionMeterDisplay();
|
||||
}
|
||||
|
||||
setUserReaction(reaction) {
|
||||
const normalized = reaction === "+" ? "+" : reaction === "-" ? "-" : "";
|
||||
if (!this.reactionState) {
|
||||
this.reactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: normalized,
|
||||
};
|
||||
} else {
|
||||
this.reactionState.userReaction = normalized;
|
||||
}
|
||||
this.syncReactionButtons();
|
||||
}
|
||||
|
||||
updateReactionSummary({ total, counts, userReaction } = {}) {
|
||||
if (!this.reactionState) {
|
||||
this.reactionState = {
|
||||
counts: { "+": 0, "-": 0 },
|
||||
total: 0,
|
||||
userReaction: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (counts && typeof counts === "object") {
|
||||
const nextCounts = { ...this.reactionState.counts };
|
||||
for (const [key, value] of Object.entries(counts)) {
|
||||
nextCounts[key] = this.normalizeReactionCount(value);
|
||||
}
|
||||
this.reactionState.counts = nextCounts;
|
||||
}
|
||||
|
||||
if (Number.isFinite(total)) {
|
||||
this.reactionState.total = Math.max(0, Number(total));
|
||||
} else if (counts && typeof counts === "object") {
|
||||
let computedTotal = 0;
|
||||
for (const value of Object.values(this.reactionState.counts)) {
|
||||
computedTotal += this.normalizeReactionCount(value);
|
||||
}
|
||||
this.reactionState.total = computedTotal;
|
||||
}
|
||||
|
||||
if (userReaction !== undefined) {
|
||||
this.setUserReaction(userReaction);
|
||||
} else {
|
||||
this.syncReactionButtons();
|
||||
}
|
||||
|
||||
this.updateReactionMeterDisplay();
|
||||
}
|
||||
|
||||
syncReactionButtons() {
|
||||
const current = this.reactionState?.userReaction || "";
|
||||
const likeButton = this.reactionButtons?.["+"] || null;
|
||||
const dislikeButton = this.reactionButtons?.["-"] || null;
|
||||
if (likeButton) {
|
||||
likeButton.setAttribute("aria-pressed", current === "+" ? "true" : "false");
|
||||
}
|
||||
if (dislikeButton) {
|
||||
dislikeButton.setAttribute("aria-pressed", current === "-" ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
updateReactionMeterDisplay() {
|
||||
if (!this.reactionState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const counts = this.reactionState.counts || {};
|
||||
const likeCount = this.normalizeReactionCount(counts["+"]);
|
||||
const dislikeCount = this.normalizeReactionCount(counts["-"]);
|
||||
const totalReactions = likeCount + dislikeCount;
|
||||
const likeRatio = totalReactions > 0
|
||||
? Math.round((likeCount / totalReactions) * 100)
|
||||
: 0;
|
||||
|
||||
if (this.reactionCountLabels?.["+"]) {
|
||||
this.reactionCountLabels["+"].textContent = this.formatReactionCount(
|
||||
likeCount
|
||||
);
|
||||
}
|
||||
if (this.reactionCountLabels?.["-"]) {
|
||||
this.reactionCountLabels["-"].textContent = this.formatReactionCount(
|
||||
dislikeCount
|
||||
);
|
||||
}
|
||||
|
||||
const meterLabel =
|
||||
totalReactions > 0
|
||||
? `${likeRatio}% positive`
|
||||
: "No reactions yet";
|
||||
const assistiveLabel =
|
||||
totalReactions > 0
|
||||
? `${likeRatio}% positive (${this.formatReactionCount(
|
||||
likeCount
|
||||
)} likes and ${this.formatReactionCount(dislikeCount)} dislikes)`
|
||||
: "No reactions yet";
|
||||
|
||||
if (this.reactionMeter) {
|
||||
this.reactionMeter.setAttribute("aria-valuenow", String(likeRatio));
|
||||
this.reactionMeter.setAttribute("aria-valuetext", assistiveLabel);
|
||||
}
|
||||
|
||||
if (this.reactionMeterLabel) {
|
||||
this.reactionMeterLabel.textContent = meterLabel;
|
||||
}
|
||||
|
||||
if (this.reactionMeterAssistive) {
|
||||
this.reactionMeterAssistive.textContent = assistiveLabel;
|
||||
}
|
||||
|
||||
if (this.reactionMeterFill) {
|
||||
this.reactionMeterFill.style.inlineSize = `${likeRatio}%`;
|
||||
this.reactionMeterFill.style.width = `${likeRatio}%`;
|
||||
}
|
||||
}
|
||||
|
||||
normalizeReactionCount(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.round(Number(value)));
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? Math.max(0, Math.round(parsed)) : 0;
|
||||
}
|
||||
|
||||
getReactionNumberFormatter() {
|
||||
if (this.reactionNumberFormatter === false) {
|
||||
return null;
|
||||
}
|
||||
if (this.reactionNumberFormatter) {
|
||||
return this.reactionNumberFormatter;
|
||||
}
|
||||
try {
|
||||
if (this.window?.Intl?.NumberFormat) {
|
||||
this.reactionNumberFormatter = new this.window.Intl.NumberFormat();
|
||||
return this.reactionNumberFormatter;
|
||||
}
|
||||
} catch (error) {
|
||||
this.reactionNumberFormatter = false;
|
||||
return null;
|
||||
}
|
||||
this.reactionNumberFormatter = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
formatReactionCount(value) {
|
||||
const numeric = Number.isFinite(value) ? Number(value) : Number(value || 0);
|
||||
const safeValue = Number.isFinite(numeric) ? Math.max(0, Math.round(numeric)) : 0;
|
||||
const formatter = this.getReactionNumberFormatter();
|
||||
if (formatter) {
|
||||
try {
|
||||
return formatter.format(safeValue);
|
||||
} catch (error) {
|
||||
// fallback below
|
||||
}
|
||||
}
|
||||
return String(safeValue);
|
||||
}
|
||||
|
||||
applyLoadingPoster() {
|
||||
if (!this.modalVideo) {
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user