Hydrate video cards with cached creator identity

This commit is contained in:
thePR0M3TH3AN committed 2025-10-27 19:03:31 -04:00
1 parent 9e8afa8f6d
commit 3ef44a6651
4 files changed
+450 -31

No files matched your search

+238 -29
View File
@@ -840,11 +840,12 @@ class Application {
if (!video || typeof video !== "object") {
return video;
}
let decoratedVideo = video;
if (typeof this.decorateVideoModeration === "function") {
try {
const decorated = this.decorateVideoModeration(video);
if (decorated && typeof decorated === "object") {
return decorated;
decoratedVideo = decorated;
}
} catch (error) {
devLogger.warn(
@@ -853,7 +854,22 @@ class Application {
);
}
}
return video;
if (typeof this.decorateVideoCreatorIdentity === "function") {
try {
const identityDecorated = this.decorateVideoCreatorIdentity(
decoratedVideo,
);
if (identityDecorated && typeof identityDecorated === "object") {
decoratedVideo = identityDecorated;
}
} catch (error) {
devLogger.warn(
"[Application] Failed to decorate similar content identity",
error,
);
}
}
return decoratedVideo;
};
const target = activeVideo && typeof activeVideo === "object" ? decorateCandidate(activeVideo) : null;
@@ -2742,12 +2758,50 @@ class Application {
pubkey,
profile,
});
} else {
devLogger.warn(
"[Application] ProfileIdentityController missing; profile identity was not refreshed in the DOM.",
);
}
const normalizedPubkey =
this.normalizeHexPubkey(pubkey) ||
(typeof pubkey === "string" ? pubkey.trim() : "");
if (!normalizedPubkey) {
return;
}
devLogger.warn(
"[Application] ProfileIdentityController missing; profile identity was not refreshed in the DOM.",
);
const maybeDecorateVideo = (video) => {
if (!video || typeof video !== "object") {
return;
}
const videoPubkey =
this.normalizeHexPubkey(video.pubkey) ||
(typeof video.pubkey === "string" ? video.pubkey.trim() : "");
if (videoPubkey !== normalizedPubkey) {
return;
}
this.decorateVideoCreatorIdentity(video);
};
if (this.videosMap instanceof Map) {
for (const video of this.videosMap.values()) {
maybeDecorateVideo(video);
}
}
if (
this.videoListView &&
Array.isArray(this.videoListView.currentVideos)
) {
this.videoListView.currentVideos.forEach((video) => {
maybeDecorateVideo(video);
});
}
if (this.currentVideo) {
maybeDecorateVideo(this.currentVideo);
}
}
async publishVideoNote(payload, { onSuccess, suppressModalClose } = {}) {
@@ -5478,7 +5532,15 @@ class Application {
}
const decoratedVideos = Array.isArray(videos)
? videos.map((video) => this.decorateVideoModeration(video))
? videos.map((video) => {
const moderated = this.decorateVideoModeration(video);
const targetVideo =
moderated && typeof moderated === "object" ? moderated : video;
const withIdentity = this.decorateVideoCreatorIdentity(targetVideo);
return withIdentity && typeof withIdentity === "object"
? withIdentity
: targetVideo;
})
: [];
this.videoListView.render(decoratedVideos, metadata);
@@ -7150,11 +7212,32 @@ class Application {
? index
: null;
const ensureCreatorIdentity = (video) => {
if (
!video ||
typeof video !== "object" ||
typeof this.decorateVideoCreatorIdentity !== "function"
) {
return video;
}
try {
const decorated = this.decorateVideoCreatorIdentity(video);
return decorated && typeof decorated === "object" ? decorated : video;
} catch (error) {
devLogger.warn(
"[Application] Failed to decorate video identity for action target:",
error,
);
return video;
}
};
const initialVideo =
providedVideo && typeof providedVideo === "object" ? providedVideo : null;
const preparedInitialVideo = ensureCreatorIdentity(initialVideo);
const providedId =
initialVideo && typeof initialVideo.id === "string"
? initialVideo.id.trim()
preparedInitialVideo && typeof preparedInitialVideo.id === "string"
? preparedInitialVideo.id.trim()
: "";
const targetEventId = trimmedEventId || providedId;
@@ -7163,9 +7246,9 @@ class Application {
: [];
if (providedId) {
this.videosMap.set(providedId, initialVideo);
this.videosMap.set(providedId, preparedInitialVideo);
if (!trimmedEventId || providedId === trimmedEventId) {
return initialVideo;
return preparedInitialVideo;
}
}
@@ -7173,8 +7256,9 @@ class Application {
if (targetEventId) {
const match = list.find((video) => video?.id === targetEventId);
if (match) {
this.videosMap.set(match.id, match);
return match;
const preparedMatch = ensureCreatorIdentity(match);
this.videosMap.set(preparedMatch.id, preparedMatch);
return preparedMatch;
}
}
if (
@@ -7184,8 +7268,9 @@ class Application {
) {
const match = list[normalizedIndex];
if (match) {
this.videosMap.set(match.id, match);
return match;
const preparedMatch = ensureCreatorIdentity(match);
this.videosMap.set(preparedMatch.id, preparedMatch);
return preparedMatch;
}
}
}
@@ -7199,20 +7284,23 @@ class Application {
const activeVideos = nostrClient.getActiveVideos();
const fromActive = activeVideos.find((video) => video.id === targetEventId);
if (fromActive) {
this.videosMap.set(fromActive.id, fromActive);
return fromActive;
const preparedActive = ensureCreatorIdentity(fromActive);
this.videosMap.set(preparedActive.id, preparedActive);
return preparedActive;
}
const fromAll = nostrClient.allEvents.get(targetEventId);
if (fromAll) {
this.videosMap.set(fromAll.id, fromAll);
return fromAll;
const preparedFromAll = ensureCreatorIdentity(fromAll);
this.videosMap.set(preparedFromAll.id, preparedFromAll);
return preparedFromAll;
}
const fetched = await nostrClient.getEventById(targetEventId);
if (fetched) {
this.videosMap.set(fetched.id, fetched);
return fetched;
const preparedFetched = ensureCreatorIdentity(fetched);
this.videosMap.set(preparedFetched.id, preparedFetched);
return preparedFetched;
}
}
@@ -7221,8 +7309,9 @@ class Application {
if (normalizedIndex >= 0 && normalizedIndex < activeVideos.length) {
const candidate = activeVideos[normalizedIndex];
if (candidate) {
this.videosMap.set(candidate.id, candidate);
return candidate;
const preparedCandidate = ensureCreatorIdentity(candidate);
this.videosMap.set(preparedCandidate.id, preparedCandidate);
return preparedCandidate;
}
}
}
@@ -8848,19 +8937,25 @@ class Application {
return "";
}
resolveModalCreatorProfile({
resolveCreatorProfileFromSources({
video,
pubkey,
cachedProfile = null,
fetchedProfile = null,
fallbackAvatar,
} = {}) {
const normalizedPubkey =
typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : "";
const fallbackAvatarCandidate = normalizedPubkey
? `https://robohash.org/${normalizedPubkey}`
: "assets/svg/default-profile.svg";
const fallbackAvatarCandidate =
typeof fallbackAvatar === "string" && fallbackAvatar.trim()
? fallbackAvatar.trim()
: normalizedPubkey
? `https://robohash.org/${normalizedPubkey}`
: "assets/svg/default-profile.svg";
const defaultAvatar =
sanitizeProfileMediaUrl(fallbackAvatarCandidate) || fallbackAvatarCandidate;
sanitizeProfileMediaUrl(fallbackAvatarCandidate) ||
fallbackAvatarCandidate ||
"assets/svg/default-profile.svg";
const nameCandidates = [];
const pictureCandidates = [];
@@ -8869,7 +8964,12 @@ class Application {
if (!source || typeof source !== "object") {
return;
}
const names = [source.display_name, source.name, source.username];
const names = [
source.display_name,
source.displayName,
source.name,
source.username,
];
names.forEach((value) => {
if (typeof value === "string") {
nameCandidates.push(value);
@@ -8902,15 +9002,124 @@ class Application {
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 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 = 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 = "",
+47 -2
View File
@@ -110,11 +110,13 @@ function decorateChannelVideo(video, app = getApp()) {
return null;
}
let decoratedVideo = video;
if (typeof app?.decorateVideoModeration === "function") {
try {
const decorated = app.decorateVideoModeration(video);
if (decorated && typeof decorated === "object") {
return decorated;
decoratedVideo = decorated;
}
} catch (error) {
devLogger.warn(
@@ -124,7 +126,21 @@ function decorateChannelVideo(video, app = getApp()) {
}
}
return video;
if (typeof app?.decorateVideoCreatorIdentity === "function") {
try {
const identityDecorated = app.decorateVideoCreatorIdentity(decoratedVideo);
if (identityDecorated && typeof identityDecorated === "object") {
decoratedVideo = identityDecorated;
}
} catch (error) {
devLogger.warn(
"[ChannelProfile] Failed to decorate channel video identity",
error,
);
}
}
return decoratedVideo;
}
function collectChannelVideos(pubkey, app = getApp()) {
@@ -4357,6 +4373,34 @@ export async function renderChannelVideosFromList({
cardState = "critical";
}
const identity = {
name:
typeof video.creatorName === "string" && video.creatorName
? video.creatorName
: typeof video.authorName === "string"
? video.authorName
: "",
picture:
typeof video.creatorPicture === "string" && video.creatorPicture
? video.creatorPicture
: typeof video.authorPicture === "string"
? video.authorPicture
: "",
pubkey: typeof video.pubkey === "string" ? video.pubkey : "",
npub:
typeof video.npub === "string" && video.npub
? video.npub
: typeof video.authorNpub === "string"
? video.authorNpub
: "",
shortNpub:
typeof video.shortNpub === "string" && video.shortNpub
? video.shortNpub
: typeof video.creatorNpub === "string"
? video.creatorNpub
: "",
};
const videoCard = new VideoCard({
document,
video,
@@ -4365,6 +4409,7 @@ export async function renderChannelVideosFromList({
pointerInfo,
timeAgo,
cardState,
identity,
capabilities: {
canEdit,
canDelete: canEdit,
+136
View File
@@ -1,5 +1,6 @@
import { normalizeDesignSystemContext } from "../../designSystem.js";
import { updateVideoCardSourceVisibility } from "../../utils/cardSourceVisibility.js";
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
import { userLogger } from "../../utils/logger.js";
import {
applyModerationContextDatasets,
@@ -9,6 +10,8 @@ import {
import { buildModerationBadgeText } from "../moderationCopy.js";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
export class VideoCard {
constructor({
@@ -27,6 +30,7 @@ export class VideoCard {
assets = {},
state = {},
variant = "default",
identity = null,
ensureGlobalMoreMenuHandlers,
onRequestCloseAllMenus,
nsfwContext = null,
@@ -91,6 +95,7 @@ export class VideoCard {
: null;
this.designSystem = normalizeDesignSystemContext(designSystem);
this.identity = this.normalizeIdentity(identity);
const normalizedVariant =
typeof variant === "string" && variant.trim().toLowerCase() === "compact"
@@ -461,6 +466,8 @@ export class VideoCard {
header.appendChild(controls);
}
this.applyIdentityToAuthorElements();
content.appendChild(title);
content.appendChild(header);
@@ -501,6 +508,135 @@ export class VideoCard {
this.refreshModerationUi();
}
normalizeIdentity(nextIdentity = {}, fallback = null) {
const baseline = fallback && typeof fallback === "object" ? fallback : {};
const candidate =
nextIdentity && typeof nextIdentity === "object" ? nextIdentity : {};
const video = this.video && typeof this.video === "object" ? this.video : {};
const pickFirstString = (
entries = [],
{ allowHex = true, sanitizer = (value) => value } = {},
) => {
for (const entry of entries) {
if (typeof entry !== "string") {
continue;
}
const trimmed = entry.trim();
if (!trimmed) {
continue;
}
if (!allowHex && HEX64_REGEX.test(trimmed)) {
continue;
}
const sanitized = sanitizer(trimmed);
if (!sanitized) {
continue;
}
return sanitized;
}
return "";
};
const pubkey = pickFirstString([
candidate.pubkey,
baseline.pubkey,
video.pubkey,
video.author?.pubkey,
video.creator?.pubkey,
video.profile?.pubkey,
]);
const npub = pickFirstString([
candidate.npub,
baseline.npub,
video.npub,
video.authorNpub,
video.profile?.npub,
]);
const shortNpub = pickFirstString([
candidate.shortNpub,
baseline.shortNpub,
video.shortNpub,
video.creatorNpub,
]);
const name = pickFirstString(
[
candidate.name,
candidate.display_name,
candidate.displayName,
candidate.username,
baseline.name,
baseline.display_name,
baseline.displayName,
baseline.username,
video.creatorName,
video.authorName,
video.creator?.name,
video.author?.name,
video.profile?.display_name,
video.profile?.name,
video.profile?.username,
],
{ allowHex: false },
);
const picture = pickFirstString(
[
candidate.picture,
candidate.image,
candidate.photo,
baseline.picture,
baseline.image,
baseline.photo,
video.creatorPicture,
video.authorPicture,
video.creator?.picture,
video.author?.picture,
video.profile?.picture,
video.profile?.image,
video.profile?.photo,
],
{ sanitizer: (value) => sanitizeProfileMediaUrl(value) },
);
const resolvedName = name || shortNpub || npub || pubkey || "";
const resolvedPicture = picture || DEFAULT_PROFILE_AVATAR;
return {
name: resolvedName,
npub,
shortNpub,
pubkey,
picture: resolvedPicture,
};
}
updateIdentity(nextIdentity = {}) {
this.identity = this.normalizeIdentity(nextIdentity, this.identity);
this.applyIdentityToAuthorElements();
}
applyIdentityToAuthorElements() {
const nameLabel = this.identity?.name || "Unknown";
if (this.authorNameEl) {
this.authorNameEl.textContent = nameLabel || "Unknown";
}
if (this.authorPicEl) {
const picture = this.identity?.picture || DEFAULT_PROFILE_AVATAR;
if (this.authorPicEl.getAttribute("src") !== picture) {
this.authorPicEl.src = picture;
}
const altFallback =
nameLabel || this.identity?.shortNpub || this.identity?.npub || "Channel";
this.authorPicEl.alt = `${altFallback}'s avatar`;
}
}
setCardBackdropImage(src) {
if (!this.root || !this.root.style) {
return;
+29
View File
@@ -556,6 +556,34 @@ export class VideoListView {
this.utils.persistWatchHistoryMetadata(video, pointerInfo);
}
const identity = {
name:
typeof video.creatorName === "string" && video.creatorName
? video.creatorName
: typeof video.authorName === "string"
? video.authorName
: "",
picture:
typeof video.creatorPicture === "string" && video.creatorPicture
? video.creatorPicture
: typeof video.authorPicture === "string"
? video.authorPicture
: "",
pubkey: typeof video.pubkey === "string" ? video.pubkey : "",
npub:
typeof video.npub === "string" && video.npub
? video.npub
: typeof video.authorNpub === "string"
? video.authorNpub
: "",
shortNpub:
typeof video.shortNpub === "string" && video.shortNpub
? video.shortNpub
: typeof video.creatorNpub === "string"
? video.creatorNpub
: "",
};
const videoCard = new VideoCard({
document: this.document,
video,
@@ -566,6 +594,7 @@ export class VideoListView {
postedAt: normalizedPostedAt,
cardState,
motionState,
identity,
nsfwContext: {
isNsfw: video?.isNsfw === true,
allowNsfw: this.allowNsfw,