mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor UI responsibilities into controllers
This commit is contained in:
1 parent
8ea778f7ca
commit
2059ef87a5
4 files changed
+673
-265
No files matched your search
@@ -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 } = {}) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user