diff --git a/js/app.js b/js/app.js index 3cbc1e02..513dc84d 100644 --- a/js/app.js +++ b/js/app.js @@ -163,6 +163,7 @@ import { import ApplicationBootstrap from "./ui/applicationBootstrap.js"; import EngagementController from "./ui/engagementController.js"; import SimilarContentController from "./ui/similarContentController.js"; +import CreatorProfileController from "./ui/creatorProfileController.js"; import UrlHealthController from "./ui/urlHealthController.js"; import VideoModalCommentController from "./ui/videoModalCommentController.js"; import VideoModalController from "./ui/videoModalController.js"; @@ -283,6 +284,26 @@ class Application { const { modalManager } = this.bootstrapper.initialize(); this.modalManager = modalManager; + this.creatorProfileController = new CreatorProfileController({ + services: { nostrClient }, + ui: { zapController: this.zapController }, + callbacks: { + getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey), + setProfileCacheEntry: (pubkey, profile, opts) => + this.setProfileCacheEntry(pubkey, profile, opts), + getCurrentVideo: () => this.currentVideo, + getVideoModal: () => this.videoModal, + }, + helpers: { + fetchProfileMetadata, + ensureProfileMetadataSubscription, + safeEncodeNpub: (val) => this.safeEncodeNpub(val), + formatShortNpub, + sanitizeProfileMediaUrl, + }, + logger: devLogger, + }); + this.videoModalController = new VideoModalController({ getVideoModal: () => this.videoModal, callbacks: { @@ -4794,422 +4815,38 @@ class Application { } selectPreferredCreatorName(candidates = []) { - for (const candidate of candidates) { - if (typeof candidate !== "string") { - continue; - } - const trimmed = candidate.trim(); - if (!trimmed) { - continue; - } - if (HEX64_REGEX.test(trimmed)) { - continue; - } - return trimmed; - } - return ""; + return this.creatorProfileController + ? this.creatorProfileController.selectPreferredCreatorName(candidates) + : ""; } selectPreferredCreatorPicture(candidates = []) { - for (const candidate of candidates) { - if (typeof candidate !== "string") { - continue; - } - const sanitized = sanitizeProfileMediaUrl(candidate); - if (sanitized) { - return sanitized; - } - } - return ""; + return this.creatorProfileController + ? this.creatorProfileController.selectPreferredCreatorPicture(candidates) + : ""; } - resolveCreatorProfileFromSources({ - video, - pubkey, - cachedProfile = null, - fetchedProfile = null, - fallbackAvatar, - } = {}) { - const normalizedPubkey = - typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : ""; - const fallbackAvatarCandidate = - typeof fallbackAvatar === "string" && fallbackAvatar.trim() - ? fallbackAvatar.trim() - : normalizedPubkey - ? `https://robohash.org/${normalizedPubkey}` - : "assets/svg/default-profile.svg"; - const defaultAvatar = - sanitizeProfileMediaUrl(fallbackAvatarCandidate) || - fallbackAvatarCandidate || - "assets/svg/default-profile.svg"; - - const nameCandidates = []; - const pictureCandidates = []; - - const collectFromSource = (source) => { - if (!source || typeof source !== "object") { - return; - } - const names = [ - source.display_name, - source.displayName, - source.name, - source.username, - ]; - names.forEach((value) => { - if (typeof value === "string") { - nameCandidates.push(value); - } - }); - const pictures = [source.picture, source.image, source.photo]; - pictures.forEach((value) => { - if (typeof value === "string") { - pictureCandidates.push(value); - } - }); - }; - - collectFromSource(fetchedProfile); - collectFromSource(cachedProfile); - - if (video && typeof video === "object") { - collectFromSource(video.creator); - if (typeof video.creatorName === "string") { - nameCandidates.push(video.creatorName); - } - if (typeof video.creatorPicture === "string") { - pictureCandidates.push(video.creatorPicture); - } - collectFromSource(video.author); - if (typeof video.authorName === "string") { - nameCandidates.push(video.authorName); - } - if (typeof video.authorPicture === "string") { - pictureCandidates.push(video.authorPicture); - } - collectFromSource(video.profile); - const extraNames = [ - video.shortNpub, - video.creatorNpub, - video.npub, - video.authorNpub, - ]; - extraNames.forEach((value) => { - if (typeof value === "string") { - nameCandidates.push(value); - } - }); - } - - const resolvedName = - this.selectPreferredCreatorName(nameCandidates) || "Unknown"; - const resolvedPicture = - this.selectPreferredCreatorPicture(pictureCandidates) || defaultAvatar; - - return { name: resolvedName, picture: resolvedPicture }; + resolveCreatorProfileFromSources(options) { + return this.creatorProfileController + ? this.creatorProfileController.resolveCreatorProfileFromSources(options) + : { name: "Unknown", picture: "assets/svg/default-profile.svg" }; } - resolveModalCreatorProfile({ - video, - pubkey, - cachedProfile = null, - fetchedProfile = null, - } = {}) { - return this.resolveCreatorProfileFromSources({ - video, - pubkey, - cachedProfile, - fetchedProfile, - }); + resolveModalCreatorProfile(options) { + return this.creatorProfileController + ? this.creatorProfileController.resolveModalCreatorProfile(options) + : { name: "Unknown", picture: "assets/svg/default-profile.svg" }; } decorateVideoCreatorIdentity(video) { - if (!video || typeof video !== "object") { - return video; - } - - const normalizedPubkey = - this.normalizeHexPubkey(video.pubkey) || - (typeof video.pubkey === "string" ? video.pubkey.trim() : ""); - if (!normalizedPubkey) { - return video; - } - - let cachedProfile = null; - if (typeof this.getProfileCacheEntry === "function") { - const cacheEntry = this.getProfileCacheEntry(normalizedPubkey); - if (cacheEntry && typeof cacheEntry === "object") { - cachedProfile = cacheEntry.profile || null; - } - } - - const resolvedProfile = this.resolveCreatorProfileFromSources({ - video, - pubkey: normalizedPubkey, - cachedProfile, - }); - - if (!video.creator || typeof video.creator !== "object") { - video.creator = {}; - } - - if (!video.creator.pubkey) { - video.creator.pubkey = normalizedPubkey; - } - - if (resolvedProfile.name) { - video.creator.name = resolvedProfile.name; - if ( - typeof video.creatorName !== "string" || - !video.creatorName.trim() || - video.creatorName === "Unknown" - ) { - video.creatorName = resolvedProfile.name; - } - if ( - typeof video.authorName !== "string" || - !video.authorName.trim() || - video.authorName === "Unknown" - ) { - video.authorName = resolvedProfile.name; - } - } - - if (resolvedProfile.picture) { - video.creator.picture = resolvedProfile.picture; - video.creatorPicture = resolvedProfile.picture; - if ( - typeof video.authorPicture !== "string" || - !video.authorPicture.trim() - ) { - video.authorPicture = resolvedProfile.picture; - } - } - - const encodedNpub = this.safeEncodeNpub(normalizedPubkey); - if (encodedNpub) { - const shortNpub = formatShortNpub(encodedNpub) || encodedNpub; - if (typeof video.npub !== "string" || !video.npub.trim()) { - video.npub = encodedNpub; - } - if (typeof video.shortNpub !== "string" || !video.shortNpub.trim()) { - video.shortNpub = shortNpub; - } - if ( - typeof video.creatorNpub !== "string" || - !video.creatorNpub.trim() - ) { - video.creatorNpub = shortNpub; - } - } - - return video; + return this.creatorProfileController + ? this.creatorProfileController.decorateVideoCreatorIdentity(video) + : video; } - async fetchModalCreatorProfile({ - pubkey, - displayNpub = "", - cachedProfile = null, - requestToken = null, - } = {}) { - const normalized = this.normalizeHexPubkey(pubkey); - if (!normalized) { - return; - } - - const relayList = - Array.isArray(nostrClient?.relays) && nostrClient.relays.length - ? nostrClient.relays - : null; - if (!relayList) { - return; - } - - const profileEntry = await fetchProfileMetadata(normalized, { - nostr: nostrClient, - relays: relayList, - logger: devLogger, - }); - - if (this.modalCreatorProfileRequestToken !== requestToken) { - return; - } - - if (!profileEntry?.event) { - if (this.modalCreatorProfileRequestToken === requestToken) { - this.modalCreatorProfileRequestToken = null; - } - return; - } - - ensureProfileMetadataSubscription({ - pubkey: normalized, - nostr: nostrClient, - relays: relayList, - onProfile: ({ profile }) => { - if (typeof this.setProfileCacheEntry === "function" && profile) { - this.setProfileCacheEntry(normalized, profile); - } - }, - }); - - let parsed = null; - try { - parsed = profileEntry.event.content - ? JSON.parse(profileEntry.event.content) - : null; - } catch (error) { - devLogger.warn( - `[Application] Failed to parse creator profile content for ${normalized}:`, - error, - ); - if (this.modalCreatorProfileRequestToken === requestToken) { - this.modalCreatorProfileRequestToken = null; - } - return; - } - - if (this.modalCreatorProfileRequestToken !== requestToken) { - return; - } - - const parsedLud16 = - typeof parsed?.lud16 === "string" ? parsed.lud16.trim() : ""; - const parsedLud06 = - typeof parsed?.lud06 === "string" ? parsed.lud06.trim() : ""; - const lightningAddressCandidate = (() => { - const fields = [parsedLud16, parsedLud06]; - for (const field of fields) { - if (typeof field !== "string") { - continue; - } - const trimmed = field.trim(); - if (trimmed) { - return trimmed; - } - } - return ""; - })(); - - const fetchedProfile = { - display_name: parsed?.display_name, - name: parsed?.name, - username: parsed?.username, - picture: parsed?.picture, - image: parsed?.image, - photo: parsed?.photo, - }; - - const resolvedProfile = this.resolveModalCreatorProfile({ - video: this.currentVideo, - pubkey: normalized, - cachedProfile, - fetchedProfile, - }); - - if (this.modalCreatorProfileRequestToken !== requestToken) { - return; - } - - const activeVideoPubkey = this.normalizeHexPubkey(this.currentVideo?.pubkey); - if (activeVideoPubkey && activeVideoPubkey !== normalized) { - return; - } - - const nextLightning = lightningAddressCandidate || ""; - const previousLightning = - typeof this.currentVideo?.lightningAddress === "string" - ? this.currentVideo.lightningAddress - : ""; - - if (this.currentVideo) { - this.currentVideo.lightningAddress = nextLightning ? nextLightning : null; - this.currentVideo.creatorName = resolvedProfile.name; - this.currentVideo.creatorPicture = resolvedProfile.picture; - this.currentVideo.creatorNpub = displayNpub; - if (this.currentVideo.creator && typeof this.currentVideo.creator === "object") { - this.currentVideo.creator = { - ...this.currentVideo.creator, - name: resolvedProfile.name, - picture: resolvedProfile.picture, - pubkey: normalized, - lightningAddress: nextLightning ? nextLightning : null, - }; - } else { - this.currentVideo.creator = { - name: resolvedProfile.name, - picture: resolvedProfile.picture, - pubkey: normalized, - lightningAddress: nextLightning ? nextLightning : null, - }; - } - } - - if (this.videoModal) { - this.videoModal.updateMetadata({ - creator: { - name: resolvedProfile.name, - avatarUrl: resolvedProfile.picture, - npub: displayNpub, - }, - }); - } - - this.zapController?.setVisibility(Boolean(this.currentVideo?.lightningAddress)); - - const sanitizedFetchedPicture = sanitizeProfileMediaUrl( - parsed?.picture || parsed?.image || parsed?.photo || "", - ); - const fetchedNameCandidate = this.selectPreferredCreatorName([ - parsed?.display_name, - parsed?.name, - parsed?.username, - ]); - - const cachedLightning = - typeof cachedProfile?.lightningAddress === "string" - ? cachedProfile.lightningAddress.trim() - : ""; - const shouldUpdateCache = - Boolean(fetchedNameCandidate) || - Boolean(sanitizedFetchedPicture) || - cachedLightning !== nextLightning || - previousLightning !== nextLightning; - - if (shouldUpdateCache) { - try { - const profileForCache = { - name: fetchedNameCandidate || resolvedProfile.name, - picture: sanitizedFetchedPicture || resolvedProfile.picture, - }; - - if (parsedLud16) { - profileForCache.lud16 = parsedLud16; - } - - if (parsedLud06) { - profileForCache.lud06 = parsedLud06; - } - - if (nextLightning) { - profileForCache.lightningAddress = nextLightning; - } - - this.setProfileCacheEntry( - normalized, - profileForCache, - { persist: false, reason: "modal-profile-fetch" }, - ); - } catch (error) { - devLogger.warn( - `[Application] Failed to update profile cache for ${normalized}:`, - error, - ); - } - } - - if (this.modalCreatorProfileRequestToken === requestToken) { - this.modalCreatorProfileRequestToken = null; + async fetchModalCreatorProfile(options) { + if (this.creatorProfileController) { + return this.creatorProfileController.fetchModalCreatorProfile(options); } } diff --git a/js/ui/creatorProfileController.js b/js/ui/creatorProfileController.js new file mode 100644 index 00000000..95917981 --- /dev/null +++ b/js/ui/creatorProfileController.js @@ -0,0 +1,489 @@ +import { normalizeHexPubkey } from "../utils/nostrHelpers.js"; + +export default class CreatorProfileController { + constructor({ + services: { nostrClient }, + ui: { zapController }, + callbacks: { + getProfileCacheEntry, + setProfileCacheEntry, + getCurrentVideo, + getVideoModal, + }, + helpers: { + fetchProfileMetadata, + ensureProfileMetadataSubscription, + safeEncodeNpub, + formatShortNpub, + sanitizeProfileMediaUrl, + }, + logger, + }) { + this.nostrClient = nostrClient; + this.zapController = zapController; + this.getProfileCacheEntry = getProfileCacheEntry; + this.setProfileCacheEntry = setProfileCacheEntry; + this.getCurrentVideo = getCurrentVideo; + this.getVideoModal = getVideoModal; + this.fetchProfileMetadata = fetchProfileMetadata; + this.ensureProfileMetadataSubscription = ensureProfileMetadataSubscription; + this.safeEncodeNpub = safeEncodeNpub; + this.formatShortNpub = formatShortNpub; + this.sanitizeProfileMediaUrl = sanitizeProfileMediaUrl; + this.logger = logger; + + this.modalCreatorProfileRequestToken = null; + } + + get videoModal() { + return this.getVideoModal ? this.getVideoModal() : null; + } + + get currentVideo() { + return this.getCurrentVideo ? this.getCurrentVideo() : null; + } + + log(message, ...args) { + if (this.logger && typeof this.logger.log === "function") { + this.logger.log("[CreatorProfileController]", message, ...args); + } + } + + warn(message, ...args) { + if (this.logger && typeof this.logger.warn === "function") { + this.logger.warn("[CreatorProfileController]", message, ...args); + } + } + + error(message, ...args) { + if (this.logger && typeof this.logger.error === "function") { + this.logger.error("[CreatorProfileController]", message, ...args); + } + } + + normalizeHexPubkey(pubkey) { + return normalizeHexPubkey(pubkey); + } + + selectPreferredCreatorName(candidates = []) { + for (const candidate of candidates) { + if (typeof candidate !== "string") { + continue; + } + const trimmed = candidate.trim(); + if (!trimmed) { + continue; + } + if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { + continue; + } + return trimmed; + } + return ""; + } + + selectPreferredCreatorPicture(candidates = []) { + for (const candidate of candidates) { + if (typeof candidate !== "string") { + continue; + } + const sanitized = this.sanitizeProfileMediaUrl(candidate); + if (sanitized) { + return sanitized; + } + } + return ""; + } + + resolveCreatorProfileFromSources({ + video, + pubkey, + cachedProfile = null, + fetchedProfile = null, + fallbackAvatar, + } = {}) { + const normalizedPubkey = + typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : ""; + const fallbackAvatarCandidate = + typeof fallbackAvatar === "string" && fallbackAvatar.trim() + ? fallbackAvatar.trim() + : normalizedPubkey + ? `https://robohash.org/${normalizedPubkey}` + : "assets/svg/default-profile.svg"; + const defaultAvatar = + this.sanitizeProfileMediaUrl(fallbackAvatarCandidate) || + fallbackAvatarCandidate || + "assets/svg/default-profile.svg"; + + const nameCandidates = []; + const pictureCandidates = []; + + const collectFromSource = (source) => { + if (!source || typeof source !== "object") { + return; + } + const names = [ + source.display_name, + source.displayName, + source.name, + source.username, + ]; + names.forEach((value) => { + if (typeof value === "string") { + nameCandidates.push(value); + } + }); + const pictures = [source.picture, source.image, source.photo]; + pictures.forEach((value) => { + if (typeof value === "string") { + pictureCandidates.push(value); + } + }); + }; + + collectFromSource(fetchedProfile); + collectFromSource(cachedProfile); + + if (video && typeof video === "object") { + collectFromSource(video.creator); + if (typeof video.creatorName === "string") { + nameCandidates.push(video.creatorName); + } + if (typeof video.creatorPicture === "string") { + pictureCandidates.push(video.creatorPicture); + } + collectFromSource(video.author); + if (typeof video.authorName === "string") { + nameCandidates.push(video.authorName); + } + if (typeof video.authorPicture === "string") { + pictureCandidates.push(video.authorPicture); + } + collectFromSource(video.profile); + const extraNames = [ + video.shortNpub, + video.creatorNpub, + video.npub, + video.authorNpub, + ]; + extraNames.forEach((value) => { + if (typeof value === "string") { + nameCandidates.push(value); + } + }); + } + + const resolvedName = + this.selectPreferredCreatorName(nameCandidates) || "Unknown"; + const resolvedPicture = + this.selectPreferredCreatorPicture(pictureCandidates) || defaultAvatar; + + return { name: resolvedName, picture: resolvedPicture }; + } + + resolveModalCreatorProfile({ + video, + pubkey, + cachedProfile = null, + fetchedProfile = null, + } = {}) { + return this.resolveCreatorProfileFromSources({ + video, + pubkey, + cachedProfile, + fetchedProfile, + }); + } + + decorateVideoCreatorIdentity(video) { + if (!video || typeof video !== "object") { + return video; + } + + const normalizedPubkey = + this.normalizeHexPubkey(video.pubkey) || + (typeof video.pubkey === "string" ? video.pubkey.trim() : ""); + if (!normalizedPubkey) { + return video; + } + + let cachedProfile = null; + if (typeof this.getProfileCacheEntry === "function") { + const cacheEntry = this.getProfileCacheEntry(normalizedPubkey); + if (cacheEntry && typeof cacheEntry === "object") { + cachedProfile = cacheEntry.profile || null; + } + } + + const resolvedProfile = this.resolveCreatorProfileFromSources({ + video, + pubkey: normalizedPubkey, + cachedProfile, + }); + + if (!video.creator || typeof video.creator !== "object") { + video.creator = {}; + } + + if (!video.creator.pubkey) { + video.creator.pubkey = normalizedPubkey; + } + + if (resolvedProfile.name) { + video.creator.name = resolvedProfile.name; + if ( + typeof video.creatorName !== "string" || + !video.creatorName.trim() || + video.creatorName === "Unknown" + ) { + video.creatorName = resolvedProfile.name; + } + if ( + typeof video.authorName !== "string" || + !video.authorName.trim() || + video.authorName === "Unknown" + ) { + video.authorName = resolvedProfile.name; + } + } + + if (resolvedProfile.picture) { + video.creator.picture = resolvedProfile.picture; + video.creatorPicture = resolvedProfile.picture; + if ( + typeof video.authorPicture !== "string" || + !video.authorPicture.trim() + ) { + video.authorPicture = resolvedProfile.picture; + } + } + + const encodedNpub = this.safeEncodeNpub(normalizedPubkey); + if (encodedNpub) { + const shortNpub = this.formatShortNpub(encodedNpub) || encodedNpub; + if (typeof video.npub !== "string" || !video.npub.trim()) { + video.npub = encodedNpub; + } + if (typeof video.shortNpub !== "string" || !video.shortNpub.trim()) { + video.shortNpub = shortNpub; + } + if ( + typeof video.creatorNpub !== "string" || + !video.creatorNpub.trim() + ) { + video.creatorNpub = shortNpub; + } + } + + return video; + } + + async fetchModalCreatorProfile({ + pubkey, + displayNpub = "", + cachedProfile = null, + requestToken = null, + } = {}) { + const normalized = this.normalizeHexPubkey(pubkey); + if (!normalized) { + return; + } + + this.modalCreatorProfileRequestToken = requestToken; + + const relayList = + Array.isArray(this.nostrClient?.relays) && this.nostrClient.relays.length + ? this.nostrClient.relays + : null; + if (!relayList) { + return; + } + + const profileEntry = await this.fetchProfileMetadata(normalized, { + nostr: this.nostrClient, + relays: relayList, + logger: this.logger, + }); + + if (this.modalCreatorProfileRequestToken !== requestToken) { + return; + } + + if (!profileEntry?.event) { + if (this.modalCreatorProfileRequestToken === requestToken) { + this.modalCreatorProfileRequestToken = null; + } + return; + } + + this.ensureProfileMetadataSubscription({ + pubkey: normalized, + nostr: this.nostrClient, + relays: relayList, + onProfile: ({ profile }) => { + if (typeof this.setProfileCacheEntry === "function" && profile) { + this.setProfileCacheEntry(normalized, profile); + } + }, + }); + + let parsed = null; + try { + parsed = profileEntry.event.content + ? JSON.parse(profileEntry.event.content) + : null; + } catch (error) { + this.warn( + `Failed to parse creator profile content for ${normalized}:`, + error, + ); + if (this.modalCreatorProfileRequestToken === requestToken) { + this.modalCreatorProfileRequestToken = null; + } + return; + } + + if (this.modalCreatorProfileRequestToken !== requestToken) { + return; + } + + const parsedLud16 = + typeof parsed?.lud16 === "string" ? parsed.lud16.trim() : ""; + const parsedLud06 = + typeof parsed?.lud06 === "string" ? parsed.lud06.trim() : ""; + const lightningAddressCandidate = (() => { + const fields = [parsedLud16, parsedLud06]; + for (const field of fields) { + if (typeof field !== "string") { + continue; + } + const trimmed = field.trim(); + if (trimmed) { + return trimmed; + } + } + return ""; + })(); + + const fetchedProfile = { + display_name: parsed?.display_name, + name: parsed?.name, + username: parsed?.username, + picture: parsed?.picture, + image: parsed?.image, + photo: parsed?.photo, + }; + + const resolvedProfile = this.resolveModalCreatorProfile({ + video: this.currentVideo, + pubkey: normalized, + cachedProfile, + fetchedProfile, + }); + + if (this.modalCreatorProfileRequestToken !== requestToken) { + return; + } + + const activeVideoPubkey = this.normalizeHexPubkey(this.currentVideo?.pubkey); + if (activeVideoPubkey && activeVideoPubkey !== normalized) { + return; + } + + const nextLightning = lightningAddressCandidate || ""; + const previousLightning = + typeof this.currentVideo?.lightningAddress === "string" + ? this.currentVideo.lightningAddress + : ""; + + if (this.currentVideo) { + this.currentVideo.lightningAddress = nextLightning ? nextLightning : null; + this.currentVideo.creatorName = resolvedProfile.name; + this.currentVideo.creatorPicture = resolvedProfile.picture; + this.currentVideo.creatorNpub = displayNpub; + if (this.currentVideo.creator && typeof this.currentVideo.creator === "object") { + this.currentVideo.creator = { + ...this.currentVideo.creator, + name: resolvedProfile.name, + picture: resolvedProfile.picture, + pubkey: normalized, + lightningAddress: nextLightning ? nextLightning : null, + }; + } else { + this.currentVideo.creator = { + name: resolvedProfile.name, + picture: resolvedProfile.picture, + pubkey: normalized, + lightningAddress: nextLightning ? nextLightning : null, + }; + } + } + + if (this.videoModal) { + this.videoModal.updateMetadata({ + creator: { + name: resolvedProfile.name, + avatarUrl: resolvedProfile.picture, + npub: displayNpub, + }, + }); + } + + this.zapController?.setVisibility(Boolean(this.currentVideo?.lightningAddress)); + + const sanitizedFetchedPicture = this.sanitizeProfileMediaUrl( + parsed?.picture || parsed?.image || parsed?.photo || "", + ); + const fetchedNameCandidate = this.selectPreferredCreatorName([ + parsed?.display_name, + parsed?.name, + parsed?.username, + ]); + + const cachedLightning = + typeof cachedProfile?.lightningAddress === "string" + ? cachedProfile.lightningAddress.trim() + : ""; + const shouldUpdateCache = + Boolean(fetchedNameCandidate) || + Boolean(sanitizedFetchedPicture) || + cachedLightning !== nextLightning || + previousLightning !== nextLightning; + + if (shouldUpdateCache) { + try { + const profileForCache = { + name: fetchedNameCandidate || resolvedProfile.name, + picture: sanitizedFetchedPicture || resolvedProfile.picture, + }; + + if (parsedLud16) { + profileForCache.lud16 = parsedLud16; + } + + if (parsedLud06) { + profileForCache.lud06 = parsedLud06; + } + + if (nextLightning) { + profileForCache.lightningAddress = nextLightning; + } + + this.setProfileCacheEntry( + normalized, + profileForCache, + { persist: false, reason: "modal-profile-fetch" }, + ); + } catch (error) { + this.warn( + `Failed to update profile cache for ${normalized}:`, + error, + ); + } + } + + if (this.modalCreatorProfileRequestToken === requestToken) { + this.modalCreatorProfileRequestToken = null; + } + } +} diff --git a/tests/ui/creatorProfileController.test.mjs b/tests/ui/creatorProfileController.test.mjs new file mode 100644 index 00000000..56637e31 --- /dev/null +++ b/tests/ui/creatorProfileController.test.mjs @@ -0,0 +1,115 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import CreatorProfileController from "../../js/ui/creatorProfileController.js"; + +test("CreatorProfileController", async (t) => { + await t.test("resolveCreatorProfileFromSources prioritizes cached profile", () => { + const controller = new CreatorProfileController({ + services: { nostrClient: {} }, + ui: { zapController: {} }, + callbacks: {}, + helpers: { + sanitizeProfileMediaUrl: (url) => url, + }, + }); + + const result = controller.resolveCreatorProfileFromSources({ + pubkey: "pubkey1", + cachedProfile: { name: "Cached Name", picture: "cached.jpg" }, + fetchedProfile: { name: "Fetched Name", picture: "fetched.jpg" }, + }); + + // logic prioritizes fetched > cached > video + assert.strictEqual(result.name, "Fetched Name"); + assert.strictEqual(result.picture, "fetched.jpg"); + }); + + await t.test("resolveCreatorProfileFromSources falls back to fetched profile", () => { + const controller = new CreatorProfileController({ + services: { nostrClient: {} }, + ui: { zapController: {} }, + callbacks: {}, + helpers: { + sanitizeProfileMediaUrl: (url) => url, + }, + }); + + const result = controller.resolveCreatorProfileFromSources({ + pubkey: "pubkey1", + cachedProfile: null, + fetchedProfile: { name: "Fetched Name", picture: "fetched.jpg" }, + }); + + assert.strictEqual(result.name, "Fetched Name"); + assert.strictEqual(result.picture, "fetched.jpg"); + }); + + await t.test("decorateVideoCreatorIdentity modifies video object", () => { + const controller = new CreatorProfileController({ + services: { nostrClient: {} }, + ui: { zapController: {} }, + callbacks: { + getProfileCacheEntry: () => ({ profile: { name: "Decorated Name" } }), + }, + helpers: { + safeEncodeNpub: () => "npub1...", + formatShortNpub: (npub) => npub, + sanitizeProfileMediaUrl: (url) => url, + }, + }); + + const video = { pubkey: "abcdef" }; + const decorated = controller.decorateVideoCreatorIdentity(video); + + assert.strictEqual(decorated.creatorName, "Decorated Name"); + assert.strictEqual(decorated.authorName, "Decorated Name"); + assert.strictEqual(decorated.creator.name, "Decorated Name"); + }); + + await t.test("fetchModalCreatorProfile fetches profile and updates modal", async () => { + let modalUpdated = false; + let zapVisibility = null; + let profileCached = false; + + const mockNostrClient = { relays: ["wss://relay.example.com"] }; + const mockVideoModal = { + updateMetadata: () => { modalUpdated = true; } + }; + const mockZapController = { + setVisibility: (val) => { zapVisibility = val; } + }; + + const validHex = "f".repeat(64); + + const controller = new CreatorProfileController({ + services: { nostrClient: mockNostrClient }, + ui: { zapController: mockZapController }, + callbacks: { + getProfileCacheEntry: () => null, + setProfileCacheEntry: () => { profileCached = true; }, + getCurrentVideo: () => ({ pubkey: validHex }), + getVideoModal: () => mockVideoModal, + }, + helpers: { + fetchProfileMetadata: async () => ({ + event: { content: JSON.stringify({ name: "Fetched" }) } + }), + ensureProfileMetadataSubscription: () => {}, + safeEncodeNpub: () => "npub1...", + formatShortNpub: (val) => val, + sanitizeProfileMediaUrl: (val) => val, + }, + logger: { warn: () => {} } + }); + + await controller.fetchModalCreatorProfile({ + pubkey: validHex, + displayNpub: "npub1...", + requestToken: Symbol("test") + }); + + assert.strictEqual(modalUpdated, true, "Video modal should be updated"); + assert.strictEqual(zapVisibility, false, "Zap visibility should be false (no lightning address)"); + assert.strictEqual(profileCached, true, "Profile should be cached"); + }); +});