mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
Improve video modal creator profile hydration
This commit is contained in:
1 parent
f72a8e4df5
commit
9e8afa8f6d
1 file changed
+329
-32
@@ -27,6 +27,7 @@ import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { updateVideoCardSourceVisibility } from "./utils/cardSourceVisibility.js";
|
||||
import { collectVideoTags } from "./utils/videoTags.js";
|
||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||
import { ADMIN_INITIAL_EVENT_BLACKLIST } from "./lists.js";
|
||||
import { userBlocks } from "./userBlocks.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
@@ -215,6 +216,8 @@ class Application {
|
||||
const { modalManager } = this.bootstrapper.initialize();
|
||||
this.modalManager = modalManager;
|
||||
|
||||
this.modalCreatorProfileRequestToken = null;
|
||||
|
||||
this.commentController = null;
|
||||
this.initializeCommentController();
|
||||
|
||||
@@ -8286,6 +8289,22 @@ class Application {
|
||||
? Math.floor(video.created_at)
|
||||
: null;
|
||||
|
||||
const normalizedCreatorPubkey =
|
||||
this.normalizeHexPubkey(video.pubkey) || video.pubkey;
|
||||
const cachedCreatorProfileEntry =
|
||||
normalizedCreatorPubkey && typeof this.getProfileCacheEntry === "function"
|
||||
? this.getProfileCacheEntry(normalizedCreatorPubkey)
|
||||
: null;
|
||||
const cachedCreatorProfile =
|
||||
cachedCreatorProfileEntry &&
|
||||
typeof cachedCreatorProfileEntry === "object"
|
||||
? cachedCreatorProfileEntry.profile || null
|
||||
: null;
|
||||
const initialLightningAddress =
|
||||
typeof video.lightningAddress === "string"
|
||||
? video.lightningAddress.trim()
|
||||
: "";
|
||||
|
||||
this.currentVideo = {
|
||||
...video,
|
||||
url: trimmedUrl,
|
||||
@@ -8294,7 +8313,7 @@ class Application {
|
||||
magnetCandidate || fallbackMagnetForCandidate || legacyInfoHash || "",
|
||||
torrentSupported: magnetSupported,
|
||||
legacyInfoHash: video.legacyInfoHash || legacyInfoHash,
|
||||
lightningAddress: null,
|
||||
lightningAddress: initialLightningAddress || null,
|
||||
lastEditedAt: normalizedEditedAt,
|
||||
};
|
||||
|
||||
@@ -8302,6 +8321,30 @@ class Application {
|
||||
|
||||
const modalTags = collectVideoTags(this.currentVideo);
|
||||
this.currentVideo.displayTags = modalTags;
|
||||
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||
const displayNpub = formatShortNpub(creatorNpub) || creatorNpub;
|
||||
const initialCreatorProfile = this.resolveModalCreatorProfile({
|
||||
video: this.currentVideo,
|
||||
pubkey: normalizedCreatorPubkey,
|
||||
cachedProfile: cachedCreatorProfile,
|
||||
});
|
||||
this.currentVideo.creatorName = initialCreatorProfile.name;
|
||||
this.currentVideo.creatorPicture = initialCreatorProfile.picture;
|
||||
this.currentVideo.creatorNpub = displayNpub;
|
||||
if (this.currentVideo.creator && typeof this.currentVideo.creator === "object") {
|
||||
this.currentVideo.creator = {
|
||||
...this.currentVideo.creator,
|
||||
name: initialCreatorProfile.name,
|
||||
picture: initialCreatorProfile.picture,
|
||||
pubkey: normalizedCreatorPubkey,
|
||||
};
|
||||
} else {
|
||||
this.currentVideo.creator = {
|
||||
name: initialCreatorProfile.name,
|
||||
picture: initialCreatorProfile.picture,
|
||||
pubkey: normalizedCreatorPubkey,
|
||||
};
|
||||
}
|
||||
this.updateModalSimilarContent({ activeVideo: this.currentVideo });
|
||||
|
||||
if (Number.isFinite(knownPostedAt)) {
|
||||
@@ -8352,8 +8395,8 @@ class Application {
|
||||
)}`;
|
||||
window.history.pushState({}, "", pushUrl);
|
||||
|
||||
this.zapController?.setVisibility(false);
|
||||
this.zapController?.resetState();
|
||||
this.zapController?.setVisibility(Boolean(this.currentVideo.lightningAddress));
|
||||
|
||||
const magnetInput =
|
||||
sanitizedMagnet ||
|
||||
@@ -8388,52 +8431,38 @@ class Application {
|
||||
playbackPromise = this.playVideoWithFallback(playbackOptions);
|
||||
}
|
||||
|
||||
let lightningAddress = "";
|
||||
let creatorProfile = {
|
||||
name: "Unknown",
|
||||
picture: `https://robohash.org/${video.pubkey}`,
|
||||
};
|
||||
try {
|
||||
const userEvents = await nostrClient.pool.list(nostrClient.relays, [
|
||||
{ kinds: [0], authors: [video.pubkey], limit: 1 },
|
||||
]);
|
||||
if (userEvents.length > 0 && userEvents[0]?.content) {
|
||||
const data = JSON.parse(userEvents[0].content);
|
||||
lightningAddress = (data.lud16 || data.lud06 || "").trim();
|
||||
creatorProfile = {
|
||||
name: data.name || data.display_name || "Unknown",
|
||||
picture: data.picture || `https://robohash.org/${video.pubkey}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("Error fetching creator profile:", error);
|
||||
}
|
||||
|
||||
this.zapController?.setVisibility(!!lightningAddress);
|
||||
if (this.currentVideo) {
|
||||
this.currentVideo.lightningAddress = lightningAddress || null;
|
||||
}
|
||||
|
||||
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||
if (this.videoModal) {
|
||||
const timestampPayload = this.buildModalTimestampPayload({
|
||||
postedAt: this.currentVideo?.rootCreatedAt ?? null,
|
||||
editedAt: normalizedEditedAt,
|
||||
});
|
||||
const displayNpub = formatShortNpub(creatorNpub);
|
||||
this.videoModal.updateMetadata({
|
||||
title: video.title || "Untitled",
|
||||
description: video.description || "No description available.",
|
||||
timestamps: timestampPayload,
|
||||
tags: modalTags,
|
||||
creator: {
|
||||
name: creatorProfile.name,
|
||||
avatarUrl: creatorProfile.picture,
|
||||
name: initialCreatorProfile.name,
|
||||
avatarUrl: initialCreatorProfile.picture,
|
||||
npub: displayNpub,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const profileRequestToken = Symbol("modal-profile-request");
|
||||
this.modalCreatorProfileRequestToken = profileRequestToken;
|
||||
this.fetchModalCreatorProfile({
|
||||
pubkey: normalizedCreatorPubkey,
|
||||
displayNpub,
|
||||
cachedProfile: cachedCreatorProfile,
|
||||
requestToken: profileRequestToken,
|
||||
}).catch((error) => {
|
||||
devLogger.error(
|
||||
"[Application] Failed to fetch creator profile for modal:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
this.ensureModalPostedTimestamp(this.currentVideo);
|
||||
|
||||
const playbackResult =
|
||||
@@ -8789,6 +8818,274 @@ class Application {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
|
||||
selectPreferredCreatorPicture(candidates = []) {
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate !== "string") {
|
||||
continue;
|
||||
}
|
||||
const sanitized = sanitizeProfileMediaUrl(candidate);
|
||||
if (sanitized) {
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
resolveModalCreatorProfile({
|
||||
video,
|
||||
pubkey,
|
||||
cachedProfile = null,
|
||||
fetchedProfile = null,
|
||||
} = {}) {
|
||||
const normalizedPubkey =
|
||||
typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : "";
|
||||
const fallbackAvatarCandidate = normalizedPubkey
|
||||
? `https://robohash.org/${normalizedPubkey}`
|
||||
: "assets/svg/default-profile.svg";
|
||||
const defaultAvatar =
|
||||
sanitizeProfileMediaUrl(fallbackAvatarCandidate) || fallbackAvatarCandidate;
|
||||
|
||||
const nameCandidates = [];
|
||||
const pictureCandidates = [];
|
||||
|
||||
const collectFromSource = (source) => {
|
||||
if (!source || typeof source !== "object") {
|
||||
return;
|
||||
}
|
||||
const names = [source.display_name, 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 resolvedName = this.selectPreferredCreatorName(nameCandidates) || "Unknown";
|
||||
const resolvedPicture =
|
||||
this.selectPreferredCreatorPicture(pictureCandidates) || defaultAvatar;
|
||||
|
||||
return { name: resolvedName, picture: resolvedPicture };
|
||||
}
|
||||
|
||||
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 || !nostrClient?.pool || typeof nostrClient.pool.list !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const events = await nostrClient.pool.list(relayList, [
|
||||
{ kinds: [0], authors: [normalized], limit: 1 },
|
||||
]);
|
||||
|
||||
if (this.modalCreatorProfileRequestToken !== requestToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newest = Array.isArray(events)
|
||||
? events.reduce((latest, event) => {
|
||||
if (!event || typeof event !== "object") {
|
||||
return latest;
|
||||
}
|
||||
const eventPubkey = this.normalizeHexPubkey(event.pubkey);
|
||||
if (eventPubkey && eventPubkey !== normalized) {
|
||||
return latest;
|
||||
}
|
||||
const createdAt = Number.isFinite(event.created_at) ? event.created_at : 0;
|
||||
if (!latest || createdAt > latest.createdAt) {
|
||||
return { createdAt, event };
|
||||
}
|
||||
return latest;
|
||||
}, null)
|
||||
: null;
|
||||
|
||||
if (!newest || !newest.event) {
|
||||
if (this.modalCreatorProfileRequestToken === requestToken) {
|
||||
this.modalCreatorProfileRequestToken = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = newest.event.content ? JSON.parse(newest.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 lightningAddressCandidate = (() => {
|
||||
const fields = [parsed?.lud16, parsed?.lud06];
|
||||
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 || 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,
|
||||
};
|
||||
} else {
|
||||
this.currentVideo.creator = {
|
||||
name: resolvedProfile.name,
|
||||
picture: resolvedProfile.picture,
|
||||
pubkey: normalized,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
if (fetchedNameCandidate || sanitizedFetchedPicture) {
|
||||
try {
|
||||
this.setProfileCacheEntry(
|
||||
normalized,
|
||||
{
|
||||
name: fetchedNameCandidate || resolvedProfile.name,
|
||||
picture: sanitizedFetchedPicture || resolvedProfile.picture,
|
||||
},
|
||||
{ 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;
|
||||
}
|
||||
}
|
||||
|
||||
normalizeReactionCount(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return Math.max(0, Math.round(Number(value)));
|
||||
|
||||
Reference in new issue
Block a user