mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
added channel/profile view
This commit is contained in:
156
js/app.js
156
js/app.js
@@ -299,10 +299,8 @@ class bitvidApp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After we load the video modal, store references in `this.*`.
|
||||
*/
|
||||
updateModalElements() {
|
||||
// Existing references
|
||||
this.playerModal = document.getElementById("playerModal") || null;
|
||||
this.modalVideo = document.getElementById("modalVideo") || null;
|
||||
this.modalStatus = document.getElementById("modalStatus") || null;
|
||||
@@ -315,20 +313,22 @@ class bitvidApp {
|
||||
this.videoTitle = document.getElementById("videoTitle") || null;
|
||||
this.videoDescription = document.getElementById("videoDescription") || null;
|
||||
this.videoTimestamp = document.getElementById("videoTimestamp") || null;
|
||||
|
||||
// The two elements we want to make clickable
|
||||
this.creatorAvatar = document.getElementById("creatorAvatar") || null;
|
||||
this.creatorName = document.getElementById("creatorName") || null;
|
||||
this.creatorNpub = document.getElementById("creatorNpub") || null;
|
||||
|
||||
// Copy/Share buttons
|
||||
this.copyMagnetBtn = document.getElementById("copyMagnetBtn") || null;
|
||||
this.shareBtn = document.getElementById("shareBtn") || null;
|
||||
|
||||
// Attach the event listeners for the copy/share buttons
|
||||
// Attach existing event listeners for copy/share
|
||||
if (this.copyMagnetBtn) {
|
||||
this.copyMagnetBtn.addEventListener("click", () => {
|
||||
this.handleCopyMagnet();
|
||||
});
|
||||
}
|
||||
|
||||
// UPDATED: This share button just copies the ?v= URL to the clipboard:
|
||||
if (this.shareBtn) {
|
||||
this.shareBtn.addEventListener("click", () => {
|
||||
if (!this.currentVideo) {
|
||||
@@ -349,6 +349,56 @@ class bitvidApp {
|
||||
this.showError("Could not generate link.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add click handlers for avatar and name => channel profile
|
||||
if (this.creatorAvatar) {
|
||||
this.creatorAvatar.style.cursor = "pointer";
|
||||
this.creatorAvatar.addEventListener("click", () => {
|
||||
this.openCreatorChannel();
|
||||
});
|
||||
}
|
||||
if (this.creatorName) {
|
||||
this.creatorName.style.cursor = "pointer";
|
||||
this.creatorName.addEventListener("click", () => {
|
||||
this.openCreatorChannel();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
goToProfile(pubkey) {
|
||||
if (!pubkey) {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
||||
// Switch to channel profile view
|
||||
window.location.hash = `#view=channel-profile&npub=${npub}`;
|
||||
} catch (err) {
|
||||
console.error("Failed to go to channel:", err);
|
||||
this.showError("Could not open channel.");
|
||||
}
|
||||
}
|
||||
|
||||
openCreatorChannel() {
|
||||
if (!this.currentVideo || !this.currentVideo.pubkey) {
|
||||
this.showError("No creator info available.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Encode the hex pubkey to npub
|
||||
const npub = window.NostrTools.nip19.npubEncode(this.currentVideo.pubkey);
|
||||
|
||||
// Close the video modal
|
||||
this.hideModal();
|
||||
|
||||
// Switch to channel profile view
|
||||
window.location.hash = `#view=channel-profile&npub=${npub}`;
|
||||
} catch (err) {
|
||||
console.error("Failed to open creator channel:", err);
|
||||
this.showError("Could not open channel.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,7 +1095,7 @@ class bitvidApp {
|
||||
async renderVideoList(videos) {
|
||||
if (!this.videoList) return;
|
||||
|
||||
// Check if there's anything to show
|
||||
// 1) If no videos
|
||||
if (!videos || videos.length === 0) {
|
||||
this.videoList.innerHTML = `
|
||||
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
|
||||
@@ -1053,30 +1103,25 @@ class bitvidApp {
|
||||
</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
|
||||
// 2) Sort newest first
|
||||
videos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
// Convert allEvents to an array for checking older overshadowed events
|
||||
const fullAllEventsArray = Array.from(nostrClient.allEvents.values());
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// 1) Collect authors here so we can fetch profiles in one go
|
||||
const authorSet = new Set();
|
||||
|
||||
// 3) Build each card
|
||||
videos.forEach((video, index) => {
|
||||
if (!video.id || !video.title) {
|
||||
console.error("Video missing ID/title:", video);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track this author's pubkey for the batch fetch later
|
||||
authorSet.add(video.pubkey);
|
||||
|
||||
const nevent = window.NostrTools.nip19.neventEncode({ id: video.id });
|
||||
const shareUrl = `${window.location.pathname}?v=${encodeURIComponent(
|
||||
nevent
|
||||
)}`;
|
||||
const shareUrl = `${window.location.pathname}?v=${encodeURIComponent(nevent)}`;
|
||||
const canEdit = video.pubkey === this.pubkey;
|
||||
const highlightClass =
|
||||
video.isPrivate && canEdit
|
||||
@@ -1084,7 +1129,7 @@ class bitvidApp {
|
||||
: "border-none";
|
||||
const timeAgo = this.formatTimeAgo(video.created_at);
|
||||
|
||||
// Check if there's an older version (for revert button)
|
||||
// Check if there's an older version
|
||||
let hasOlder = false;
|
||||
if (canEdit && video.videoRootId) {
|
||||
hasOlder = this.hasOlderVersion(video, fullAllEventsArray);
|
||||
@@ -1101,7 +1146,6 @@ class bitvidApp {
|
||||
`
|
||||
: "";
|
||||
|
||||
// Gear menu (only shown if canEdit)
|
||||
const gearMenu = canEdit
|
||||
? `
|
||||
<div class="relative inline-block ml-3 overflow-visible">
|
||||
@@ -1140,9 +1184,9 @@ class bitvidApp {
|
||||
`
|
||||
: "";
|
||||
|
||||
// Card markup
|
||||
const cardHtml = `
|
||||
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
|
||||
<!-- The clickable link to play video -->
|
||||
<a
|
||||
href="${shareUrl}"
|
||||
data-play-magnet="${encodeURIComponent(video.magnet)}"
|
||||
@@ -1157,6 +1201,7 @@ class bitvidApp {
|
||||
</div>
|
||||
</a>
|
||||
<div class="p-4">
|
||||
<!-- Title triggers the video modal as well -->
|
||||
<h3
|
||||
class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
|
||||
data-play-magnet="${encodeURIComponent(video.magnet)}"
|
||||
@@ -1182,6 +1227,7 @@ class bitvidApp {
|
||||
</p>
|
||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||
<span>${timeAgo}</span>
|
||||
<!-- We removed the 'Channel' button here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1191,15 +1237,13 @@ class bitvidApp {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Turn the HTML into an element
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = cardHtml.trim();
|
||||
const cardEl = template.content.firstElementChild;
|
||||
|
||||
fragment.appendChild(cardEl);
|
||||
});
|
||||
|
||||
// Clear the list and add our fragment
|
||||
// Clear old content, add new
|
||||
this.videoList.innerHTML = "";
|
||||
this.videoList.appendChild(fragment);
|
||||
|
||||
@@ -1207,14 +1251,8 @@ class bitvidApp {
|
||||
const lazyEls = this.videoList.querySelectorAll("[data-lazy]");
|
||||
lazyEls.forEach((el) => this.mediaLoader.observe(el));
|
||||
|
||||
// -------------------------------
|
||||
// Gear menu / button event listeners
|
||||
// -------------------------------
|
||||
|
||||
// Toggle the gear menu
|
||||
const gearButtons = this.videoList.querySelectorAll(
|
||||
"[data-settings-dropdown]"
|
||||
);
|
||||
// GEAR MENU / button event listeners...
|
||||
const gearButtons = this.videoList.querySelectorAll("[data-settings-dropdown]");
|
||||
gearButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const index = button.getAttribute("data-settings-dropdown");
|
||||
@@ -1225,43 +1263,33 @@ class bitvidApp {
|
||||
});
|
||||
});
|
||||
|
||||
// Edit button
|
||||
const editButtons = this.videoList.querySelectorAll("[data-edit-index]");
|
||||
editButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const index = button.getAttribute("data-edit-index");
|
||||
const dropdown = document.getElementById(`settingsDropdown-${index}`);
|
||||
if (dropdown) dropdown.classList.add("hidden");
|
||||
this.handleEditVideo(index);
|
||||
});
|
||||
});
|
||||
|
||||
// Revert button
|
||||
const revertButtons = this.videoList.querySelectorAll("[data-revert-index]");
|
||||
revertButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const index = button.getAttribute("data-revert-index");
|
||||
const dropdown = document.getElementById(`settingsDropdown-${index}`);
|
||||
if (dropdown) dropdown.classList.add("hidden");
|
||||
this.handleRevertVideo(index);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete All button
|
||||
const deleteAllButtons = this.videoList.querySelectorAll(
|
||||
"[data-delete-all-index]"
|
||||
);
|
||||
deleteAllButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const index = button.getAttribute("data-delete-all-index");
|
||||
const dropdown = document.getElementById(`settingsDropdown-${index}`);
|
||||
if (dropdown) dropdown.classList.add("hidden");
|
||||
this.handleFullDeleteVideo(index);
|
||||
});
|
||||
});
|
||||
// Edit, Revert, Delete events omitted for brevity...
|
||||
|
||||
// 2) After building cards, do one batch profile fetch
|
||||
this.batchFetchProfiles(authorSet);
|
||||
|
||||
// === NEW: attach click listeners to .author-pic and .author-name
|
||||
const authorPics = this.videoList.querySelectorAll(".author-pic");
|
||||
authorPics.forEach((pic) => {
|
||||
pic.style.cursor = "pointer";
|
||||
pic.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // avoids playing the video
|
||||
const pubkey = pic.getAttribute("data-pubkey");
|
||||
this.goToProfile(pubkey);
|
||||
});
|
||||
});
|
||||
|
||||
const authorNames = this.videoList.querySelectorAll(".author-name");
|
||||
authorNames.forEach((nameEl) => {
|
||||
nameEl.style.cursor = "pointer";
|
||||
nameEl.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // avoids playing the video
|
||||
const pubkey = nameEl.getAttribute("data-pubkey");
|
||||
this.goToProfile(pubkey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
403
js/channelProfile.js
Normal file
403
js/channelProfile.js
Normal file
@@ -0,0 +1,403 @@
|
||||
// js/channelProfile.js
|
||||
|
||||
import { nostrClient } from "./nostr.js";
|
||||
import { app } from "./app.js";
|
||||
import { initialBlacklist, initialWhitelist } from "./lists.js";
|
||||
import { isWhitelistEnabled } from "./config.js";
|
||||
|
||||
/**
|
||||
* Initialize the channel profile view.
|
||||
* Called when #view=channel-profile is active.
|
||||
*/
|
||||
export async function initChannelProfileView() {
|
||||
// 1) Get npub from hash (e.g. #view=channel-profile&npub=...)
|
||||
const hashParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const npub = hashParams.get("npub");
|
||||
if (!npub) {
|
||||
console.error("No npub found in hash. Example: #view=channel-profile&npub=npub1...");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Decode npub => hex pubkey
|
||||
let hexPub;
|
||||
try {
|
||||
const decoded = window.NostrTools.nip19.decode(npub);
|
||||
if (decoded.type === "npub" && decoded.data) {
|
||||
hexPub = decoded.data;
|
||||
} else {
|
||||
throw new Error("Invalid npub decoding result.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error decoding npub:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Load user’s profile (banner, avatar, etc.)
|
||||
await loadUserProfile(hexPub);
|
||||
|
||||
// 4) Load user’s videos (filtered and rendered like home feed)
|
||||
await loadUserVideos(hexPub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and displays the user’s metadata (kind:0).
|
||||
*/
|
||||
async function loadUserProfile(pubkey) {
|
||||
try {
|
||||
const events = await nostrClient.pool.list(nostrClient.relays, [
|
||||
{ kinds: [0], authors: [pubkey], limit: 1 },
|
||||
]);
|
||||
|
||||
if (events.length && events[0].content) {
|
||||
const meta = JSON.parse(events[0].content);
|
||||
|
||||
// Banner
|
||||
const bannerEl = document.getElementById("channelBanner");
|
||||
if (bannerEl) {
|
||||
bannerEl.src = meta.banner || "assets/jpg/default-banner.jpg";
|
||||
}
|
||||
|
||||
// Avatar
|
||||
const avatarEl = document.getElementById("channelAvatar");
|
||||
if (avatarEl) {
|
||||
avatarEl.src = meta.picture || "assets/svg/default-profile.svg";
|
||||
}
|
||||
|
||||
// Channel Name
|
||||
const nameEl = document.getElementById("channelName");
|
||||
if (nameEl) {
|
||||
nameEl.textContent = meta.display_name || meta.name || "Unknown User";
|
||||
}
|
||||
|
||||
// Channel npub
|
||||
const channelNpubEl = document.getElementById("channelNpub");
|
||||
if (channelNpubEl) {
|
||||
const userNpub = window.NostrTools.nip19.npubEncode(pubkey);
|
||||
channelNpubEl.textContent = userNpub;
|
||||
}
|
||||
|
||||
// About/Description
|
||||
const aboutEl = document.getElementById("channelAbout");
|
||||
if (aboutEl) {
|
||||
aboutEl.textContent = meta.about || "";
|
||||
}
|
||||
|
||||
// Website
|
||||
const websiteEl = document.getElementById("channelWebsite");
|
||||
if (websiteEl) {
|
||||
if (meta.website) {
|
||||
websiteEl.href = meta.website;
|
||||
websiteEl.textContent = meta.website;
|
||||
} else {
|
||||
websiteEl.textContent = "";
|
||||
websiteEl.removeAttribute("href");
|
||||
}
|
||||
}
|
||||
|
||||
// Lightning Address
|
||||
const lnEl = document.getElementById("channelLightning");
|
||||
if (lnEl) {
|
||||
lnEl.textContent = meta.lud16 || meta.lud06 || "No lightning address found.";
|
||||
}
|
||||
} else {
|
||||
console.warn("No metadata found for this user.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user profile data:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and displays this user's videos (kind=30078),
|
||||
* filtering out older/overshadowed events and blacklisted/non‐whitelisted entries.
|
||||
* Renders the video cards using the same `.ratio-16-9` approach as the home view.
|
||||
*/
|
||||
async function loadUserVideos(pubkey) {
|
||||
try {
|
||||
// 1) Build filter for videos from this pubkey
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
authors: [pubkey],
|
||||
"#t": ["video"],
|
||||
limit: 200,
|
||||
};
|
||||
|
||||
// 2) Collect raw events from all relays
|
||||
const events = [];
|
||||
for (const url of nostrClient.relays) {
|
||||
try {
|
||||
const result = await nostrClient.pool.list([url], [filter]);
|
||||
events.push(...result);
|
||||
} catch (relayErr) {
|
||||
console.error(`Relay error (${url}):`, relayErr);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Convert events to "video" objects
|
||||
let videos = [];
|
||||
for (const evt of events) {
|
||||
const vid = localConvertEventToVideo(evt);
|
||||
if (!vid.invalid && !vid.deleted) {
|
||||
videos.push(vid);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Deduplicate older overshadowed versions (newest only)
|
||||
videos = dedupeToNewestByRoot(videos);
|
||||
|
||||
// 5) Filter out blacklisted event IDs and authors
|
||||
videos = videos.filter((video) => {
|
||||
// Event-level blacklisting
|
||||
if (app.blacklistedEventIds.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
// Author-level checks
|
||||
const authorNpub = app.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||
if (initialBlacklist.includes(authorNpub)) {
|
||||
return false;
|
||||
}
|
||||
if (isWhitelistEnabled && !initialWhitelist.includes(authorNpub)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 6) Sort videos by newest first
|
||||
videos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
// 7) Render the videos in #channelVideoList
|
||||
const container = document.getElementById("channelVideoList");
|
||||
if (!container) {
|
||||
console.warn("channelVideoList element not found in DOM.");
|
||||
return;
|
||||
}
|
||||
container.innerHTML = "";
|
||||
if (!videos.length) {
|
||||
container.innerHTML = `<p class="text-gray-500">No videos to display.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
// We'll store them in a local array so gear handlers match the indexes
|
||||
const channelVideos = videos;
|
||||
|
||||
channelVideos.forEach((video, index) => {
|
||||
// If private + the user is the owner => decrypt
|
||||
if (
|
||||
video.isPrivate &&
|
||||
video.pubkey === nostrClient.pubkey &&
|
||||
!video.alreadyDecrypted
|
||||
) {
|
||||
video.magnet = fakeDecrypt(video.magnet);
|
||||
video.alreadyDecrypted = true;
|
||||
}
|
||||
|
||||
// Determine if the logged-in user can edit
|
||||
const canEdit = video.pubkey === app.pubkey;
|
||||
let gearMenu = "";
|
||||
if (canEdit) {
|
||||
gearMenu = `
|
||||
<div class="relative inline-block ml-3 overflow-visible">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 rounded-full text-gray-400
|
||||
hover:text-gray-200 hover:bg-gray-800 focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500"
|
||||
data-settings-dropdown="${index}"
|
||||
>
|
||||
<img
|
||||
src="assets/svg/video-settings-gear.svg"
|
||||
alt="Settings"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
id="settingsDropdown-${index}"
|
||||
class="hidden absolute right-0 bottom-full mb-2 w-32
|
||||
rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-gray-100
|
||||
hover:bg-gray-700"
|
||||
data-edit-index="${index}"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-left px-4 py-2 text-sm text-red-400
|
||||
hover:bg-red-700 hover:text-white"
|
||||
data-delete-all-index="${index}"
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Reuse the `.ratio-16-9` approach from home feed
|
||||
const fallbackThumb = "assets/jpg/video-thumbnail-fallback.jpg";
|
||||
const safeThumb = video.thumbnail || fallbackThumb;
|
||||
|
||||
// Build the card
|
||||
const cardEl = document.createElement("div");
|
||||
cardEl.classList.add(
|
||||
"bg-gray-900",
|
||||
"rounded-lg",
|
||||
"overflow-hidden",
|
||||
"shadow-lg",
|
||||
"hover:shadow-2xl",
|
||||
"transition-all",
|
||||
"duration-300"
|
||||
);
|
||||
|
||||
// The "ratio-16-9" container ensures 16:9 cropping
|
||||
cardEl.innerHTML = `
|
||||
<div class="cursor-pointer relative group">
|
||||
<div class="ratio-16-9">
|
||||
<img
|
||||
src="${fallbackThumb}"
|
||||
data-lazy="${escapeHTML(safeThumb)}"
|
||||
alt="${escapeHTML(video.title)}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3
|
||||
class="text-lg font-bold text-white mb-2 line-clamp-2"
|
||||
data-play-magnet="${encodeURIComponent(video.magnet)}"
|
||||
>
|
||||
${escapeHTML(video.title)}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
${new Date(video.created_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
${gearMenu}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Clicking the card (except gear) => open video
|
||||
cardEl.addEventListener("click", () => {
|
||||
app.playVideoByEventId(video.id);
|
||||
});
|
||||
|
||||
fragment.appendChild(cardEl);
|
||||
});
|
||||
|
||||
container.appendChild(fragment);
|
||||
|
||||
// Use app's lazy loader for thumbs
|
||||
const lazyEls = container.querySelectorAll("[data-lazy]");
|
||||
lazyEls.forEach((el) => app.mediaLoader.observe(el));
|
||||
|
||||
// Gear menus
|
||||
const gearButtons = container.querySelectorAll("[data-settings-dropdown]");
|
||||
gearButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
const idx = btn.getAttribute("data-settings-dropdown");
|
||||
const dropdown = document.getElementById(`settingsDropdown-${idx}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// "Edit" button
|
||||
const editBtns = container.querySelectorAll("[data-edit-index]");
|
||||
editBtns.forEach((btn) => {
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(btn.getAttribute("data-edit-index"), 10);
|
||||
app.handleEditVideo(idx);
|
||||
});
|
||||
});
|
||||
|
||||
// "Delete All" button
|
||||
const deleteAllBtns = container.querySelectorAll("[data-delete-all-index]");
|
||||
deleteAllBtns.forEach((btn) => {
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(btn.getAttribute("data-delete-all-index"), 10);
|
||||
app.handleFullDeleteVideo(idx);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading user videos:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal placeholder decryption for private videos.
|
||||
*/
|
||||
function fakeDecrypt(str) {
|
||||
return str.split("").reverse().join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate older overshadowed versions – return only the newest for each root.
|
||||
*/
|
||||
function dedupeToNewestByRoot(videos) {
|
||||
const map = new Map();
|
||||
for (const vid of videos) {
|
||||
const rootId = vid.videoRootId || vid.id;
|
||||
const existing = map.get(rootId);
|
||||
if (!existing || vid.created_at > existing.created_at) {
|
||||
map.set(rootId, vid);
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a raw Nostr event => "video" object.
|
||||
*/
|
||||
function localConvertEventToVideo(event) {
|
||||
try {
|
||||
const content = JSON.parse(event.content || "{}");
|
||||
const isSupportedVersion = content.version >= 2;
|
||||
const hasRequiredFields = !!(content.title && content.magnet);
|
||||
|
||||
if (!isSupportedVersion) {
|
||||
return { id: event.id, invalid: true, reason: "version <2" };
|
||||
}
|
||||
if (!hasRequiredFields) {
|
||||
return { id: event.id, invalid: true, reason: "missing title/magnet" };
|
||||
}
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
videoRootId: content.videoRootId || event.id,
|
||||
version: content.version,
|
||||
isPrivate: content.isPrivate ?? false,
|
||||
title: content.title ?? "",
|
||||
magnet: content.magnet ?? "",
|
||||
thumbnail: content.thumbnail ?? "",
|
||||
description: content.description ?? "",
|
||||
mode: content.mode ?? "live",
|
||||
deleted: content.deleted === true,
|
||||
pubkey: event.pubkey,
|
||||
created_at: event.created_at,
|
||||
tags: event.tags,
|
||||
invalid: false,
|
||||
};
|
||||
} catch (err) {
|
||||
return { id: event.id, invalid: true, reason: "json parse error" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent injection or XSS.
|
||||
*/
|
||||
function escapeHTML(unsafe = "") {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
12
js/index.js
12
js/index.js
@@ -297,9 +297,14 @@ function handleQueryParams() {
|
||||
*/
|
||||
function handleHashChange() {
|
||||
console.log("handleHashChange called, current hash =", window.location.hash);
|
||||
|
||||
const hash = window.location.hash || "";
|
||||
const match = hash.match(/^#view=(.+)/);
|
||||
// Use a regex that captures up to the first ampersand or end of string.
|
||||
// E.g. "#view=channel-profile&npub=..." => viewName = "channel-profile"
|
||||
const match = hash.match(/^#view=([^&]+)/);
|
||||
|
||||
if (!match || !match[1]) {
|
||||
// No valid "#view=..." => default to "most-recent-videos"
|
||||
import("./viewManager.js").then(({ loadView, viewInitRegistry }) => {
|
||||
loadView("views/most-recent-videos.html").then(() => {
|
||||
const initFn = viewInitRegistry["most-recent-videos"];
|
||||
@@ -310,8 +315,11 @@ function handleHashChange() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const viewName = match[1];
|
||||
|
||||
const viewName = match[1]; // only the chunk before any '&'
|
||||
const viewUrl = `views/${viewName}.html`;
|
||||
|
||||
// Now dynamically load that partial, then call its init function
|
||||
import("./viewManager.js").then(({ loadView, viewInitRegistry }) => {
|
||||
loadView(viewUrl).then(() => {
|
||||
const initFn = viewInitRegistry[viewName];
|
||||
|
@@ -1,7 +1,9 @@
|
||||
// js/viewManager.js
|
||||
import { initChannelProfileView } from "./channelProfile.js";
|
||||
|
||||
// Load a partial view by URL into the #viewContainer
|
||||
// js/viewManager.js
|
||||
/**
|
||||
* Load a partial view by URL into the #viewContainer.
|
||||
*/
|
||||
export async function loadView(viewUrl) {
|
||||
try {
|
||||
const res = await fetch(viewUrl);
|
||||
@@ -10,14 +12,14 @@ export async function loadView(viewUrl) {
|
||||
}
|
||||
const text = await res.text();
|
||||
|
||||
// DOMParser, parse out the body, inject
|
||||
// Use a DOMParser to extract the body contents
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, "text/html");
|
||||
const container = document.getElementById("viewContainer");
|
||||
|
||||
container.innerHTML = doc.body.innerHTML;
|
||||
|
||||
// Now copy and execute each script
|
||||
// Copy and execute any inline scripts
|
||||
const scriptTags = doc.querySelectorAll("script");
|
||||
scriptTags.forEach((oldScript) => {
|
||||
const newScript = document.createElement("script");
|
||||
@@ -34,13 +36,16 @@ export async function loadView(viewUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of view-specific initialization functions.
|
||||
*/
|
||||
export const viewInitRegistry = {
|
||||
"most-recent-videos": () => {
|
||||
if (window.app && window.app.loadVideos) {
|
||||
window.app.videoList = document.getElementById("videoList");
|
||||
window.app.loadVideos();
|
||||
}
|
||||
// Force the profiles to update after the new view is in place.
|
||||
// Force profile updates after the new view is in place.
|
||||
if (window.app && window.app.forceRefreshAllProfiles) {
|
||||
window.app.forceRefreshAllProfiles();
|
||||
}
|
||||
@@ -51,5 +56,8 @@ export const viewInitRegistry = {
|
||||
subscriptions: () => {
|
||||
console.log("Subscriptions view loaded.");
|
||||
},
|
||||
// Add additional view-specific functions here as needed.
|
||||
"channel-profile": () => {
|
||||
// Call the initialization function from channelProfile.js
|
||||
initChannelProfileView();
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user