mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Respect NSFW flag while letting authors review their uploads
This commit is contained in:
1 parent
e15aff51a4
commit
85258e27ea
10 files changed
+174
-21
No files matched your search
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user