From 2059ef87a5dd489cb269b751b4d7038eafedffbc Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 26 Oct 2025 16:28:41 -0400 Subject: [PATCH] Refactor UI responsibilities into controllers --- js/app.js | 361 ++++++++--------------------- js/ui/appChromeController.js | 303 ++++++++++++++++++++++++ js/ui/profileIdentityController.js | 179 ++++++++++++++ js/ui/videoListViewController.js | 95 ++++++++ 4 files changed, 673 insertions(+), 265 deletions(-) create mode 100644 js/ui/appChromeController.js create mode 100644 js/ui/profileIdentityController.js create mode 100644 js/ui/videoListViewController.js diff --git a/js/app.js b/js/app.js index f6c4020d..b78d72f3 100644 --- a/js/app.js +++ b/js/app.js @@ -71,6 +71,9 @@ import { devLogger, userLogger } from "./utils/logger.js"; import createPopover from "./ui/overlay/popoverEngine.js"; import { createVideoSettingsMenuPanel } from "./ui/components/videoMenuRenderers.js"; import moderationService from "./services/moderationService.js"; +import AppChromeController from "./ui/appChromeController.js"; +import VideoListViewController from "./ui/videoListViewController.js"; +import ProfileIdentityController from "./ui/profileIdentityController.js"; import { initViewCounter, subscribeToVideoViewCount, @@ -279,6 +282,18 @@ class Application { this.modalCommentPublishPromise = null; this.pendingModeratedPlayback = null; this.lastIdentityRefreshPromise = null; + this.profileIdentityController = new ProfileIdentityController({ + callbacks: { + safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey), + formatShortNpub: (value) => formatShortNpub(value), + }, + logger: devLogger, + }); + this.videoListViewController = new VideoListViewController({ + getSidebarLoadingMarkup, + logger: devLogger, + }); + this.appChromeController = null; this.nostrService = services.nostrService || nostrService; this.r2Service = services.r2Service || r2Service; @@ -1500,6 +1515,51 @@ class Application { this.closeLoginModalBtn = document.getElementById("closeLoginModal") || null; + this.appChromeController = new AppChromeController({ + elements: { + logoutButton: this.logoutButton, + profileButton: this.profileButton, + uploadButton: this.uploadButton, + loginButton: this.loginButton, + closeLoginModalButton: this.closeLoginModalBtn, + }, + callbacks: { + requestLogout: () => this.requestLogout(), + showError: (message) => this.showError(message), + showProfileModal: () => + this.profileController && + typeof this.profileController.show === "function" + ? this.profileController.show() + : null, + openUploadModal: ({ triggerElement }) => { + if (this.uploadModal) { + this.uploadModal.open({ triggerElement }); + } + }, + onLoginModalOpened: () => setGlobalModalState("login", true), + onLoginModalClosed: () => setGlobalModalState("login", false), + flushWatchHistory: (reason, context) => + this.flushWatchHistory(reason, context), + cleanup: () => this.cleanup(), + hideModal: () => this.hideModal(), + }, + utilities: { + prepareLoginModal: () => + prepareStaticModal({ id: "loginModal" }) || + document.getElementById("loginModal"), + prepareApplicationModal: () => + prepareStaticModal({ id: "nostrFormModal" }) || + document.getElementById("nostrFormModal"), + openModal: (modal, options) => openStaticModal(modal, options), + closeModal: (modalId) => closeStaticModal(modalId), + }, + environment: { + document: typeof document !== "undefined" ? document : null, + window: typeof window !== "undefined" ? window : null, + }, + logger: devLogger, + }); + // Build a set of blacklisted event IDs (hex) from nevent strings, skipping empties this.blacklistedEventIds = new Set(); for (const neventStr of ADMIN_INITIAL_EVENT_BLACKLIST) { @@ -4372,112 +4432,14 @@ class Application { * Setup general event listeners for logout, modals, etc. */ setupEventListeners() { - // 1) Logout button - if (this.logoutButton) { - this.logoutButton.addEventListener("click", async () => { - try { - await this.requestLogout(); - } catch (error) { - devLogger.error("Logout failed:", error); - this.showError("Failed to logout. Please try again."); - } - }); + if (this.appChromeController) { + this.appChromeController.initialize(); + return; } - // 2) Profile button - if (this.profileButton) { - this.profileButton.addEventListener("click", () => { - if (!this.profileController) { - return; - } - - this.profileController - .show() - .catch((error) => { - devLogger.error("Failed to open profile modal:", error); - }); - }); - } - - // 3) Upload button => show upload modal - if (this.uploadButton) { - this.uploadButton.addEventListener("click", (event) => { - if (this.uploadModal) { - const trigger = event?.currentTarget || event?.target || null; - this.uploadModal.open({ triggerElement: trigger }); - } - }); - } - - // 4) Login button => show the login modal - if (this.loginButton) { - this.loginButton.addEventListener("click", (event) => { - devLogger.log("Login button clicked!"); - const loginModal = - prepareStaticModal({ id: "loginModal" }) || document.getElementById("loginModal"); - const trigger = event?.currentTarget || event?.target || null; - if (loginModal && openStaticModal(loginModal, { triggerElement: trigger })) { - setGlobalModalState("login", true); - } - }); - } - - // 5) Close login modal button => hide modal - if (this.closeLoginModalBtn) { - this.closeLoginModalBtn.addEventListener("click", () => { - devLogger.log("[app.js] closeLoginModal button clicked!"); - if (closeStaticModal("loginModal")) { - setGlobalModalState("login", false); - } - }); - } - - // 6) Cleanup on page unload - window.addEventListener("beforeunload", () => { - this.flushWatchHistory("session-end", "beforeunload").catch((error) => { - devLogger.warn("[beforeunload] Watch history flush failed:", error); - }); - this.cleanup().catch((err) => { - devLogger.error("Cleanup before unload failed:", err); - }); - }); - - document.addEventListener("visibilitychange", () => { - if (document.visibilityState === "hidden") { - this.flushWatchHistory("session-end", "visibilitychange").catch( - (error) => { - devLogger.warn( - "[visibilitychange] Watch history flush failed:", - error - ); - } - ); - } - }); - - // 7) Handle back/forward navigation => hide video modal - window.addEventListener("popstate", async () => { - devLogger.log("[popstate] user navigated back/forward; cleaning modal..."); - await this.hideModal(); - }); - - // 8) Event delegation for the “Application Form” button inside the login modal - document.addEventListener("click", (event) => { - if (event.target && event.target.id === "openApplicationModal") { - // Hide the login modal - if (closeStaticModal("loginModal")) { - setGlobalModalState("login", false); - } - // Show the application modal - const appModal = - prepareStaticModal({ id: "nostrFormModal" }) || - document.getElementById("nostrFormModal"); - if (appModal) { - openStaticModal(appModal, { triggerElement: event.target }); - } - } - }); - + devLogger.warn( + "[Application] AppChromeController missing; global UI events were not bound.", + ); } mountVideoListView(container = null) { @@ -4485,6 +4447,17 @@ class Application { return null; } + if (this.videoListViewController) { + const { videoList, popularTags } = this.videoListViewController.mount({ + container, + view: this.videoListView, + currentVideoList: this.videoList, + }); + this.videoList = videoList || null; + this.videoListPopularTags = popularTags || null; + return this.videoList; + } + const target = container || document.getElementById("videoList"); this.videoList = target || null; const tagsRoot = document.getElementById("recentVideoTags"); @@ -4501,6 +4474,18 @@ class Application { return; } + if (this.videoListViewController) { + const { videoList, popularTags } = this.videoListViewController.reinitialize({ + view: this.videoListView, + reason, + postLoginResult, + currentVideoList: this.videoList, + }); + this.videoList = videoList || null; + this.videoListPopularTags = popularTags || null; + return; + } + const isElement = (value) => typeof HTMLElement !== "undefined" && value instanceof HTMLElement; @@ -4558,171 +4543,17 @@ class Application { } updateProfileInDOM(pubkey, profile) { - const normalizedPubkey = - typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : ""; - if (!normalizedPubkey) { + if (this.profileIdentityController) { + this.profileIdentityController.updateProfileIdentity({ + pubkey, + profile, + }); return; } - const normalizedProfile = - profile && typeof profile === "object" ? profile : {}; - - const pictureUrl = - typeof normalizedProfile.picture === "string" - ? normalizedProfile.picture - : ""; - - const resolveProfileName = () => { - const candidates = [ - normalizedProfile.name, - normalizedProfile.display_name, - normalizedProfile.displayName, - ]; - for (const candidate of candidates) { - if (typeof candidate !== "string") { - continue; - } - const trimmed = candidate.trim(); - if (trimmed) { - return trimmed; - } - } - return ""; - }; - - const resolvedName = resolveProfileName(); - - const explicitNpub = - typeof normalizedProfile.npub === "string" - ? normalizedProfile.npub.trim() - : ""; - - const encodedPubkeyNpub = this.safeEncodeNpub(normalizedPubkey); - const resolvedNpub = explicitNpub || encodedPubkeyNpub || ""; - const shortNpubLabel = resolvedNpub - ? formatShortNpub(resolvedNpub) || resolvedNpub - : ""; - - // For any .author-pic[data-pubkey=...] - const picEls = document.querySelectorAll( - `.author-pic[data-pubkey="${normalizedPubkey}"]` + devLogger.warn( + "[Application] ProfileIdentityController missing; profile identity was not refreshed in the DOM.", ); - picEls.forEach((el) => { - if (!el) { - return; - } - el.src = pictureUrl; - }); - - const nameLabel = - resolvedName || shortNpubLabel || resolvedNpub || ""; - - // For any .author-name[data-pubkey=...] - const nameEls = document.querySelectorAll( - `.author-name[data-pubkey="${normalizedPubkey}"]` - ); - nameEls.forEach((el) => { - if (!el) { - return; - } - el.textContent = nameLabel; - }); - - const npubSelectors = new Set(); - if (resolvedNpub) { - npubSelectors.add(`.author-npub[data-npub="${resolvedNpub}"]`); - } - npubSelectors.add(`.author-npub[data-pubkey="${normalizedPubkey}"]`); - - const npubElements = new Set(); - npubSelectors.forEach((selector) => { - document.querySelectorAll(selector).forEach((el) => { - if (el) { - npubElements.add(el); - } - }); - }); - - const npubEls = Array.from(npubElements); - - npubEls.forEach((el) => { - if (!el) { - return; - } - - const displayNpub = resolvedNpub - ? shortNpubLabel || resolvedNpub - : ""; - const hasDisplayNpub = Boolean(displayNpub); - - if (hasDisplayNpub) { - el.textContent = displayNpub; - el.setAttribute("aria-hidden", "false"); - } else { - el.textContent = ""; - el.setAttribute("aria-hidden", "true"); - } - - if (resolvedNpub) { - el.setAttribute("title", resolvedNpub); - if (el.dataset) { - el.dataset.npub = resolvedNpub; - } - } else { - el.removeAttribute("title"); - if (el.dataset && "npub" in el.dataset) { - delete el.dataset.npub; - } - } - - if (el.dataset && normalizedPubkey) { - el.dataset.pubkey = normalizedPubkey; - } - }); - - if (!nameEls.length && !npubEls.length) { - return; - } - - const cardInstances = new Set(); - const collectCardInstance = (el) => { - if (!el || typeof el.closest !== "function") { - return; - } - const cardRoot = el.closest('[data-component="similar-content-card"]'); - if (!cardRoot) { - return; - } - const instance = cardRoot.__bitvidSimilarContentCard; - if (instance && typeof instance.updateIdentity === "function") { - cardInstances.add(instance); - } - }; - - nameEls.forEach(collectCardInstance); - npubEls.forEach(collectCardInstance); - - if (cardInstances.size) { - const identityPayload = { - name: resolvedName, - npub: resolvedNpub, - shortNpub: shortNpubLabel, - pubkey: normalizedPubkey, - }; - - cardInstances.forEach((card) => { - try { - card.updateIdentity(identityPayload); - } catch (error) { - if (devLogger?.warn) { - devLogger.warn( - "[app] Failed to update similar content card identity", - error - ); - } - } - }); - } } async publishVideoNote(payload, { onSuccess, suppressModalClose } = {}) { diff --git a/js/ui/appChromeController.js b/js/ui/appChromeController.js new file mode 100644 index 00000000..f8cb7467 --- /dev/null +++ b/js/ui/appChromeController.js @@ -0,0 +1,303 @@ +import { devLogger as defaultLogger } from "../utils/logger.js"; +import { + prepareStaticModal, + openStaticModal, + closeStaticModal, +} from "./components/staticModalAccessibility.js"; + +const DEFAULT_LOGOUT_ERROR_MESSAGE = "Failed to logout. Please try again."; + +export default class AppChromeController { + constructor({ + elements = {}, + callbacks = {}, + utilities = {}, + environment = {}, + logger, + } = {}) { + this.elements = { + logoutButton: elements.logoutButton || null, + profileButton: elements.profileButton || null, + uploadButton: elements.uploadButton || null, + loginButton: elements.loginButton || null, + closeLoginModalButton: elements.closeLoginModalButton || null, + }; + + this.callbacks = { + requestLogout: callbacks.requestLogout || null, + showError: callbacks.showError || null, + showProfileModal: callbacks.showProfileModal || null, + openUploadModal: callbacks.openUploadModal || null, + onLoginModalOpened: callbacks.onLoginModalOpened || null, + onLoginModalClosed: callbacks.onLoginModalClosed || null, + flushWatchHistory: callbacks.flushWatchHistory || null, + cleanup: callbacks.cleanup || null, + hideModal: callbacks.hideModal || null, + }; + + this.utilities = { + prepareLoginModal: + typeof utilities.prepareLoginModal === "function" + ? utilities.prepareLoginModal + : () => prepareStaticModal({ id: "loginModal" }) || document.getElementById("loginModal"), + prepareApplicationModal: + typeof utilities.prepareApplicationModal === "function" + ? utilities.prepareApplicationModal + : () => prepareStaticModal({ id: "nostrFormModal" }) || document.getElementById("nostrFormModal"), + openModal: + typeof utilities.openModal === "function" + ? utilities.openModal + : (modal, options) => openStaticModal(modal, options), + closeModal: + typeof utilities.closeModal === "function" + ? utilities.closeModal + : (modalId) => closeStaticModal(modalId), + }; + + this.document = environment.document || (typeof document !== "undefined" ? document : null); + this.window = environment.window || (typeof window !== "undefined" ? window : null); + this.logger = logger || defaultLogger; + + this.isInitialized = false; + + this.handleBeforeUnload = this.handleBeforeUnload.bind(this); + this.handleVisibilityChange = this.handleVisibilityChange.bind(this); + this.handlePopstate = this.handlePopstate.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); + } + + initialize() { + if (this.isInitialized) { + return; + } + + this.bindLogoutButton(); + this.bindProfileButton(); + this.bindUploadButton(); + this.bindLoginButton(); + this.bindCloseLoginButton(); + this.bindGlobalEvents(); + + this.isInitialized = true; + } + + getTriggerFromEvent(event) { + if (!event) { + return null; + } + return event.currentTarget || event.target || null; + } + + bindLogoutButton() { + const { logoutButton } = this.elements; + if (!logoutButton || typeof logoutButton.addEventListener !== "function") { + return; + } + + logoutButton.addEventListener("click", async () => { + try { + if (typeof this.callbacks.requestLogout === "function") { + await this.callbacks.requestLogout(); + } + } catch (error) { + if (this.logger?.error) { + this.logger.error("Logout failed:", error); + } + if (typeof this.callbacks.showError === "function") { + this.callbacks.showError(DEFAULT_LOGOUT_ERROR_MESSAGE); + } + } + }); + } + + bindProfileButton() { + const { profileButton } = this.elements; + if (!profileButton || typeof profileButton.addEventListener !== "function") { + return; + } + + profileButton.addEventListener("click", () => { + if (typeof this.callbacks.showProfileModal !== "function") { + return; + } + + Promise.resolve() + .then(() => this.callbacks.showProfileModal()) + .catch((error) => { + if (this.logger?.error) { + this.logger.error("Failed to open profile modal:", error); + } + }); + }); + } + + bindUploadButton() { + const { uploadButton } = this.elements; + if (!uploadButton || typeof uploadButton.addEventListener !== "function") { + return; + } + + uploadButton.addEventListener("click", (event) => { + if (typeof this.callbacks.openUploadModal !== "function") { + return; + } + const triggerElement = this.getTriggerFromEvent(event); + try { + this.callbacks.openUploadModal({ triggerElement }); + } catch (error) { + if (this.logger?.error) { + this.logger.error("Failed to open upload modal:", error); + } + } + }); + } + + bindLoginButton() { + const { loginButton } = this.elements; + if (!loginButton || typeof loginButton.addEventListener !== "function") { + return; + } + + loginButton.addEventListener("click", (event) => { + if (this.logger?.log) { + this.logger.log("Login button clicked!"); + } + + const triggerElement = this.getTriggerFromEvent(event); + const loginModal = + (typeof this.utilities.prepareLoginModal === "function" + ? this.utilities.prepareLoginModal() + : null) || null; + + if ( + loginModal && + typeof this.utilities.openModal === "function" && + this.utilities.openModal(loginModal, { triggerElement }) + ) { + if (typeof this.callbacks.onLoginModalOpened === "function") { + this.callbacks.onLoginModalOpened(); + } + } + }); + } + + bindCloseLoginButton() { + const { closeLoginModalButton } = this.elements; + if ( + !closeLoginModalButton || + typeof closeLoginModalButton.addEventListener !== "function" + ) { + return; + } + + closeLoginModalButton.addEventListener("click", () => { + if (this.logger?.log) { + this.logger.log("[app.js] closeLoginModal button clicked!"); + } + + if (typeof this.utilities.closeModal === "function") { + const wasClosed = this.utilities.closeModal("loginModal"); + if (wasClosed && typeof this.callbacks.onLoginModalClosed === "function") { + this.callbacks.onLoginModalClosed(); + } + } + }); + } + + bindGlobalEvents() { + if (this.window && typeof this.window.addEventListener === "function") { + this.window.addEventListener("beforeunload", this.handleBeforeUnload); + this.window.addEventListener("popstate", this.handlePopstate); + } + + if (this.document && typeof this.document.addEventListener === "function") { + this.document.addEventListener("visibilitychange", this.handleVisibilityChange); + this.document.addEventListener("click", this.handleDocumentClick); + } + } + + handleBeforeUnload() { + if (typeof this.callbacks.flushWatchHistory === "function") { + Promise.resolve() + .then(() => this.callbacks.flushWatchHistory("session-end", "beforeunload")) + .catch((error) => { + if (this.logger?.warn) { + this.logger.warn("[beforeunload] Watch history flush failed:", error); + } + }); + } + + if (typeof this.callbacks.cleanup === "function") { + Promise.resolve() + .then(() => this.callbacks.cleanup()) + .catch((error) => { + if (this.logger?.error) { + this.logger.error("Cleanup before unload failed:", error); + } + }); + } + } + + handleVisibilityChange() { + if (!this.document || this.document.visibilityState !== "hidden") { + return; + } + + if (typeof this.callbacks.flushWatchHistory !== "function") { + return; + } + + Promise.resolve() + .then(() => this.callbacks.flushWatchHistory("session-end", "visibilitychange")) + .catch((error) => { + if (this.logger?.warn) { + this.logger.warn("[visibilitychange] Watch history flush failed:", error); + } + }); + } + + async handlePopstate() { + if (this.logger?.log) { + this.logger.log("[popstate] user navigated back/forward; cleaning modal..."); + } + + if (typeof this.callbacks.hideModal !== "function") { + return; + } + + try { + await this.callbacks.hideModal(); + } catch (error) { + if (this.logger?.warn) { + this.logger.warn("[popstate] Failed to hide modal:", error); + } + } + } + + handleDocumentClick(event) { + if (!event?.target || event.target.id !== "openApplicationModal") { + return; + } + + if (typeof this.utilities.closeModal === "function") { + const closed = this.utilities.closeModal("loginModal"); + if (closed && typeof this.callbacks.onLoginModalClosed === "function") { + this.callbacks.onLoginModalClosed(); + } + } + + if (typeof this.utilities.prepareApplicationModal !== "function") { + return; + } + + const applicationModal = this.utilities.prepareApplicationModal(); + if ( + applicationModal && + typeof this.utilities.openModal === "function" + ) { + this.utilities.openModal(applicationModal, { + triggerElement: event.target, + }); + } + } +} diff --git a/js/ui/profileIdentityController.js b/js/ui/profileIdentityController.js new file mode 100644 index 00000000..5c2b52f1 --- /dev/null +++ b/js/ui/profileIdentityController.js @@ -0,0 +1,179 @@ +import { devLogger as defaultLogger } from "../utils/logger.js"; + +export default class ProfileIdentityController { + constructor({ callbacks = {}, environment = {}, logger } = {}) { + this.safeEncodeNpub = + typeof callbacks.safeEncodeNpub === "function" + ? callbacks.safeEncodeNpub + : () => ""; + this.formatShortNpub = + typeof callbacks.formatShortNpub === "function" + ? callbacks.formatShortNpub + : (value) => value || ""; + this.document = environment.document || (typeof document !== "undefined" ? document : null); + this.logger = logger || defaultLogger; + } + + updateProfileIdentity({ pubkey, profile } = {}) { + if (!this.document) { + return; + } + + const normalizedPubkey = + typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : ""; + if (!normalizedPubkey) { + return; + } + + const normalizedProfile = profile && typeof profile === "object" ? profile : {}; + + const pictureUrl = + typeof normalizedProfile.picture === "string" ? normalizedProfile.picture : ""; + + const resolveProfileName = () => { + const candidates = [ + normalizedProfile.name, + normalizedProfile.display_name, + normalizedProfile.displayName, + ]; + for (const candidate of candidates) { + if (typeof candidate !== "string") { + continue; + } + const trimmed = candidate.trim(); + if (trimmed) { + return trimmed; + } + } + return ""; + }; + + const resolvedName = resolveProfileName(); + + const explicitNpub = + typeof normalizedProfile.npub === "string" + ? normalizedProfile.npub.trim() + : ""; + + const encodedPubkeyNpub = this.safeEncodeNpub(normalizedPubkey); + const resolvedNpub = explicitNpub || encodedPubkeyNpub || ""; + const shortNpubLabel = resolvedNpub + ? this.formatShortNpub(resolvedNpub) || resolvedNpub + : ""; + + const picEls = this.document.querySelectorAll( + `.author-pic[data-pubkey="${normalizedPubkey}"]` + ); + picEls.forEach((el) => { + if (el) { + el.src = pictureUrl; + } + }); + + const nameLabel = resolvedName || shortNpubLabel || resolvedNpub || ""; + const nameEls = this.document.querySelectorAll( + `.author-name[data-pubkey="${normalizedPubkey}"]` + ); + nameEls.forEach((el) => { + if (el) { + el.textContent = nameLabel; + } + }); + + const npubSelectors = new Set(); + if (resolvedNpub) { + npubSelectors.add(`.author-npub[data-npub="${resolvedNpub}"]`); + } + npubSelectors.add(`.author-npub[data-pubkey="${normalizedPubkey}"]`); + + const npubElements = new Set(); + npubSelectors.forEach((selector) => { + this.document.querySelectorAll(selector).forEach((el) => { + if (el) { + npubElements.add(el); + } + }); + }); + + const npubEls = Array.from(npubElements); + + npubEls.forEach((el) => { + if (!el) { + return; + } + + const displayNpub = resolvedNpub ? shortNpubLabel || resolvedNpub : ""; + const hasDisplayNpub = Boolean(displayNpub); + + if (hasDisplayNpub) { + el.textContent = displayNpub; + el.setAttribute("aria-hidden", "false"); + } else { + el.textContent = ""; + el.setAttribute("aria-hidden", "true"); + } + + if (resolvedNpub) { + el.setAttribute("title", resolvedNpub); + if (el.dataset) { + el.dataset.npub = resolvedNpub; + } + } else { + el.removeAttribute("title"); + if (el.dataset && "npub" in el.dataset) { + delete el.dataset.npub; + } + } + + if (el.dataset) { + el.dataset.pubkey = normalizedPubkey; + } + }); + + if (!nameEls.length && !npubEls.length) { + return; + } + + const cardInstances = new Set(); + const collectCardInstance = (el) => { + if (!el || typeof el.closest !== "function") { + return; + } + const cardRoot = el.closest('[data-component="similar-content-card"]'); + if (!cardRoot) { + return; + } + const instance = cardRoot.__bitvidSimilarContentCard; + if (instance && typeof instance.updateIdentity === "function") { + cardInstances.add(instance); + } + }; + + nameEls.forEach(collectCardInstance); + npubEls.forEach(collectCardInstance); + + if (!cardInstances.size) { + return; + } + + const identityPayload = { + name: resolvedName, + npub: resolvedNpub, + shortNpub: shortNpubLabel, + pubkey: normalizedPubkey, + }; + + cardInstances.forEach((card) => { + try { + card.updateIdentity(identityPayload); + } catch (error) { + if (this.logger?.warn) { + this.logger.warn( + "[ProfileIdentityController] Failed to update similar content card identity", + error, + ); + } + } + }); + } +} diff --git a/js/ui/videoListViewController.js b/js/ui/videoListViewController.js new file mode 100644 index 00000000..498d50f9 --- /dev/null +++ b/js/ui/videoListViewController.js @@ -0,0 +1,95 @@ +import { devLogger as defaultLogger } from "../utils/logger.js"; + +export default class VideoListViewController { + constructor({ getSidebarLoadingMarkup, environment = {}, logger } = {}) { + this.getSidebarLoadingMarkup = + typeof getSidebarLoadingMarkup === "function" + ? getSidebarLoadingMarkup + : () => ""; + + this.document = environment.document || (typeof document !== "undefined" ? document : null); + this.logger = logger || defaultLogger; + } + + isElement(value) { + return ( + typeof HTMLElement !== "undefined" && value instanceof HTMLElement + ); + } + + mount({ container = null, view, currentVideoList = null } = {}) { + if (!view) { + return { videoList: currentVideoList || null, popularTags: null }; + } + + const target = container || this.getElementById("videoList"); + const popularTags = this.getElementById("recentVideoTags"); + + if (typeof view.setPopularTagsContainer === "function") { + view.setPopularTagsContainer(popularTags || null); + } + + view.mount(target || null); + + return { + videoList: target || null, + popularTags: popularTags || null, + }; + } + + reinitialize({ + view, + reason, + postLoginResult, + currentVideoList = null, + } = {}) { + if (!view) { + return { + videoList: currentVideoList || null, + popularTags: null, + }; + } + + const container = + this.isElement(currentVideoList) && currentVideoList + ? currentVideoList + : this.getElementById("videoList"); + + try { + view.destroy(); + } catch (error) { + if (this.logger?.warn) { + this.logger.warn( + "[VideoListViewController] Failed to destroy VideoListView during reinitialization:", + error, + ); + } + } + + const messageContext = + reason === "login" && postLoginResult?.blocksLoaded !== false + ? "Applying your filters…" + : "Refreshing videos…"; + + const tagsRoot = this.getElementById("recentVideoTags"); + if (typeof view.setPopularTagsContainer === "function") { + view.setPopularTagsContainer(this.isElement(tagsRoot) ? tagsRoot : null); + } + + if (this.isElement(container)) { + container.innerHTML = this.getSidebarLoadingMarkup(messageContext); + } + + return { + videoList: this.isElement(container) ? container : null, + popularTags: this.isElement(tagsRoot) ? tagsRoot : null, + }; + } + + getElementById(id) { + if (!this.document || typeof this.document.getElementById !== "function") { + return null; + } + return this.document.getElementById(id) || null; + } +}