mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
Merge pull request #352 from PR0M3TH3AN/codex/refactor-profilemodal-integration-in-app.js-565efn
Improve channel profile loading reliability and modal playback
This commit is contained in:
8 files changed
+1183
-699
No files matched your search
@@ -987,6 +987,28 @@ body.modal-open #app {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.modal-open #sidebar,
|
||||
body.modal-open #sidebarOverlay {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body.modal-open #sidebar {
|
||||
z-index: var(--z-sidebar-overlay);
|
||||
}
|
||||
|
||||
body.modal-open #sidebarOverlay {
|
||||
z-index: calc(var(--z-sidebar-overlay) - 1);
|
||||
}
|
||||
|
||||
#sidebar.is-modal-hidden,
|
||||
#sidebarOverlay.is-modal-hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Modal Content Container */
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
|
||||
@@ -193,6 +193,7 @@ class Application {
|
||||
this.watchHistoryPreferenceUnsubscribe = null;
|
||||
this.authEventUnsubscribes = [];
|
||||
this.unsubscribeFromNostrService = null;
|
||||
this.videoModalReadyPromise = null;
|
||||
this.boundUploadSubmitHandler = null;
|
||||
this.boundEditModalSubmitHandler = null;
|
||||
this.boundEditModalCancelHandler = null;
|
||||
@@ -291,11 +292,7 @@ class Application {
|
||||
this.profileAvatar = document.getElementById("profileAvatar") || null;
|
||||
|
||||
// Profile modal controller state
|
||||
this.activeProfilePane = null;
|
||||
this.isWalletPaneBusy = false;
|
||||
this.profileModalCachedSelection = null;
|
||||
this.profileModalController = null;
|
||||
this.profileSwitcherSelectionPubkey = null;
|
||||
this.profileController = null;
|
||||
this.currentUserNpub = null;
|
||||
|
||||
// Upload modal component
|
||||
@@ -388,6 +385,118 @@ class Application {
|
||||
this.boundRevertConfirmHandler
|
||||
);
|
||||
|
||||
try {
|
||||
const profileModalContainer = document.getElementById("modalContainer") || null;
|
||||
if (profileModalContainer) {
|
||||
const profileModalServices = {
|
||||
normalizeHexPubkey: (value) => this.normalizeHexPubkey(value),
|
||||
safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey),
|
||||
safeDecodeNpub: (npub) => this.safeDecodeNpub(npub),
|
||||
truncateMiddle: (value, maxLength) => truncateMiddle(value, maxLength),
|
||||
getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey),
|
||||
batchFetchProfiles: (authorSet) => this.batchFetchProfiles(authorSet),
|
||||
switchProfile: (pubkey) => this.authService.switchProfile(pubkey),
|
||||
removeSavedProfile: (pubkey) =>
|
||||
this.authService.removeSavedProfile(pubkey),
|
||||
relayManager,
|
||||
userBlocks,
|
||||
nostrClient,
|
||||
accessControl,
|
||||
getCurrentUserNpub: () => this.getCurrentUserNpub(),
|
||||
getActiveNwcSettings: () => this.getActiveNwcSettings(),
|
||||
updateActiveNwcSettings: (partial) =>
|
||||
this.updateActiveNwcSettings(partial),
|
||||
hydrateNwcSettingsForPubkey: (pubkey) =>
|
||||
this.hydrateNwcSettingsForPubkey(pubkey),
|
||||
createDefaultNwcSettings: () => createDefaultNwcSettings(),
|
||||
ensureWallet: (options) => this.ensureWallet(options),
|
||||
loadVideos: (forceFetch, context) =>
|
||||
this.loadVideos(forceFetch, context),
|
||||
onVideosShouldRefresh: (context) =>
|
||||
this.onVideosShouldRefresh(context),
|
||||
describeAdminError: (code) => this.describeAdminError(code),
|
||||
describeNotificationError: (code) =>
|
||||
this.describeNotificationError(code),
|
||||
onAccessControlUpdated: () => this.onAccessControlUpdated(),
|
||||
persistSavedProfiles: (options) => persistSavedProfiles(options),
|
||||
watchHistoryService,
|
||||
authService: this.authService,
|
||||
log: (...args) => this.log(...args),
|
||||
closeAllMoreMenus: () => this.closeAllMoreMenus(),
|
||||
};
|
||||
|
||||
const profileModalState = {
|
||||
getSavedProfiles: () => getSavedProfiles(),
|
||||
setSavedProfiles: (profiles, options) =>
|
||||
setSavedProfiles(Array.isArray(profiles) ? profiles : [], options),
|
||||
persistSavedProfiles: (options) => persistSavedProfiles(options),
|
||||
getActivePubkey: () => getActiveProfilePubkey(),
|
||||
setActivePubkey: (pubkey, options) => {
|
||||
const normalized =
|
||||
typeof pubkey === "string" && pubkey.trim()
|
||||
? pubkey.trim()
|
||||
: null;
|
||||
setStoredActiveProfilePubkey(normalized, options);
|
||||
return getActiveProfilePubkey();
|
||||
},
|
||||
};
|
||||
|
||||
const profileModalCallbacks = {
|
||||
onClose: () => this.handleProfileModalClosed(),
|
||||
onLogout: async () => this.authService.logout(),
|
||||
onChannelLink: (element) => this.handleProfileChannelLink(element),
|
||||
onAddAccount: (controller) => this.handleAddProfile(controller),
|
||||
onRequestSwitchProfile: (payload) =>
|
||||
this.handleProfileSwitchRequest(payload),
|
||||
onRelayOperation: (payload) =>
|
||||
this.handleProfileRelayOperation(payload),
|
||||
onRelayModeToggle: (payload) =>
|
||||
this.handleProfileRelayModeToggle(payload),
|
||||
onRelayRestore: (payload) =>
|
||||
this.handleProfileRelayRestore(payload),
|
||||
onBlocklistMutation: (payload) =>
|
||||
this.handleProfileBlocklistMutation(payload),
|
||||
onWalletPersist: (payload) =>
|
||||
this.handleProfileWalletPersist(payload),
|
||||
onWalletTestRequest: (payload) =>
|
||||
this.handleProfileWalletTest(payload),
|
||||
onWalletDisconnectRequest: (payload) =>
|
||||
this.handleProfileWalletDisconnect(payload),
|
||||
onAdminMutation: (payload) =>
|
||||
this.handleProfileAdminMutation(payload),
|
||||
onAdminNotifyError: (payload) =>
|
||||
this.handleProfileAdminNotifyError(payload),
|
||||
onHistoryReady: (payload) =>
|
||||
this.handleProfileHistoryEvent(payload),
|
||||
};
|
||||
|
||||
this.profileController = new ProfileModalController({
|
||||
modalContainer: profileModalContainer,
|
||||
removeTrackingScripts,
|
||||
createWatchHistoryRenderer,
|
||||
setGlobalModalState,
|
||||
showError: (message) => this.showError(message),
|
||||
showSuccess: (message) => this.showSuccess(message),
|
||||
showStatus: (message) => this.showStatus(message),
|
||||
constants: {
|
||||
MAX_WALLET_DEFAULT_ZAP,
|
||||
ADMIN_SUPER_NPUB,
|
||||
ADMIN_DM_IMAGE_URL,
|
||||
BITVID_WEBSITE_URL,
|
||||
},
|
||||
services: profileModalServices,
|
||||
state: profileModalState,
|
||||
callbacks: profileModalCallbacks,
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"[Application] Profile modal controller disabled: modal container not found.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize profile modal controller:", error);
|
||||
}
|
||||
|
||||
|
||||
// Optional small inline player stats
|
||||
this.status = document.getElementById("status") || null;
|
||||
@@ -478,11 +587,11 @@ class Application {
|
||||
this.boundVideoModalZapCommentHandler
|
||||
);
|
||||
this.boundVideoModalZapWalletHandler = () => {
|
||||
if (!this.profileModalController) {
|
||||
if (!this.profileController) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.profileModalController
|
||||
this.profileController
|
||||
.showWalletPane()
|
||||
.catch((error) => {
|
||||
console.error("Failed to open wallet pane:", error);
|
||||
@@ -894,7 +1003,34 @@ class Application {
|
||||
this.showError(`Failed to initialize edit modal: ${error.message}`);
|
||||
});
|
||||
|
||||
const profileModalPromise = this.initProfileModal();
|
||||
const profileModalPromise = this.profileController
|
||||
? this.profileController
|
||||
.load()
|
||||
.then(() => {
|
||||
try {
|
||||
this.renderSavedProfiles();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to render saved profiles after load:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
this.profileController.refreshWalletPaneState();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to refresh wallet pane after load:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load profile modal:", error);
|
||||
return false;
|
||||
})
|
||||
: Promise.resolve(false);
|
||||
|
||||
const modalBootstrapPromise = Promise.all([
|
||||
videoModalPromise,
|
||||
@@ -930,12 +1066,16 @@ class Application {
|
||||
);
|
||||
});
|
||||
|
||||
const adminPanePromise = this.refreshAdminPaneState().catch((error) => {
|
||||
console.warn(
|
||||
"Failed to update admin pane after connecting to Nostr:",
|
||||
error
|
||||
);
|
||||
});
|
||||
const adminPanePromise = this.profileController
|
||||
? Promise.resolve()
|
||||
.then(() => this.profileController.refreshAdminPaneState())
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
"Failed to update admin pane after connecting to Nostr:",
|
||||
error,
|
||||
);
|
||||
})
|
||||
: Promise.resolve(null);
|
||||
|
||||
await Promise.all([accessControlPromise, adminPanePromise]);
|
||||
|
||||
@@ -1047,11 +1187,39 @@ class Application {
|
||||
/**
|
||||
* Show the modal and set the "Please stand by" poster on the video.
|
||||
*/
|
||||
showModalWithPoster(video = this.currentVideo) {
|
||||
async showModalWithPoster(video = this.currentVideo) {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetVideo = video || this.currentVideo;
|
||||
|
||||
try {
|
||||
const { root } = await this.ensureVideoModalReady({
|
||||
ensureVideoElement: true,
|
||||
});
|
||||
|
||||
if (!this.videoModal) {
|
||||
return root || null;
|
||||
}
|
||||
|
||||
this.videoModal.open(targetVideo);
|
||||
this.applyModalLoadingPoster();
|
||||
|
||||
return (
|
||||
root ||
|
||||
(typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[Application] Failed to open the video modal before playback:",
|
||||
error
|
||||
);
|
||||
this.showError("Could not open the video player. Please try again.");
|
||||
return null;
|
||||
}
|
||||
this.videoModal.open(video || this.currentVideo);
|
||||
}
|
||||
|
||||
applyModalLoadingPoster() {
|
||||
@@ -1068,6 +1236,69 @@ class Application {
|
||||
return this.videoModal.forceRemovePoster(reason);
|
||||
}
|
||||
|
||||
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
|
||||
if (!this.videoModal) {
|
||||
throw new Error("Video modal instance is not available.");
|
||||
}
|
||||
|
||||
const getRoot = () =>
|
||||
typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null;
|
||||
const getVideoElement = () =>
|
||||
typeof this.videoModal.getVideoElement === "function"
|
||||
? this.videoModal.getVideoElement()
|
||||
: null;
|
||||
|
||||
const existingRoot = getRoot();
|
||||
const existingVideoElement = getVideoElement();
|
||||
|
||||
if (existingRoot && existingRoot.isConnected) {
|
||||
if (ensureVideoElement && !existingVideoElement) {
|
||||
throw new Error("Video modal video element is not ready.");
|
||||
}
|
||||
if (existingVideoElement) {
|
||||
this.modalVideo = existingVideoElement;
|
||||
}
|
||||
return {
|
||||
root: existingRoot,
|
||||
videoElement: existingVideoElement,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.videoModalReadyPromise) {
|
||||
if (typeof this.videoModal.load !== "function") {
|
||||
throw new Error("Video modal does not expose a load() method.");
|
||||
}
|
||||
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
|
||||
}
|
||||
|
||||
try {
|
||||
await this.videoModalReadyPromise;
|
||||
} catch (error) {
|
||||
this.videoModalReadyPromise = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.videoModalReadyPromise = null;
|
||||
|
||||
const readyRoot = getRoot();
|
||||
const readyVideoElement = getVideoElement();
|
||||
|
||||
if (ensureVideoElement && !readyVideoElement) {
|
||||
throw new Error("Video modal video element is missing after load().");
|
||||
}
|
||||
|
||||
if (readyVideoElement) {
|
||||
this.modalVideo = readyVideoElement;
|
||||
}
|
||||
|
||||
return {
|
||||
root: readyRoot,
|
||||
videoElement: readyVideoElement,
|
||||
};
|
||||
}
|
||||
|
||||
extractDTagValue(tags) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return "";
|
||||
@@ -1217,187 +1448,25 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
async initProfileModal() {
|
||||
try {
|
||||
const modalContainer = document.getElementById("modalContainer");
|
||||
if (!modalContainer) {
|
||||
throw new Error("Modal container element not found!");
|
||||
}
|
||||
|
||||
if (!this.profileModalController) {
|
||||
const profileModalServices = {
|
||||
normalizeHexPubkey: (value) => this.normalizeHexPubkey(value),
|
||||
safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey),
|
||||
safeDecodeNpub: (npub) => this.safeDecodeNpub(npub),
|
||||
truncateMiddle: (value, maxLength) => truncateMiddle(value, maxLength),
|
||||
getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey),
|
||||
batchFetchProfiles: (authorSet) => this.batchFetchProfiles(authorSet),
|
||||
switchProfile: (pubkey) => this.authService.switchProfile(pubkey),
|
||||
removeSavedProfile: (pubkey) =>
|
||||
this.authService.removeSavedProfile(pubkey),
|
||||
relayManager,
|
||||
userBlocks,
|
||||
nostrClient,
|
||||
accessControl,
|
||||
getCurrentUserNpub: () => this.getCurrentUserNpub(),
|
||||
getActiveNwcSettings: () => this.getActiveNwcSettings(),
|
||||
updateActiveNwcSettings: (partial) =>
|
||||
this.updateActiveNwcSettings(partial),
|
||||
hydrateNwcSettingsForPubkey: (pubkey) =>
|
||||
this.hydrateNwcSettingsForPubkey(pubkey),
|
||||
createDefaultNwcSettings: () => createDefaultNwcSettings(),
|
||||
ensureWallet: (options) => this.ensureWallet(options),
|
||||
loadVideos: (forceFetch) => this.loadVideos(forceFetch),
|
||||
onVideosShouldRefresh: (context) => this.onVideosShouldRefresh(context),
|
||||
describeAdminError: (code) => this.describeAdminError(code),
|
||||
describeNotificationError: (code) =>
|
||||
this.describeNotificationError(code),
|
||||
onAccessControlUpdated: () => this.onAccessControlUpdated(),
|
||||
persistSavedProfiles: (options) => persistSavedProfiles(options),
|
||||
};
|
||||
|
||||
const profileModalState = {
|
||||
getSavedProfiles: () => getSavedProfiles(),
|
||||
setSavedProfiles: (profiles, options) =>
|
||||
setSavedProfiles(Array.isArray(profiles) ? profiles : [], options),
|
||||
persistSavedProfiles: (options) => persistSavedProfiles(options),
|
||||
getActivePubkey: () => this.activeProfilePubkey,
|
||||
setActivePubkey: (pubkey, options) => {
|
||||
this.activeProfilePubkey =
|
||||
typeof pubkey === "string" && pubkey.trim()
|
||||
? pubkey.trim()
|
||||
: null;
|
||||
setStoredActiveProfilePubkey(this.activeProfilePubkey, options);
|
||||
return this.activeProfilePubkey;
|
||||
},
|
||||
getCachedSelection: () => this.profileModalCachedSelection || null,
|
||||
setCachedSelection: (value) => {
|
||||
const normalized =
|
||||
typeof value === "string" && value.trim()
|
||||
? value.trim()
|
||||
: null;
|
||||
this.profileModalCachedSelection = normalized;
|
||||
return this.profileModalCachedSelection;
|
||||
},
|
||||
getActivePane: () => {
|
||||
const pane =
|
||||
typeof this.activeProfilePane === "string" && this.activeProfilePane
|
||||
? this.activeProfilePane
|
||||
: "account";
|
||||
return pane;
|
||||
},
|
||||
setActivePane: (pane) => {
|
||||
if (pane === null) {
|
||||
this.activeProfilePane = null;
|
||||
return this.activeProfilePane;
|
||||
}
|
||||
|
||||
const normalized =
|
||||
typeof pane === "string" && pane.trim()
|
||||
? pane.trim().toLowerCase()
|
||||
: "account";
|
||||
this.activeProfilePane = normalized;
|
||||
return this.activeProfilePane;
|
||||
},
|
||||
getWalletBusy: () => Boolean(this.isWalletPaneBusy),
|
||||
setWalletBusy: (flag) => {
|
||||
this.isWalletPaneBusy = Boolean(flag);
|
||||
return this.isWalletPaneBusy;
|
||||
},
|
||||
};
|
||||
|
||||
const profileModalCallbacks = {
|
||||
onClose: () => this.handleProfileModalClosed(),
|
||||
onLogout: async () => this.authService.logout(),
|
||||
onChannelLink: (element) => this.handleProfileChannelLink(element),
|
||||
onAddAccount: (controller) => this.handleAddProfile(controller),
|
||||
onSelectPane: (pane) => {
|
||||
this.activeProfilePane = pane;
|
||||
},
|
||||
onPaneShown: (pane) => {
|
||||
this.activeProfilePane = pane;
|
||||
},
|
||||
onRequestSwitchProfile: (payload) =>
|
||||
this.handleProfileSwitchRequest(payload),
|
||||
onRelayOperation: (payload) =>
|
||||
this.handleProfileRelayOperation(payload),
|
||||
onRelayModeToggle: (payload) =>
|
||||
this.handleProfileRelayModeToggle(payload),
|
||||
onRelayRestore: (payload) =>
|
||||
this.handleProfileRelayRestore(payload),
|
||||
onBlocklistMutation: (payload) =>
|
||||
this.handleProfileBlocklistMutation(payload),
|
||||
onWalletPersist: (payload) =>
|
||||
this.handleProfileWalletPersist(payload),
|
||||
onWalletTestRequest: (payload) =>
|
||||
this.handleProfileWalletTest(payload),
|
||||
onWalletDisconnectRequest: (payload) =>
|
||||
this.handleProfileWalletDisconnect(payload),
|
||||
onAdminMutation: (payload) =>
|
||||
this.handleProfileAdminMutation(payload),
|
||||
onAdminNotifyError: (payload) =>
|
||||
this.handleProfileAdminNotifyError(payload),
|
||||
onHistoryReady: (payload) =>
|
||||
this.handleProfileHistoryEvent(payload),
|
||||
};
|
||||
|
||||
this.profileModalController = new ProfileModalController({
|
||||
modalContainer,
|
||||
removeTrackingScripts,
|
||||
createWatchHistoryRenderer,
|
||||
setGlobalModalState,
|
||||
showError: (message) => this.showError(message),
|
||||
showSuccess: (message) => this.showSuccess(message),
|
||||
showStatus: (message) => this.showStatus(message),
|
||||
constants: {
|
||||
MAX_WALLET_DEFAULT_ZAP,
|
||||
ADMIN_SUPER_NPUB,
|
||||
ADMIN_DM_IMAGE_URL,
|
||||
BITVID_WEBSITE_URL,
|
||||
},
|
||||
services: profileModalServices,
|
||||
state: profileModalState,
|
||||
callbacks: profileModalCallbacks,
|
||||
});
|
||||
}
|
||||
|
||||
const alreadyLoaded =
|
||||
this.profileModalController.profileModal instanceof HTMLElement;
|
||||
if (!alreadyLoaded) {
|
||||
const loaded = await this.profileModalController.load();
|
||||
if (!loaded) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this.profileModalController.cacheDomReferences();
|
||||
}
|
||||
|
||||
this.syncProfileModalState();
|
||||
this.renderSavedProfiles();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("initProfileModal failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
syncProfileModalState({
|
||||
includeSavedProfiles = true,
|
||||
includeActivePubkey = true,
|
||||
} = {}) {
|
||||
if (!this.profileModalController) {
|
||||
if (!this.profileController) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (includeSavedProfiles) {
|
||||
try {
|
||||
const entries = Array.isArray(this.savedProfiles)
|
||||
? this.savedProfiles.slice()
|
||||
: [];
|
||||
this.profileModalController.setSavedProfiles(entries, {
|
||||
persist: false,
|
||||
persistActive: false,
|
||||
});
|
||||
if (typeof this.profileController.setSavedProfiles === "function") {
|
||||
const entries = Array.isArray(this.savedProfiles)
|
||||
? this.savedProfiles.slice()
|
||||
: [];
|
||||
this.profileController.setSavedProfiles(entries, {
|
||||
persist: false,
|
||||
persistActive: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to synchronize saved profiles with controller:",
|
||||
@@ -1408,10 +1477,12 @@ class Application {
|
||||
|
||||
if (includeActivePubkey) {
|
||||
try {
|
||||
this.profileModalController.setActivePubkey(
|
||||
this.activeProfilePubkey || null,
|
||||
{ persist: false },
|
||||
);
|
||||
if (typeof this.profileController.setActivePubkey === "function") {
|
||||
this.profileController.setActivePubkey(
|
||||
this.activeProfilePubkey || null,
|
||||
{ persist: false },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to synchronize active profile with controller:",
|
||||
@@ -1422,44 +1493,22 @@ class Application {
|
||||
}
|
||||
|
||||
renderSavedProfiles() {
|
||||
if (!this.profileModalController) {
|
||||
if (!this.profileController) {
|
||||
return;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
this.profileModalController.renderSavedProfiles();
|
||||
}
|
||||
|
||||
selectProfilePane(name = "account") {
|
||||
if (!this.profileModalController) {
|
||||
return;
|
||||
try {
|
||||
this.profileController.renderSavedProfiles();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to render saved profiles:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
this.profileModalController.selectPane(name);
|
||||
}
|
||||
|
||||
setWalletPaneBusy(isBusy = false) {
|
||||
this.isWalletPaneBusy = Boolean(isBusy);
|
||||
if (!this.profileModalController) {
|
||||
return;
|
||||
}
|
||||
this.profileModalController.setWalletPaneBusy(this.isWalletPaneBusy);
|
||||
}
|
||||
|
||||
refreshWalletPaneState() {
|
||||
if (!this.profileModalController) {
|
||||
return;
|
||||
}
|
||||
this.syncProfileModalState({ includeSavedProfiles: false });
|
||||
this.profileModalController.refreshWalletPaneState();
|
||||
}
|
||||
|
||||
updateWalletStatus(message, variant = "info") {
|
||||
this.profileModalController?.updateWalletStatus(message, variant);
|
||||
}
|
||||
|
||||
handleProfileModalClosed() {
|
||||
this.closeAllMoreMenus();
|
||||
this.activeProfilePane = null;
|
||||
}
|
||||
|
||||
handleProfileChannelLink(element) {
|
||||
@@ -1471,8 +1520,8 @@ class Application {
|
||||
typeof element.dataset.targetNpub === "string"
|
||||
? element.dataset.targetNpub
|
||||
: "";
|
||||
if (this.profileModalController) {
|
||||
this.profileModalController.hide();
|
||||
if (this.profileController) {
|
||||
this.profileController.hide();
|
||||
}
|
||||
if (targetNpub) {
|
||||
window.location.hash = `#view=channel-profile&npub=${encodeURIComponent(
|
||||
@@ -1481,74 +1530,10 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
populateProfileRelays(relayEntries = null) {
|
||||
if (!this.profileModalController) {
|
||||
return;
|
||||
}
|
||||
this.syncProfileModalState({ includeSavedProfiles: false });
|
||||
this.profileModalController.populateProfileRelays(relayEntries);
|
||||
}
|
||||
|
||||
populateBlockedList(blocked = null) {
|
||||
if (!this.profileModalController) {
|
||||
return;
|
||||
}
|
||||
this.syncProfileModalState({ includeSavedProfiles: false });
|
||||
this.profileModalController.populateBlockedList(blocked);
|
||||
}
|
||||
|
||||
refreshAdminPaneState() {
|
||||
if (!this.profileModalController) {
|
||||
return null;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
return this.profileModalController.refreshAdminPaneState();
|
||||
}
|
||||
|
||||
populateAdminLists() {
|
||||
if (!this.profileModalController) {
|
||||
return;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
this.profileModalController.populateAdminLists();
|
||||
}
|
||||
|
||||
renderAdminList(...args) {
|
||||
if (!this.profileModalController) {
|
||||
return null;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
return this.profileModalController.renderAdminList(...args);
|
||||
}
|
||||
|
||||
handleAdminListMutation(...args) {
|
||||
if (!this.profileModalController) {
|
||||
return null;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
return this.profileModalController.handleAdminListMutation(...args);
|
||||
}
|
||||
|
||||
handleAddModerator(...args) {
|
||||
if (!this.profileModalController) {
|
||||
return null;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
return this.profileModalController.handleAddModerator(...args);
|
||||
}
|
||||
|
||||
handleRemoveModerator(...args) {
|
||||
if (!this.profileModalController) {
|
||||
return null;
|
||||
}
|
||||
this.syncProfileModalState();
|
||||
return this.profileModalController.handleRemoveModerator(...args);
|
||||
}
|
||||
|
||||
async handleAddProfile(controller) {
|
||||
const button =
|
||||
(controller && controller.addAccountButton) ||
|
||||
this.profileModalController?.addAccountButton ||
|
||||
this.profileController?.addAccountButton ||
|
||||
null;
|
||||
if (!(button instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -1725,10 +1710,12 @@ class Application {
|
||||
}
|
||||
|
||||
async onAccessControlUpdated() {
|
||||
try {
|
||||
await this.refreshAdminPaneState();
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh admin pane after update:", error);
|
||||
if (this.profileController) {
|
||||
try {
|
||||
await this.profileController.refreshAdminPaneState();
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh admin pane after update:", error);
|
||||
}
|
||||
}
|
||||
|
||||
this.loadVideos(true).catch((error) => {
|
||||
@@ -1765,11 +1752,11 @@ class Application {
|
||||
// 2) Profile button
|
||||
if (this.profileButton) {
|
||||
this.profileButton.addEventListener("click", () => {
|
||||
if (!this.profileModalController) {
|
||||
if (!this.profileController) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.profileModalController
|
||||
this.profileController
|
||||
.show()
|
||||
.catch((error) => {
|
||||
console.error("Failed to open profile modal:", error);
|
||||
@@ -2706,8 +2693,8 @@ class Application {
|
||||
}
|
||||
|
||||
updateActiveProfileUI(pubkey, profile = {}) {
|
||||
if (this.profileModalController) {
|
||||
this.profileModalController.handleProfileUpdated({
|
||||
if (this.profileController) {
|
||||
this.profileController.handleProfileUpdated({
|
||||
pubkey,
|
||||
profile,
|
||||
});
|
||||
@@ -2905,20 +2892,17 @@ class Application {
|
||||
await this.hydrateNwcSettingsForPubkey(normalizedActive);
|
||||
}
|
||||
|
||||
if (this.profileModalController) {
|
||||
await this.profileModalController.handleAuthLogin(detail);
|
||||
if (this.profileController) {
|
||||
try {
|
||||
await this.profileController.handleAuthLogin(detail);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to process login within the profile controller:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.renderSavedProfiles();
|
||||
|
||||
try {
|
||||
await this.refreshAdminPaneState();
|
||||
} catch (error) {
|
||||
console.warn("Failed to refresh admin pane after login:", error);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
this.populateProfileRelays();
|
||||
this.refreshWalletPaneState();
|
||||
}
|
||||
|
||||
if (this.loginButton) {
|
||||
@@ -2978,20 +2962,17 @@ class Application {
|
||||
}
|
||||
this.nwcSettings.clear();
|
||||
|
||||
if (this.profileModalController) {
|
||||
await this.profileModalController.handleAuthLogout(detail);
|
||||
if (this.profileController) {
|
||||
try {
|
||||
await this.profileController.handleAuthLogout(detail);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to process logout within the profile controller:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.renderSavedProfiles();
|
||||
|
||||
try {
|
||||
await this.refreshAdminPaneState();
|
||||
} catch (error) {
|
||||
console.warn("Failed to refresh admin pane after logout:", error);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
this.populateProfileRelays();
|
||||
this.refreshWalletPaneState();
|
||||
}
|
||||
|
||||
if (this.loginButton) {
|
||||
@@ -3040,8 +3021,8 @@ class Application {
|
||||
}
|
||||
|
||||
handleProfileUpdated(detail = {}) {
|
||||
if (this.profileModalController) {
|
||||
this.profileModalController.handleProfileUpdated(detail);
|
||||
if (this.profileController) {
|
||||
this.profileController.handleProfileUpdated(detail);
|
||||
} else if (Array.isArray(detail?.savedProfiles)) {
|
||||
this.renderSavedProfiles();
|
||||
}
|
||||
@@ -3054,7 +3035,7 @@ class Application {
|
||||
if (normalizedPubkey && profile) {
|
||||
this.updateProfileInDOM(normalizedPubkey, profile);
|
||||
if (
|
||||
!this.profileModalController &&
|
||||
!this.profileController &&
|
||||
this.normalizeHexPubkey(this.pubkey) === normalizedPubkey
|
||||
) {
|
||||
this.updateActiveProfileUI(normalizedPubkey, profile);
|
||||
@@ -4190,8 +4171,8 @@ class Application {
|
||||
typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : "";
|
||||
if (!normalizedUri) {
|
||||
this.showError("Connect a Lightning wallet to send zaps.");
|
||||
if (this.profileModalController) {
|
||||
this.profileModalController
|
||||
if (this.profileController) {
|
||||
this.profileController
|
||||
.showWalletPane()
|
||||
.catch((error) => {
|
||||
console.error("Failed to open wallet pane:", error);
|
||||
@@ -6211,7 +6192,16 @@ class Application {
|
||||
);
|
||||
}
|
||||
|
||||
this.populateBlockedList();
|
||||
if (this.profileController) {
|
||||
try {
|
||||
this.profileController.populateBlockedList();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[profileModal] Failed to refresh blocked list after update:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.loadVideos();
|
||||
subscriptions
|
||||
.refreshActiveFeed({ reason: "user-block-update" })
|
||||
@@ -7038,20 +7028,19 @@ class Application {
|
||||
this.cancelPendingViewLogging();
|
||||
|
||||
let modalVideoEl = this.modalVideo;
|
||||
if (!modalVideoEl && this.videoModal) {
|
||||
const maybeLoader = this.videoModal.load;
|
||||
if (typeof maybeLoader === "function") {
|
||||
try {
|
||||
await maybeLoader.call(this.videoModal);
|
||||
} catch (error) {
|
||||
this.log(
|
||||
"[playVideoWithFallback] Failed to load video modal before playback:",
|
||||
error
|
||||
);
|
||||
this.showError("Could not prepare the video player. Please try again.");
|
||||
return { source: null, error };
|
||||
}
|
||||
modalVideoEl = this.modalVideo;
|
||||
if (!modalVideoEl) {
|
||||
try {
|
||||
const { videoElement } = await this.ensureVideoModalReady({
|
||||
ensureVideoElement: true,
|
||||
});
|
||||
modalVideoEl = videoElement;
|
||||
} catch (error) {
|
||||
this.log(
|
||||
"[playVideoWithFallback] Failed to load video modal before playback:",
|
||||
error
|
||||
);
|
||||
this.showError("Could not prepare the video player. Please try again.");
|
||||
return { source: null, error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7415,8 +7404,7 @@ class Application {
|
||||
legacyInfoHash ||
|
||||
"";
|
||||
|
||||
this.showModalWithPoster(this.currentVideo);
|
||||
this.applyModalLoadingPoster();
|
||||
await this.showModalWithPoster(this.currentVideo);
|
||||
|
||||
const playbackPromise = this.playVideoWithFallback({
|
||||
url: trimmedUrl,
|
||||
@@ -7545,6 +7533,8 @@ class Application {
|
||||
});
|
||||
}
|
||||
|
||||
await this.showModalWithPoster(this.currentVideo);
|
||||
|
||||
const urlObj = new URL(window.location.href);
|
||||
urlObj.searchParams.delete("v");
|
||||
const cleaned = `${urlObj.pathname}${urlObj.search}${urlObj.hash}`;
|
||||
@@ -7712,6 +7702,7 @@ class Application {
|
||||
destroy() {
|
||||
this.clearActiveIntervals();
|
||||
this.teardownModalViewCountSubscription();
|
||||
this.videoModalReadyPromise = null;
|
||||
|
||||
if (typeof this.unsubscribeFromPubkeyState === "function") {
|
||||
try {
|
||||
@@ -7948,6 +7939,20 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.profileController) {
|
||||
if (typeof this.profileController.destroy === "function") {
|
||||
try {
|
||||
this.profileController.destroy();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[Application] Failed to destroy profile controller:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.profileController = null;
|
||||
}
|
||||
|
||||
this.videoList = null;
|
||||
}
|
||||
|
||||
|
||||
+660
-349
File diff suppressed because it is too large.
Load diff
+16
-1
@@ -800,7 +800,22 @@ function sanitizeRelayList(list) {
|
||||
if (!/^wss?:\/\//i.test(trimmed)) {
|
||||
return;
|
||||
}
|
||||
const normalized = trimmed.replace(/\/+$/, "");
|
||||
if (/\s/.test(trimmed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let normalized = trimmed.replace(/\/+$/, "");
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (!parsed.hostname) {
|
||||
return;
|
||||
}
|
||||
const pathname = parsed.pathname.replace(/\/+$/, "");
|
||||
normalized = `${parsed.protocol}//${parsed.host}${pathname}${parsed.search || ""}`;
|
||||
} catch (error) {
|
||||
// Ignore URL parsing failures and fall back to the trimmed string.
|
||||
}
|
||||
|
||||
if (seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,11 @@ const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const FALLBACK_PROFILE = {
|
||||
name: "Unknown",
|
||||
picture: "assets/svg/default-profile.svg",
|
||||
about: "",
|
||||
website: "",
|
||||
banner: "",
|
||||
lud16: "",
|
||||
lud06: "",
|
||||
};
|
||||
|
||||
export default class AuthService {
|
||||
@@ -556,8 +561,15 @@ export default class AuthService {
|
||||
try {
|
||||
const data = JSON.parse(events[0].content);
|
||||
profile = {
|
||||
name: data.name || data.display_name || FALLBACK_PROFILE.name,
|
||||
name: data.display_name || data.name || FALLBACK_PROFILE.name,
|
||||
picture: data.picture || FALLBACK_PROFILE.picture,
|
||||
about: typeof data.about === "string" ? data.about : FALLBACK_PROFILE.about,
|
||||
website:
|
||||
typeof data.website === "string" ? data.website : FALLBACK_PROFILE.website,
|
||||
banner:
|
||||
typeof data.banner === "string" ? data.banner : FALLBACK_PROFILE.banner,
|
||||
lud16: typeof data.lud16 === "string" ? data.lud16 : FALLBACK_PROFILE.lud16,
|
||||
lud06: typeof data.lud06 === "string" ? data.lud06 : FALLBACK_PROFILE.lud06,
|
||||
};
|
||||
} catch (error) {
|
||||
this.log("[AuthService] Failed to parse profile metadata", error);
|
||||
@@ -618,8 +630,15 @@ export default class AuthService {
|
||||
if (newest?.content) {
|
||||
const data = JSON.parse(newest.content);
|
||||
const profile = {
|
||||
name: data.name || data.display_name || FALLBACK_PROFILE.name,
|
||||
name: data.display_name || data.name || FALLBACK_PROFILE.name,
|
||||
picture: data.picture || FALLBACK_PROFILE.picture,
|
||||
about: typeof data.about === "string" ? data.about : FALLBACK_PROFILE.about,
|
||||
website:
|
||||
typeof data.website === "string" ? data.website : FALLBACK_PROFILE.website,
|
||||
banner:
|
||||
typeof data.banner === "string" ? data.banner : FALLBACK_PROFILE.banner,
|
||||
lud16: typeof data.lud16 === "string" ? data.lud16 : FALLBACK_PROFILE.lud16,
|
||||
lud06: typeof data.lud06 === "string" ? data.lud06 : FALLBACK_PROFILE.lud06,
|
||||
};
|
||||
this.setProfileCacheEntry(normalized, profile, {
|
||||
persist: true,
|
||||
|
||||
@@ -434,7 +434,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
}
|
||||
|
||||
if (typeof showModalWithPoster === "function") {
|
||||
showModalWithPoster();
|
||||
await Promise.resolve(showModalWithPoster());
|
||||
}
|
||||
|
||||
this.emit("status", { message: "Preparing video..." });
|
||||
|
||||
@@ -18,6 +18,15 @@ const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds
|
||||
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
|
||||
function sanitizeProfileString(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
let savedProfiles = [];
|
||||
let activeProfilePubkey = null;
|
||||
const profileCache = new Map();
|
||||
@@ -444,6 +453,40 @@ export function loadProfileCacheFromStorage() {
|
||||
picture: profile.picture || "assets/svg/default-profile.svg",
|
||||
};
|
||||
|
||||
const about = sanitizeProfileString(profile.about || profile.aboutMe);
|
||||
if (about) {
|
||||
normalized.about = about;
|
||||
}
|
||||
|
||||
const website = sanitizeProfileString(profile.website || profile.url);
|
||||
if (website) {
|
||||
normalized.website = website;
|
||||
}
|
||||
|
||||
const banner = sanitizeProfileString(profile.banner || profile.header);
|
||||
if (banner) {
|
||||
normalized.banner = banner;
|
||||
}
|
||||
|
||||
const lud16 = sanitizeProfileString(profile.lud16);
|
||||
if (lud16) {
|
||||
normalized.lud16 = lud16;
|
||||
}
|
||||
|
||||
const lud06 = sanitizeProfileString(profile.lud06);
|
||||
if (lud06) {
|
||||
normalized.lud06 = lud06;
|
||||
}
|
||||
|
||||
const lightningCandidates = [
|
||||
sanitizeProfileString(profile.lightningAddress),
|
||||
lud16,
|
||||
lud06,
|
||||
].filter(Boolean);
|
||||
if (lightningCandidates.length) {
|
||||
normalized.lightningAddress = lightningCandidates[0];
|
||||
}
|
||||
|
||||
profileCache.set(pubkey, {
|
||||
profile: normalized,
|
||||
timestamp,
|
||||
@@ -536,6 +579,40 @@ export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) {
|
||||
picture: profile.picture || "assets/svg/default-profile.svg",
|
||||
};
|
||||
|
||||
const about = sanitizeProfileString(profile.about || profile.aboutMe);
|
||||
if (about) {
|
||||
normalized.about = about;
|
||||
}
|
||||
|
||||
const website = sanitizeProfileString(profile.website || profile.url);
|
||||
if (website) {
|
||||
normalized.website = website;
|
||||
}
|
||||
|
||||
const banner = sanitizeProfileString(profile.banner || profile.header);
|
||||
if (banner) {
|
||||
normalized.banner = banner;
|
||||
}
|
||||
|
||||
const lud16 = sanitizeProfileString(profile.lud16);
|
||||
if (lud16) {
|
||||
normalized.lud16 = lud16;
|
||||
}
|
||||
|
||||
const lud06 = sanitizeProfileString(profile.lud06);
|
||||
if (lud06) {
|
||||
normalized.lud06 = lud06;
|
||||
}
|
||||
|
||||
const lightningCandidates = [
|
||||
sanitizeProfileString(profile.lightningAddress),
|
||||
lud16,
|
||||
lud06,
|
||||
].filter(Boolean);
|
||||
if (lightningCandidates.length) {
|
||||
normalized.lightningAddress = lightningCandidates[0];
|
||||
}
|
||||
|
||||
const entry = {
|
||||
profile: normalized,
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -151,9 +151,14 @@ export class VideoModal {
|
||||
const wrapper = this.document.createElement("div");
|
||||
wrapper.innerHTML = html;
|
||||
this.removeTrackingScripts(wrapper);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
const playerModal = wrapper.querySelector("#playerModal");
|
||||
const fragment = this.document.createDocumentFragment();
|
||||
while (wrapper.firstChild) {
|
||||
fragment.appendChild(wrapper.firstChild);
|
||||
}
|
||||
container.appendChild(fragment);
|
||||
|
||||
const playerModal = container.querySelector("#playerModal");
|
||||
if (!playerModal) {
|
||||
throw new Error("Player modal root not found in markup.");
|
||||
}
|
||||
@@ -386,6 +391,34 @@ export class VideoModal {
|
||||
this.dispatch("creator:navigate", { video: this.activeVideo });
|
||||
}
|
||||
|
||||
toggleBackgroundInert(enable) {
|
||||
const ids = ["sidebar", "sidebarOverlay"];
|
||||
for (const id of ids) {
|
||||
const element = this.document.getElementById(id);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
if (!element.hasAttribute("inert")) {
|
||||
element.setAttribute("inert", "");
|
||||
}
|
||||
element.dataset.modalInert = "true";
|
||||
element.setAttribute("aria-hidden", "true");
|
||||
element.classList.add("is-modal-hidden");
|
||||
} else {
|
||||
if (element.dataset.modalInert === "true") {
|
||||
element.removeAttribute("inert");
|
||||
delete element.dataset.modalInert;
|
||||
}
|
||||
if (element.getAttribute("aria-hidden") === "true") {
|
||||
element.removeAttribute("aria-hidden");
|
||||
}
|
||||
element.classList.remove("is-modal-hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open(video) {
|
||||
this.activeVideo = video || null;
|
||||
if (!this.playerModal) {
|
||||
@@ -396,6 +429,7 @@ export class VideoModal {
|
||||
this.playerModal.classList.remove("hidden");
|
||||
this.document.body.classList.add("modal-open");
|
||||
this.document.documentElement.classList.add("modal-open");
|
||||
this.toggleBackgroundInert(true);
|
||||
if (this.scrollRegion) {
|
||||
this.scrollRegion.scrollTop = 0;
|
||||
}
|
||||
@@ -411,6 +445,7 @@ export class VideoModal {
|
||||
}
|
||||
this.document.body.classList.remove("modal-open");
|
||||
this.document.documentElement.classList.remove("modal-open");
|
||||
this.toggleBackgroundInert(false);
|
||||
this.setGlobalModalState("player", false);
|
||||
this.forceRemovePoster("close");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user