mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
Merge pull request #648 from PR0M3TH3AN/codex/improve-zap-button-menu-responsiveness
Improve zap popover responsiveness
This commit is contained in:
7 files changed
+594
-61
No files matched your search
@@ -585,14 +585,18 @@ class Application {
|
||||
getCurrentVideo: () => this.currentVideo,
|
||||
nwcSettings: this.nwcSettingsService,
|
||||
isUserLoggedIn: () => this.isUserLoggedIn(),
|
||||
hasSessionActor: () =>
|
||||
Boolean(
|
||||
typeof nostrClient?.sessionActor?.pubkey === "string" &&
|
||||
nostrClient.sessionActor.pubkey.trim()
|
||||
),
|
||||
splitAndZap: (...args) => this.splitAndZap(...args),
|
||||
payments: this.payments,
|
||||
callbacks: {
|
||||
onSuccess: (message) => this.showSuccess(message),
|
||||
onError: (message) => this.showError(message),
|
||||
},
|
||||
requestWalletPane: () =>
|
||||
this.profileController?.showWalletPane?.(),
|
||||
requestWalletPane: () => this.openWalletPane(),
|
||||
});
|
||||
this.boundVideoModalCloseHandler = () => {
|
||||
this.hideModal();
|
||||
@@ -1307,6 +1311,47 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
openWalletPane() {
|
||||
if (
|
||||
!this.profileController ||
|
||||
typeof this.profileController.showWalletPane !== "function"
|
||||
) {
|
||||
devLogger.warn(
|
||||
"[Application] Wallet pane requested before profile controller initialized.",
|
||||
);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.profileController.showWalletPane();
|
||||
if (result && typeof result.then === "function") {
|
||||
return result.catch((error) => {
|
||||
devLogger.error("[Application] Failed to open wallet pane:", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
} catch (error) {
|
||||
devLogger.error("[Application] Failed to open wallet pane:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchAuthChange(detail = {}) {
|
||||
if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = { ...(typeof detail === "object" && detail ? detail : {}) };
|
||||
window.dispatchEvent(new CustomEvent("bitvid:auth-changed", { detail: payload }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to dispatch auth change event:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
normalizeModalTrigger(candidate) {
|
||||
if (!candidate) {
|
||||
return null;
|
||||
@@ -3095,6 +3140,23 @@ class Application {
|
||||
identityChanged: Boolean(detail?.identityChanged),
|
||||
};
|
||||
|
||||
if (this.zapController) {
|
||||
try {
|
||||
this.zapController.setVisibility(
|
||||
Boolean(this.currentVideo?.lightningAddress),
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to refresh zap visibility after login:", error);
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatchAuthChange({
|
||||
status: "login",
|
||||
loggedIn: true,
|
||||
pubkey: loginContext.pubkey || null,
|
||||
previousPubkey: loginContext.previousPubkey || null,
|
||||
});
|
||||
|
||||
const nwcPromise = Promise.resolve()
|
||||
.then(() => this.nwcSettingsService.onLogin(loginContext))
|
||||
.catch((error) => {
|
||||
@@ -3186,6 +3248,51 @@ class Application {
|
||||
|
||||
this.applyLoggedOutUiState();
|
||||
|
||||
if (this.videoModal?.closeZapDialog) {
|
||||
try {
|
||||
this.videoModal.closeZapDialog({ silent: true, restoreFocus: false });
|
||||
} catch (error) {
|
||||
devLogger.warn("Failed to close zap dialog during logout:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.zapController) {
|
||||
try {
|
||||
this.zapController.resetState();
|
||||
this.zapController.setVisibility(Boolean(this.currentVideo?.lightningAddress));
|
||||
} catch (error) {
|
||||
devLogger.warn("Failed to reset zap controller during logout:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.nostrService?.clearVideoSubscription === "function") {
|
||||
try {
|
||||
this.nostrService.clearVideoSubscription();
|
||||
} catch (error) {
|
||||
devLogger.warn("Failed to clear video subscription during logout:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.nostrService?.resetVideosCache === "function") {
|
||||
try {
|
||||
this.nostrService.resetVideosCache();
|
||||
} catch (error) {
|
||||
devLogger.warn("Failed to reset cached videos during logout:", error);
|
||||
}
|
||||
}
|
||||
|
||||
await this.renderVideoList({
|
||||
videos: [],
|
||||
metadata: { reason: "auth:logout" },
|
||||
});
|
||||
|
||||
this.dispatchAuthChange({
|
||||
status: "logout",
|
||||
loggedIn: false,
|
||||
pubkey: detail?.pubkey || null,
|
||||
previousPubkey: detail?.previousPubkey || null,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.loadVideos(true);
|
||||
} catch (error) {
|
||||
|
||||
+349
-28
@@ -112,6 +112,8 @@ function logZapError(stage, details = {}, error) {
|
||||
userLogger.error("[zap] Channel zap failure", summary, error);
|
||||
}
|
||||
let currentChannelProfileEvent = null;
|
||||
let currentChannelProfileSnapshot = null;
|
||||
let currentChannelProfileHasExplicitPayload = false;
|
||||
|
||||
let cachedZapButton = null;
|
||||
let cachedChannelShareButton = null;
|
||||
@@ -133,6 +135,7 @@ let zapControlsOpen = false;
|
||||
let zapPopover = null;
|
||||
let zapPopoverTrigger = null;
|
||||
let zapShouldFocusOnOpen = false;
|
||||
let zapPopoverOpenPromise = null;
|
||||
let channelMenuPopover = null;
|
||||
let channelMenuOpen = false;
|
||||
let currentVideoLoadToken = 0;
|
||||
@@ -144,6 +147,33 @@ const PROFILE_EVENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
const channelProfileMetadataCache = new Map();
|
||||
|
||||
function hasExplicitChannelProfilePayload(profile) {
|
||||
return (
|
||||
profile &&
|
||||
typeof profile === "object" &&
|
||||
Object.keys(profile).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function touchChannelProfileCacheEntry(pubkey) {
|
||||
if (typeof pubkey !== "string" || !pubkey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existing = channelProfileMetadataCache.get(pubkey);
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
channelProfileMetadataCache.set(pubkey, {
|
||||
timestamp: Date.now(),
|
||||
profile: { ...existing.profile },
|
||||
event: existing.event ? { ...existing.event } : null
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const SUPPORTED_BANNER_REFERRER_POLICIES = new Set([
|
||||
"no-referrer",
|
||||
"origin",
|
||||
@@ -469,7 +499,11 @@ function setChannelZapVisibility(visible) {
|
||||
typeof app?.isUserLoggedIn === "function"
|
||||
? app.isUserLoggedIn()
|
||||
: Boolean(app?.normalizeHexPubkey?.(app?.pubkey));
|
||||
const shouldShow = !!visible && isLoggedIn;
|
||||
const sessionActorPubkey = getSessionActorPubkey();
|
||||
const hasSessionActor = sessionActorPubkey.length > 0;
|
||||
const sessionOnly = !isLoggedIn && hasSessionActor;
|
||||
const shouldShow = !!visible && (isLoggedIn || sessionOnly);
|
||||
const requiresLogin = shouldShow && !isLoggedIn;
|
||||
|
||||
if (
|
||||
shouldShow &&
|
||||
@@ -480,7 +514,10 @@ function setChannelZapVisibility(visible) {
|
||||
|
||||
zapButton.toggleAttribute("hidden", !shouldShow);
|
||||
zapButton.disabled = !shouldShow;
|
||||
zapButton.setAttribute("aria-disabled", (!shouldShow).toString());
|
||||
zapButton.setAttribute(
|
||||
"aria-disabled",
|
||||
(requiresLogin || !shouldShow).toString()
|
||||
);
|
||||
zapButton.setAttribute("aria-hidden", (!shouldShow).toString());
|
||||
zapButton.setAttribute("aria-expanded", "false");
|
||||
if (shouldShow) {
|
||||
@@ -492,18 +529,24 @@ function setChannelZapVisibility(visible) {
|
||||
clearZapReceipts();
|
||||
setZapStatus("", "neutral");
|
||||
}
|
||||
if (requiresLogin) {
|
||||
zapButton.dataset.requiresLogin = "true";
|
||||
} else {
|
||||
delete zapButton.dataset.requiresLogin;
|
||||
}
|
||||
closeZapControls();
|
||||
if (controls) {
|
||||
controls.setAttribute("aria-hidden", "true");
|
||||
controls.hidden = true;
|
||||
}
|
||||
const shouldEnableInputs = shouldShow && !requiresLogin;
|
||||
if (amountInput) {
|
||||
amountInput.disabled = !shouldShow;
|
||||
amountInput.disabled = !shouldEnableInputs;
|
||||
}
|
||||
if (sendButton) {
|
||||
sendButton.disabled = !shouldShow;
|
||||
sendButton.setAttribute("aria-hidden", (!shouldShow).toString());
|
||||
if (shouldShow) {
|
||||
sendButton.disabled = !shouldEnableInputs;
|
||||
sendButton.setAttribute("aria-hidden", (!shouldEnableInputs).toString());
|
||||
if (shouldEnableInputs) {
|
||||
sendButton.removeAttribute("tabindex");
|
||||
sendButton.removeAttribute("aria-busy");
|
||||
delete sendButton.dataset.state;
|
||||
@@ -525,12 +568,31 @@ function setChannelZapVisibility(visible) {
|
||||
typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : "";
|
||||
return uri.length > 0;
|
||||
})();
|
||||
setZapWalletPromptVisible(shouldShow && !hasWallet);
|
||||
if (shouldShow) {
|
||||
setZapWalletPromptVisible(shouldEnableInputs && !hasWallet);
|
||||
if (shouldEnableInputs) {
|
||||
setupZapWalletLink();
|
||||
}
|
||||
}
|
||||
|
||||
function isSessionActorWithoutLogin() {
|
||||
const app = getApp();
|
||||
const isLoggedIn =
|
||||
typeof app?.isUserLoggedIn === "function"
|
||||
? app.isUserLoggedIn()
|
||||
: Boolean(app?.normalizeHexPubkey?.(app?.pubkey) || app?.pubkey);
|
||||
if (isLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getSessionActorPubkey().length > 0;
|
||||
}
|
||||
|
||||
function getSessionActorPubkey() {
|
||||
return typeof nostrClient?.sessionActor?.pubkey === "string"
|
||||
? nostrClient.sessionActor.pubkey.trim()
|
||||
: "";
|
||||
}
|
||||
|
||||
function getChannelShareButton() {
|
||||
if (
|
||||
cachedChannelShareButton &&
|
||||
@@ -804,6 +866,8 @@ function cacheZapPanelElements(panel) {
|
||||
cachedZapWalletLink = null;
|
||||
cachedZapCloseBtn = null;
|
||||
cachedZapSendBtn = null;
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -910,7 +974,20 @@ function setupZapWalletLink() {
|
||||
event?.preventDefault?.();
|
||||
const app = getApp();
|
||||
if (typeof app?.openWalletPane === "function") {
|
||||
app.openWalletPane();
|
||||
Promise.resolve()
|
||||
.then(() => app.openWalletPane())
|
||||
.catch((error) => {
|
||||
devLogger.warn(
|
||||
"[zap] Failed to open wallet pane from channel view:",
|
||||
error
|
||||
);
|
||||
app?.showError?.("Wallet settings are not available right now.");
|
||||
});
|
||||
} else {
|
||||
devLogger.warn(
|
||||
"[zap] Wallet pane requested but application did not expose openWalletPane()."
|
||||
);
|
||||
app?.showError?.("Wallet settings are not available right now.");
|
||||
}
|
||||
});
|
||||
link.dataset.initialized = "true";
|
||||
@@ -936,24 +1013,61 @@ function isZapControlsOpen() {
|
||||
|
||||
function openZapControls({ focus = false } = {}) {
|
||||
const zapButton = getChannelZapButton();
|
||||
if (!zapButton || !zapPopover) {
|
||||
if (!zapButton) {
|
||||
zapShouldFocusOnOpen = false;
|
||||
return false;
|
||||
}
|
||||
zapShouldFocusOnOpen = focus;
|
||||
const result = zapPopover.open();
|
||||
if (result && typeof result.then === "function") {
|
||||
result.catch(() => {}).finally(() => {
|
||||
|
||||
if (!zapPopover || zapPopoverTrigger !== zapButton) {
|
||||
const initialized = setupZapButton({ force: true });
|
||||
if (!initialized || !zapPopover || zapPopoverTrigger !== zapButton) {
|
||||
zapShouldFocusOnOpen = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (zapPopoverOpenPromise) {
|
||||
if (focus) {
|
||||
zapShouldFocusOnOpen = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
zapShouldFocusOnOpen = focus;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = zapPopover.open();
|
||||
} catch (error) {
|
||||
zapShouldFocusOnOpen = false;
|
||||
zapPopoverOpenPromise = null;
|
||||
devLogger.warn("[zap] Failed to open zap popover", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result && typeof result.then === "function") {
|
||||
const pending = result
|
||||
.catch((error) => {
|
||||
devLogger.warn("[zap] Zap popover open rejected", error);
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
});
|
||||
zapPopoverOpenPromise = pending;
|
||||
return true;
|
||||
}
|
||||
|
||||
zapShouldFocusOnOpen = false;
|
||||
zapPopoverOpenPromise = null;
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
function closeZapControls({ focusButton = false } = {}) {
|
||||
const zapButton = getChannelZapButton();
|
||||
zapShouldFocusOnOpen = false;
|
||||
zapPopoverOpenPromise = null;
|
||||
|
||||
if (zapPopover) {
|
||||
const result = zapPopover.close({ restoreFocus: focusButton });
|
||||
@@ -1636,10 +1750,27 @@ function handleZapButtonClick(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popoverIsOpen =
|
||||
typeof zapPopover?.isOpen === "function"
|
||||
? zapPopover.isOpen()
|
||||
: isZapControlsOpen();
|
||||
if (isSessionActorWithoutLogin()) {
|
||||
const app = getApp();
|
||||
const message = "Log in with your Nostr profile to send zaps.";
|
||||
app?.showError?.(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zapPopover || zapPopoverTrigger !== zapButton) {
|
||||
setupZapButton({ force: true });
|
||||
}
|
||||
|
||||
if (!zapPopover) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (zapPopoverOpenPromise) {
|
||||
zapShouldFocusOnOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const popoverIsOpen = isZapControlsOpen();
|
||||
|
||||
if (!popoverIsOpen) {
|
||||
openZapControls({ focus: true });
|
||||
@@ -1667,11 +1798,27 @@ async function handleZapSend(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zapPopover || zapPopoverTrigger !== zapButton) {
|
||||
setupZapButton({ force: true });
|
||||
}
|
||||
|
||||
if (!isZapControlsOpen()) {
|
||||
if (zapPopoverOpenPromise) {
|
||||
zapShouldFocusOnOpen = true;
|
||||
return;
|
||||
}
|
||||
openZapControls({ focus: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (zapPopoverOpenPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zapPopover) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentChannelLightningAddress) {
|
||||
const message = "This creator has not configured a Lightning address yet.";
|
||||
setZapStatus(message, "error");
|
||||
@@ -2174,6 +2321,8 @@ export async function initChannelProfileView() {
|
||||
currentChannelNpub = null;
|
||||
currentChannelLightningAddress = "";
|
||||
currentChannelProfileEvent = null;
|
||||
currentChannelProfileSnapshot = null;
|
||||
currentChannelProfileHasExplicitPayload = false;
|
||||
setCachedPlatformLightningAddress("");
|
||||
resetZapRetryState();
|
||||
clearZapReceipts();
|
||||
@@ -2296,6 +2445,9 @@ function setupZapButton({ force = false } = {}) {
|
||||
zapPopover.destroy();
|
||||
zapPopover = null;
|
||||
zapPopoverTrigger = null;
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
delete zapButton.dataset.zapPopoverWarmBound;
|
||||
}
|
||||
|
||||
const documentRef =
|
||||
@@ -2413,13 +2565,22 @@ function setupZapButton({ force = false } = {}) {
|
||||
});
|
||||
|
||||
if (!popover) {
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const originalOpen = popover.open?.bind(popover);
|
||||
if (originalOpen) {
|
||||
popover.open = async (...args) => {
|
||||
const result = await originalOpen(...args);
|
||||
let result;
|
||||
try {
|
||||
result = await originalOpen(...args);
|
||||
} catch (error) {
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
throw error;
|
||||
}
|
||||
if (result) {
|
||||
zapControlsOpen = true;
|
||||
const controls = getZapControlsContainer();
|
||||
@@ -2454,6 +2615,7 @@ function setupZapButton({ force = false } = {}) {
|
||||
zapButton.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
zapShouldFocusOnOpen = false;
|
||||
zapPopoverOpenPromise = null;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@@ -2466,6 +2628,8 @@ function setupZapButton({ force = false } = {}) {
|
||||
zapPopover = null;
|
||||
zapPopoverTrigger = null;
|
||||
}
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
cacheZapPanelElements(null);
|
||||
};
|
||||
}
|
||||
@@ -2473,6 +2637,49 @@ function setupZapButton({ force = false } = {}) {
|
||||
zapPopover = popover;
|
||||
zapPopoverTrigger = zapButton;
|
||||
|
||||
const warmZapPopover = () => {
|
||||
if (!zapPopover || zapPopover !== popover) {
|
||||
return;
|
||||
}
|
||||
if (zapButton.hasAttribute("hidden")) {
|
||||
return;
|
||||
}
|
||||
if (typeof popover.preload !== "function") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
popover.preload();
|
||||
} catch (error) {
|
||||
devLogger.warn("[zap] Failed to preload zap controls", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
typeof popover.preload === "function" &&
|
||||
!zapButton.hasAttribute("hidden")
|
||||
) {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.requestIdleCallback === "function"
|
||||
) {
|
||||
window.requestIdleCallback(() => warmZapPopover(), { timeout: 250 });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
warmZapPopover();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (zapButton.dataset.zapPopoverWarmBound !== "true") {
|
||||
const handleWarm = () => {
|
||||
warmZapPopover();
|
||||
};
|
||||
zapButton.addEventListener("pointerenter", handleWarm, { once: true });
|
||||
zapButton.addEventListener("pointerdown", handleWarm, { once: true });
|
||||
zapButton.addEventListener("focus", handleWarm, { once: true });
|
||||
zapButton.dataset.zapPopoverWarmBound = "true";
|
||||
}
|
||||
|
||||
if (zapButton.dataset.initialized !== "true") {
|
||||
zapButton.addEventListener("click", handleZapButtonClick);
|
||||
zapButton.dataset.initialized = "true";
|
||||
@@ -2581,7 +2788,30 @@ function normalizeChannelProfileMetadata(raw = {}) {
|
||||
|
||||
function rememberChannelProfile(pubkey, { profile = {}, event = null } = {}) {
|
||||
if (typeof pubkey !== "string" || !pubkey) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasPayload = hasExplicitChannelProfilePayload(profile);
|
||||
if (!hasPayload) {
|
||||
touchChannelProfileCacheEntry(pubkey);
|
||||
return false;
|
||||
}
|
||||
|
||||
const incomingEventTimestamp =
|
||||
typeof event?.created_at === "number" ? event.created_at : 0;
|
||||
const existing = channelProfileMetadataCache.get(pubkey);
|
||||
const existingEventTimestamp =
|
||||
typeof existing?.event?.created_at === "number"
|
||||
? existing.event.created_at
|
||||
: 0;
|
||||
|
||||
if (
|
||||
existingEventTimestamp &&
|
||||
incomingEventTimestamp &&
|
||||
incomingEventTimestamp < existingEventTimestamp
|
||||
) {
|
||||
touchChannelProfileCacheEntry(pubkey);
|
||||
return false;
|
||||
}
|
||||
|
||||
channelProfileMetadataCache.set(pubkey, {
|
||||
@@ -2589,6 +2819,8 @@ function rememberChannelProfile(pubkey, { profile = {}, event = null } = {}) {
|
||||
profile: normalizeChannelProfileMetadata(profile),
|
||||
event: event ? { ...event } : null
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCachedChannelProfile(pubkey) {
|
||||
@@ -3062,6 +3294,38 @@ function applyChannelProfileMetadata({
|
||||
}
|
||||
|
||||
const normalized = normalizeChannelProfileMetadata(profile);
|
||||
const hasExplicitProfilePayload =
|
||||
hasExplicitChannelProfilePayload(profile);
|
||||
const incomingEventTimestamp =
|
||||
typeof event?.created_at === "number" ? event.created_at : 0;
|
||||
const existingEventTimestamp =
|
||||
typeof currentChannelProfileEvent?.created_at === "number"
|
||||
? currentChannelProfileEvent.created_at
|
||||
: 0;
|
||||
|
||||
if (currentChannelProfileSnapshot) {
|
||||
const isOlderEvent =
|
||||
incomingEventTimestamp &&
|
||||
existingEventTimestamp &&
|
||||
incomingEventTimestamp < existingEventTimestamp;
|
||||
if (isOlderEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasExplicitProfilePayload) {
|
||||
if (currentChannelProfileHasExplicitPayload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldIgnoreEmptyPayload =
|
||||
!incomingEventTimestamp ||
|
||||
incomingEventTimestamp <= existingEventTimestamp;
|
||||
if (shouldIgnoreEmptyPayload) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const expectedLoadToken =
|
||||
loadToken !== null ? loadToken : currentProfileLoadToken;
|
||||
|
||||
@@ -3152,6 +3416,9 @@ function applyChannelProfileMetadata({
|
||||
currentChannelProfileEvent = null;
|
||||
}
|
||||
|
||||
currentChannelProfileSnapshot = { ...normalized };
|
||||
currentChannelProfileHasExplicitPayload = hasExplicitProfilePayload;
|
||||
|
||||
if (lightningAddress) {
|
||||
if (lightningAddress !== previousLightning) {
|
||||
fetchLightningMetadata(lightningAddress)
|
||||
@@ -3264,18 +3531,27 @@ async function loadUserProfile(pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
rememberChannelProfile(pubkey, result);
|
||||
const cachedUpdated = rememberChannelProfile(pubkey, result);
|
||||
|
||||
if (typeof app?.setProfileCacheEntry === "function") {
|
||||
if (cachedUpdated && typeof app?.setProfileCacheEntry === "function") {
|
||||
try {
|
||||
app.setProfileCacheEntry(pubkey, {
|
||||
...result.profile,
|
||||
lightningAddress:
|
||||
const profileForCache =
|
||||
result && typeof result.profile === "object" && result.profile
|
||||
? { ...result.profile }
|
||||
: {};
|
||||
|
||||
if (!profileForCache.lightningAddress) {
|
||||
const lightningFallback =
|
||||
result.profile?.lightningAddress ||
|
||||
result.profile?.lud16 ||
|
||||
result.profile?.lud06 ||
|
||||
""
|
||||
});
|
||||
"";
|
||||
if (lightningFallback) {
|
||||
profileForCache.lightningAddress = lightningFallback;
|
||||
}
|
||||
}
|
||||
|
||||
app.setProfileCacheEntry(pubkey, profileForCache);
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"Failed to persist channel profile metadata to cache:",
|
||||
@@ -3469,6 +3745,51 @@ window.addEventListener("bitvid:access-control-updated", () => {
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("bitvid:auth-changed", (event) => {
|
||||
const hashParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
if (hashParams.get("view") !== "channel-profile") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentChannelHex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeNpub = hashParams.get("npub");
|
||||
if (currentChannelNpub && activeNpub && activeNpub !== currentChannelNpub) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChannelZapVisibility(Boolean(currentChannelLightningAddress));
|
||||
syncChannelShareButtonState();
|
||||
|
||||
const detail = event?.detail;
|
||||
if (!detail || typeof detail !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.status === "logout") {
|
||||
zapInFlight = false;
|
||||
zapPopoverOpenPromise = null;
|
||||
zapShouldFocusOnOpen = false;
|
||||
resetZapRetryState();
|
||||
setZapStatus("", "neutral");
|
||||
clearZapReceipts();
|
||||
const amountInput = getZapAmountInput();
|
||||
if (amountInput) {
|
||||
amountInput.value = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.status === "login") {
|
||||
zapInFlight = false;
|
||||
resetZapRetryState();
|
||||
setZapStatus("", "neutral");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Keep only the newest version of each video root.
|
||||
*/
|
||||
|
||||
@@ -218,6 +218,13 @@ export class NostrService {
|
||||
this.emit("videos:cache", { size: map.size });
|
||||
}
|
||||
|
||||
resetVideosCache() {
|
||||
const map = new Map();
|
||||
this.videosMap = map;
|
||||
setStoredVideosMap(map);
|
||||
this.emit("videos:cache", { size: 0 });
|
||||
}
|
||||
|
||||
shouldIncludeVideo(video, {
|
||||
blacklistedEventIds = new Set(),
|
||||
isAuthorBlocked = () => false,
|
||||
|
||||
@@ -77,7 +77,10 @@ export class VideoModal {
|
||||
this.modalZapWalletLink = null;
|
||||
this.modalZapDialogOpen = false;
|
||||
this.modalZapPending = false;
|
||||
this.modalZapRequiresLogin = false;
|
||||
this.modalZapPopover = null;
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapPendingToggle = null;
|
||||
|
||||
this.modalMorePopover = null;
|
||||
this.modalMoreMenuPanel = null;
|
||||
@@ -237,6 +240,8 @@ export class VideoModal {
|
||||
this.modalMorePopover.destroy();
|
||||
}
|
||||
this.modalZapPopover = null;
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapPendingToggle = null;
|
||||
this.modalMorePopover = null;
|
||||
this.modalMoreMenuPanel = null;
|
||||
|
||||
@@ -414,11 +419,24 @@ export class VideoModal {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modalZapRequiresLogin) {
|
||||
this.dispatch("zap:open", {
|
||||
video: this.activeVideo,
|
||||
requiresLogin: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const popoverIsOpen =
|
||||
typeof this.modalZapPopover?.isOpen === "function"
|
||||
? this.modalZapPopover.isOpen()
|
||||
: this.modalZapDialogOpen === true;
|
||||
|
||||
if (this.modalZapOpenPromise) {
|
||||
this.modalZapPendingToggle = "close";
|
||||
return;
|
||||
}
|
||||
|
||||
if (popoverIsOpen) {
|
||||
if (this.modalZapPending) {
|
||||
return;
|
||||
@@ -428,6 +446,7 @@ export class VideoModal {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalZapPendingToggle = null;
|
||||
this.openZapDialog();
|
||||
this.dispatch("zap:open", { video: this.activeVideo });
|
||||
});
|
||||
@@ -578,6 +597,8 @@ export class VideoModal {
|
||||
const wasOpen = popover.isOpen?.() === true;
|
||||
const result = originalClose(rest);
|
||||
if (wasOpen && result) {
|
||||
this.modalZapPendingToggle = null;
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapDialog.dataset.state = "closed";
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "true");
|
||||
this.modalZapDialog.hidden = true;
|
||||
@@ -966,13 +987,29 @@ export class VideoModal {
|
||||
this.shareBtn.classList.toggle("cursor-not-allowed", !enabled);
|
||||
}
|
||||
|
||||
setZapVisibility(visible) {
|
||||
const shouldShow = !!visible;
|
||||
setZapVisibility(visible, options = {}) {
|
||||
let config;
|
||||
if (typeof visible === "object" && visible !== null) {
|
||||
config = { ...visible };
|
||||
} else {
|
||||
config = {
|
||||
visible,
|
||||
...(options && typeof options === "object" ? options : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const shouldShow = !!config.visible;
|
||||
const requiresLogin = shouldShow && !!config.requiresLogin;
|
||||
this.modalZapRequiresLogin = requiresLogin;
|
||||
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.toggleAttribute("hidden", !shouldShow);
|
||||
const disableButton = !shouldShow || this.modalZapPending;
|
||||
const disableButton =
|
||||
!shouldShow || (this.modalZapPending && !requiresLogin);
|
||||
this.modalZapBtn.disabled = disableButton;
|
||||
this.modalZapBtn.setAttribute("aria-disabled", (!shouldShow).toString());
|
||||
const ariaDisabledValue =
|
||||
requiresLogin || !shouldShow ? "true" : "false";
|
||||
this.modalZapBtn.setAttribute("aria-disabled", ariaDisabledValue);
|
||||
this.modalZapBtn.setAttribute("aria-hidden", (!shouldShow).toString());
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "false");
|
||||
if (shouldShow) {
|
||||
@@ -980,16 +1017,26 @@ export class VideoModal {
|
||||
} else {
|
||||
this.modalZapBtn.setAttribute("tabindex", "-1");
|
||||
}
|
||||
if (this.modalZapPending) {
|
||||
this.modalZapBtn.setAttribute("aria-busy", "true");
|
||||
this.modalZapBtn.classList.add("opacity-50", "pointer-events-none");
|
||||
} else {
|
||||
if (requiresLogin) {
|
||||
this.modalZapBtn.dataset.requiresLogin = "true";
|
||||
this.modalZapBtn.removeAttribute("aria-busy");
|
||||
this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none");
|
||||
} else {
|
||||
delete this.modalZapBtn.dataset.requiresLogin;
|
||||
if (this.modalZapPending) {
|
||||
this.modalZapBtn.setAttribute("aria-busy", "true");
|
||||
this.modalZapBtn.classList.add("opacity-50", "pointer-events-none");
|
||||
} else {
|
||||
this.modalZapBtn.removeAttribute("aria-busy");
|
||||
this.modalZapBtn.classList.remove(
|
||||
"opacity-50",
|
||||
"pointer-events-none",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldShow) {
|
||||
if (!shouldShow || requiresLogin) {
|
||||
this.closeZapDialog({ silent: true, restoreFocus: false });
|
||||
}
|
||||
}
|
||||
@@ -1007,8 +1054,12 @@ export class VideoModal {
|
||||
}
|
||||
|
||||
async openZapDialog() {
|
||||
if (this.modalZapPopover?.open) {
|
||||
try {
|
||||
if (this.modalZapOpenPromise) {
|
||||
return this.modalZapOpenPromise;
|
||||
}
|
||||
|
||||
const runOpen = async () => {
|
||||
if (this.modalZapPopover?.open) {
|
||||
const opened = await this.modalZapPopover.open();
|
||||
if (opened) {
|
||||
this.modalZapDialogOpen = true;
|
||||
@@ -1019,27 +1070,43 @@ export class VideoModal {
|
||||
}
|
||||
this.focusZapAmount();
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
this.log("[VideoModal] Failed to open zap popover", error);
|
||||
return opened;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.modalZapDialog) {
|
||||
return;
|
||||
}
|
||||
if (!this.modalZapDialog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.modalZapDialog.hidden = false;
|
||||
this.modalZapDialog.dataset.state = "open";
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "false");
|
||||
this.modalZapDialogOpen = true;
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
this.focusZapAmount();
|
||||
this.modalZapDialog.hidden = false;
|
||||
this.modalZapDialog.dataset.state = "open";
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "false");
|
||||
this.modalZapDialogOpen = true;
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
this.focusZapAmount();
|
||||
return true;
|
||||
};
|
||||
|
||||
const promise = runOpen().catch((error) => {
|
||||
this.log("[VideoModal] Failed to open zap popover", error);
|
||||
return false;
|
||||
});
|
||||
|
||||
this.modalZapOpenPromise = promise.finally(() => {
|
||||
const shouldClose = this.modalZapPendingToggle === "close";
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapPendingToggle = null;
|
||||
if (shouldClose && !this.modalZapPending) {
|
||||
this.closeZapDialog();
|
||||
}
|
||||
});
|
||||
|
||||
return this.modalZapOpenPromise;
|
||||
}
|
||||
|
||||
closeZapDialog({ silent = false, restoreFocus } = {}) {
|
||||
this.modalZapPendingToggle = null;
|
||||
if (this.modalZapPopover?.close) {
|
||||
const options = { silent };
|
||||
if (restoreFocus !== undefined) {
|
||||
@@ -1292,11 +1359,12 @@ export class VideoModal {
|
||||
}
|
||||
|
||||
if (this.modalZapBtn) {
|
||||
if (isPending) {
|
||||
const buttonHidden = this.modalZapBtn.hasAttribute("hidden");
|
||||
if (isPending && !this.modalZapRequiresLogin) {
|
||||
this.modalZapBtn.disabled = true;
|
||||
this.modalZapBtn.setAttribute("aria-busy", "true");
|
||||
this.modalZapBtn.classList.add("opacity-50", "pointer-events-none");
|
||||
} else if (!this.modalZapBtn.hasAttribute("hidden")) {
|
||||
} else if (!buttonHidden) {
|
||||
this.modalZapBtn.disabled = false;
|
||||
this.modalZapBtn.removeAttribute("aria-busy");
|
||||
this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none");
|
||||
|
||||
@@ -671,6 +671,10 @@ export function createPopover(trigger, render, options = {}) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
function preload() {
|
||||
return Boolean(ensurePanel());
|
||||
}
|
||||
|
||||
async function updatePosition() {
|
||||
if (!anchor || !panel) {
|
||||
return;
|
||||
@@ -918,6 +922,7 @@ export function createPopover(trigger, render, options = {}) {
|
||||
update: updatePosition,
|
||||
isOpen: () => isOpen,
|
||||
getPanel: () => panel,
|
||||
preload,
|
||||
};
|
||||
|
||||
return api;
|
||||
|
||||
@@ -4016,6 +4016,15 @@ export class ProfileModalController {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentElement = modalRoot.parentElement;
|
||||
if (parentElement) {
|
||||
if (parentElement.lastElementChild !== modalRoot) {
|
||||
parentElement.appendChild(modalRoot);
|
||||
}
|
||||
} else if (typeof document !== "undefined" && document.body) {
|
||||
document.body.appendChild(modalRoot);
|
||||
}
|
||||
|
||||
modalRoot.classList.remove("hidden");
|
||||
modalRoot.setAttribute("aria-hidden", "false");
|
||||
this.setGlobalModalState("profile", true);
|
||||
|
||||
+19
-3
@@ -33,6 +33,7 @@ export default class ZapController {
|
||||
nwcSettings,
|
||||
getActiveNwcSettings,
|
||||
isUserLoggedIn,
|
||||
hasSessionActor,
|
||||
hasActiveWalletConnection,
|
||||
splitAndZap,
|
||||
payments = {},
|
||||
@@ -58,6 +59,8 @@ export default class ZapController {
|
||||
}
|
||||
this.isUserLoggedIn =
|
||||
typeof isUserLoggedIn === "function" ? isUserLoggedIn : () => false;
|
||||
this.hasSessionActor =
|
||||
typeof hasSessionActor === "function" ? hasSessionActor : () => false;
|
||||
if (
|
||||
this.nwcSettings &&
|
||||
typeof this.nwcSettings.hasActiveWalletConnection === "function"
|
||||
@@ -89,11 +92,20 @@ export default class ZapController {
|
||||
|
||||
setVisibility(visible) {
|
||||
const lightningVisible = !!visible;
|
||||
const shouldShow = lightningVisible && this.isUserLoggedIn();
|
||||
const loggedIn = this.isUserLoggedIn();
|
||||
const sessionActorAvailable = this.hasSessionActor();
|
||||
const shouldShow =
|
||||
lightningVisible && (loggedIn || sessionActorAvailable);
|
||||
const requiresLogin = shouldShow && !loggedIn;
|
||||
const hasWallet = this.hasActiveWalletConnection();
|
||||
if (this.videoModal) {
|
||||
this.videoModal.setZapVisibility(shouldShow);
|
||||
this.videoModal.setWalletPromptVisible(shouldShow && !hasWallet);
|
||||
this.videoModal.setZapVisibility({
|
||||
visible: shouldShow,
|
||||
requiresLogin,
|
||||
});
|
||||
this.videoModal.setWalletPromptVisible(
|
||||
shouldShow && !requiresLogin && !hasWallet,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +129,10 @@ export default class ZapController {
|
||||
|
||||
open() {
|
||||
this.resetFeedback();
|
||||
if (!this.isUserLoggedIn()) {
|
||||
this.notifyError("Log in with your Nostr profile to send zaps.");
|
||||
return;
|
||||
}
|
||||
this.applyDefaultAmount();
|
||||
this.preloadLightningMetadata().catch((error) => {
|
||||
devLogger.warn("[zap] Preload metadata error:", error);
|
||||
|
||||
Reference in new issue
Block a user