Respect NSFW flag while letting authors review their uploads

This commit is contained in:
thePR0M3TH3AN committed 2025-10-09 22:58:07 -04:00
1 parent e15aff51a4
commit 85258e27ea
10 files changed
+174 -21

No files matched your search

+9
View File
@@ -106,6 +106,15 @@ export const ADMIN_WHITELIST_MODE_STORAGE_KEY = "bitvid_admin_whitelist_mode";
*/
export const DEFAULT_WHITELIST_MODE_ENABLED = true;
/**
* Whether the public client should display content flagged as NSFW.
*
* When set to `false`, BitVid filters out any video notes marked with the
* `isNsfw` flag so they never appear in feeds or video lists. Toggle to `true`
* only if your deployment allows NSFW media to surface publicly.
*/
export const ALLOW_NSFW_CONTENT = false;
/**
* Nostr kind used when persisting watch history events.
*
+19
View File
@@ -171,6 +171,25 @@ header img {
var(--shadow-lg);
}
.video-card.video-card--nsfw-owner {
background-color: #000;
box-shadow:
0 0 0 0.0625rem rgba(239, 68, 68, 0.4),
var(--shadow-md);
}
.video-card.video-card--nsfw-owner:hover {
box-shadow:
0 0 0 0.0625rem rgba(239, 68, 68, 0.55),
var(--shadow-lg);
}
.video-card img.video-card__thumbnail--blurred {
filter: blur(18px);
transform: scale(1.05);
transition: filter 200ms ease, transform 200ms ease;
}
.video-card[data-owner-is-viewer="true"][data-url-health-state="offline"][data-stream-health-state="unhealthy"] {
box-shadow:
0 0 0 0.0625rem rgba(239, 68, 68, 0.35),
+14
View File
@@ -134,6 +134,20 @@
@apply bg-card-private;
}
.video-card.video-card--nsfw-owner {
@apply border border-critical border-opacity-40 bg-black;
}
.video-card.video-card--nsfw-owner:hover {
@apply border-opacity-60 shadow-lg;
}
.video-card img.video-card__thumbnail--blurred {
@apply transition duration-base ease-in-out;
filter: blur(18px);
transform: scale(1.05);
}
.video-card[data-owner-is-viewer="true"][data-url-health-state="offline"][data-stream-health-state="unhealthy"] {
@apply border border-critical border-opacity-40 bg-card-critical;
}
+2
View File
@@ -8,6 +8,7 @@ import {
ADMIN_DM_IMAGE_URL,
BITVID_WEBSITE_URL,
MAX_WALLET_DEFAULT_ZAP,
ALLOW_NSFW_CONTENT,
} from "./config.js";
import { accessControl } from "./accessControl.js";
import { safeDecodeMagnet } from "./magnetUtils.js";
@@ -849,6 +850,7 @@ class Application {
renderers: {
getLoadingMarkup: (message) => getSidebarLoadingMarkup(message),
},
allowNsfw: ALLOW_NSFW_CONTENT === true,
};
this.videoListView =
(typeof ui.videoListView === "function"
+20 -2
View File
@@ -12,6 +12,7 @@ import { accessControl } from "./accessControl.js";
import { getApplication } from "./applicationContext.js";
import { escapeHTML } from "./utils/domUtils.js";
import { VideoCard } from "./ui/components/VideoCard.js";
import { ALLOW_NSFW_CONTENT } from "./config.js";
import {
calculateZapShares,
describeShareType,
@@ -1797,6 +1798,8 @@ function renderChannelVideosFromList({
}
};
const allowNsfw = ALLOW_NSFW_CONTENT === true;
videos.forEach((video) => {
if (!video || !video.id || !video.title) {
return;
@@ -1812,6 +1815,10 @@ function renderChannelVideosFromList({
return;
}
if (!allowNsfw && video?.isNsfw === true && !canEdit) {
return;
}
app?.videosMap?.set(video.id, video);
const index = renderIndex++;
@@ -1837,8 +1844,14 @@ function renderChannelVideosFromList({
? app.formatTimeAgo(video.created_at)
: new Date(video.created_at * 1000).toLocaleString();
const highlightClass =
canEdit && video.isPrivate ? "video-card--owner-private" : "";
const highlightClasses = [];
if (canEdit && video.isPrivate) {
highlightClasses.push("video-card--owner-private");
}
if (!allowNsfw && video?.isNsfw === true && canEdit) {
highlightClasses.push("video-card--nsfw-owner");
}
const highlightClass = highlightClasses.join(" ");
const videoCard = new VideoCard({
document,
@@ -1857,6 +1870,11 @@ function renderChannelVideosFromList({
? app.canCurrentUserManageBlacklist()
: false,
},
nsfwContext: {
isNsfw: video?.isNsfw === true,
allowNsfw,
viewerIsOwner: canEdit,
},
helpers: {
escapeHtml: (value) => escapeHTML(value),
isMagnetSupported: (magnet) => app?.isMagnetUriSupported?.(magnet),
+2
View File
@@ -10,6 +10,7 @@ import {
DEFAULT_RELAY_URLS_OVERRIDE,
ADMIN_WHITELIST_MODE_STORAGE_KEY,
DEFAULT_WHITELIST_MODE_ENABLED,
ALLOW_NSFW_CONTENT,
WATCH_HISTORY_KIND,
WATCH_HISTORY_LIST_IDENTIFIER,
WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS,
@@ -41,6 +42,7 @@ export { PLATFORM_LUD16_OVERRIDE };
export { DEFAULT_RELAY_URLS_OVERRIDE };
export const ADMIN_EDITORS_NPUBS = []; // Default moderators (optional)
export const ADMIN_LIST_NAMESPACE = "bitvid:admin"; // Reserved for Nostr lists
export { ALLOW_NSFW_CONTENT };
const WHITELIST_MODE_KEY = ADMIN_WHITELIST_MODE_STORAGE_KEY;
const DEFAULT_WHITELIST_ENABLED = DEFAULT_WHITELIST_MODE_ENABLED;
+41 -14
View File
@@ -3,6 +3,7 @@ import {
convertEventToVideo,
} from "../nostr.js";
import { accessControl } from "../accessControl.js";
import { ALLOW_NSFW_CONTENT } from "../config.js";
import {
getVideosMap as getStoredVideosMap,
setVideosMap as setStoredVideosMap,
@@ -192,6 +193,12 @@ export class NostrService {
return false;
}
const viewerIsAuthor = this.isViewerVideoAuthor(video);
if (ALLOW_NSFW_CONTENT !== true && video.isNsfw === true && !viewerIsAuthor) {
return false;
}
if (blacklistedEventIds.has(video.id)) {
return false;
}
@@ -206,19 +213,8 @@ export class NostrService {
}
}
if (video.isPrivate === true) {
const normalizedVideoPubkey = normalizeHexPubkey(video.pubkey);
const normalizedViewerPubkey = normalizeHexPubkey(
this.nostrClient?.pubkey
);
if (
!normalizedViewerPubkey ||
!normalizedVideoPubkey ||
normalizedVideoPubkey !== normalizedViewerPubkey
) {
return false;
}
if (video.isPrivate === true && !viewerIsAuthor) {
return false;
}
if (this.accessControl && typeof this.accessControl.canAccess === "function") {
@@ -482,12 +478,26 @@ export class NostrService {
async getOldEventById(eventId) {
const map = this.ensureVideosMap();
const isBlockedNsfw = (video) =>
ALLOW_NSFW_CONTENT !== true &&
video?.isNsfw === true &&
!this.isViewerVideoAuthor(video);
if (map.has(eventId)) {
return map.get(eventId);
const existing = map.get(eventId);
if (isBlockedNsfw(existing)) {
map.delete(eventId);
setStoredVideosMap(map);
return null;
}
return existing;
}
const cached = this.nostrClient.allEvents.get(eventId);
if (cached && !cached.deleted) {
if (isBlockedNsfw(cached)) {
return null;
}
map.set(eventId, cached);
setStoredVideosMap(map);
return cached;
@@ -495,6 +505,9 @@ export class NostrService {
const fetched = await this.nostrClient.getEventById(eventId);
if (fetched && !fetched.deleted) {
if (isBlockedNsfw(fetched)) {
return null;
}
map.set(eventId, fetched);
setStoredVideosMap(map);
return fetched;
@@ -502,6 +515,20 @@ export class NostrService {
return null;
}
isViewerVideoAuthor(video) {
if (!video || typeof video !== "object") {
return false;
}
const viewerPubkey = normalizeHexPubkey(this.nostrClient?.pubkey);
if (!viewerPubkey) {
return false;
}
const videoPubkey = normalizeHexPubkey(video.pubkey);
return !!videoPubkey && videoPubkey === viewerPubkey;
}
}
const nostrService = new NostrService();
+2
View File
@@ -14,6 +14,7 @@ import {
} from "./nostrPublish.js";
import { getApplication } from "./applicationContext.js";
import { VideoListView } from "./ui/views/VideoListView.js";
import { ALLOW_NSFW_CONTENT } from "./config.js";
const getApp = () => getApplication();
@@ -590,6 +591,7 @@ class SubscriptionsManager {
renderers: {
getLoadingMarkup: (message) => getSidebarLoadingMarkup(message),
},
allowNsfw: ALLOW_NSFW_CONTENT === true,
};
const listView = new VideoListView(listViewConfig);
+39
View File
@@ -16,6 +16,7 @@ export class VideoCard {
state = {},
ensureGlobalMoreMenuHandlers,
onRequestCloseAllMenus,
nsfwContext = null,
} = {}) {
if (!doc) {
throw new Error("VideoCard requires a document reference.");
@@ -72,6 +73,16 @@ export class VideoCard {
? onRequestCloseAllMenus
: null;
this.nsfwContext = {
isNsfw: Boolean(nsfwContext?.isNsfw),
allowNsfw: nsfwContext?.allowNsfw !== false,
viewerIsOwner: nsfwContext?.viewerIsOwner === true,
};
this.shouldMaskNsfwForOwner =
this.nsfwContext.isNsfw &&
!this.nsfwContext.allowNsfw &&
this.nsfwContext.viewerIsOwner;
this.callbacks = {
onPlay: null,
onEdit: null,
@@ -220,6 +231,7 @@ export class VideoCard {
root.dataset.videoId = this.video.id;
}
this.applyNsfwContext();
this.applyPointerDataset();
this.applyOwnerDataset();
this.applySourceDatasets();
@@ -340,6 +352,10 @@ export class VideoCard {
this.thumbnailEl = img;
if (this.shouldMaskNsfwForOwner) {
img.classList.add("video-card__thumbnail--blurred");
}
const markThumbnailAsLoaded = () => {
if (!thumbnailUrl) {
return;
@@ -1257,6 +1273,29 @@ export class VideoCard {
return container;
}
applyNsfwContext() {
if (!this.root) {
return;
}
if (!this.nsfwContext?.isNsfw) {
if (this.root.dataset.nsfwVisibility) {
delete this.root.dataset.nsfwVisibility;
}
return;
}
if (this.shouldMaskNsfwForOwner) {
this.root.dataset.nsfwVisibility = "owner-only";
this.root.classList.add("video-card--nsfw-owner");
return;
}
this.root.dataset.nsfwVisibility = this.nsfwContext.allowNsfw
? "allowed"
: "hidden";
}
applyPointerDataset() {
if (!this.root) {
return;
+26 -5
View File
@@ -19,6 +19,7 @@ export class VideoListView {
state = {},
utils = {},
renderers = {},
allowNsfw = true,
} = options;
this.document = doc;
@@ -163,6 +164,8 @@ export class VideoListView {
blacklist: null,
};
this.allowNsfw = allowNsfw !== false;
this.emitter = typeof EventTarget !== "undefined" ? new EventTarget() : null;
this._boundClickHandler = this.handleContainerClick.bind(this);
@@ -270,9 +273,14 @@ export class VideoListView {
const source = Array.isArray(videos) ? videos : [];
const dedupedVideos = this.utils.dedupeVideos(source);
const displayVideos = dedupedVideos.filter(
(video) => this.utils.canEditVideo(video) || !video?.isPrivate
);
const displayVideos = dedupedVideos.filter((video) => {
const canEdit = this.utils.canEditVideo(video);
const isPrivate = video?.isPrivate === true;
if (!this.allowNsfw && video?.isNsfw === true && !canEdit) {
return false;
}
return canEdit || !isPrivate;
});
this.syncViewCountSubscriptions(displayVideos);
this.cleanupThumbnailCache(displayVideos);
@@ -340,8 +348,16 @@ export class VideoListView {
const shareUrl = this.buildShareUrl(video, shareBase);
const canEdit = this.utils.canEditVideo(video);
const canDelete = this.utils.canDeleteVideo(video);
const highlightClass =
canEdit && video.isPrivate ? "video-card--owner-private" : "";
const highlightClasses = [];
if (canEdit && video.isPrivate) {
highlightClasses.push("video-card--owner-private");
}
const viewerSeesBlockedNsfw =
!this.allowNsfw && video?.isNsfw === true && canEdit;
if (viewerSeesBlockedNsfw) {
highlightClasses.push("video-card--nsfw-owner");
}
const highlightClass = highlightClasses.join(" ");
const isNewlyRendered = !previouslyRenderedIds.has(video.id);
const animationClass = isNewlyRendered ? "video-card--enter" : "";
const knownPostedAt = this.utils.getKnownVideoPostedAt(video);
@@ -379,6 +395,11 @@ export class VideoListView {
postedAt: normalizedPostedAt,
highlightClass,
animationClass,
nsfwContext: {
isNsfw: video?.isNsfw === true,
allowNsfw: this.allowNsfw,
viewerIsOwner: canEdit,
},
capabilities: {
canEdit,
canDelete,