mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add nostr service layer for video operations
This commit is contained in:
1 parent
9fa359f9b5
commit
61815ba289
5 files changed
+518
-148
No files matched your search
@@ -23,6 +23,7 @@ import watchHistoryService from "./watchHistoryService.js";
|
||||
import r2Service from "./services/r2Service.js";
|
||||
import PlaybackService from "./services/playbackService.js";
|
||||
import AuthService from "./services/authService.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import { initQuickR2Upload } from "./r2-quick.js";
|
||||
import { createWatchHistoryRenderer } from "./historyView.js";
|
||||
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
|
||||
@@ -583,7 +584,7 @@ class bitvidApp {
|
||||
this.currentVideoPointerKey = null;
|
||||
this.playbackTelemetryState = null;
|
||||
this.loggedViewPointerKeys = new Set();
|
||||
this.videoSubscription = null;
|
||||
this.videoSubscription = nostrService.getVideoSubscription() || null;
|
||||
this.videoList = null;
|
||||
this._videoListElement = null;
|
||||
this._videoListClickHandler = null;
|
||||
@@ -592,7 +593,10 @@ class bitvidApp {
|
||||
this.videoViewCountEl = null;
|
||||
|
||||
// Videos stored as a Map (key=event.id)
|
||||
this.videosMap = new Map();
|
||||
this.videosMap = nostrService.getVideosMap();
|
||||
nostrService.on("subscription:changed", ({ subscription }) => {
|
||||
this.videoSubscription = subscription || null;
|
||||
});
|
||||
this.moreMenuGlobalHandlerBound = false;
|
||||
this.boundMoreMenuDocumentClick = null;
|
||||
this.boundMoreMenuDocumentKeydown = null;
|
||||
@@ -5394,7 +5398,7 @@ class bitvidApp {
|
||||
}
|
||||
|
||||
try {
|
||||
await nostrClient.publishVideo(formData, this.pubkey);
|
||||
await nostrService.publishVideoNote(formData, this.pubkey);
|
||||
if (typeof onSuccess === "function") {
|
||||
await onSuccess();
|
||||
}
|
||||
@@ -5532,7 +5536,11 @@ class bitvidApp {
|
||||
};
|
||||
|
||||
try {
|
||||
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
|
||||
await nostrService.handleEditVideoSubmit({
|
||||
originalEvent,
|
||||
updatedData,
|
||||
pubkey: this.pubkey,
|
||||
});
|
||||
await this.loadVideos();
|
||||
this.videosMap.clear();
|
||||
this.showSuccess("Video updated successfully!");
|
||||
@@ -5845,16 +5853,9 @@ class bitvidApp {
|
||||
this.pruneDetachedViewCountElements();
|
||||
}
|
||||
|
||||
if (!preserveSubscriptions && this.videoSubscription) {
|
||||
try {
|
||||
if (typeof this.videoSubscription.unsub === "function") {
|
||||
this.videoSubscription.unsub();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to unsubscribe from video feed:", err);
|
||||
} finally {
|
||||
this.videoSubscription = null;
|
||||
}
|
||||
if (!preserveSubscriptions) {
|
||||
nostrService.clearVideoSubscription();
|
||||
this.videoSubscription = nostrService.getVideoSubscription() || null;
|
||||
}
|
||||
|
||||
// If there's a small inline player
|
||||
@@ -6951,104 +6952,43 @@ class bitvidApp {
|
||||
async loadVideos(forceFetch = false) {
|
||||
console.log("Starting loadVideos... (forceFetch =", forceFetch, ")");
|
||||
|
||||
try {
|
||||
await accessControl.ensureReady();
|
||||
} catch (error) {
|
||||
console.warn("Failed to ensure admin lists were loaded before fetching videos:", error);
|
||||
}
|
||||
|
||||
const shouldIncludeVideo = (video) => {
|
||||
if (!video || typeof video !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.blacklistedEventIds.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAuthorBlocked(video.pubkey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!accessControl.canAccess(video)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// If forceFetch is true, unsubscribe from the old subscription to start fresh
|
||||
if (forceFetch && this.videoSubscription) {
|
||||
// Call unsubscribe on the subscription object directly.
|
||||
this.videoSubscription.unsub();
|
||||
this.videoSubscription = null;
|
||||
}
|
||||
|
||||
if (this.videoList) {
|
||||
// Always reset the cached signature before showing the loading state so the
|
||||
// next render isn't skipped when the incoming payload matches the last
|
||||
// successful render. Without this, the spinner could remain visible for
|
||||
// returning users who already have the latest list cached.
|
||||
this.lastRenderedVideoSignature = null;
|
||||
this.videoList.innerHTML = getSidebarLoadingMarkup(
|
||||
"Fetching recent videos…"
|
||||
);
|
||||
}
|
||||
|
||||
// The rest of your existing logic:
|
||||
if (!this.videoSubscription) {
|
||||
const videos = await nostrService.loadVideos({
|
||||
forceFetch,
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
onVideos: (payload) => this.renderVideoList(payload),
|
||||
});
|
||||
|
||||
// Create a new subscription
|
||||
this.videoSubscription = nostrClient.subscribeVideos(() => {
|
||||
const updatedAll = nostrClient.getActiveVideos();
|
||||
|
||||
// Filter out blacklisted authors & blacklisted event IDs
|
||||
const filteredVideos = updatedAll.filter(shouldIncludeVideo);
|
||||
|
||||
this.renderVideoList(filteredVideos);
|
||||
});
|
||||
|
||||
const cachedActiveVideos = nostrClient.getActiveVideos();
|
||||
const cachedFiltered = cachedActiveVideos.filter(shouldIncludeVideo);
|
||||
if (cachedFiltered.length) {
|
||||
this.renderVideoList(cachedFiltered);
|
||||
}
|
||||
|
||||
if (this.videoSubscription) {
|
||||
console.log(
|
||||
"[loadVideos] subscription remains open to get live updates."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Already subscribed: just show what's cached
|
||||
const allCached = nostrClient.getActiveVideos();
|
||||
|
||||
const filteredCached = allCached.filter(shouldIncludeVideo);
|
||||
|
||||
this.renderVideoList(filteredCached);
|
||||
if (!Array.isArray(videos) || videos.length === 0) {
|
||||
this.renderVideoList([]);
|
||||
}
|
||||
|
||||
this.videoSubscription = nostrService.getVideoSubscription() || null;
|
||||
this.videosMap = nostrService.getVideosMap();
|
||||
}
|
||||
|
||||
async loadOlderVideos(lastTimestamp) {
|
||||
// 1) Use nostrClient to fetch older slices
|
||||
const olderVideos = await nostrClient.fetchOlderVideos(lastTimestamp);
|
||||
const olderVideos = await nostrService.loadOlderVideos(lastTimestamp, {
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
});
|
||||
|
||||
if (!olderVideos || olderVideos.length === 0) {
|
||||
if (!Array.isArray(olderVideos) || olderVideos.length === 0) {
|
||||
this.showSuccess("No more older videos found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Merge them into the client’s allEvents / activeMap
|
||||
for (const v of olderVideos) {
|
||||
nostrClient.allEvents.set(v.id, v);
|
||||
// If it’s the newest version for its root, update activeMap
|
||||
const rootKey = v.videoRootId || v.id;
|
||||
// You can call getActiveKey(v) if you want to match your code’s approach.
|
||||
// Then re-check if this one is newer than what’s stored, etc.
|
||||
}
|
||||
|
||||
// 3) Re-render
|
||||
const all = nostrClient.getActiveVideos();
|
||||
const all = nostrService.getFilteredActiveVideos({
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
});
|
||||
this.renderVideoList(all);
|
||||
}
|
||||
|
||||
@@ -8943,7 +8883,10 @@ class bitvidApp {
|
||||
async handleEditVideo(target) {
|
||||
try {
|
||||
const normalizedTarget = this.normalizeActionTarget(target);
|
||||
const latestVideos = await nostrClient.fetchVideos();
|
||||
const latestVideos = await nostrService.fetchVideos({
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
});
|
||||
const video = await this.resolveVideoActionTarget({
|
||||
...normalizedTarget,
|
||||
preloadedList: latestVideos,
|
||||
@@ -8981,7 +8924,10 @@ class bitvidApp {
|
||||
async handleRevertVideo(target) {
|
||||
try {
|
||||
const normalizedTarget = this.normalizeActionTarget(target);
|
||||
const activeVideos = await nostrClient.fetchVideos();
|
||||
const activeVideos = await nostrService.fetchVideos({
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
});
|
||||
const video = await this.resolveVideoActionTarget({
|
||||
...normalizedTarget,
|
||||
preloadedList: activeVideos,
|
||||
@@ -9022,7 +8968,10 @@ class bitvidApp {
|
||||
async handleFullDeleteVideo(target) {
|
||||
try {
|
||||
const normalizedTarget = this.normalizeActionTarget(target);
|
||||
const all = await nostrClient.fetchVideos();
|
||||
const all = nostrService.getFilteredActiveVideos({
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
});
|
||||
const video = await this.resolveVideoActionTarget({
|
||||
...normalizedTarget,
|
||||
preloadedList: all,
|
||||
@@ -9050,7 +8999,11 @@ class bitvidApp {
|
||||
// We assume video.videoRootId is not empty, or fallback to video.id if needed
|
||||
const rootId = video.videoRootId || video.id;
|
||||
|
||||
await nostrClient.deleteAllVersions(rootId, this.pubkey);
|
||||
await nostrService.handleFullDeleteVideo({
|
||||
videoRootId: rootId,
|
||||
pubkey: this.pubkey,
|
||||
confirm: false,
|
||||
});
|
||||
|
||||
// Reload
|
||||
await this.loadVideos();
|
||||
@@ -9952,39 +9905,7 @@ class bitvidApp {
|
||||
* this.videosMap or from a bulk fetch. Uses nostrClient.getEventById.
|
||||
*/
|
||||
async getOldEventById(eventId) {
|
||||
// 1) Already in our local videosMap?
|
||||
let video = this.videosMap.get(eventId);
|
||||
if (video) {
|
||||
return video;
|
||||
}
|
||||
|
||||
// 2) Already in nostrClient.allEvents?
|
||||
// (assuming nostrClient.allEvents is a Map of id => video)
|
||||
const fromAll = nostrClient.allEvents.get(eventId);
|
||||
if (fromAll && !fromAll.deleted) {
|
||||
this.videosMap.set(eventId, fromAll);
|
||||
return fromAll;
|
||||
}
|
||||
|
||||
// 3) Direct single-event fetch (fewer resources than full fetchVideos)
|
||||
const single = await nostrClient.getEventById(eventId);
|
||||
if (single && !single.deleted) {
|
||||
this.videosMap.set(single.id, single);
|
||||
return single;
|
||||
}
|
||||
|
||||
// 4) If you wanted a final fallback, you could do it here:
|
||||
// But it's typically better to avoid repeated full fetches
|
||||
// console.log("Falling back to full fetchVideos...");
|
||||
// const allFetched = await nostrClient.fetchVideos();
|
||||
// video = allFetched.find(v => v.id === eventId && !v.deleted);
|
||||
// if (video) {
|
||||
// this.videosMap.set(video.id, video);
|
||||
// return video;
|
||||
// }
|
||||
|
||||
// Not found or was deleted
|
||||
return null;
|
||||
return nostrService.getOldEventById(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
-10
@@ -7,6 +7,7 @@ import {
|
||||
nostrClient,
|
||||
updateWatchHistoryList,
|
||||
} from "./nostr.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import {
|
||||
WATCH_HISTORY_BATCH_RESOLVE,
|
||||
WATCH_HISTORY_BATCH_PAGE_SIZE,
|
||||
@@ -409,11 +410,15 @@ async function resolveVideoFromPointer(pointer, caches) {
|
||||
}
|
||||
}
|
||||
|
||||
const filterOptions = {
|
||||
blacklistedEventIds: app?.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) =>
|
||||
(typeof app?.isAuthorBlocked === "function" && app.isAuthorBlocked(pubkey)) || false,
|
||||
};
|
||||
|
||||
const activeVideos = Array.isArray(caches?.activeVideos)
|
||||
? caches.activeVideos
|
||||
: typeof nostrClient?.getActiveVideos === "function"
|
||||
? nostrClient.getActiveVideos()
|
||||
: [];
|
||||
: nostrService.getFilteredActiveVideos(filterOptions);
|
||||
for (const candidate of activeVideos) {
|
||||
if (compareAddress(candidate)) {
|
||||
const normalized = normalizeVideo(candidate);
|
||||
@@ -423,13 +428,15 @@ async function resolveVideoFromPointer(pointer, caches) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!caches?.catalogPromise && typeof nostrClient?.fetchVideos === "function") {
|
||||
caches.catalogPromise = nostrClient.fetchVideos().catch((error) => {
|
||||
if (isDevEnv) {
|
||||
console.warn("[historyView] Failed to fetch video catalog:", error);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
if (!caches?.catalogPromise) {
|
||||
caches.catalogPromise = nostrService
|
||||
.fetchVideos(filterOptions)
|
||||
.catch((error) => {
|
||||
if (isDevEnv) {
|
||||
console.warn("[historyView] Failed to fetch video catalog:", error);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
if (caches?.catalogPromise) {
|
||||
try {
|
||||
|
||||
+10
-6
@@ -4611,17 +4611,21 @@ class NostrClient {
|
||||
*
|
||||
* This version now asks for confirmation before proceeding.
|
||||
*/
|
||||
async deleteAllVersions(videoRootId, pubkey) {
|
||||
async deleteAllVersions(videoRootId, pubkey, options = {}) {
|
||||
if (!pubkey) {
|
||||
throw new Error("Not logged in to delete all versions.");
|
||||
}
|
||||
|
||||
// Ask for confirmation before proceeding
|
||||
if (
|
||||
!window.confirm(
|
||||
const shouldConfirm = options?.confirm !== false;
|
||||
let confirmed = true;
|
||||
|
||||
if (shouldConfirm && typeof window?.confirm === "function") {
|
||||
confirmed = window.confirm(
|
||||
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
||||
)
|
||||
) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
console.log("Deletion cancelled by user.");
|
||||
return null; // Cancel deletion if user clicks "Cancel"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
import {
|
||||
nostrClient,
|
||||
convertEventToVideo,
|
||||
} from "../nostr.js";
|
||||
import { accessControl } from "../accessControl.js";
|
||||
import {
|
||||
getVideosMap as getStoredVideosMap,
|
||||
setVideosMap as setStoredVideosMap,
|
||||
getVideoSubscription as getStoredVideoSubscription,
|
||||
setVideoSubscription as setStoredVideoSubscription,
|
||||
} from "../state/appState.js";
|
||||
|
||||
const VIDEO_KIND = 30078;
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
try {
|
||||
this.logger(`nostrService listener for "${eventName}" threw`, error);
|
||||
} catch (logError) {
|
||||
console.warn("[nostrService] listener logger threw", logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLogger(logger) {
|
||||
if (typeof logger === "function") {
|
||||
return logger;
|
||||
}
|
||||
if (logger && typeof logger.log === "function") {
|
||||
return (...args) => logger.log(...args);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
function ensureSet(value) {
|
||||
if (value instanceof Set) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return new Set(value);
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function normalizeUntil(timestamp) {
|
||||
const value = Number(timestamp);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const seconds = Math.floor(value);
|
||||
return seconds > 0 ? seconds : 0;
|
||||
}
|
||||
|
||||
export class NostrService {
|
||||
constructor({ logger } = {}) {
|
||||
this.nostrClient = nostrClient;
|
||||
this.accessControl = accessControl;
|
||||
this.logger = normalizeLogger(logger);
|
||||
this.emitter = new SimpleEventEmitter((message, error) => {
|
||||
try {
|
||||
this.logger(message, error);
|
||||
} catch (logError) {
|
||||
console.warn("[nostrService] logger threw", logError);
|
||||
}
|
||||
});
|
||||
this.videosMap = null;
|
||||
}
|
||||
|
||||
log(...args) {
|
||||
try {
|
||||
this.logger(...args);
|
||||
} catch (error) {
|
||||
console.warn("[nostrService] logger threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
return this.emitter.on(eventName, handler);
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
this.emitter.emit(eventName, detail);
|
||||
}
|
||||
|
||||
ensureVideosMap() {
|
||||
if (this.videosMap instanceof Map) {
|
||||
return this.videosMap;
|
||||
}
|
||||
|
||||
let map = getStoredVideosMap();
|
||||
if (!(map instanceof Map)) {
|
||||
map = new Map();
|
||||
setStoredVideosMap(map);
|
||||
}
|
||||
|
||||
this.videosMap = map;
|
||||
return this.videosMap;
|
||||
}
|
||||
|
||||
getVideosMap() {
|
||||
return this.ensureVideosMap();
|
||||
}
|
||||
|
||||
getVideoSubscription() {
|
||||
return getStoredVideoSubscription();
|
||||
}
|
||||
|
||||
setVideoSubscription(subscription) {
|
||||
setStoredVideoSubscription(subscription || null);
|
||||
this.emit("subscription:changed", { subscription: subscription || null });
|
||||
}
|
||||
|
||||
clearVideoSubscription() {
|
||||
const current = getStoredVideoSubscription();
|
||||
if (current && typeof current.unsub === "function") {
|
||||
try {
|
||||
current.unsub();
|
||||
} catch (error) {
|
||||
console.warn("[nostrService] Failed to unsubscribe from video feed:", error);
|
||||
}
|
||||
}
|
||||
this.setVideoSubscription(null);
|
||||
}
|
||||
|
||||
cacheVideos(videos = []) {
|
||||
if (!Array.isArray(videos) || !videos.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const map = this.ensureVideosMap();
|
||||
for (const video of videos) {
|
||||
if (video && typeof video.id === "string" && video.id) {
|
||||
map.set(video.id, video);
|
||||
}
|
||||
}
|
||||
setStoredVideosMap(map);
|
||||
this.emit("videos:cache", { size: map.size });
|
||||
}
|
||||
|
||||
shouldIncludeVideo(video, {
|
||||
blacklistedEventIds = new Set(),
|
||||
isAuthorBlocked = () => false,
|
||||
} = {}) {
|
||||
if (!video || typeof video !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blacklistedEventIds.has(video.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof video.pubkey === "string" && video.pubkey) {
|
||||
try {
|
||||
if (isAuthorBlocked(video.pubkey)) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[nostrService] isAuthorBlocked handler threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.accessControl && typeof this.accessControl.canAccess === "function") {
|
||||
try {
|
||||
if (!this.accessControl.canAccess(video)) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[nostrService] access control check failed", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
filterVideos(videos = [], options = {}) {
|
||||
const blacklist = ensureSet(options.blacklistedEventIds);
|
||||
const isAuthorBlocked =
|
||||
typeof options.isAuthorBlocked === "function"
|
||||
? options.isAuthorBlocked
|
||||
: () => false;
|
||||
|
||||
return videos.filter((video) =>
|
||||
this.shouldIncludeVideo(video, {
|
||||
blacklistedEventIds: blacklist,
|
||||
isAuthorBlocked,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async ensureAccessControlReady() {
|
||||
if (!this.accessControl || typeof this.accessControl.ensureReady !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.accessControl.ensureReady();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[nostrService] Failed to ensure access control lists are ready:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getFilteredActiveVideos(options = {}) {
|
||||
const all = this.nostrClient.getActiveVideos();
|
||||
return this.filterVideos(all, options);
|
||||
}
|
||||
|
||||
async loadVideos({
|
||||
forceFetch = false,
|
||||
blacklistedEventIds,
|
||||
isAuthorBlocked,
|
||||
onVideos,
|
||||
} = {}) {
|
||||
await this.ensureAccessControlReady();
|
||||
|
||||
if (forceFetch) {
|
||||
this.clearVideoSubscription();
|
||||
}
|
||||
|
||||
const applyAndNotify = (videos, reason) => {
|
||||
const filtered = this.filterVideos(videos, {
|
||||
blacklistedEventIds,
|
||||
isAuthorBlocked,
|
||||
});
|
||||
this.cacheVideos(filtered);
|
||||
if (typeof onVideos === "function") {
|
||||
try {
|
||||
onVideos(filtered, { reason });
|
||||
} catch (error) {
|
||||
console.warn("[nostrService] onVideos handler threw", error);
|
||||
}
|
||||
}
|
||||
this.emit("videos:updated", { videos: filtered, reason });
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const cached = this.nostrClient.getActiveVideos();
|
||||
const initial = applyAndNotify(cached, "cache");
|
||||
|
||||
if (!getStoredVideoSubscription()) {
|
||||
const subscription = this.nostrClient.subscribeVideos(() => {
|
||||
const updated = this.nostrClient.getActiveVideos();
|
||||
applyAndNotify(updated, "subscription");
|
||||
});
|
||||
this.setVideoSubscription(subscription);
|
||||
this.emit("subscription:started", { subscription });
|
||||
}
|
||||
|
||||
return initial;
|
||||
}
|
||||
|
||||
async fetchVideos(options = {}) {
|
||||
try {
|
||||
const videos = await this.nostrClient.fetchVideos();
|
||||
const filtered = this.filterVideos(videos, options);
|
||||
this.cacheVideos(filtered);
|
||||
this.emit("videos:fetched", { videos: filtered });
|
||||
return filtered;
|
||||
} catch (error) {
|
||||
console.error("[nostrService] Failed to fetch videos:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async loadOlderVideos(lastTimestamp, {
|
||||
blacklistedEventIds,
|
||||
isAuthorBlocked,
|
||||
limit = 150,
|
||||
} = {}) {
|
||||
const until = normalizeUntil(lastTimestamp) - 1;
|
||||
if (until <= 0 || !this.nostrClient?.pool || !Array.isArray(this.nostrClient?.relays)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filter = {
|
||||
kinds: [VIDEO_KIND],
|
||||
"#t": ["video"],
|
||||
until,
|
||||
limit,
|
||||
};
|
||||
|
||||
const collected = new Map();
|
||||
|
||||
try {
|
||||
const events = await this.nostrClient.pool.list(this.nostrClient.relays, [filter]);
|
||||
for (const event of Array.isArray(events) ? events : []) {
|
||||
try {
|
||||
const video = convertEventToVideo(event);
|
||||
if (video.invalid) {
|
||||
continue;
|
||||
}
|
||||
if (collected.has(video.id)) {
|
||||
continue;
|
||||
}
|
||||
collected.set(video.id, video);
|
||||
this.nostrClient.allEvents.set(video.id, video);
|
||||
} catch (error) {
|
||||
console.warn("[nostrService] Failed to convert older event", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[nostrService] Failed to load older videos:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
const videos = Array.from(collected.values());
|
||||
const filtered = this.filterVideos(videos, {
|
||||
blacklistedEventIds,
|
||||
isAuthorBlocked,
|
||||
});
|
||||
|
||||
this.cacheVideos(filtered);
|
||||
this.emit("videos:older", { videos: filtered, until });
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async publishVideoNote(formData, pubkey) {
|
||||
const result = await this.nostrClient.publishVideo(formData, pubkey);
|
||||
this.emit("videos:published", { formData, pubkey, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
async handleEditVideoSubmit({ originalEvent, updatedData, pubkey }) {
|
||||
const result = await this.nostrClient.editVideo(originalEvent, updatedData, pubkey);
|
||||
this.emit("videos:edited", { originalEvent, updatedData, pubkey, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
async handleFullDeleteVideo({ videoRootId, pubkey, confirm = true } = {}) {
|
||||
const result = await this.nostrClient.deleteAllVersions(videoRootId, pubkey, {
|
||||
confirm,
|
||||
});
|
||||
if (result) {
|
||||
this.emit("videos:deleted", { videoRootId, pubkey });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getOldEventById(eventId) {
|
||||
const map = this.ensureVideosMap();
|
||||
if (map.has(eventId)) {
|
||||
return map.get(eventId);
|
||||
}
|
||||
|
||||
const cached = this.nostrClient.allEvents.get(eventId);
|
||||
if (cached && !cached.deleted) {
|
||||
map.set(eventId, cached);
|
||||
setStoredVideosMap(map);
|
||||
return cached;
|
||||
}
|
||||
|
||||
const fetched = await this.nostrClient.getEventById(eventId);
|
||||
if (fetched && !fetched.deleted) {
|
||||
map.set(eventId, fetched);
|
||||
setStoredVideosMap(map);
|
||||
return fetched;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const nostrService = new NostrService();
|
||||
|
||||
export default nostrService;
|
||||
@@ -3,6 +3,8 @@ const state = {
|
||||
currentUserNpub: null,
|
||||
currentVideo: null,
|
||||
modals: Object.create(null),
|
||||
videosMap: null,
|
||||
videoSubscription: null,
|
||||
};
|
||||
|
||||
const globalSubscribers = new Set();
|
||||
@@ -15,6 +17,8 @@ function createSnapshot() {
|
||||
currentUserNpub: state.currentUserNpub,
|
||||
currentVideo: state.currentVideo,
|
||||
modals: { ...state.modals },
|
||||
videosMap: state.videosMap,
|
||||
videoSubscription: state.videoSubscription,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +122,25 @@ export function setCurrentVideo(value) {
|
||||
return updateKey("currentVideo", value ?? null);
|
||||
}
|
||||
|
||||
export function getVideosMap() {
|
||||
return state.videosMap;
|
||||
}
|
||||
|
||||
export function setVideosMap(value) {
|
||||
if (value instanceof Map) {
|
||||
return updateKey("videosMap", value);
|
||||
}
|
||||
return updateKey("videosMap", value ?? null);
|
||||
}
|
||||
|
||||
export function getVideoSubscription() {
|
||||
return state.videoSubscription;
|
||||
}
|
||||
|
||||
export function setVideoSubscription(value) {
|
||||
return updateKey("videoSubscription", value ?? null);
|
||||
}
|
||||
|
||||
export function getModalState(name) {
|
||||
if (typeof name !== "string" || !name) {
|
||||
return false;
|
||||
@@ -183,8 +206,16 @@ export function resetAppState() {
|
||||
state.currentUserNpub = null;
|
||||
state.currentVideo = null;
|
||||
state.modals = Object.create(null);
|
||||
state.videosMap = null;
|
||||
state.videoSubscription = null;
|
||||
notifyKey("pubkey", state.pubkey, previous.pubkey);
|
||||
notifyKey("currentUserNpub", state.currentUserNpub, previous.currentUserNpub);
|
||||
notifyKey("currentVideo", state.currentVideo, previous.currentVideo);
|
||||
notifyKey("modals", { ...state.modals }, previous.modals);
|
||||
notifyKey("videosMap", state.videosMap, previous.videosMap);
|
||||
notifyKey(
|
||||
"videoSubscription",
|
||||
state.videoSubscription,
|
||||
previous.videoSubscription
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user