Merge pull request #398 from PR0M3TH3AN/codex/add-nsfw-and-kids-checkboxes-to-upload-modal

Add NSFW and kid-friendly toggles to upload flows
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-09 22:13:10 -04:00
commit f33f56c923
4 files changed
+218 -16

No files matched your search

+70
View File
@@ -711,6 +711,7 @@
id="uploadEnableComments"
class="mt-1 h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
checked
data-default-checked="true"
/>
<div>
<label for="uploadEnableComments" class="text-sm font-medium text-gray-200"
@@ -721,6 +722,40 @@
</p>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2">
<div class="flex items-start gap-3 rounded-md border border-gray-800 bg-black/30 p-3">
<input
type="checkbox"
id="uploadIsNsfw"
class="mt-1 h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
data-default-checked="false"
/>
<div>
<label for="uploadIsNsfw" class="text-sm font-medium text-gray-200"
>Is this content NSFW?</label
>
<p class="mt-1 text-xs text-gray-400">
Flag mature or explicit uploads so clients can warn viewers before playback.
</p>
</div>
</div>
<div class="flex items-start gap-3 rounded-md border border-gray-800 bg-black/30 p-3">
<input
type="checkbox"
id="uploadIsForKids"
class="mt-1 h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
data-default-checked="false"
/>
<div>
<label for="uploadIsForKids" class="text-sm font-medium text-gray-200"
>Is this content created for Kids?</label
>
<p class="mt-1 text-xs text-gray-400">
Mark clearly kid-friendly videos so parental controls and playlists stay accurate.
</p>
</div>
</div>
</div>
<!--
<div class="flex items-center space-x-2">
<input
@@ -1385,6 +1420,7 @@
id="cloudflareEnableComments"
class="mt-1 h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
checked
data-default-checked="true"
/>
<div>
<label for="cloudflareEnableComments" class="text-sm font-medium text-gray-200"
@@ -1395,6 +1431,40 @@
</p>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2">
<div class="flex items-start gap-3 rounded-md border border-gray-800 bg-black/30 p-3">
<input
type="checkbox"
id="cloudflareIsNsfw"
class="mt-1 h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
data-default-checked="false"
/>
<div>
<label for="cloudflareIsNsfw" class="text-sm font-medium text-gray-200"
>Is this content NSFW?</label
>
<p class="mt-1 text-xs text-gray-400">
Turn this on to signal mature or explicit material to downstream clients.
</p>
</div>
</div>
<div class="flex items-start gap-3 rounded-md border border-gray-800 bg-black/30 p-3">
<input
type="checkbox"
id="cloudflareIsForKids"
class="mt-1 h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
data-default-checked="false"
/>
<div>
<label for="cloudflareIsForKids" class="text-sm font-medium text-gray-200"
>Is this content created for Kids?</label
>
<p class="mt-1 text-xs text-gray-400">
Highlight family-safe uploads so moderation tooling and filters stay accurate.
</p>
</div>
</div>
</div>
<div>
<label for="cloudflareFile" class="block text-sm font-medium text-gray-200"
>Video or HLS file</label
+20 -6
View File
@@ -2163,6 +2163,16 @@ class Application {
.filter((entry) => Boolean(entry));
};
const normalizeBooleanFlag = (value, defaultValue = false) => {
if (value === true) {
return true;
}
if (value === false) {
return false;
}
return Boolean(defaultValue);
};
const normalizeImetaVariant = (variant) => {
if (!variant || typeof variant !== "object") {
return null;
@@ -2377,12 +2387,14 @@ class Application {
const description = normalizeString(legacyPayload?.description || "");
const ws = normalizeString(legacyPayload?.ws || "");
const xs = normalizeString(legacyPayload?.xs || "");
const enableComments =
legacyPayload?.enableComments === false
? false
: legacyPayload?.enableComments === true
? true
: true;
const enableComments = normalizeBooleanFlag(
legacyPayload?.enableComments,
true
);
const rawIsNsfw = normalizeBooleanFlag(legacyPayload?.isNsfw, false);
const rawIsForKids = normalizeBooleanFlag(legacyPayload?.isForKids, false);
const isNsfw = rawIsNsfw;
const isForKids = rawIsNsfw ? false : rawIsForKids;
const legacyFormData = {
version: 3,
@@ -2393,6 +2405,8 @@ class Application {
description,
mode: isDevMode ? "dev" : "live",
enableComments,
isNsfw,
isForKids,
};
const normalizedNip71 = normalizeNip71Metadata(rawNip71);
+7
View File
@@ -673,6 +673,11 @@ class R2Service {
const ws = String(metadata.ws || "").trim();
const xs = String(metadata.xs || "").trim();
const enableComments = metadata.enableComments !== false;
const isNsfw = metadata.isNsfw === true;
const isForKids = metadata.isForKids === true && !isNsfw;
metadata.isNsfw = isNsfw;
metadata.isForKids = isForKids;
const accountId = (this.cloudflareSettings?.accountId || "").trim();
const accessKeyId = (this.cloudflareSettings?.accessKeyId || "").trim();
@@ -769,6 +774,8 @@ class R2Service {
ws,
xs,
enableComments,
isNsfw,
isForKids,
};
const mergedNip71 = this.buildNip71MetadataForUpload(metadata?.nip71, {
+121 -10
View File
@@ -62,6 +62,8 @@ export class UploadModal {
this.cloudflareWsInput = null;
this.cloudflareXsInput = null;
this.cloudflareEnableCommentsInput = null;
this.cloudflareIsNsfwInput = null;
this.cloudflareIsForKidsInput = null;
this.cloudflareAdvancedToggle = null;
this.cloudflareAdvancedToggleLabel = null;
this.cloudflareAdvancedToggleIcon = null;
@@ -74,6 +76,7 @@ export class UploadModal {
this.r2BaseDomainInput = null;
this.nip71FormManager = new Nip71FormManager();
this.cleanupHandlers = [];
}
addEventListener(type, listener, options) {
@@ -167,6 +170,8 @@ export class UploadModal {
thumbnail: context.querySelector("#uploadThumbnail") || null,
description: context.querySelector("#uploadDescription") || null,
enableComments: context.querySelector("#uploadEnableComments") || null,
isNsfw: context.querySelector("#uploadIsNsfw") || null,
isForKids: context.querySelector("#uploadIsForKids") || null,
isPrivate: context.querySelector("#uploadIsPrivate") || null,
};
@@ -196,6 +201,9 @@ export class UploadModal {
this.cloudflareXsInput = context.querySelector("#cloudflareXs") || null;
this.cloudflareEnableCommentsInput =
context.querySelector("#cloudflareEnableComments") || null;
this.cloudflareIsNsfwInput = context.querySelector("#cloudflareIsNsfw") || null;
this.cloudflareIsForKidsInput =
context.querySelector("#cloudflareIsForKids") || null;
this.cloudflareAdvancedToggle =
context.querySelector("#cloudflareAdvancedToggle") || null;
this.cloudflareAdvancedToggleLabel =
@@ -215,6 +223,59 @@ export class UploadModal {
this.nip71FormManager.registerSection("cloudflare", this.cloudflareSection);
}
readCheckboxValue(input, defaultValue = false) {
if (!input) {
return Boolean(defaultValue);
}
return input.checked === true;
}
resetCheckbox(input, defaultValue = false) {
if (!input) {
return;
}
let fallback = Boolean(defaultValue);
if (input.dataset && typeof input.dataset.defaultChecked === "string") {
fallback = input.dataset.defaultChecked === "true";
} else if (typeof input.defaultChecked === "boolean") {
fallback = input.defaultChecked;
}
input.checked = fallback;
}
sanitizeAudienceFlags(flags = {}) {
const isNsfw = flags?.isNsfw === true;
const isForKids = flags?.isForKids === true && !isNsfw;
return { isNsfw, isForKids };
}
setupMutuallyExclusiveCheckboxes(firstInput, secondInput) {
if (!firstInput || !secondInput) {
return;
}
const enforceExclusion = (primary, secondary) => {
if (primary.checked) {
secondary.checked = false;
}
};
const handleFirstChange = () => enforceExclusion(firstInput, secondInput);
const handleSecondChange = () => enforceExclusion(secondInput, firstInput);
if (firstInput.checked && secondInput.checked) {
secondInput.checked = false;
}
firstInput.addEventListener("change", handleFirstChange);
secondInput.addEventListener("change", handleSecondChange);
this.cleanupHandlers.push(() => {
firstInput.removeEventListener("change", handleFirstChange);
secondInput.removeEventListener("change", handleSecondChange);
});
}
buildAutoGeneratedImetaVariant(file) {
if (!file) {
return null;
@@ -291,6 +352,20 @@ export class UploadModal {
});
}
if (this.customFormInputs?.isNsfw && this.customFormInputs?.isForKids) {
this.setupMutuallyExclusiveCheckboxes(
this.customFormInputs.isNsfw,
this.customFormInputs.isForKids
);
}
if (this.cloudflareIsNsfwInput && this.cloudflareIsForKidsInput) {
this.setupMutuallyExclusiveCheckboxes(
this.cloudflareIsNsfwInput,
this.cloudflareIsForKidsInput
);
}
this.nip71FormManager.bindSection("custom");
this.nip71FormManager.bindSection("cloudflare");
}
@@ -436,6 +511,10 @@ export class UploadModal {
}
handleCustomSubmit() {
const audienceFlags = this.sanitizeAudienceFlags({
isNsfw: this.readCheckboxValue(this.customFormInputs.isNsfw, false),
isForKids: this.readCheckboxValue(this.customFormInputs.isForKids, false),
});
const payload = {
title: this.customFormInputs.title?.value?.trim() || "",
url: this.customFormInputs.url?.value?.trim() || "",
@@ -444,13 +523,18 @@ export class UploadModal {
xs: this.customFormInputs.xs?.value?.trim() || "",
thumbnail: this.customFormInputs.thumbnail?.value?.trim() || "",
description: this.customFormInputs.description?.value?.trim() || "",
enableComments: this.customFormInputs.enableComments
? !!this.customFormInputs.enableComments.checked
: true,
enableComments: this.readCheckboxValue(
this.customFormInputs.enableComments,
true
),
...audienceFlags,
};
if (this.customFormInputs.isPrivate) {
payload.isPrivate = !!this.customFormInputs.isPrivate.checked;
payload.isPrivate = this.readCheckboxValue(
this.customFormInputs.isPrivate,
false
);
}
const nip71Metadata = this.nip71FormManager.collectSection("custom");
@@ -631,7 +715,11 @@ export class UploadModal {
if (this.cloudflareWsInput) this.cloudflareWsInput.value = "";
if (this.cloudflareXsInput) this.cloudflareXsInput.value = "";
if (this.cloudflareEnableCommentsInput)
this.cloudflareEnableCommentsInput.checked = true;
this.resetCheckbox(this.cloudflareEnableCommentsInput, true);
if (this.cloudflareIsNsfwInput)
this.resetCheckbox(this.cloudflareIsNsfwInput, false);
if (this.cloudflareIsForKidsInput)
this.resetCheckbox(this.cloudflareIsForKidsInput, false);
if (this.cloudflareFileInput) this.cloudflareFileInput.value = "";
this.nip71FormManager.resetSection("cloudflare");
this.updateCloudflareProgress(Number.NaN);
@@ -647,9 +735,13 @@ export class UploadModal {
if (this.customFormInputs.description)
this.customFormInputs.description.value = "";
if (this.customFormInputs.enableComments)
this.customFormInputs.enableComments.checked = true;
this.resetCheckbox(this.customFormInputs.enableComments, true);
if (this.customFormInputs.isNsfw)
this.resetCheckbox(this.customFormInputs.isNsfw, false);
if (this.customFormInputs.isForKids)
this.resetCheckbox(this.customFormInputs.isForKids, false);
if (this.customFormInputs.isPrivate)
this.customFormInputs.isPrivate.checked = false;
this.resetCheckbox(this.customFormInputs.isPrivate, false);
this.nip71FormManager.resetSection("custom");
}
@@ -706,6 +798,10 @@ export class UploadModal {
const npub = this.safeEncodeNpub(pubkey) || "";
const file = this.cloudflareFileInput?.files?.[0] || null;
const audienceFlags = this.sanitizeAudienceFlags({
isNsfw: this.readCheckboxValue(this.cloudflareIsNsfwInput, false),
isForKids: this.readCheckboxValue(this.cloudflareIsForKidsInput, false),
});
const metadata = {
title: this.cloudflareTitleInput?.value?.trim() || "",
description: this.cloudflareDescriptionInput?.value?.trim() || "",
@@ -713,9 +809,11 @@ export class UploadModal {
magnet: this.cloudflareMagnetInput?.value?.trim() || "",
ws: this.cloudflareWsInput?.value?.trim() || "",
xs: this.cloudflareXsInput?.value?.trim() || "",
enableComments: this.cloudflareEnableCommentsInput
? !!this.cloudflareEnableCommentsInput.checked
: true,
enableComments: this.readCheckboxValue(
this.cloudflareEnableCommentsInput,
true
),
...audienceFlags,
};
const nip71Metadata = this.nip71FormManager.collectSection("cloudflare");
@@ -749,6 +847,19 @@ export class UploadModal {
}
destroy() {
if (Array.isArray(this.cleanupHandlers)) {
this.cleanupHandlers.forEach((cleanup) => {
try {
if (typeof cleanup === "function") {
cleanup();
}
} catch (error) {
console.warn("[UploadModal] Failed to remove handler", error);
}
});
this.cleanupHandlers = [];
}
if (Array.isArray(this.r2Unsubscribes)) {
this.r2Unsubscribes.forEach((unsubscribe) => {
try {