mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2243 from PR0M3TH3AN/refactor-application-monolith-3243348981324014635
Refactor Application class into Router and UI Coordinators
This commit is contained in:
5 files changed
+577
-448
No files matched your search
@@ -177,7 +177,14 @@ import { createPlaybackCoordinator } from "./app/playbackCoordinator.js";
|
||||
import { createAuthSessionCoordinator } from "./app/authSessionCoordinator.js";
|
||||
import { createModalCoordinator } from "./app/modalCoordinator.js";
|
||||
import { createModerationCoordinator } from "./app/moderationCoordinator.js";
|
||||
import { createRouterCoordinator } from "./app/routerCoordinator.js";
|
||||
import { createUiCoordinator } from "./app/uiCoordinator.js";
|
||||
import { HEX64_REGEX } from "./utils/hex.js";
|
||||
import {
|
||||
safeEncodeNpub,
|
||||
safeDecodeNpub,
|
||||
normalizeHexPubkey,
|
||||
} from "./utils/nostrHelpers.js";
|
||||
|
||||
const recordVideoViewApi = (...args) => recordVideoView(nostrClient, ...args);
|
||||
|
||||
@@ -640,6 +647,22 @@ class Application {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "_router", {
|
||||
value: bindCoordinator(this, createRouterCoordinator({
|
||||
devLogger,
|
||||
})),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "_ui", {
|
||||
value: bindCoordinator(this, createUiCoordinator({
|
||||
devLogger,
|
||||
})),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
get modalVideo() {
|
||||
@@ -1266,56 +1289,14 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
goToProfile(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate = pubkey.trim();
|
||||
if (!candidate) {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidate.startsWith("nostr:")) {
|
||||
candidate = candidate.slice("nostr:".length);
|
||||
}
|
||||
|
||||
const normalizedHex = this.normalizeHexPubkey(candidate);
|
||||
const npub = normalizedHex ? this.safeEncodeNpub(normalizedHex) : null;
|
||||
|
||||
if (!npub) {
|
||||
devLogger.warn(
|
||||
"[Application] Invalid pubkey for profile navigation:",
|
||||
candidate,
|
||||
);
|
||||
this.showError("Invalid creator profile.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.hash = `#view=channel-profile&npub=${npub}`;
|
||||
goToProfile(...args) {
|
||||
this._initCoordinators();
|
||||
return this._router.goToProfile(...args);
|
||||
}
|
||||
|
||||
openCreatorChannel() {
|
||||
if (!this.currentVideo || !this.currentVideo.pubkey) {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Encode the hex pubkey to npub
|
||||
const npub = window.NostrTools.nip19.npubEncode(this.currentVideo.pubkey);
|
||||
|
||||
// Close the video modal
|
||||
this.hideModal();
|
||||
|
||||
// Switch to channel profile view
|
||||
window.location.hash = `#view=channel-profile&npub=${npub}`;
|
||||
} catch (err) {
|
||||
devLogger.error("Failed to open creator channel:", err);
|
||||
this.showError("Could not open channel.");
|
||||
}
|
||||
openCreatorChannel(...args) {
|
||||
this._initCoordinators();
|
||||
return this._router.openCreatorChannel(...args);
|
||||
}
|
||||
|
||||
openWalletPane() {
|
||||
@@ -1359,41 +1340,14 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
dispatchAuthLoadingState(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-loading-state", { detail: payload }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to dispatch auth loading state:", error);
|
||||
return false;
|
||||
}
|
||||
dispatchAuthLoadingState(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.dispatchAuthLoadingState(...args);
|
||||
}
|
||||
|
||||
updateAuthLoadingState(partial = {}) {
|
||||
const baseState =
|
||||
this.authLoadingState && typeof this.authLoadingState === "object"
|
||||
? this.authLoadingState
|
||||
: { profile: "idle", lists: "idle", dms: "idle" };
|
||||
const nextState = {
|
||||
...baseState,
|
||||
...(partial && typeof partial === "object" ? partial : {}),
|
||||
};
|
||||
this.authLoadingState = nextState;
|
||||
|
||||
const root = typeof document !== "undefined" ? document.documentElement : null;
|
||||
if (root) {
|
||||
root.dataset.authProfileLoading = nextState.profile || "idle";
|
||||
root.dataset.authListsLoading = nextState.lists || "idle";
|
||||
root.dataset.authDmsLoading = nextState.dms || "idle";
|
||||
}
|
||||
|
||||
this.dispatchAuthLoadingState(nextState);
|
||||
return nextState;
|
||||
updateAuthLoadingState(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.updateAuthLoadingState(...args);
|
||||
}
|
||||
|
||||
normalizeModalTrigger(...args) {
|
||||
@@ -1685,23 +1639,9 @@ class Application {
|
||||
this.closeAllMoreMenus();
|
||||
}
|
||||
|
||||
handleProfileChannelLink(element) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNpub =
|
||||
typeof element.dataset.targetNpub === "string"
|
||||
? element.dataset.targetNpub
|
||||
: "";
|
||||
if (this.profileController) {
|
||||
this.profileController.hide();
|
||||
}
|
||||
if (targetNpub) {
|
||||
window.location.hash = `#view=channel-profile&npub=${encodeURIComponent(
|
||||
targetNpub,
|
||||
)}`;
|
||||
}
|
||||
handleProfileChannelLink(...args) {
|
||||
this._initCoordinators();
|
||||
return this._router.handleProfileChannelLink(...args);
|
||||
}
|
||||
|
||||
async handleLoginModalSuccess(payload = {}) {
|
||||
@@ -3282,36 +3222,9 @@ class Application {
|
||||
return this._moderation.refreshVisibleModerationUi(...args);
|
||||
}
|
||||
|
||||
updateActiveProfileUI(pubkey, profile = {}) {
|
||||
if (this.profileController) {
|
||||
this.profileController.handleProfileUpdated({
|
||||
pubkey,
|
||||
profile,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const picture = profile.picture || "assets/svg/default-profile.svg";
|
||||
|
||||
if (this.profileAvatar) {
|
||||
this.profileAvatar.src = picture;
|
||||
}
|
||||
|
||||
const channelLink = document.getElementById("profileChannelLink");
|
||||
if (channelLink instanceof HTMLElement) {
|
||||
const targetNpub = this.safeEncodeNpub(pubkey);
|
||||
if (targetNpub) {
|
||||
channelLink.href = `#view=channel-profile&npub=${targetNpub}`;
|
||||
channelLink.dataset.targetNpub = targetNpub;
|
||||
channelLink.classList.remove("hidden");
|
||||
} else {
|
||||
channelLink.removeAttribute("href");
|
||||
if (channelLink.dataset) {
|
||||
delete channelLink.dataset.targetNpub;
|
||||
}
|
||||
channelLink.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
updateActiveProfileUI(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.updateActiveProfileUI(...args);
|
||||
}
|
||||
|
||||
async hydrateNwcSettingsForPubkey(pubkey) {
|
||||
@@ -3392,226 +3305,49 @@ class Application {
|
||||
return this.nwcSettingsService.clearStoredNwcSettings(pubkey, options);
|
||||
}
|
||||
|
||||
applyAuthenticatedUiState() {
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.add("hidden");
|
||||
this.loginButton.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
if (this.logoutButton) {
|
||||
this.logoutButton.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (this.userStatus) {
|
||||
this.userStatus.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.uploadButton) {
|
||||
this.uploadButton.classList.remove("hidden");
|
||||
this.uploadButton.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
if (this.profileButton) {
|
||||
this.profileButton.classList.remove("hidden");
|
||||
this.profileButton.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
const subscriptionsLink = this.resolveSubscriptionsLink();
|
||||
if (subscriptionsLink) {
|
||||
subscriptionsLink.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const forYouLink = this.resolveForYouLink();
|
||||
if (forYouLink) {
|
||||
forYouLink.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const exploreLink = this.resolveExploreLink();
|
||||
if (exploreLink) {
|
||||
exploreLink.classList.remove("hidden");
|
||||
}
|
||||
applyAuthenticatedUiState(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.applyAuthenticatedUiState(...args);
|
||||
}
|
||||
|
||||
applyLoggedOutUiState() {
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.remove("hidden");
|
||||
this.loginButton.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
if (this.logoutButton) {
|
||||
this.logoutButton.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.userStatus) {
|
||||
this.userStatus.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.userPubKey) {
|
||||
this.userPubKey.textContent = "";
|
||||
}
|
||||
|
||||
if (this.uploadButton) {
|
||||
this.uploadButton.classList.add("hidden");
|
||||
this.uploadButton.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
if (this.profileButton) {
|
||||
this.profileButton.classList.add("hidden");
|
||||
this.profileButton.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
const subscriptionsLink = this.resolveSubscriptionsLink();
|
||||
if (subscriptionsLink) {
|
||||
subscriptionsLink.classList.add("hidden");
|
||||
}
|
||||
|
||||
const forYouLink = this.resolveForYouLink();
|
||||
if (forYouLink) {
|
||||
forYouLink.classList.add("hidden");
|
||||
}
|
||||
|
||||
const exploreLink = this.resolveExploreLink();
|
||||
if (exploreLink) {
|
||||
exploreLink.classList.add("hidden");
|
||||
}
|
||||
applyLoggedOutUiState(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.applyLoggedOutUiState(...args);
|
||||
}
|
||||
|
||||
syncAuthUiState() {
|
||||
if (this.isUserLoggedIn()) {
|
||||
this.applyAuthenticatedUiState();
|
||||
} else {
|
||||
this.applyLoggedOutUiState();
|
||||
}
|
||||
syncAuthUiState(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.syncAuthUiState(...args);
|
||||
}
|
||||
|
||||
refreshChromeElements() {
|
||||
const doc = typeof document !== "undefined" ? document : null;
|
||||
const findElement = (id) => {
|
||||
if (!doc || typeof doc.getElementById !== "function") {
|
||||
return null;
|
||||
}
|
||||
const element = doc.getElementById(id);
|
||||
if (!element || typeof element.addEventListener !== "function") {
|
||||
return null;
|
||||
}
|
||||
return element;
|
||||
};
|
||||
|
||||
this.loginButton = findElement("loginButton");
|
||||
this.logoutButton = findElement("logoutButton");
|
||||
this.uploadButton = findElement("uploadButton");
|
||||
this.profileButton = findElement("profileButton");
|
||||
this.closeLoginModalBtn = findElement("closeLoginModal");
|
||||
|
||||
return {
|
||||
loginButton: this.loginButton,
|
||||
logoutButton: this.logoutButton,
|
||||
uploadButton: this.uploadButton,
|
||||
profileButton: this.profileButton,
|
||||
closeLoginModalButton: this.closeLoginModalBtn,
|
||||
};
|
||||
refreshChromeElements(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.refreshChromeElements(...args);
|
||||
}
|
||||
|
||||
resolveSubscriptionsLink() {
|
||||
if (
|
||||
this.subscriptionsLink instanceof HTMLElement &&
|
||||
this.subscriptionsLink.isConnected
|
||||
) {
|
||||
return this.subscriptionsLink;
|
||||
}
|
||||
|
||||
const linkCandidate = document.getElementById("subscriptionsLink");
|
||||
if (linkCandidate instanceof HTMLElement) {
|
||||
this.subscriptionsLink = linkCandidate;
|
||||
return this.subscriptionsLink;
|
||||
}
|
||||
|
||||
this.subscriptionsLink = null;
|
||||
return null;
|
||||
resolveSubscriptionsLink(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.resolveSubscriptionsLink(...args);
|
||||
}
|
||||
|
||||
resolveForYouLink() {
|
||||
if (this.forYouLink instanceof HTMLElement && this.forYouLink.isConnected) {
|
||||
return this.forYouLink;
|
||||
}
|
||||
|
||||
const linkCandidate = document.getElementById("forYouLink");
|
||||
if (linkCandidate instanceof HTMLElement) {
|
||||
this.forYouLink = linkCandidate;
|
||||
return this.forYouLink;
|
||||
}
|
||||
|
||||
this.forYouLink = null;
|
||||
return null;
|
||||
resolveForYouLink(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.resolveForYouLink(...args);
|
||||
}
|
||||
|
||||
resolveExploreLink() {
|
||||
if (
|
||||
this.exploreLink instanceof HTMLElement &&
|
||||
this.exploreLink.isConnected
|
||||
) {
|
||||
return this.exploreLink;
|
||||
}
|
||||
|
||||
const linkCandidate = document.getElementById("exploreLink");
|
||||
if (linkCandidate instanceof HTMLElement) {
|
||||
this.exploreLink = linkCandidate;
|
||||
return this.exploreLink;
|
||||
}
|
||||
|
||||
this.exploreLink = null;
|
||||
return null;
|
||||
resolveExploreLink(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.resolveExploreLink(...args);
|
||||
}
|
||||
|
||||
hydrateSidebarNavigation() {
|
||||
const chromeElements = this.refreshChromeElements();
|
||||
this.resolveSubscriptionsLink();
|
||||
this.resolveForYouLink();
|
||||
this.resolveExploreLink();
|
||||
|
||||
if (this.appChromeController) {
|
||||
if (typeof this.appChromeController.setElements === "function") {
|
||||
this.appChromeController.setElements(chromeElements);
|
||||
} else {
|
||||
if (this.appChromeController.elements) {
|
||||
Object.assign(this.appChromeController.elements, chromeElements);
|
||||
}
|
||||
this.appChromeController.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
this.syncAuthUiState();
|
||||
hydrateSidebarNavigation(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.hydrateSidebarNavigation(...args);
|
||||
}
|
||||
|
||||
maybeShowExperimentalLoginWarning(provider) {
|
||||
const normalizedProvider =
|
||||
typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
||||
|
||||
if (normalizedProvider !== "nsec" && normalizedProvider !== "nip46") {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (
|
||||
this.lastExperimentalWarningKey === normalizedProvider &&
|
||||
typeof this.lastExperimentalWarningAt === "number" &&
|
||||
now - this.lastExperimentalWarningAt < 2000
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastExperimentalWarningKey = normalizedProvider;
|
||||
this.lastExperimentalWarningAt = now;
|
||||
|
||||
const providerLabel =
|
||||
normalizedProvider === "nsec"
|
||||
? "Direct nsec or seed"
|
||||
: "NIP-46 remote signer";
|
||||
|
||||
this.showStatus(
|
||||
`${providerLabel} logins are still in development and may not work well yet. We recommend using a NIP-07 browser extension for the most reliable experience.`,
|
||||
{ showSpinner: false, autoHideMs: 5000 },
|
||||
);
|
||||
maybeShowExperimentalLoginWarning(...args) {
|
||||
this._initCoordinators();
|
||||
return this._ui.maybeShowExperimentalLoginWarning(...args);
|
||||
}
|
||||
|
||||
async handlePermissionPromptRequest() {
|
||||
@@ -4908,46 +4644,11 @@ class Application {
|
||||
* Simple helper to safely encode an npub.
|
||||
*/
|
||||
safeEncodeNpub(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.NostrTools.nip19.npubEncode(trimmed);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
return safeEncodeNpub(pubkey);
|
||||
}
|
||||
|
||||
safeDecodeNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||||
if (decoded.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return safeDecodeNpub(npub);
|
||||
}
|
||||
|
||||
selectPreferredCreatorName(candidates = []) {
|
||||
@@ -5379,27 +5080,7 @@ class Application {
|
||||
}
|
||||
|
||||
normalizeHexPubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HEX64_REGEX.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(trimmed);
|
||||
if (decoded && HEX64_REGEX.test(decoded)) {
|
||||
return decoded.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return normalizeHexPubkey(pubkey);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// js/app/routerCoordinator.js
|
||||
|
||||
/**
|
||||
* Routing logic for the application.
|
||||
*
|
||||
* All module-level dependencies are injected from the Application
|
||||
* composition root rather than imported at module scope.
|
||||
*
|
||||
* Methods use `this` which is bound to the Application instance.
|
||||
*/
|
||||
export function createRouterCoordinator(deps) {
|
||||
const {
|
||||
devLogger,
|
||||
} = deps;
|
||||
|
||||
return {
|
||||
goToProfile(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate = pubkey.trim();
|
||||
if (!candidate) {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidate.startsWith("nostr:")) {
|
||||
candidate = candidate.slice("nostr:".length);
|
||||
}
|
||||
|
||||
const normalizedHex = this.normalizeHexPubkey(candidate);
|
||||
const npub = normalizedHex ? this.safeEncodeNpub(normalizedHex) : null;
|
||||
|
||||
if (!npub) {
|
||||
devLogger.warn(
|
||||
"[Application] Invalid pubkey for profile navigation:",
|
||||
candidate,
|
||||
);
|
||||
this.showError("Invalid creator profile.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.hash = `#view=channel-profile&npub=${npub}`;
|
||||
},
|
||||
|
||||
openCreatorChannel() {
|
||||
if (!this.currentVideo || !this.currentVideo.pubkey) {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Encode the hex pubkey to npub
|
||||
const npub = window.NostrTools.nip19.npubEncode(this.currentVideo.pubkey);
|
||||
|
||||
// Close the video modal
|
||||
this.hideModal();
|
||||
|
||||
// Switch to channel profile view
|
||||
window.location.hash = `#view=channel-profile&npub=${npub}`;
|
||||
} catch (err) {
|
||||
devLogger.error("Failed to open creator channel:", err);
|
||||
this.showError("Could not open channel.");
|
||||
}
|
||||
},
|
||||
|
||||
handleProfileChannelLink(element) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNpub =
|
||||
typeof element.dataset.targetNpub === "string"
|
||||
? element.dataset.targetNpub
|
||||
: "";
|
||||
if (this.profileController) {
|
||||
this.profileController.hide();
|
||||
}
|
||||
if (targetNpub) {
|
||||
window.location.hash = `#view=channel-profile&npub=${encodeURIComponent(
|
||||
targetNpub,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
// js/app/uiCoordinator.js
|
||||
|
||||
/**
|
||||
* UI State and Chrome management.
|
||||
*
|
||||
* All module-level dependencies are injected from the Application
|
||||
* composition root rather than imported at module scope.
|
||||
*
|
||||
* Methods use `this` which is bound to the Application instance.
|
||||
*/
|
||||
export function createUiCoordinator(deps) {
|
||||
const {
|
||||
devLogger,
|
||||
} = deps;
|
||||
|
||||
return {
|
||||
dispatchAuthLoadingState(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-loading-state", { detail: payload }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to dispatch auth loading state:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateAuthLoadingState(partial = {}) {
|
||||
const baseState =
|
||||
this.authLoadingState && typeof this.authLoadingState === "object"
|
||||
? this.authLoadingState
|
||||
: { profile: "idle", lists: "idle", dms: "idle" };
|
||||
const nextState = {
|
||||
...baseState,
|
||||
...(partial && typeof partial === "object" ? partial : {}),
|
||||
};
|
||||
this.authLoadingState = nextState;
|
||||
|
||||
const root = typeof document !== "undefined" ? document.documentElement : null;
|
||||
if (root) {
|
||||
root.dataset.authProfileLoading = nextState.profile || "idle";
|
||||
root.dataset.authListsLoading = nextState.lists || "idle";
|
||||
root.dataset.authDmsLoading = nextState.dms || "idle";
|
||||
}
|
||||
|
||||
this.dispatchAuthLoadingState(nextState);
|
||||
return nextState;
|
||||
},
|
||||
|
||||
updateActiveProfileUI(pubkey, profile = {}) {
|
||||
if (this.profileController) {
|
||||
this.profileController.handleProfileUpdated({
|
||||
pubkey,
|
||||
profile,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const picture = profile.picture || "assets/svg/default-profile.svg";
|
||||
|
||||
if (this.profileAvatar) {
|
||||
this.profileAvatar.src = picture;
|
||||
}
|
||||
|
||||
const channelLink = document.getElementById("profileChannelLink");
|
||||
if (channelLink instanceof HTMLElement) {
|
||||
const targetNpub = this.safeEncodeNpub(pubkey);
|
||||
if (targetNpub) {
|
||||
channelLink.href = `#view=channel-profile&npub=${targetNpub}`;
|
||||
channelLink.dataset.targetNpub = targetNpub;
|
||||
channelLink.classList.remove("hidden");
|
||||
} else {
|
||||
channelLink.removeAttribute("href");
|
||||
if (channelLink.dataset) {
|
||||
delete channelLink.dataset.targetNpub;
|
||||
}
|
||||
channelLink.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
applyAuthenticatedUiState() {
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.add("hidden");
|
||||
this.loginButton.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
if (this.logoutButton) {
|
||||
this.logoutButton.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (this.userStatus) {
|
||||
this.userStatus.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.uploadButton) {
|
||||
this.uploadButton.classList.remove("hidden");
|
||||
this.uploadButton.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
if (this.profileButton) {
|
||||
this.profileButton.classList.remove("hidden");
|
||||
this.profileButton.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
const subscriptionsLink = this.resolveSubscriptionsLink();
|
||||
if (subscriptionsLink) {
|
||||
subscriptionsLink.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const forYouLink = this.resolveForYouLink();
|
||||
if (forYouLink) {
|
||||
forYouLink.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const exploreLink = this.resolveExploreLink();
|
||||
if (exploreLink) {
|
||||
exploreLink.classList.remove("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
applyLoggedOutUiState() {
|
||||
if (this.loginButton) {
|
||||
this.loginButton.classList.remove("hidden");
|
||||
this.loginButton.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
if (this.logoutButton) {
|
||||
this.logoutButton.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.userStatus) {
|
||||
this.userStatus.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.userPubKey) {
|
||||
this.userPubKey.textContent = "";
|
||||
}
|
||||
|
||||
if (this.uploadButton) {
|
||||
this.uploadButton.classList.add("hidden");
|
||||
this.uploadButton.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
if (this.profileButton) {
|
||||
this.profileButton.classList.add("hidden");
|
||||
this.profileButton.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
const subscriptionsLink = this.resolveSubscriptionsLink();
|
||||
if (subscriptionsLink) {
|
||||
subscriptionsLink.classList.add("hidden");
|
||||
}
|
||||
|
||||
const forYouLink = this.resolveForYouLink();
|
||||
if (forYouLink) {
|
||||
forYouLink.classList.add("hidden");
|
||||
}
|
||||
|
||||
const exploreLink = this.resolveExploreLink();
|
||||
if (exploreLink) {
|
||||
exploreLink.classList.add("hidden");
|
||||
}
|
||||
},
|
||||
|
||||
syncAuthUiState() {
|
||||
if (this.isUserLoggedIn()) {
|
||||
this.applyAuthenticatedUiState();
|
||||
} else {
|
||||
this.applyLoggedOutUiState();
|
||||
}
|
||||
},
|
||||
|
||||
refreshChromeElements() {
|
||||
const doc = typeof document !== "undefined" ? document : null;
|
||||
const findElement = (id) => {
|
||||
if (!doc || typeof doc.getElementById !== "function") {
|
||||
return null;
|
||||
}
|
||||
const element = doc.getElementById(id);
|
||||
if (!element || typeof element.addEventListener !== "function") {
|
||||
return null;
|
||||
}
|
||||
return element;
|
||||
};
|
||||
|
||||
this.loginButton = findElement("loginButton");
|
||||
this.logoutButton = findElement("logoutButton");
|
||||
this.uploadButton = findElement("uploadButton");
|
||||
this.profileButton = findElement("profileButton");
|
||||
this.closeLoginModalBtn = findElement("closeLoginModal");
|
||||
|
||||
return {
|
||||
loginButton: this.loginButton,
|
||||
logoutButton: this.logoutButton,
|
||||
uploadButton: this.uploadButton,
|
||||
profileButton: this.profileButton,
|
||||
closeLoginModalButton: this.closeLoginModalBtn,
|
||||
};
|
||||
},
|
||||
|
||||
resolveSubscriptionsLink() {
|
||||
if (
|
||||
this.subscriptionsLink instanceof HTMLElement &&
|
||||
this.subscriptionsLink.isConnected
|
||||
) {
|
||||
return this.subscriptionsLink;
|
||||
}
|
||||
|
||||
const linkCandidate = document.getElementById("subscriptionsLink");
|
||||
if (linkCandidate instanceof HTMLElement) {
|
||||
this.subscriptionsLink = linkCandidate;
|
||||
return this.subscriptionsLink;
|
||||
}
|
||||
|
||||
this.subscriptionsLink = null;
|
||||
return null;
|
||||
},
|
||||
|
||||
resolveForYouLink() {
|
||||
if (this.forYouLink instanceof HTMLElement && this.forYouLink.isConnected) {
|
||||
return this.forYouLink;
|
||||
}
|
||||
|
||||
const linkCandidate = document.getElementById("forYouLink");
|
||||
if (linkCandidate instanceof HTMLElement) {
|
||||
this.forYouLink = linkCandidate;
|
||||
return this.forYouLink;
|
||||
}
|
||||
|
||||
this.forYouLink = null;
|
||||
return null;
|
||||
},
|
||||
|
||||
resolveExploreLink() {
|
||||
if (
|
||||
this.exploreLink instanceof HTMLElement &&
|
||||
this.exploreLink.isConnected
|
||||
) {
|
||||
return this.exploreLink;
|
||||
}
|
||||
|
||||
const linkCandidate = document.getElementById("exploreLink");
|
||||
if (linkCandidate instanceof HTMLElement) {
|
||||
this.exploreLink = linkCandidate;
|
||||
return this.exploreLink;
|
||||
}
|
||||
|
||||
this.exploreLink = null;
|
||||
return null;
|
||||
},
|
||||
|
||||
hydrateSidebarNavigation() {
|
||||
const chromeElements = this.refreshChromeElements();
|
||||
this.resolveSubscriptionsLink();
|
||||
this.resolveForYouLink();
|
||||
this.resolveExploreLink();
|
||||
|
||||
if (this.appChromeController) {
|
||||
if (typeof this.appChromeController.setElements === "function") {
|
||||
this.appChromeController.setElements(chromeElements);
|
||||
} else {
|
||||
if (this.appChromeController.elements) {
|
||||
Object.assign(this.appChromeController.elements, chromeElements);
|
||||
}
|
||||
this.appChromeController.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
this.syncAuthUiState();
|
||||
},
|
||||
|
||||
maybeShowExperimentalLoginWarning(provider) {
|
||||
const normalizedProvider =
|
||||
typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
||||
|
||||
if (normalizedProvider !== "nsec" && normalizedProvider !== "nip46") {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (
|
||||
this.lastExperimentalWarningKey === normalizedProvider &&
|
||||
typeof this.lastExperimentalWarningAt === "number" &&
|
||||
now - this.lastExperimentalWarningAt < 2000
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastExperimentalWarningKey = normalizedProvider;
|
||||
this.lastExperimentalWarningAt = now;
|
||||
|
||||
const providerLabel =
|
||||
normalizedProvider === "nsec"
|
||||
? "Direct nsec or seed"
|
||||
: "NIP-46 remote signer";
|
||||
|
||||
this.showStatus(
|
||||
`${providerLabel} logins are still in development and may not work well yet. We recommend using a NIP-07 browser extension for the most reliable experience.`,
|
||||
{ showSpinner: false, autoHideMs: 5000 },
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -29,6 +29,11 @@ import getDefaultAuthProvider, {
|
||||
} from "./authProviders/index.js";
|
||||
import { fetchProfileMetadata } from "./profileMetadataService.js";
|
||||
import { HEX64_REGEX } from "../utils/hex.js";
|
||||
import {
|
||||
safeEncodeNpub,
|
||||
safeDecodeNpub,
|
||||
normalizeHexPubkey,
|
||||
} from "../utils/nostrHelpers.js";
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
@@ -267,70 +272,15 @@ export default class AuthService {
|
||||
}
|
||||
|
||||
safeEncodeNpub(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
try {
|
||||
return window?.NostrTools?.nip19?.npubEncode(trimmed) || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return safeEncodeNpub(pubkey);
|
||||
}
|
||||
|
||||
safeDecodeNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = window?.NostrTools?.nip19?.decode(trimmed);
|
||||
if (decoded?.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return safeDecodeNpub(npub);
|
||||
}
|
||||
|
||||
normalizeHexPubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HEX64_REGEX.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = this.safeDecodeNpub(trimmed);
|
||||
if (decoded && HEX64_REGEX.test(decoded)) {
|
||||
return decoded.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return normalizeHexPubkey(pubkey);
|
||||
}
|
||||
|
||||
async requestLogin(rawOptions = {}) {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { HEX64_REGEX } from "./hex.js";
|
||||
|
||||
/**
|
||||
* Safely encode a hex pubkey to npub.
|
||||
*
|
||||
* @param {string} pubkey - The hex public key.
|
||||
* @returns {string|null} The npub string or null if encoding failed.
|
||||
*/
|
||||
export function safeEncodeNpub(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.NostrTools &&
|
||||
window.NostrTools.nip19 &&
|
||||
typeof window.NostrTools.nip19.npubEncode === "function"
|
||||
) {
|
||||
return window.NostrTools.nip19.npubEncode(trimmed);
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely decode an npub to hex pubkey.
|
||||
*
|
||||
* @param {string} npub - The npub string.
|
||||
* @returns {string|null} The hex public key or null if decoding failed.
|
||||
*/
|
||||
export function safeDecodeNpub(npub) {
|
||||
if (typeof npub !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = npub.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.NostrTools &&
|
||||
window.NostrTools.nip19 &&
|
||||
typeof window.NostrTools.nip19.decode === "function"
|
||||
) {
|
||||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||||
if (decoded.type === "npub" && typeof decoded.data === "string") {
|
||||
return decoded.data;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a public key to hex format, handling npub inputs.
|
||||
*
|
||||
* @param {string} pubkey - The public key (hex or npub).
|
||||
* @returns {string|null} The normalized hex public key or null.
|
||||
*/
|
||||
export function normalizeHexPubkey(pubkey) {
|
||||
if (typeof pubkey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = pubkey.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HEX64_REGEX.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("npub1")) {
|
||||
const decoded = safeDecodeNpub(trimmed);
|
||||
if (decoded && HEX64_REGEX.test(decoded)) {
|
||||
return decoded.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user