mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 23:18:43 +00:00
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"liveServer.settings.root": "./src"
|
||||||
|
}
|
41
site.webmanifest
Normal file
41
site.webmanifest
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "bitvid - Decentralized Video Sharing",
|
||||||
|
"short_name": "bitvid",
|
||||||
|
"description": "seed. zap. subscribe.",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "src/assets/png/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "src/assets/png/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "src/assets/png/apple-touch-icon.png",
|
||||||
|
"sizes": "180x180",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "src/assets/png/favicon-32x32.png",
|
||||||
|
"sizes": "32x32",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "src/assets/png/favicon-16x16.png",
|
||||||
|
"sizes": "16x16",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "src/index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0f172a",
|
||||||
|
"theme_color": "#0f172a",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"scope": "/",
|
||||||
|
"categories": ["video", "entertainment", "decentralized", "streaming"],
|
||||||
|
"related_applications": [],
|
||||||
|
"lang": "en"
|
||||||
|
}
|
@@ -67,7 +67,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://beta.bitvid.network/components/iframe_forms/iframe-application-form.html"
|
src="./components/iframe_forms/iframe-application-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -56,7 +56,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://beta.bitvid.network/components/iframe_forms/iframe-bug-fix-form.html"
|
src="./components/iframe_forms/iframe-bug-fix-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://beta.bitvid.network/components/iframe_forms/iframe-content-appeals-form.html"
|
src="./components/iframe_forms/iframe-content-appeals-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://beta.bitvid.network/components/iframe_forms/iframe-request-form.html"
|
src="./components/iframe_forms/iframe-request-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -57,7 +57,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="w-full" style="height: 80vh">
|
<div class="w-full" style="height: 80vh">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://beta.bitvid.network/components/iframe_forms/iframe-feedback-form.html"
|
src="./components/iframe_forms/iframe-feedback-form.html"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>bitvid Whitelist Application Form</title>
|
<title>bitvid Whitelist Application Form</title>
|
||||||
<!-- Link to your main stylesheet -->
|
<!-- Link to your main stylesheet -->
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="../../css/style.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Override for form page to match modal field styling */
|
/* Override for form page to match modal field styling */
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #E5E7EB; /* Tailwind's text-gray-200 */
|
color: #e5e7eb; /* Tailwind's text-gray-200 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input, textarea, select mimic modal field styles */
|
/* Input, textarea, select mimic modal field styles */
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
||||||
background-color: #1F2937; /* Tailwind's bg-gray-800 */
|
background-color: #1f2937; /* Tailwind's bg-gray-800 */
|
||||||
color: #F3F4F6; /* Tailwind's text-gray-100 */
|
color: #f3f4f6; /* Tailwind's text-gray-100 */
|
||||||
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.375rem; /* rounded-md */
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: #3B82F6; /* blue-500 */
|
border-color: #3b82f6; /* blue-500 */
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px #3B82F6;
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for checkboxes – display inline with a label */
|
/* Style for checkboxes – display inline with a label */
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
/* Button styled similarly to modal publish button */
|
/* Button styled similarly to modal publish button */
|
||||||
button {
|
button {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
background: #3B82F6; /* blue-500 */
|
background: #3b82f6; /* blue-500 */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
@@ -97,13 +97,13 @@
|
|||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: #F87171; /* a red tint */
|
color: #f87171; /* a red tint */
|
||||||
}
|
}
|
||||||
.success {
|
.success {
|
||||||
color: #3B82F6; /* blue-500 */
|
color: #3b82f6; /* blue-500 */
|
||||||
}
|
}
|
||||||
.warn {
|
.warn {
|
||||||
color: #FACC15; /* a yellow tone */
|
color: #facc15; /* a yellow tone */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar styling for WebKit browsers */
|
/* Custom Scrollbar styling for WebKit browsers */
|
||||||
@@ -115,13 +115,13 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #3B82F6;
|
background-color: #3b82f6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
/* Custom Scrollbar styling for Firefox */
|
/* Custom Scrollbar styling for Firefox */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #3B82F6 transparent;
|
scrollbar-color: #3b82f6 transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Load nostr‑tools v2.10.4 -->
|
<!-- Load nostr‑tools v2.10.4 -->
|
||||||
@@ -131,72 +131,163 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<p>
|
<p>
|
||||||
bitvid is currently in early access. If you would like to request access, please fill out this application. Applications will be reviewed manually, and approvals will be based on alignment with bitvid’s community values.
|
bitvid is currently in early access. If you would like to request
|
||||||
|
access, please fill out this application. Applications will be
|
||||||
|
reviewed manually, and approvals will be based on alignment with
|
||||||
|
bitvid’s community values.
|
||||||
</p>
|
</p>
|
||||||
<form id="wl-form">
|
<form id="wl-form">
|
||||||
<!-- 1. Applicant Information -->
|
<!-- 1. Applicant Information -->
|
||||||
<h2>1. Applicant Information</h2>
|
<h2>1. Applicant Information</h2>
|
||||||
<label for="applicantNpub">Nostr Public Key (npub):</label>
|
<label for="applicantNpub">Nostr Public Key (npub):</label>
|
||||||
<input type="text" id="applicantNpub" placeholder="Enter your npub" required />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="applicantNpub"
|
||||||
|
placeholder="Enter your npub"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="contactMethod">Preferred Contact Method (if applicable):</label>
|
<label for="contactMethod"
|
||||||
<input type="text" id="contactMethod" placeholder="Nostr DM, email, or other" />
|
>Preferred Contact Method (if applicable):</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="contactMethod"
|
||||||
|
placeholder="Nostr DM, email, or other"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="username">Username or Alias (if applicable):</label>
|
<label for="username">Username or Alias (if applicable):</label>
|
||||||
<input type="text" id="username" placeholder="Enter your preferred name" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
placeholder="Enter your preferred name"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 2. Content Intent -->
|
<!-- 2. Content Intent -->
|
||||||
<h2>2. Content Intent</h2>
|
<h2>2. Content Intent</h2>
|
||||||
<p>What type of content do you plan to upload? (Check all that apply)</p>
|
<p>
|
||||||
|
What type of content do you plan to upload? (Check all that apply)
|
||||||
|
</p>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label><input type="checkbox" name="contentType" value="Educational" /> Educational</label>
|
<label
|
||||||
<label><input type="checkbox" name="contentType" value="Entertainment" /> Entertainment</label>
|
><input type="checkbox" name="contentType" value="Educational" />
|
||||||
<label><input type="checkbox" name="contentType" value="News & Journalism" /> News & Journalism</label>
|
Educational</label
|
||||||
<label><input type="checkbox" name="contentType" value="Creative Works" /> Creative Works</label>
|
>
|
||||||
<label><input type="checkbox" name="contentType" value="Discussions & Opinions" /> Discussions & Opinions</label>
|
<label
|
||||||
<label><input type="checkbox" name="contentType" value="Other" /> Other</label>
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="contentType"
|
||||||
|
value="Entertainment"
|
||||||
|
/>
|
||||||
|
Entertainment</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="contentType"
|
||||||
|
value="News & Journalism"
|
||||||
|
/>
|
||||||
|
News & Journalism</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="contentType"
|
||||||
|
value="Creative Works"
|
||||||
|
/>
|
||||||
|
Creative Works</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="contentType"
|
||||||
|
value="Discussions & Opinions"
|
||||||
|
/>
|
||||||
|
Discussions & Opinions</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="contentType" value="Other" />
|
||||||
|
Other</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<label for="otherContent">Other (please specify):</label>
|
<label for="otherContent">Other (please specify):</label>
|
||||||
<input type="text" id="otherContent" placeholder="Describe if Other was selected" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="otherContent"
|
||||||
|
placeholder="Describe if Other was selected"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="whyJoin">Why do you want to join bitvid?</label>
|
<label for="whyJoin">Why do you want to join bitvid?</label>
|
||||||
<textarea id="whyJoin" rows="3" placeholder="Explain your motivation"></textarea>
|
<textarea
|
||||||
|
id="whyJoin"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Explain your motivation"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<label for="priorExperience">Have you created content on other platforms before?</label>
|
<label for="priorExperience"
|
||||||
|
>Have you created content on other platforms before?</label
|
||||||
|
>
|
||||||
<select id="priorExperience">
|
<select id="priorExperience">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
<option value="No">No</option>
|
<option value="No">No</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="experienceLinks">If yes, provide links or references to your previous work:</label>
|
<label for="experienceLinks"
|
||||||
<textarea id="experienceLinks" rows="2" placeholder="Paste links or describe your work"></textarea>
|
>If yes, provide links or references to your previous work:</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="experienceLinks"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Paste links or describe your work"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<!-- 3. Community Engagement -->
|
<!-- 3. Community Engagement -->
|
||||||
<h2>3. Community Engagement</h2>
|
<h2>3. Community Engagement</h2>
|
||||||
<label for="familiarGuidelines">Are you familiar with bitvid’s Community Guidelines?</label>
|
<label for="familiarGuidelines"
|
||||||
|
>Are you familiar with bitvid’s Community Guidelines?</label
|
||||||
|
>
|
||||||
<select id="familiarGuidelines">
|
<select id="familiarGuidelines">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
<option value="No">No</option>
|
<option value="No">No</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="agreeGuidelines">Do you agree to follow the guidelines and respect decentralized moderation?</label>
|
<label for="agreeGuidelines"
|
||||||
|
>Do you agree to follow the guidelines and respect decentralized
|
||||||
|
moderation?</label
|
||||||
|
>
|
||||||
<select id="agreeGuidelines">
|
<select id="agreeGuidelines">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
<option value="No">No</option>
|
<option value="No">No</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="communityContribution">How do you plan to contribute positively to the bitvid community?</label>
|
<label for="communityContribution"
|
||||||
<textarea id="communityContribution" rows="3" placeholder="Explain your approach"></textarea>
|
>How do you plan to contribute positively to the bitvid
|
||||||
|
community?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="communityContribution"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Explain your approach"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<!-- 4. Additional Information -->
|
<!-- 4. Additional Information -->
|
||||||
<h2>4. Additional Information</h2>
|
<h2>4. Additional Information</h2>
|
||||||
<label for="specialSkills">Do you have any special skills or interests that could help improve bitvid?</label>
|
<label for="specialSkills"
|
||||||
<textarea id="specialSkills" rows="3" placeholder="e.g., software development, moderation, design, advocacy"></textarea>
|
>Do you have any special skills or interests that could help improve
|
||||||
|
bitvid?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="specialSkills"
|
||||||
|
rows="3"
|
||||||
|
placeholder="e.g., software development, moderation, design, advocacy"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<label for="testFeatures">Would you be interested in helping test new features?</label>
|
<label for="testFeatures"
|
||||||
|
>Would you be interested in helping test new features?</label
|
||||||
|
>
|
||||||
<select id="testFeatures">
|
<select id="testFeatures">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
@@ -207,9 +298,10 @@
|
|||||||
<h2>5. Declaration</h2>
|
<h2>5. Declaration</h2>
|
||||||
<p>
|
<p>
|
||||||
By submitting this application, you confirm that:
|
By submitting this application, you confirm that:
|
||||||
<br />- The information provided is accurate.
|
<br />- The information provided is accurate. <br />- You understand
|
||||||
<br />- You understand that approval is based on community alignment and available capacity.
|
that approval is based on community alignment and available
|
||||||
<br />- You acknowledge that whitelist status may be revoked if guidelines are violated.
|
capacity. <br />- You acknowledge that whitelist status may be
|
||||||
|
revoked if guidelines are violated.
|
||||||
</p>
|
</p>
|
||||||
<label for="signature">Signature (Digital or Written):</label>
|
<label for="signature">Signature (Digital or Written):</label>
|
||||||
<input type="text" id="signature" placeholder="Your signature" />
|
<input type="text" id="signature" placeholder="Your signature" />
|
||||||
@@ -242,50 +334,87 @@
|
|||||||
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
|
const {
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
finalizeEvent,
|
||||||
|
nip04,
|
||||||
|
nip19,
|
||||||
|
SimplePool,
|
||||||
|
Relay,
|
||||||
|
} = window.NostrTools;
|
||||||
|
|
||||||
// Set the recipient's NPUB (your personal NPUB) here.
|
// Set the recipient's NPUB (your personal NPUB) here.
|
||||||
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
const recipientNpub =
|
||||||
|
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
||||||
const RELAYS = [
|
const RELAYS = [
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net"
|
"wss://relay.primal.net",
|
||||||
];
|
];
|
||||||
const pool = new SimplePool();
|
const pool = new SimplePool();
|
||||||
|
|
||||||
document.getElementById("wl-form").addEventListener("submit", async (ev) => {
|
document
|
||||||
|
.getElementById("wl-form")
|
||||||
|
.addEventListener("submit", async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
clear();
|
clear();
|
||||||
try {
|
try {
|
||||||
// Retrieve applicant-provided input.
|
// Retrieve applicant-provided input.
|
||||||
const applicantNpub = document.getElementById("applicantNpub").value.trim();
|
const applicantNpub = document
|
||||||
const contactMethod = document.getElementById("contactMethod").value.trim();
|
.getElementById("applicantNpub")
|
||||||
|
.value.trim();
|
||||||
|
const contactMethod = document
|
||||||
|
.getElementById("contactMethod")
|
||||||
|
.value.trim();
|
||||||
const username = document.getElementById("username").value.trim();
|
const username = document.getElementById("username").value.trim();
|
||||||
|
|
||||||
// Section 2: Content Intent
|
// Section 2: Content Intent
|
||||||
// Get all checked content types
|
// Get all checked content types
|
||||||
const contentTypeNodes = document.querySelectorAll('input[name="contentType"]:checked');
|
const contentTypeNodes = document.querySelectorAll(
|
||||||
|
'input[name="contentType"]:checked'
|
||||||
|
);
|
||||||
let contentTypes = [];
|
let contentTypes = [];
|
||||||
contentTypeNodes.forEach((node) => {
|
contentTypeNodes.forEach((node) => {
|
||||||
contentTypes.push(node.value);
|
contentTypes.push(node.value);
|
||||||
});
|
});
|
||||||
const otherContent = document.getElementById("otherContent").value.trim();
|
const otherContent = document
|
||||||
|
.getElementById("otherContent")
|
||||||
|
.value.trim();
|
||||||
const whyJoin = document.getElementById("whyJoin").value.trim();
|
const whyJoin = document.getElementById("whyJoin").value.trim();
|
||||||
const priorExperience = document.getElementById("priorExperience").value.trim();
|
const priorExperience = document
|
||||||
const experienceLinks = document.getElementById("experienceLinks").value.trim();
|
.getElementById("priorExperience")
|
||||||
|
.value.trim();
|
||||||
|
const experienceLinks = document
|
||||||
|
.getElementById("experienceLinks")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 3: Community Engagement
|
// Section 3: Community Engagement
|
||||||
const familiarGuidelines = document.getElementById("familiarGuidelines").value.trim();
|
const familiarGuidelines = document
|
||||||
const agreeGuidelines = document.getElementById("agreeGuidelines").value.trim();
|
.getElementById("familiarGuidelines")
|
||||||
const communityContribution = document.getElementById("communityContribution").value.trim();
|
.value.trim();
|
||||||
|
const agreeGuidelines = document
|
||||||
|
.getElementById("agreeGuidelines")
|
||||||
|
.value.trim();
|
||||||
|
const communityContribution = document
|
||||||
|
.getElementById("communityContribution")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 4: Additional Information
|
// Section 4: Additional Information
|
||||||
const specialSkills = document.getElementById("specialSkills").value.trim();
|
const specialSkills = document
|
||||||
const testFeatures = document.getElementById("testFeatures").value.trim();
|
.getElementById("specialSkills")
|
||||||
|
.value.trim();
|
||||||
|
const testFeatures = document
|
||||||
|
.getElementById("testFeatures")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 5: Declaration
|
// Section 5: Declaration
|
||||||
const signature = document.getElementById("signature").value.trim();
|
const signature = document
|
||||||
const declarationDate = document.getElementById("declarationDate").value.trim();
|
.getElementById("signature")
|
||||||
|
.value.trim();
|
||||||
|
const declarationDate = document
|
||||||
|
.getElementById("declarationDate")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Construct the whitelist application content.
|
// Construct the whitelist application content.
|
||||||
const applicationContent = `
|
const applicationContent = `
|
||||||
@@ -301,13 +430,19 @@
|
|||||||
- **Other (if applicable):** ${otherContent || "N/A"}
|
- **Other (if applicable):** ${otherContent || "N/A"}
|
||||||
- **Why do you want to join bitvid?**
|
- **Why do you want to join bitvid?**
|
||||||
${whyJoin || "N/A"}
|
${whyJoin || "N/A"}
|
||||||
- **Have you created content on other platforms before?** ${priorExperience || "N/A"}
|
- **Have you created content on other platforms before?** ${
|
||||||
|
priorExperience || "N/A"
|
||||||
|
}
|
||||||
- **Links or references to your previous work:**
|
- **Links or references to your previous work:**
|
||||||
${experienceLinks || "N/A"}
|
${experienceLinks || "N/A"}
|
||||||
|
|
||||||
**3. Community Engagement**
|
**3. Community Engagement**
|
||||||
- **Familiar with bitvid’s Community Guidelines?** ${familiarGuidelines || "N/A"}
|
- **Familiar with bitvid’s Community Guidelines?** ${
|
||||||
- **Agree to follow guidelines and respect moderation?** ${agreeGuidelines || "N/A"}
|
familiarGuidelines || "N/A"
|
||||||
|
}
|
||||||
|
- **Agree to follow guidelines and respect moderation?** ${
|
||||||
|
agreeGuidelines || "N/A"
|
||||||
|
}
|
||||||
- **How do you plan to contribute to the community?**
|
- **How do you plan to contribute to the community?**
|
||||||
${communityContribution || "N/A"}
|
${communityContribution || "N/A"}
|
||||||
|
|
||||||
@@ -332,7 +467,10 @@ By submitting this application, you confirm that:
|
|||||||
For further questions, contact us through bitvid’s Nostr support channels.
|
For further questions, contact us through bitvid’s Nostr support channels.
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
log("[DEBUG] Constructed application content:\n" + applicationContent);
|
log(
|
||||||
|
"[DEBUG] Constructed application content:\n" +
|
||||||
|
applicationContent
|
||||||
|
);
|
||||||
|
|
||||||
// Decode the recipient NPUB to get the public key.
|
// Decode the recipient NPUB to get the public key.
|
||||||
log("Decoding recipient npub...");
|
log("Decoding recipient npub...");
|
||||||
@@ -352,7 +490,11 @@ For further questions, contact us through bitvid’s Nostr support channels.
|
|||||||
|
|
||||||
// Encrypt the application content.
|
// Encrypt the application content.
|
||||||
log("Encrypting application content (nip04)...");
|
log("Encrypting application content (nip04)...");
|
||||||
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, applicationContent);
|
const ciphertext = await nip04.encrypt(
|
||||||
|
ephemeralPriv,
|
||||||
|
targetPubHex,
|
||||||
|
applicationContent
|
||||||
|
);
|
||||||
log("[DEBUG] Ciphertext: " + ciphertext);
|
log("[DEBUG] Ciphertext: " + ciphertext);
|
||||||
log("Encryption done.");
|
log("Encryption done.");
|
||||||
|
|
||||||
@@ -364,7 +506,10 @@ For further questions, contact us through bitvid’s Nostr support channels.
|
|||||||
tags: [["p", targetPubHex]],
|
tags: [["p", targetPubHex]],
|
||||||
content: ciphertext,
|
content: ciphertext,
|
||||||
};
|
};
|
||||||
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
|
log(
|
||||||
|
"[DEBUG] Event template before finalizing: " +
|
||||||
|
JSON.stringify(eventTemplate)
|
||||||
|
);
|
||||||
|
|
||||||
// Finalize the event (computing the id and signature).
|
// Finalize the event (computing the id and signature).
|
||||||
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
||||||
@@ -382,7 +527,14 @@ For further questions, contact us through bitvid’s Nostr support channels.
|
|||||||
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
||||||
onEvent(foundEvent) {
|
onEvent(foundEvent) {
|
||||||
if (foundEvent.id === event.id) {
|
if (foundEvent.id === event.id) {
|
||||||
log("[" + url + "] => Found our application in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
|
log(
|
||||||
|
"[" +
|
||||||
|
url +
|
||||||
|
"] => Found our application in storage! ID: " +
|
||||||
|
foundEvent.id.slice(0, 8) +
|
||||||
|
"...",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose() {
|
onEose() {
|
||||||
@@ -391,7 +543,9 @@ For further questions, contact us through bitvid’s Nostr support channels.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Done. If the logs show that at least one relay accepted the event and the application was found in storage, a moderator will review your application within 7-14 days.");
|
log(
|
||||||
|
"Done. If the logs show that at least one relay accepted the event and the application was found in storage, a moderator will review your application within 7-14 days."
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("Error: " + err.message, "error");
|
log("Error: " + err.message, "error");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>bitvid Bug Report Form</title>
|
<title>bitvid Bug Report Form</title>
|
||||||
<!-- Link to your main stylesheet -->
|
<!-- Link to your main stylesheet -->
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="../../css/style.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Override for form page to match modal field styling */
|
/* Override for form page to match modal field styling */
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #E5E7EB; /* Tailwind's text-gray-200 */
|
color: #e5e7eb; /* Tailwind's text-gray-200 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input, textarea, and select mimic modal field styles */
|
/* Input, textarea, and select mimic modal field styles */
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
||||||
background-color: #1F2937; /* Tailwind's bg-gray-800 */
|
background-color: #1f2937; /* Tailwind's bg-gray-800 */
|
||||||
color: #F3F4F6; /* Tailwind's text-gray-100 */
|
color: #f3f4f6; /* Tailwind's text-gray-100 */
|
||||||
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.375rem; /* rounded-md */
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: #3B82F6; /* blue-500 */
|
border-color: #3b82f6; /* blue-500 */
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px #3B82F6;
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox group styling */
|
/* Checkbox group styling */
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
/* Button styled similarly to modal publish button */
|
/* Button styled similarly to modal publish button */
|
||||||
button {
|
button {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
background: #3B82F6; /* blue-500 */
|
background: #3b82f6; /* blue-500 */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
@@ -97,13 +97,13 @@
|
|||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: #F87171; /* a red tint */
|
color: #f87171; /* a red tint */
|
||||||
}
|
}
|
||||||
.success {
|
.success {
|
||||||
color: #3B82F6; /* blue-500 */
|
color: #3b82f6; /* blue-500 */
|
||||||
}
|
}
|
||||||
.warn {
|
.warn {
|
||||||
color: #FACC15; /* a yellow tone */
|
color: #facc15; /* a yellow tone */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar styling for WebKit browsers */
|
/* Custom Scrollbar styling for WebKit browsers */
|
||||||
@@ -115,13 +115,13 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #3B82F6;
|
background-color: #3b82f6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
/* Custom Scrollbar styling for Firefox */
|
/* Custom Scrollbar styling for Firefox */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #3B82F6 transparent;
|
scrollbar-color: #3b82f6 transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Load nostr‑tools v2.10.4 -->
|
<!-- Load nostr‑tools v2.10.4 -->
|
||||||
@@ -131,44 +131,99 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<p>
|
<p>
|
||||||
If you have encountered a bug or technical issue on bitvid, please fill out this form to help us diagnose and resolve the problem. Providing detailed information will assist us in troubleshooting more efficiently.
|
If you have encountered a bug or technical issue on bitvid, please
|
||||||
|
fill out this form to help us diagnose and resolve the problem.
|
||||||
|
Providing detailed information will assist us in troubleshooting more
|
||||||
|
efficiently.
|
||||||
</p>
|
</p>
|
||||||
<form id="bug-form">
|
<form id="bug-form">
|
||||||
<!-- Section 1: User Information -->
|
<!-- Section 1: User Information -->
|
||||||
<h2>1. User Information</h2>
|
<h2>1. User Information</h2>
|
||||||
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
|
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
|
||||||
<input type="text" id="userNpub" placeholder="Enter your npub (optional)" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="userNpub"
|
||||||
|
placeholder="Enter your npub (optional)"
|
||||||
|
/>
|
||||||
<p>Are you a (check all that apply):</p>
|
<p>Are you a (check all that apply):</p>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label><input type="checkbox" name="userRole" value="Viewer" /> Viewer</label>
|
<label
|
||||||
<label><input type="checkbox" name="userRole" value="Content Creator" /> Content Creator</label>
|
><input type="checkbox" name="userRole" value="Viewer" />
|
||||||
<label><input type="checkbox" name="userRole" value="Developer/Contributor" /> Developer / Contributor</label>
|
Viewer</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="userRole" value="Content Creator" />
|
||||||
|
Content Creator</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="userRole"
|
||||||
|
value="Developer/Contributor"
|
||||||
|
/>
|
||||||
|
Developer / Contributor</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section 2: Bug Details -->
|
<!-- Section 2: Bug Details -->
|
||||||
<h2>2. Bug Details</h2>
|
<h2>2. Bug Details</h2>
|
||||||
<label for="issueDescription">Describe the issue:</label>
|
<label for="issueDescription">Describe the issue:</label>
|
||||||
<textarea id="issueDescription" rows="3" placeholder="Provide a clear and concise description of the problem"></textarea>
|
<textarea
|
||||||
|
id="issueDescription"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Provide a clear and concise description of the problem"
|
||||||
|
></textarea>
|
||||||
<label for="stepsToReproduce">Steps to reproduce the bug:</label>
|
<label for="stepsToReproduce">Steps to reproduce the bug:</label>
|
||||||
<textarea id="stepsToReproduce" rows="3" placeholder="List the actions taken before encountering the issue"></textarea>
|
<textarea
|
||||||
|
id="stepsToReproduce"
|
||||||
|
rows="3"
|
||||||
|
placeholder="List the actions taken before encountering the issue"
|
||||||
|
></textarea>
|
||||||
<label for="expectedBehavior">Expected behavior:</label>
|
<label for="expectedBehavior">Expected behavior:</label>
|
||||||
<textarea id="expectedBehavior" rows="2" placeholder="What should have happened instead?"></textarea>
|
<textarea
|
||||||
|
id="expectedBehavior"
|
||||||
|
rows="2"
|
||||||
|
placeholder="What should have happened instead?"
|
||||||
|
></textarea>
|
||||||
<label for="actualBehavior">Actual behavior:</label>
|
<label for="actualBehavior">Actual behavior:</label>
|
||||||
<textarea id="actualBehavior" rows="2" placeholder="What actually happened?"></textarea>
|
<textarea
|
||||||
|
id="actualBehavior"
|
||||||
|
rows="2"
|
||||||
|
placeholder="What actually happened?"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<!-- Section 3: Device & Environment -->
|
<!-- Section 3: Device & Environment -->
|
||||||
<h2>3. Device & Environment</h2>
|
<h2>3. Device & Environment</h2>
|
||||||
<p>What device were you using? (check all that apply):</p>
|
<p>What device were you using? (check all that apply):</p>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label><input type="checkbox" name="deviceUsed" value="Desktop" /> Desktop</label>
|
<label
|
||||||
<label><input type="checkbox" name="deviceUsed" value="Mobile" /> Mobile</label>
|
><input type="checkbox" name="deviceUsed" value="Desktop" />
|
||||||
<label><input type="checkbox" name="deviceUsed" value="Tablet" /> Tablet</label>
|
Desktop</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="deviceUsed" value="Mobile" />
|
||||||
|
Mobile</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="deviceUsed" value="Tablet" />
|
||||||
|
Tablet</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<label for="operatingSystem">Operating System:</label>
|
<label for="operatingSystem">Operating System:</label>
|
||||||
<input type="text" id="operatingSystem" placeholder="e.g., Windows, macOS, Linux, iOS, Android" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="operatingSystem"
|
||||||
|
placeholder="e.g., Windows, macOS, Linux, iOS, Android"
|
||||||
|
/>
|
||||||
<label for="browserInfo">Browser (if applicable):</label>
|
<label for="browserInfo">Browser (if applicable):</label>
|
||||||
<input type="text" id="browserInfo" placeholder="e.g., Chrome, Firefox, Safari" />
|
<input
|
||||||
<label for="usingVPN">Are you using a VPN or privacy-focused browser settings?</label>
|
type="text"
|
||||||
|
id="browserInfo"
|
||||||
|
placeholder="e.g., Chrome, Firefox, Safari"
|
||||||
|
/>
|
||||||
|
<label for="usingVPN"
|
||||||
|
>Are you using a VPN or privacy-focused browser settings?</label
|
||||||
|
>
|
||||||
<select id="usingVPN">
|
<select id="usingVPN">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
@@ -177,12 +232,28 @@
|
|||||||
|
|
||||||
<!-- Section 4: Screenshots or Logs -->
|
<!-- Section 4: Screenshots or Logs -->
|
||||||
<h2>4. Screenshots or Logs (If Available)</h2>
|
<h2>4. Screenshots or Logs (If Available)</h2>
|
||||||
<label for="screenshotInfo">Can you provide a screenshot or screen recording?</label>
|
<label for="screenshotInfo"
|
||||||
<textarea id="screenshotInfo" rows="2" placeholder="Attach if possible (e.g., URL to screenshot)"></textarea>
|
>Can you provide a screenshot or screen recording?</label
|
||||||
<label for="errorMessages">Did the issue generate any error messages?</label>
|
>
|
||||||
<textarea id="errorMessages" rows="2" placeholder="Include the exact error messages, if any"></textarea>
|
<textarea
|
||||||
|
id="screenshotInfo"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Attach if possible (e.g., URL to screenshot)"
|
||||||
|
></textarea>
|
||||||
|
<label for="errorMessages"
|
||||||
|
>Did the issue generate any error messages?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="errorMessages"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Include the exact error messages, if any"
|
||||||
|
></textarea>
|
||||||
<label for="consoleLogs">Any relevant console logs?</label>
|
<label for="consoleLogs">Any relevant console logs?</label>
|
||||||
<textarea id="consoleLogs" rows="2" placeholder="Paste any browser console logs if available"></textarea>
|
<textarea
|
||||||
|
id="consoleLogs"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Paste any browser console logs if available"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<!-- Section 5: Additional Information -->
|
<!-- Section 5: Additional Information -->
|
||||||
<h2>5. Additional Information</h2>
|
<h2>5. Additional Information</h2>
|
||||||
@@ -193,14 +264,23 @@
|
|||||||
<option value="Occasionally">Occasionally</option>
|
<option value="Occasionally">Occasionally</option>
|
||||||
<option value="Rarely">Rarely</option>
|
<option value="Rarely">Rarely</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="impactCore">Does this bug impact core functionality (e.g., video playback, uploads, etc.)?</label>
|
<label for="impactCore"
|
||||||
|
>Does this bug impact core functionality (e.g., video playback,
|
||||||
|
uploads, etc.)?</label
|
||||||
|
>
|
||||||
<select id="impactCore">
|
<select id="impactCore">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
<option value="No">No</option>
|
<option value="No">No</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="additionalNotes">Any additional notes or suggestions?</label>
|
<label for="additionalNotes"
|
||||||
<textarea id="additionalNotes" rows="3" placeholder="Describe any other relevant details"></textarea>
|
>Any additional notes or suggestions?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="additionalNotes"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe any other relevant details"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<button type="submit">Submit Bug Report</button>
|
<button type="submit">Submit Bug Report</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -234,55 +314,86 @@
|
|||||||
nip04,
|
nip04,
|
||||||
nip19,
|
nip19,
|
||||||
SimplePool,
|
SimplePool,
|
||||||
Relay
|
Relay,
|
||||||
} = window.NostrTools;
|
} = window.NostrTools;
|
||||||
|
|
||||||
// Set the recipient's NPUB (your personal NPUB)
|
// Set the recipient's NPUB (your personal NPUB)
|
||||||
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
const recipientNpub =
|
||||||
|
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
||||||
const RELAYS = [
|
const RELAYS = [
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net"
|
"wss://relay.primal.net",
|
||||||
];
|
];
|
||||||
const pool = new SimplePool();
|
const pool = new SimplePool();
|
||||||
|
|
||||||
document.getElementById("bug-form").addEventListener("submit", async (ev) => {
|
document
|
||||||
|
.getElementById("bug-form")
|
||||||
|
.addEventListener("submit", async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
clear();
|
clear();
|
||||||
try {
|
try {
|
||||||
// Section 1: User Information
|
// Section 1: User Information
|
||||||
const userNpub = document.getElementById("userNpub").value.trim();
|
const userNpub = document.getElementById("userNpub").value.trim();
|
||||||
const roleNodes = document.querySelectorAll('input[name="userRole"]:checked');
|
const roleNodes = document.querySelectorAll(
|
||||||
|
'input[name="userRole"]:checked'
|
||||||
|
);
|
||||||
let userRoles = [];
|
let userRoles = [];
|
||||||
roleNodes.forEach(node => {
|
roleNodes.forEach((node) => {
|
||||||
userRoles.push(node.value);
|
userRoles.push(node.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Section 2: Bug Details
|
// Section 2: Bug Details
|
||||||
const issueDescription = document.getElementById("issueDescription").value.trim();
|
const issueDescription = document
|
||||||
const stepsToReproduce = document.getElementById("stepsToReproduce").value.trim();
|
.getElementById("issueDescription")
|
||||||
const expectedBehavior = document.getElementById("expectedBehavior").value.trim();
|
.value.trim();
|
||||||
const actualBehavior = document.getElementById("actualBehavior").value.trim();
|
const stepsToReproduce = document
|
||||||
|
.getElementById("stepsToReproduce")
|
||||||
|
.value.trim();
|
||||||
|
const expectedBehavior = document
|
||||||
|
.getElementById("expectedBehavior")
|
||||||
|
.value.trim();
|
||||||
|
const actualBehavior = document
|
||||||
|
.getElementById("actualBehavior")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 3: Device & Environment
|
// Section 3: Device & Environment
|
||||||
const deviceNodes = document.querySelectorAll('input[name="deviceUsed"]:checked');
|
const deviceNodes = document.querySelectorAll(
|
||||||
|
'input[name="deviceUsed"]:checked'
|
||||||
|
);
|
||||||
let devicesUsed = [];
|
let devicesUsed = [];
|
||||||
deviceNodes.forEach(node => {
|
deviceNodes.forEach((node) => {
|
||||||
devicesUsed.push(node.value);
|
devicesUsed.push(node.value);
|
||||||
});
|
});
|
||||||
const operatingSystem = document.getElementById("operatingSystem").value.trim();
|
const operatingSystem = document
|
||||||
const browserInfo = document.getElementById("browserInfo").value.trim();
|
.getElementById("operatingSystem")
|
||||||
|
.value.trim();
|
||||||
|
const browserInfo = document
|
||||||
|
.getElementById("browserInfo")
|
||||||
|
.value.trim();
|
||||||
const usingVPN = document.getElementById("usingVPN").value.trim();
|
const usingVPN = document.getElementById("usingVPN").value.trim();
|
||||||
|
|
||||||
// Section 4: Screenshots or Logs
|
// Section 4: Screenshots or Logs
|
||||||
const screenshotInfo = document.getElementById("screenshotInfo").value.trim();
|
const screenshotInfo = document
|
||||||
const errorMessages = document.getElementById("errorMessages").value.trim();
|
.getElementById("screenshotInfo")
|
||||||
const consoleLogs = document.getElementById("consoleLogs").value.trim();
|
.value.trim();
|
||||||
|
const errorMessages = document
|
||||||
|
.getElementById("errorMessages")
|
||||||
|
.value.trim();
|
||||||
|
const consoleLogs = document
|
||||||
|
.getElementById("consoleLogs")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 5: Additional Information
|
// Section 5: Additional Information
|
||||||
const bugFrequency = document.getElementById("bugFrequency").value.trim();
|
const bugFrequency = document
|
||||||
const impactCore = document.getElementById("impactCore").value.trim();
|
.getElementById("bugFrequency")
|
||||||
const additionalNotes = document.getElementById("additionalNotes").value.trim();
|
.value.trim();
|
||||||
|
const impactCore = document
|
||||||
|
.getElementById("impactCore")
|
||||||
|
.value.trim();
|
||||||
|
const additionalNotes = document
|
||||||
|
.getElementById("additionalNotes")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Construct the bug report content as Markdown.
|
// Construct the bug report content as Markdown.
|
||||||
const bugReportContent = `
|
const bugReportContent = `
|
||||||
@@ -328,7 +439,9 @@ Our team reviews bug reports regularly. While we aim to fix critical issues as s
|
|||||||
For urgent issues, contact us through bitvid’s Nostr support channels.
|
For urgent issues, contact us through bitvid’s Nostr support channels.
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
log("[DEBUG] Constructed bug report content:\n" + bugReportContent);
|
log(
|
||||||
|
"[DEBUG] Constructed bug report content:\n" + bugReportContent
|
||||||
|
);
|
||||||
|
|
||||||
// Decode the recipient NPUB to get the public key.
|
// Decode the recipient NPUB to get the public key.
|
||||||
log("Decoding recipient npub...");
|
log("Decoding recipient npub...");
|
||||||
@@ -348,7 +461,11 @@ For urgent issues, contact us through bitvid’s Nostr support channels.
|
|||||||
|
|
||||||
// Encrypt the bug report content.
|
// Encrypt the bug report content.
|
||||||
log("Encrypting bug report content (nip04)...");
|
log("Encrypting bug report content (nip04)...");
|
||||||
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, bugReportContent);
|
const ciphertext = await nip04.encrypt(
|
||||||
|
ephemeralPriv,
|
||||||
|
targetPubHex,
|
||||||
|
bugReportContent
|
||||||
|
);
|
||||||
log("[DEBUG] Ciphertext: " + ciphertext);
|
log("[DEBUG] Ciphertext: " + ciphertext);
|
||||||
log("Encryption done.");
|
log("Encryption done.");
|
||||||
|
|
||||||
@@ -360,7 +477,10 @@ For urgent issues, contact us through bitvid’s Nostr support channels.
|
|||||||
tags: [["p", targetPubHex]],
|
tags: [["p", targetPubHex]],
|
||||||
content: ciphertext,
|
content: ciphertext,
|
||||||
};
|
};
|
||||||
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
|
log(
|
||||||
|
"[DEBUG] Event template before finalizing: " +
|
||||||
|
JSON.stringify(eventTemplate)
|
||||||
|
);
|
||||||
|
|
||||||
// Finalize the event.
|
// Finalize the event.
|
||||||
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
||||||
@@ -378,7 +498,14 @@ For urgent issues, contact us through bitvid’s Nostr support channels.
|
|||||||
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
||||||
onEvent(foundEvent) {
|
onEvent(foundEvent) {
|
||||||
if (foundEvent.id === event.id) {
|
if (foundEvent.id === event.id) {
|
||||||
log("[" + url + "] => Found our bug report in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
|
log(
|
||||||
|
"[" +
|
||||||
|
url +
|
||||||
|
"] => Found our bug report in storage! ID: " +
|
||||||
|
foundEvent.id.slice(0, 8) +
|
||||||
|
"...",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose() {
|
onEose() {
|
||||||
@@ -387,7 +514,9 @@ For urgent issues, contact us through bitvid’s Nostr support channels.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Done. If the logs show that at least one relay accepted the event and the bug report was found in storage, our team will review your report within 7-14 days.");
|
log(
|
||||||
|
"Done. If the logs show that at least one relay accepted the event and the bug report was found in storage, our team will review your report within 7-14 days."
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("Error: " + err.message, "error");
|
log("Error: " + err.message, "error");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>bitvid Content Appeals Form</title>
|
<title>bitvid Content Appeals Form</title>
|
||||||
<!-- Link to your main stylesheet -->
|
<!-- Link to your main stylesheet -->
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="../../css/style.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Override for form page to match modal field styling */
|
/* Override for form page to match modal field styling */
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #E5E7EB; /* Tailwind's text-gray-200 */
|
color: #e5e7eb; /* Tailwind's text-gray-200 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input, textarea, and select mimic modal field styles */
|
/* Input, textarea, and select mimic modal field styles */
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
||||||
background-color: #1F2937; /* Tailwind's bg-gray-800 */
|
background-color: #1f2937; /* Tailwind's bg-gray-800 */
|
||||||
color: #F3F4F6; /* Tailwind's text-gray-100 */
|
color: #f3f4f6; /* Tailwind's text-gray-100 */
|
||||||
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.375rem; /* rounded-md */
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
@@ -62,15 +62,15 @@
|
|||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: #3B82F6; /* blue-500 */
|
border-color: #3b82f6; /* blue-500 */
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px #3B82F6;
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button styled similarly to modal publish button */
|
/* Button styled similarly to modal publish button */
|
||||||
button {
|
button {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
background: #3B82F6; /* blue-500 */
|
background: #3b82f6; /* blue-500 */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
@@ -90,13 +90,13 @@
|
|||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: #F87171; /* a red tint */
|
color: #f87171; /* a red tint */
|
||||||
}
|
}
|
||||||
.success {
|
.success {
|
||||||
color: #3B82F6; /* blue-500 */
|
color: #3b82f6; /* blue-500 */
|
||||||
}
|
}
|
||||||
.warn {
|
.warn {
|
||||||
color: #FACC15; /* a yellow tone */
|
color: #facc15; /* a yellow tone */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar styling for WebKit browsers */
|
/* Custom Scrollbar styling for WebKit browsers */
|
||||||
@@ -108,13 +108,13 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #3B82F6;
|
background-color: #3b82f6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
/* Custom Scrollbar styling for Firefox */
|
/* Custom Scrollbar styling for Firefox */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #3B82F6 transparent;
|
scrollbar-color: #3b82f6 transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Load nostr‑tools v2.10.4 -->
|
<!-- Load nostr‑tools v2.10.4 -->
|
||||||
@@ -124,41 +124,69 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<p>
|
<p>
|
||||||
If you believe your content was unfairly blocked or restricted on bitvid,
|
If you believe your content was unfairly blocked or restricted on
|
||||||
please complete this form. Appeals will be reviewed manually, and
|
bitvid, please complete this form. Appeals will be reviewed manually,
|
||||||
decisions will be communicated back to you.
|
and decisions will be communicated back to you.
|
||||||
</p>
|
</p>
|
||||||
<form id="dm-form">
|
<form id="dm-form">
|
||||||
<!-- NPUB input removed as the recipient is now set by the code -->
|
<!-- NPUB input removed as the recipient is now set by the code -->
|
||||||
<h2>1. User Information</h2>
|
<h2>1. User Information</h2>
|
||||||
<label for="contactMethod">Contact Method (if applicable):</label>
|
<label for="contactMethod">Contact Method (if applicable):</label>
|
||||||
<input type="text" id="contactMethod" placeholder="Nostr DM, email, or other" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="contactMethod"
|
||||||
|
placeholder="Nostr DM, email, or other"
|
||||||
|
/>
|
||||||
|
|
||||||
<h2>2. Content Details</h2>
|
<h2>2. Content Details</h2>
|
||||||
<label for="videoTitle">Title of the Video:</label>
|
<label for="videoTitle">Title of the Video:</label>
|
||||||
<input type="text" id="videoTitle" placeholder="Enter the exact title" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="videoTitle"
|
||||||
|
placeholder="Enter the exact title"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="magnetLink">Magnet Link:</label>
|
<label for="magnetLink">Magnet Link:</label>
|
||||||
<input type="text" id="magnetLink" placeholder="Enter the magnet link" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="magnetLink"
|
||||||
|
placeholder="Enter the magnet link"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="submissionDate">Date of Content Submission:</label>
|
<label for="submissionDate">Date of Content Submission:</label>
|
||||||
<input type="date" id="submissionDate" />
|
<input type="date" id="submissionDate" />
|
||||||
|
|
||||||
<h2>3. Reason for Appeal</h2>
|
<h2>3. Reason for Appeal</h2>
|
||||||
<label for="reasonBlocked">Why do you believe your content was unfairly blocked?</label>
|
<label for="reasonBlocked"
|
||||||
<textarea id="reasonBlocked" rows="3" placeholder="Explain in detail"></textarea>
|
>Why do you believe your content was unfairly blocked?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="reasonBlocked"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Explain in detail"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<label for="fitsGuidelines">Does your content fit within bitvid's Community Guidelines?</label>
|
<label for="fitsGuidelines"
|
||||||
|
>Does your content fit within bitvid's Community Guidelines?</label
|
||||||
|
>
|
||||||
<select id="fitsGuidelines">
|
<select id="fitsGuidelines">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
<option value="No">No</option>
|
<option value="No">No</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="guidelinesCited">If yes, which guideline(s) support your appeal?</label>
|
<label for="guidelinesCited"
|
||||||
<textarea id="guidelinesCited" rows="2" placeholder="Cite the specific guidelines"></textarea>
|
>If yes, which guideline(s) support your appeal?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="guidelinesCited"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Cite the specific guidelines"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<label for="editedContent">Was this content edited after being blocked?</label>
|
<label for="editedContent"
|
||||||
|
>Was this content edited after being blocked?</label
|
||||||
|
>
|
||||||
<select id="editedContent">
|
<select id="editedContent">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
@@ -166,21 +194,38 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label for="changesMade">If yes, what changes were made?</label>
|
<label for="changesMade">If yes, what changes were made?</label>
|
||||||
<textarea id="changesMade" rows="2" placeholder="Describe the modifications"></textarea>
|
<textarea
|
||||||
|
id="changesMade"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Describe the modifications"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<h2>4. Additional Context</h2>
|
<h2>4. Additional Context</h2>
|
||||||
<label for="misunderstanding">Was there any misunderstanding or misclassification?</label>
|
<label for="misunderstanding"
|
||||||
<textarea id="misunderstanding" rows="2" placeholder="Provide context"></textarea>
|
>Was there any misunderstanding or misclassification?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="misunderstanding"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Provide context"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<label for="externalReferences">Are there external references that validate your appeal?</label>
|
<label for="externalReferences"
|
||||||
<textarea id="externalReferences" rows="2" placeholder="Links, citations, or additional info"></textarea>
|
>Are there external references that validate your appeal?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="externalReferences"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Links, citations, or additional info"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<h2>5. Declaration</h2>
|
<h2>5. Declaration</h2>
|
||||||
<p>
|
<p>
|
||||||
By submitting this appeal, you confirm that:
|
By submitting this appeal, you confirm that:
|
||||||
<br />- You are the original creator or authorized representative of the content.
|
<br />- You are the original creator or authorized representative of
|
||||||
<br />- Your appeal is submitted in good faith and aligns with bitvid’s policies.
|
the content. <br />- Your appeal is submitted in good faith and
|
||||||
<br />- You understand that final decisions are at the discretion of bitvid’s moderation process.
|
aligns with bitvid’s policies. <br />- You understand that final
|
||||||
|
decisions are at the discretion of bitvid’s moderation process.
|
||||||
</p>
|
</p>
|
||||||
<label for="signature">Signature (Digital or Written):</label>
|
<label for="signature">Signature (Digital or Written):</label>
|
||||||
<input type="text" id="signature" placeholder="Your signature" />
|
<input type="text" id="signature" placeholder="Your signature" />
|
||||||
@@ -213,38 +258,75 @@
|
|||||||
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
|
const {
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
finalizeEvent,
|
||||||
|
nip04,
|
||||||
|
nip19,
|
||||||
|
SimplePool,
|
||||||
|
Relay,
|
||||||
|
} = window.NostrTools;
|
||||||
|
|
||||||
// Set the recipient's NPUB (your personal NPUB) here.
|
// Set the recipient's NPUB (your personal NPUB) here.
|
||||||
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
const recipientNpub =
|
||||||
|
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
||||||
|
|
||||||
const RELAYS = [
|
const RELAYS = [
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net"
|
"wss://relay.primal.net",
|
||||||
];
|
];
|
||||||
const pool = new SimplePool();
|
const pool = new SimplePool();
|
||||||
|
|
||||||
document.getElementById("dm-form").addEventListener("submit", async (ev) => {
|
document
|
||||||
|
.getElementById("dm-form")
|
||||||
|
.addEventListener("submit", async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
clear();
|
clear();
|
||||||
try {
|
try {
|
||||||
// Note: The customer's NPUB input is removed.
|
// Note: The customer's NPUB input is removed.
|
||||||
// Use the recipientNpub constant to get the public key.
|
// Use the recipientNpub constant to get the public key.
|
||||||
|
|
||||||
const contactMethod = document.getElementById("contactMethod").value.trim();
|
const contactMethod = document
|
||||||
const videoTitle = document.getElementById("videoTitle").value.trim();
|
.getElementById("contactMethod")
|
||||||
const magnetLink = document.getElementById("magnetLink").value.trim();
|
.value.trim();
|
||||||
const submissionDate = document.getElementById("submissionDate").value.trim();
|
const videoTitle = document
|
||||||
const reasonBlocked = document.getElementById("reasonBlocked").value.trim();
|
.getElementById("videoTitle")
|
||||||
const fitsGuidelines = document.getElementById("fitsGuidelines").value.trim();
|
.value.trim();
|
||||||
const guidelinesCited = document.getElementById("guidelinesCited").value.trim();
|
const magnetLink = document
|
||||||
const editedContent = document.getElementById("editedContent").value.trim();
|
.getElementById("magnetLink")
|
||||||
const changesMade = document.getElementById("changesMade").value.trim();
|
.value.trim();
|
||||||
const misunderstanding = document.getElementById("misunderstanding").value.trim();
|
const submissionDate = document
|
||||||
const externalReferences = document.getElementById("externalReferences").value.trim();
|
.getElementById("submissionDate")
|
||||||
const signature = document.getElementById("signature").value.trim();
|
.value.trim();
|
||||||
const declarationDate = document.getElementById("declarationDate").value.trim();
|
const reasonBlocked = document
|
||||||
|
.getElementById("reasonBlocked")
|
||||||
|
.value.trim();
|
||||||
|
const fitsGuidelines = document
|
||||||
|
.getElementById("fitsGuidelines")
|
||||||
|
.value.trim();
|
||||||
|
const guidelinesCited = document
|
||||||
|
.getElementById("guidelinesCited")
|
||||||
|
.value.trim();
|
||||||
|
const editedContent = document
|
||||||
|
.getElementById("editedContent")
|
||||||
|
.value.trim();
|
||||||
|
const changesMade = document
|
||||||
|
.getElementById("changesMade")
|
||||||
|
.value.trim();
|
||||||
|
const misunderstanding = document
|
||||||
|
.getElementById("misunderstanding")
|
||||||
|
.value.trim();
|
||||||
|
const externalReferences = document
|
||||||
|
.getElementById("externalReferences")
|
||||||
|
.value.trim();
|
||||||
|
const signature = document
|
||||||
|
.getElementById("signature")
|
||||||
|
.value.trim();
|
||||||
|
const declarationDate = document
|
||||||
|
.getElementById("declarationDate")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Construct the appeal content.
|
// Construct the appeal content.
|
||||||
const appealContent = `
|
const appealContent = `
|
||||||
@@ -312,7 +394,11 @@ For further questions, reach out through bitvid’s Nostr support channels.
|
|||||||
|
|
||||||
// Encrypt the appeal content.
|
// Encrypt the appeal content.
|
||||||
log("Encrypting appeal content (nip04)...");
|
log("Encrypting appeal content (nip04)...");
|
||||||
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, appealContent);
|
const ciphertext = await nip04.encrypt(
|
||||||
|
ephemeralPriv,
|
||||||
|
targetPubHex,
|
||||||
|
appealContent
|
||||||
|
);
|
||||||
log("[DEBUG] Ciphertext: " + ciphertext);
|
log("[DEBUG] Ciphertext: " + ciphertext);
|
||||||
log("Encryption done.");
|
log("Encryption done.");
|
||||||
|
|
||||||
@@ -324,7 +410,10 @@ For further questions, reach out through bitvid’s Nostr support channels.
|
|||||||
tags: [["p", targetPubHex]],
|
tags: [["p", targetPubHex]],
|
||||||
content: ciphertext,
|
content: ciphertext,
|
||||||
};
|
};
|
||||||
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
|
log(
|
||||||
|
"[DEBUG] Event template before finalizing: " +
|
||||||
|
JSON.stringify(eventTemplate)
|
||||||
|
);
|
||||||
|
|
||||||
// Finalize the event.
|
// Finalize the event.
|
||||||
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
||||||
@@ -342,7 +431,14 @@ For further questions, reach out through bitvid’s Nostr support channels.
|
|||||||
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
||||||
onEvent(foundEvent) {
|
onEvent(foundEvent) {
|
||||||
if (foundEvent.id === event.id) {
|
if (foundEvent.id === event.id) {
|
||||||
log("[" + url + "] => Found our appeal in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
|
log(
|
||||||
|
"[" +
|
||||||
|
url +
|
||||||
|
"] => Found our appeal in storage! ID: " +
|
||||||
|
foundEvent.id.slice(0, 8) +
|
||||||
|
"...",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose() {
|
onEose() {
|
||||||
@@ -351,7 +447,9 @@ For further questions, reach out through bitvid’s Nostr support channels.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Done. If the logs show that at least one relay accepted the event and the appeal was found in storage, a moderator will review your appeal within 7-14 days.");
|
log(
|
||||||
|
"Done. If the logs show that at least one relay accepted the event and the appeal was found in storage, a moderator will review your appeal within 7-14 days."
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("Error: " + err.message, "error");
|
log("Error: " + err.message, "error");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>bitvid General Feedback Form</title>
|
<title>bitvid General Feedback Form</title>
|
||||||
<!-- Link to your main stylesheet -->
|
<!-- Link to your main stylesheet -->
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="../../css/style.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Override for form page to match modal field styling */
|
/* Override for form page to match modal field styling */
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #E5E7EB; /* Tailwind's text-gray-200 */
|
color: #e5e7eb; /* Tailwind's text-gray-200 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input, textarea, and select styling */
|
/* Input, textarea, and select styling */
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
||||||
background-color: #1F2937; /* Tailwind's bg-gray-800 */
|
background-color: #1f2937; /* Tailwind's bg-gray-800 */
|
||||||
color: #F3F4F6; /* Tailwind's text-gray-100 */
|
color: #f3f4f6; /* Tailwind's text-gray-100 */
|
||||||
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
border: 1px solid #374151; /* Tailwind's border-gray-700 */
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.375rem; /* rounded-md */
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: #3B82F6; /* blue-500 */
|
border-color: #3b82f6; /* blue-500 */
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px #3B82F6;
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox group styling */
|
/* Checkbox group styling */
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
/* Button styling */
|
/* Button styling */
|
||||||
button {
|
button {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
background: #3B82F6; /* blue-500 */
|
background: #3b82f6; /* blue-500 */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
@@ -104,13 +104,13 @@
|
|||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: #F87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
.success {
|
.success {
|
||||||
color: #3B82F6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
.warn {
|
.warn {
|
||||||
color: #FACC15;
|
color: #facc15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar styling for WebKit browsers */
|
/* Custom Scrollbar styling for WebKit browsers */
|
||||||
@@ -122,13 +122,13 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #3B82F6;
|
background-color: #3b82f6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
/* Custom Scrollbar styling for Firefox */
|
/* Custom Scrollbar styling for Firefox */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #3B82F6 transparent;
|
scrollbar-color: #3b82f6 transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Load nostr‑tools v2.10.4 -->
|
<!-- Load nostr‑tools v2.10.4 -->
|
||||||
@@ -138,48 +138,112 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<p>
|
<p>
|
||||||
Your feedback helps us improve bitvid! Whether it’s a suggestion, a concern, or general thoughts on the platform, we’d love to hear from you.
|
Your feedback helps us improve bitvid! Whether it’s a suggestion, a
|
||||||
|
concern, or general thoughts on the platform, we’d love to hear from
|
||||||
|
you.
|
||||||
</p>
|
</p>
|
||||||
<form id="feedback-form">
|
<form id="feedback-form">
|
||||||
<!-- Section 1: User Information -->
|
<!-- Section 1: User Information -->
|
||||||
<h2>1. User Information</h2>
|
<h2>1. User Information</h2>
|
||||||
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
|
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
|
||||||
<input type="text" id="userNpub" placeholder="Enter your npub (optional)" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="userNpub"
|
||||||
|
placeholder="Enter your npub (optional)"
|
||||||
|
/>
|
||||||
<p>Are you a (check all that apply):</p>
|
<p>Are you a (check all that apply):</p>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label><input type="checkbox" name="userRole" value="Viewer" /> Viewer</label>
|
<label
|
||||||
<label><input type="checkbox" name="userRole" value="Content Creator" /> Content Creator</label>
|
><input type="checkbox" name="userRole" value="Viewer" />
|
||||||
<label><input type="checkbox" name="userRole" value="Developer/Contributor" /> Developer / Contributor</label>
|
Viewer</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="userRole" value="Content Creator" />
|
||||||
|
Content Creator</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="userRole"
|
||||||
|
value="Developer/Contributor"
|
||||||
|
/>
|
||||||
|
Developer / Contributor</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section 2: General Feedback -->
|
<!-- Section 2: General Feedback -->
|
||||||
<h2>2. General Feedback</h2>
|
<h2>2. General Feedback</h2>
|
||||||
<label>How would you rate your experience on bitvid so far?</label>
|
<label>How would you rate your experience on bitvid so far?</label>
|
||||||
<div class="radio-group">
|
<div class="radio-group">
|
||||||
<label><input type="radio" name="experienceRating" value="Excellent" /> Excellent</label>
|
<label
|
||||||
<label><input type="radio" name="experienceRating" value="Good" /> Good</label>
|
><input type="radio" name="experienceRating" value="Excellent" />
|
||||||
<label><input type="radio" name="experienceRating" value="Average" /> Average</label>
|
Excellent</label
|
||||||
<label><input type="radio" name="experienceRating" value="Needs Improvement" /> Needs Improvement</label>
|
>
|
||||||
|
<label
|
||||||
|
><input type="radio" name="experienceRating" value="Good" />
|
||||||
|
Good</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="radio" name="experienceRating" value="Average" />
|
||||||
|
Average</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
name="experienceRating"
|
||||||
|
value="Needs Improvement"
|
||||||
|
/>
|
||||||
|
Needs Improvement</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<label for="likeMost">What do you like most about bitvid?</label>
|
<label for="likeMost">What do you like most about bitvid?</label>
|
||||||
<textarea id="likeMost" rows="3" placeholder="Describe the features, usability, or content you enjoy"></textarea>
|
<textarea
|
||||||
|
id="likeMost"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe the features, usability, or content you enjoy"
|
||||||
|
></textarea>
|
||||||
<label for="improvements">What would you like to see improved?</label>
|
<label for="improvements">What would you like to see improved?</label>
|
||||||
<textarea id="improvements" rows="3" placeholder="Provide specific areas for improvement"></textarea>
|
<textarea
|
||||||
<label for="confusingFeatures">Are there any features or tools you find confusing or difficult to use?</label>
|
id="improvements"
|
||||||
<textarea id="confusingFeatures" rows="3" placeholder="Explain any challenges you’ve encountered"></textarea>
|
rows="3"
|
||||||
|
placeholder="Provide specific areas for improvement"
|
||||||
|
></textarea>
|
||||||
|
<label for="confusingFeatures"
|
||||||
|
>Are there any features or tools you find confusing or difficult to
|
||||||
|
use?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="confusingFeatures"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Explain any challenges you’ve encountered"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<!-- Section 3: Additional Comments -->
|
<!-- Section 3: Additional Comments -->
|
||||||
<h2>3. Additional Comments</h2>
|
<h2>3. Additional Comments</h2>
|
||||||
<label for="otherSuggestions">Do you have any other suggestions or thoughts about bitvid?</label>
|
<label for="otherSuggestions"
|
||||||
<textarea id="otherSuggestions" rows="3" placeholder="Share any other feedback"></textarea>
|
>Do you have any other suggestions or thoughts about bitvid?</label
|
||||||
<label for="followUp">Would you like to be contacted for follow-up discussions?</label>
|
>
|
||||||
|
<textarea
|
||||||
|
id="otherSuggestions"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Share any other feedback"
|
||||||
|
></textarea>
|
||||||
|
<label for="followUp"
|
||||||
|
>Would you like to be contacted for follow-up discussions?</label
|
||||||
|
>
|
||||||
<select id="followUp">
|
<select id="followUp">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
<option value="No">No</option>
|
<option value="No">No</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="preferredContact">Preferred contact method (if applicable):</label>
|
<label for="preferredContact"
|
||||||
<input type="text" id="preferredContact" placeholder="Nostr DM, email, or other" />
|
>Preferred contact method (if applicable):</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="preferredContact"
|
||||||
|
placeholder="Nostr DM, email, or other"
|
||||||
|
/>
|
||||||
|
|
||||||
<button type="submit">Submit General Feedback</button>
|
<button type="submit">Submit General Feedback</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -206,41 +270,66 @@
|
|||||||
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
|
const {
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
finalizeEvent,
|
||||||
|
nip04,
|
||||||
|
nip19,
|
||||||
|
SimplePool,
|
||||||
|
Relay,
|
||||||
|
} = window.NostrTools;
|
||||||
|
|
||||||
// Set the recipient's NPUB (your personal NPUB)
|
// Set the recipient's NPUB (your personal NPUB)
|
||||||
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
const recipientNpub =
|
||||||
|
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
||||||
const RELAYS = [
|
const RELAYS = [
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net"
|
"wss://relay.primal.net",
|
||||||
];
|
];
|
||||||
const pool = new SimplePool();
|
const pool = new SimplePool();
|
||||||
|
|
||||||
document.getElementById("feedback-form").addEventListener("submit", async (ev) => {
|
document
|
||||||
|
.getElementById("feedback-form")
|
||||||
|
.addEventListener("submit", async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Section 1: User Information
|
// Section 1: User Information
|
||||||
const userNpub = document.getElementById("userNpub").value.trim();
|
const userNpub = document.getElementById("userNpub").value.trim();
|
||||||
const roleNodes = document.querySelectorAll('input[name="userRole"]:checked');
|
const roleNodes = document.querySelectorAll(
|
||||||
|
'input[name="userRole"]:checked'
|
||||||
|
);
|
||||||
let userRoles = [];
|
let userRoles = [];
|
||||||
roleNodes.forEach(node => {
|
roleNodes.forEach((node) => {
|
||||||
userRoles.push(node.value);
|
userRoles.push(node.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Section 2: General Feedback
|
// Section 2: General Feedback
|
||||||
const experienceRadio = document.querySelector('input[name="experienceRating"]:checked');
|
const experienceRadio = document.querySelector(
|
||||||
const experienceRating = experienceRadio ? experienceRadio.value : "N/A";
|
'input[name="experienceRating"]:checked'
|
||||||
|
);
|
||||||
|
const experienceRating = experienceRadio
|
||||||
|
? experienceRadio.value
|
||||||
|
: "N/A";
|
||||||
const likeMost = document.getElementById("likeMost").value.trim();
|
const likeMost = document.getElementById("likeMost").value.trim();
|
||||||
const improvements = document.getElementById("improvements").value.trim();
|
const improvements = document
|
||||||
const confusingFeatures = document.getElementById("confusingFeatures").value.trim();
|
.getElementById("improvements")
|
||||||
|
.value.trim();
|
||||||
|
const confusingFeatures = document
|
||||||
|
.getElementById("confusingFeatures")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 3: Additional Comments
|
// Section 3: Additional Comments
|
||||||
const otherSuggestions = document.getElementById("otherSuggestions").value.trim();
|
const otherSuggestions = document
|
||||||
|
.getElementById("otherSuggestions")
|
||||||
|
.value.trim();
|
||||||
const followUp = document.getElementById("followUp").value.trim();
|
const followUp = document.getElementById("followUp").value.trim();
|
||||||
const preferredContact = document.getElementById("preferredContact").value.trim();
|
const preferredContact = document
|
||||||
|
.getElementById("preferredContact")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Construct the Markdown feedback content
|
// Construct the Markdown feedback content
|
||||||
const feedbackContent = `
|
const feedbackContent = `
|
||||||
@@ -250,7 +339,9 @@ Your feedback helps us improve bitvid! Whether it’s a suggestion, a concern, o
|
|||||||
|
|
||||||
## **1. User Information**
|
## **1. User Information**
|
||||||
- **Nostr Public Key (npub) (optional):** ${userNpub || "N/A"}
|
- **Nostr Public Key (npub) (optional):** ${userNpub || "N/A"}
|
||||||
- **Are you a (check all that apply):** ${userRoles.length > 0 ? userRoles.join(", ") : "N/A"}
|
- **Are you a (check all that apply):** ${
|
||||||
|
userRoles.length > 0 ? userRoles.join(", ") : "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
## **2. General Feedback**
|
## **2. General Feedback**
|
||||||
- **How would you rate your experience on bitvid so far?** ${experienceRating}
|
- **How would you rate your experience on bitvid so far?** ${experienceRating}
|
||||||
@@ -264,7 +355,9 @@ Your feedback helps us improve bitvid! Whether it’s a suggestion, a concern, o
|
|||||||
## **3. Additional Comments**
|
## **3. Additional Comments**
|
||||||
- **Do you have any other suggestions or thoughts about bitvid?**
|
- **Do you have any other suggestions or thoughts about bitvid?**
|
||||||
${otherSuggestions || "N/A"}
|
${otherSuggestions || "N/A"}
|
||||||
- **Would you like to be contacted for follow-up discussions?** ${followUp || "N/A"}
|
- **Would you like to be contacted for follow-up discussions?** ${
|
||||||
|
followUp || "N/A"
|
||||||
|
}
|
||||||
- **Preferred contact method (if applicable):** ${preferredContact || "N/A"}
|
- **Preferred contact method (if applicable):** ${preferredContact || "N/A"}
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -294,7 +387,11 @@ For additional discussions, reach out via bitvid’s Nostr support channels.
|
|||||||
|
|
||||||
// Encrypt the feedback content.
|
// Encrypt the feedback content.
|
||||||
log("Encrypting feedback content (nip04)...");
|
log("Encrypting feedback content (nip04)...");
|
||||||
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, feedbackContent);
|
const ciphertext = await nip04.encrypt(
|
||||||
|
ephemeralPriv,
|
||||||
|
targetPubHex,
|
||||||
|
feedbackContent
|
||||||
|
);
|
||||||
log("[DEBUG] Ciphertext: " + ciphertext);
|
log("[DEBUG] Ciphertext: " + ciphertext);
|
||||||
log("Encryption done.");
|
log("Encryption done.");
|
||||||
|
|
||||||
@@ -306,7 +403,10 @@ For additional discussions, reach out via bitvid’s Nostr support channels.
|
|||||||
tags: [["p", targetPubHex]],
|
tags: [["p", targetPubHex]],
|
||||||
content: ciphertext,
|
content: ciphertext,
|
||||||
};
|
};
|
||||||
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
|
log(
|
||||||
|
"[DEBUG] Event template before finalizing: " +
|
||||||
|
JSON.stringify(eventTemplate)
|
||||||
|
);
|
||||||
|
|
||||||
// Finalize the event.
|
// Finalize the event.
|
||||||
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
||||||
@@ -324,7 +424,14 @@ For additional discussions, reach out via bitvid’s Nostr support channels.
|
|||||||
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
||||||
onEvent(foundEvent) {
|
onEvent(foundEvent) {
|
||||||
if (foundEvent.id === event.id) {
|
if (foundEvent.id === event.id) {
|
||||||
log("[" + url + "] => Found our feedback in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
|
log(
|
||||||
|
"[" +
|
||||||
|
url +
|
||||||
|
"] => Found our feedback in storage! ID: " +
|
||||||
|
foundEvent.id.slice(0, 8) +
|
||||||
|
"...",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose() {
|
onEose() {
|
||||||
@@ -333,7 +440,9 @@ For additional discussions, reach out via bitvid’s Nostr support channels.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Done. If the logs show that at least one relay accepted the event and the feedback was stored, it will be reviewed accordingly.");
|
log(
|
||||||
|
"Done. If the logs show that at least one relay accepted the event and the feedback was stored, it will be reviewed accordingly."
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("Error: " + err.message, "error");
|
log("Error: " + err.message, "error");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>bitvid Feature Request Form</title>
|
<title>bitvid Feature Request Form</title>
|
||||||
<!-- Link to your main stylesheet -->
|
<!-- Link to your main stylesheet -->
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="../../css/style.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Override for form page to match modal field styling */
|
/* Override for form page to match modal field styling */
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #E5E7EB;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input, textarea, and select styling */
|
/* Input, textarea, and select styling */
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
||||||
background-color: #1F2937;
|
background-color: #1f2937;
|
||||||
color: #F3F4F6;
|
color: #f3f4f6;
|
||||||
border: 1px solid #374151;
|
border: 1px solid #374151;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: #3B82F6;
|
border-color: #3b82f6;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 1px #3B82F6;
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox group styling */
|
/* Checkbox group styling */
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
/* Button styling */
|
/* Button styling */
|
||||||
button {
|
button {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
background: #3B82F6;
|
background: #3b82f6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
@@ -97,13 +97,13 @@
|
|||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: #F87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
.success {
|
.success {
|
||||||
color: #3B82F6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
.warn {
|
.warn {
|
||||||
color: #FACC15;
|
color: #facc15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar styling */
|
/* Custom Scrollbar styling */
|
||||||
@@ -115,12 +115,12 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #3B82F6;
|
background-color: #3b82f6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #3B82F6 transparent;
|
scrollbar-color: #3b82f6 transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Load nostr‑tools v2.10.4 -->
|
<!-- Load nostr‑tools v2.10.4 -->
|
||||||
@@ -130,38 +130,87 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<p>
|
<p>
|
||||||
Have an idea for improving bitvid? We’d love to hear it! Please use this form to request new features or enhancements. Your feedback helps shape the future of bitvid.
|
Have an idea for improving bitvid? We’d love to hear it! Please use
|
||||||
|
this form to request new features or enhancements. Your feedback helps
|
||||||
|
shape the future of bitvid.
|
||||||
</p>
|
</p>
|
||||||
<form id="feature-form">
|
<form id="feature-form">
|
||||||
<!-- Section 1: User Information -->
|
<!-- Section 1: User Information -->
|
||||||
<h2>1. User Information</h2>
|
<h2>1. User Information</h2>
|
||||||
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
|
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
|
||||||
<input type="text" id="userNpub" placeholder="Enter your npub (optional)" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="userNpub"
|
||||||
|
placeholder="Enter your npub (optional)"
|
||||||
|
/>
|
||||||
<p>Are you a (check all that apply):</p>
|
<p>Are you a (check all that apply):</p>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label><input type="checkbox" name="userRole" value="Viewer" /> Viewer</label>
|
<label
|
||||||
<label><input type="checkbox" name="userRole" value="Content Creator" /> Content Creator</label>
|
><input type="checkbox" name="userRole" value="Viewer" />
|
||||||
<label><input type="checkbox" name="userRole" value="Developer/Contributor" /> Developer / Contributor</label>
|
Viewer</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="userRole" value="Content Creator" />
|
||||||
|
Content Creator</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
name="userRole"
|
||||||
|
value="Developer/Contributor"
|
||||||
|
/>
|
||||||
|
Developer / Contributor</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section 2: Feature Request Details -->
|
<!-- Section 2: Feature Request Details -->
|
||||||
<h2>2. Feature Request Details</h2>
|
<h2>2. Feature Request Details</h2>
|
||||||
<label for="featureName">Feature Name:</label>
|
<label for="featureName">Feature Name:</label>
|
||||||
<input type="text" id="featureName" placeholder="Short, descriptive title" required />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="featureName"
|
||||||
|
placeholder="Short, descriptive title"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<label for="featureDescription">Describe the feature:</label>
|
<label for="featureDescription">Describe the feature:</label>
|
||||||
<textarea id="featureDescription" rows="3" placeholder="Explain in detail what the feature does and how it works"></textarea>
|
<textarea
|
||||||
|
id="featureDescription"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Explain in detail what the feature does and how it works"
|
||||||
|
></textarea>
|
||||||
<label for="featureImportance">Why is this feature important?</label>
|
<label for="featureImportance">Why is this feature important?</label>
|
||||||
<textarea id="featureImportance" rows="2" placeholder="Describe how this will improve the platform and benefit users"></textarea>
|
<textarea
|
||||||
|
id="featureImportance"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Describe how this will improve the platform and benefit users"
|
||||||
|
></textarea>
|
||||||
<label for="beneficiary">Who would benefit from this feature?</label>
|
<label for="beneficiary">Who would benefit from this feature?</label>
|
||||||
<input type="text" id="beneficiary" placeholder="e.g., Content Creators, Viewers, Both" />
|
<input
|
||||||
|
type="text"
|
||||||
|
id="beneficiary"
|
||||||
|
placeholder="e.g., Content Creators, Viewers, Both"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Section 3: Additional Information -->
|
<!-- Section 3: Additional Information -->
|
||||||
<h2>3. Additional Information</h2>
|
<h2>3. Additional Information</h2>
|
||||||
<label for="existingPlatforms">Are there existing platforms that have this feature?</label>
|
<label for="existingPlatforms"
|
||||||
<textarea id="existingPlatforms" rows="2" placeholder="Provide examples if applicable"></textarea>
|
>Are there existing platforms that have this feature?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="existingPlatforms"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Provide examples if applicable"
|
||||||
|
></textarea>
|
||||||
<label for="mockups">Do you have any mockups or examples?</label>
|
<label for="mockups">Do you have any mockups or examples?</label>
|
||||||
<textarea id="mockups" rows="2" placeholder="Links, screenshots, or diagrams"></textarea>
|
<textarea
|
||||||
<label for="willingToTest">Would you be willing to help test this feature if implemented?</label>
|
id="mockups"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Links, screenshots, or diagrams"
|
||||||
|
></textarea>
|
||||||
|
<label for="willingToTest"
|
||||||
|
>Would you be willing to help test this feature if
|
||||||
|
implemented?</label
|
||||||
|
>
|
||||||
<select id="willingToTest">
|
<select id="willingToTest">
|
||||||
<option value="">Select an option</option>
|
<option value="">Select an option</option>
|
||||||
<option value="Yes">Yes</option>
|
<option value="Yes">Yes</option>
|
||||||
@@ -172,12 +221,27 @@
|
|||||||
<h2>4. Priority & Impact</h2>
|
<h2>4. Priority & Impact</h2>
|
||||||
<p>How urgent is this feature?</p>
|
<p>How urgent is this feature?</p>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label><input type="checkbox" name="featurePriority" value="High" /> High (Essential for platform success)</label>
|
<label
|
||||||
<label><input type="checkbox" name="featurePriority" value="Medium" /> Medium (Would improve experience significantly)</label>
|
><input type="checkbox" name="featurePriority" value="High" />
|
||||||
<label><input type="checkbox" name="featurePriority" value="Low" /> Low (Nice to have, but not urgent)</label>
|
High (Essential for platform success)</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="featurePriority" value="Medium" />
|
||||||
|
Medium (Would improve experience significantly)</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
><input type="checkbox" name="featurePriority" value="Low" /> Low
|
||||||
|
(Nice to have, but not urgent)</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<label for="techChallenges">Does this feature require technical expertise to implement?</label>
|
<label for="techChallenges"
|
||||||
<textarea id="techChallenges" rows="2" placeholder="Describe any dependencies or challenges"></textarea>
|
>Does this feature require technical expertise to implement?</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="techChallenges"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Describe any dependencies or challenges"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<button type="submit">Submit Feature Request</button>
|
<button type="submit">Submit Feature Request</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -204,47 +268,76 @@
|
|||||||
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
log("NostrTools not loaded. Check console or ad-blockers.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
|
const {
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
finalizeEvent,
|
||||||
|
nip04,
|
||||||
|
nip19,
|
||||||
|
SimplePool,
|
||||||
|
Relay,
|
||||||
|
} = window.NostrTools;
|
||||||
|
|
||||||
// Set the recipient's NPUB (your personal NPUB)
|
// Set the recipient's NPUB (your personal NPUB)
|
||||||
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
const recipientNpub =
|
||||||
|
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
|
||||||
const RELAYS = [
|
const RELAYS = [
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net"
|
"wss://relay.primal.net",
|
||||||
];
|
];
|
||||||
const pool = new SimplePool();
|
const pool = new SimplePool();
|
||||||
|
|
||||||
document.getElementById("feature-form").addEventListener("submit", async (ev) => {
|
document
|
||||||
|
.getElementById("feature-form")
|
||||||
|
.addEventListener("submit", async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
clear();
|
clear();
|
||||||
try {
|
try {
|
||||||
// Section 1: User Information
|
// Section 1: User Information
|
||||||
const userNpub = document.getElementById("userNpub").value.trim();
|
const userNpub = document.getElementById("userNpub").value.trim();
|
||||||
const roleNodes = document.querySelectorAll('input[name="userRole"]:checked');
|
const roleNodes = document.querySelectorAll(
|
||||||
|
'input[name="userRole"]:checked'
|
||||||
|
);
|
||||||
let userRoles = [];
|
let userRoles = [];
|
||||||
roleNodes.forEach(node => {
|
roleNodes.forEach((node) => {
|
||||||
userRoles.push(node.value);
|
userRoles.push(node.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Section 2: Feature Request Details
|
// Section 2: Feature Request Details
|
||||||
const featureName = document.getElementById("featureName").value.trim();
|
const featureName = document
|
||||||
const featureDescription = document.getElementById("featureDescription").value.trim();
|
.getElementById("featureName")
|
||||||
const featureImportance = document.getElementById("featureImportance").value.trim();
|
.value.trim();
|
||||||
const beneficiary = document.getElementById("beneficiary").value.trim();
|
const featureDescription = document
|
||||||
|
.getElementById("featureDescription")
|
||||||
|
.value.trim();
|
||||||
|
const featureImportance = document
|
||||||
|
.getElementById("featureImportance")
|
||||||
|
.value.trim();
|
||||||
|
const beneficiary = document
|
||||||
|
.getElementById("beneficiary")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 3: Additional Information
|
// Section 3: Additional Information
|
||||||
const existingPlatforms = document.getElementById("existingPlatforms").value.trim();
|
const existingPlatforms = document
|
||||||
|
.getElementById("existingPlatforms")
|
||||||
|
.value.trim();
|
||||||
const mockups = document.getElementById("mockups").value.trim();
|
const mockups = document.getElementById("mockups").value.trim();
|
||||||
const willingToTest = document.getElementById("willingToTest").value.trim();
|
const willingToTest = document
|
||||||
|
.getElementById("willingToTest")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Section 4: Priority & Impact
|
// Section 4: Priority & Impact
|
||||||
const priorityNodes = document.querySelectorAll('input[name="featurePriority"]:checked');
|
const priorityNodes = document.querySelectorAll(
|
||||||
|
'input[name="featurePriority"]:checked'
|
||||||
|
);
|
||||||
let featurePriorities = [];
|
let featurePriorities = [];
|
||||||
priorityNodes.forEach(node => {
|
priorityNodes.forEach((node) => {
|
||||||
featurePriorities.push(node.value);
|
featurePriorities.push(node.value);
|
||||||
});
|
});
|
||||||
const techChallenges = document.getElementById("techChallenges").value.trim();
|
const techChallenges = document
|
||||||
|
.getElementById("techChallenges")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
// Construct Markdown feature request
|
// Construct Markdown feature request
|
||||||
const featureRequestContent = `
|
const featureRequestContent = `
|
||||||
@@ -270,7 +363,9 @@ Have an idea for improving bitvid? We’d love to hear it! Please use this form
|
|||||||
${existingPlatforms || "N/A"}
|
${existingPlatforms || "N/A"}
|
||||||
- **Do you have any mockups or examples?**
|
- **Do you have any mockups or examples?**
|
||||||
${mockups || "N/A"}
|
${mockups || "N/A"}
|
||||||
- **Would you be willing to help test this feature if implemented?** ${willingToTest || "N/A"}
|
- **Would you be willing to help test this feature if implemented?** ${
|
||||||
|
willingToTest || "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
## **4. Priority & Impact**
|
## **4. Priority & Impact**
|
||||||
- **How urgent is this feature?**
|
- **How urgent is this feature?**
|
||||||
@@ -286,7 +381,10 @@ All feature requests are reviewed, but not all may be implemented. We prioritize
|
|||||||
For further discussions, reach out through bitvid’s Nostr support channels.
|
For further discussions, reach out through bitvid’s Nostr support channels.
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
log("[DEBUG] Constructed feature request content:\n" + featureRequestContent);
|
log(
|
||||||
|
"[DEBUG] Constructed feature request content:\n" +
|
||||||
|
featureRequestContent
|
||||||
|
);
|
||||||
|
|
||||||
// Decode the recipient NPUB
|
// Decode the recipient NPUB
|
||||||
log("Decoding recipient npub...");
|
log("Decoding recipient npub...");
|
||||||
@@ -306,7 +404,11 @@ For further discussions, reach out through bitvid’s Nostr support channels.
|
|||||||
|
|
||||||
// Encrypt the feature request content
|
// Encrypt the feature request content
|
||||||
log("Encrypting feature request content (nip04)...");
|
log("Encrypting feature request content (nip04)...");
|
||||||
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, featureRequestContent);
|
const ciphertext = await nip04.encrypt(
|
||||||
|
ephemeralPriv,
|
||||||
|
targetPubHex,
|
||||||
|
featureRequestContent
|
||||||
|
);
|
||||||
log("[DEBUG] Ciphertext: " + ciphertext);
|
log("[DEBUG] Ciphertext: " + ciphertext);
|
||||||
log("Encryption done.");
|
log("Encryption done.");
|
||||||
|
|
||||||
@@ -318,7 +420,10 @@ For further discussions, reach out through bitvid’s Nostr support channels.
|
|||||||
tags: [["p", targetPubHex]],
|
tags: [["p", targetPubHex]],
|
||||||
content: ciphertext,
|
content: ciphertext,
|
||||||
};
|
};
|
||||||
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
|
log(
|
||||||
|
"[DEBUG] Event template before finalizing: " +
|
||||||
|
JSON.stringify(eventTemplate)
|
||||||
|
);
|
||||||
|
|
||||||
// Finalize the event
|
// Finalize the event
|
||||||
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
const event = finalizeEvent(eventTemplate, ephemeralPriv);
|
||||||
@@ -336,7 +441,14 @@ For further discussions, reach out through bitvid’s Nostr support channels.
|
|||||||
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
|
||||||
onEvent(foundEvent) {
|
onEvent(foundEvent) {
|
||||||
if (foundEvent.id === event.id) {
|
if (foundEvent.id === event.id) {
|
||||||
log("[" + url + "] => Found our feature request in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
|
log(
|
||||||
|
"[" +
|
||||||
|
url +
|
||||||
|
"] => Found our feature request in storage! ID: " +
|
||||||
|
foundEvent.id.slice(0, 8) +
|
||||||
|
"...",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEose() {
|
onEose() {
|
||||||
@@ -345,7 +457,9 @@ For further discussions, reach out through bitvid’s Nostr support channels.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Done. If the logs show that at least one relay accepted the event and the feature request was stored, it will be reviewed accordingly.");
|
log(
|
||||||
|
"Done. If the logs show that at least one relay accepted the event and the feature request was stored, it will be reviewed accordingly."
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("Error: " + err.message, "error");
|
log("Error: " + err.message, "error");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@@ -475,32 +475,66 @@
|
|||||||
if (appModal) {
|
if (appModal) {
|
||||||
appModal.classList.add("hidden");
|
appModal.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
// ADDED: If user has not seen disclaimer yet, show it after application modal is closed
|
||||||
|
if (!localStorage.getItem("hasSeenDisclaimer")) {
|
||||||
|
const disclaimerModal =
|
||||||
|
document.getElementById("disclaimerModal");
|
||||||
|
if (disclaimerModal) {
|
||||||
|
disclaimerModal.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// 5) ?modal=appeals => open content appeals form
|
// 5) ?modal=appeals => open content appeals form
|
||||||
|
// ?modal=application => open application form
|
||||||
//
|
//
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const modalParam = urlParams.get("modal");
|
const modalParam = urlParams.get("modal");
|
||||||
|
|
||||||
if (modalParam === "appeals") {
|
if (modalParam === "appeals") {
|
||||||
const appealsModal = document.getElementById("contentAppealsModal");
|
const appealsModal = document.getElementById("contentAppealsModal");
|
||||||
if (appealsModal) {
|
if (appealsModal) {
|
||||||
appealsModal.classList.remove("hidden");
|
appealsModal.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADDED: After user closes appeals, show disclaimer if needed
|
||||||
|
const closeAppealsBtn = document.getElementById(
|
||||||
|
"closeContentAppealsModal"
|
||||||
|
);
|
||||||
|
if (closeAppealsBtn) {
|
||||||
|
closeAppealsBtn.addEventListener("click", () => {
|
||||||
|
appealsModal.classList.add("hidden");
|
||||||
|
if (!localStorage.getItem("hasSeenDisclaimer")) {
|
||||||
|
const disclaimerModal =
|
||||||
|
document.getElementById("disclaimerModal");
|
||||||
|
if (disclaimerModal) {
|
||||||
|
disclaimerModal.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
//
|
}
|
||||||
// 5.1) ?modal=application => open application form
|
});
|
||||||
//
|
}
|
||||||
else if (modalParam === "application") {
|
} else if (modalParam === "application") {
|
||||||
|
// Show application form, but DO NOT show disclaimer until user closes
|
||||||
const appModal = document.getElementById("nostrFormModal");
|
const appModal = document.getElementById("nostrFormModal");
|
||||||
if (appModal) {
|
if (appModal) {
|
||||||
appModal.classList.remove("hidden");
|
appModal.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
// Note: The close event above (closeNostrFormBtn) handles the disclaimer after closing.
|
||||||
|
} else {
|
||||||
|
// If there's no special param in the URL, we can consider showing the disclaimer right away
|
||||||
|
const hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
|
||||||
|
if (!hasSeenDisclaimer) {
|
||||||
|
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||||
|
if (disclaimerModal) {
|
||||||
|
disclaimerModal.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// 6) Close content appeals modal
|
// 6) Close content appeals modal (needed if user navigates w/o param, then opens appeals)
|
||||||
//
|
//
|
||||||
const closeAppealsBtn = document.getElementById(
|
const closeAppealsBtn = document.getElementById(
|
||||||
"closeContentAppealsModal"
|
"closeContentAppealsModal"
|
||||||
@@ -515,23 +549,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// 7) Show disclaimer modal on page load, hide on "I Understand"
|
// 7) Disclaimer 'I Understand' Button
|
||||||
//
|
//
|
||||||
const disclaimerModal = document.getElementById("disclaimerModal");
|
|
||||||
const acceptDisclaimerBtn = document.getElementById("acceptDisclaimer");
|
const acceptDisclaimerBtn = document.getElementById("acceptDisclaimer");
|
||||||
if (disclaimerModal) {
|
|
||||||
// Show immediately
|
|
||||||
disclaimerModal.classList.remove("hidden");
|
|
||||||
if (acceptDisclaimerBtn) {
|
if (acceptDisclaimerBtn) {
|
||||||
acceptDisclaimerBtn.addEventListener("click", () => {
|
acceptDisclaimerBtn.addEventListener("click", () => {
|
||||||
|
// Hide disclaimer
|
||||||
|
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||||
|
if (disclaimerModal) {
|
||||||
disclaimerModal.classList.add("hidden");
|
disclaimerModal.classList.add("hidden");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// Store the fact that user has seen it
|
||||||
|
localStorage.setItem("hasSeenDisclaimer", "true");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// 8) Query param checks for the three new forms
|
// 8) Query param checks for the three new forms
|
||||||
//
|
|
||||||
// https://bitvid.network?modal=feedback => open generalFeedbackModal
|
// https://bitvid.network?modal=feedback => open generalFeedbackModal
|
||||||
// https://bitvid.network?modal=feature => open featureRequestModal
|
// https://bitvid.network?modal=feature => open featureRequestModal
|
||||||
// https://bitvid.network?modal=bug => open bugFixModal
|
// https://bitvid.network?modal=bug => open bugFixModal
|
||||||
@@ -599,6 +633,10 @@
|
|||||||
|
|
||||||
<!-- Other Scripts -->
|
<!-- Other Scripts -->
|
||||||
<script src="js/libs/nostr.bundle.js"></script>
|
<script src="js/libs/nostr.bundle.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.8.3";
|
||||||
|
window.NostrTools = { nip19, SimplePool };
|
||||||
|
</script>
|
||||||
<script type="module" src="js/config.js"></script>
|
<script type="module" src="js/config.js"></script>
|
||||||
<script type="module" src="js/lists.js"></script>
|
<script type="module" src="js/lists.js"></script>
|
||||||
<script type="module" src="js/accessControl.js"></script>
|
<script type="module" src="js/accessControl.js"></script>
|
||||||
|
174
src/js/app.js
174
src/js/app.js
@@ -5,6 +5,7 @@ import { nostrClient } from "./nostr.js";
|
|||||||
import { torrentClient } from "./webtorrent.js";
|
import { torrentClient } from "./webtorrent.js";
|
||||||
import { isDevMode } from "./config.js";
|
import { isDevMode } from "./config.js";
|
||||||
import { disclaimerModal } from "./disclaimer.js";
|
import { disclaimerModal } from "./disclaimer.js";
|
||||||
|
import { initialBlacklist, initialEventBlacklist } from "./lists.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple "decryption" placeholder for private videos.
|
* Simple "decryption" placeholder for private videos.
|
||||||
@@ -82,6 +83,27 @@ class bitvidApp {
|
|||||||
// NEW: reference to the login modal's close button
|
// NEW: reference to the login modal's close button
|
||||||
this.closeLoginModalBtn =
|
this.closeLoginModalBtn =
|
||||||
document.getElementById("closeLoginModal") || null;
|
document.getElementById("closeLoginModal") || null;
|
||||||
|
|
||||||
|
// Build a set of blacklisted event IDs (hex) from nevent strings, skipping empties
|
||||||
|
this.blacklistedEventIds = new Set();
|
||||||
|
for (const neventStr of initialEventBlacklist) {
|
||||||
|
// Skip any empty or obviously invalid strings
|
||||||
|
if (!neventStr || neventStr.trim().length < 8) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const decoded = window.NostrTools.nip19.decode(neventStr);
|
||||||
|
if (decoded.type === "nevent" && decoded.data.id) {
|
||||||
|
this.blacklistedEventIds.add(decoded.data.id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
"[bitvidApp] Invalid nevent in blacklist:",
|
||||||
|
neventStr,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
forceRefreshAllProfiles() {
|
forceRefreshAllProfiles() {
|
||||||
@@ -99,6 +121,13 @@ class bitvidApp {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
|
// Force update of any registered service workers to ensure latest code is used.
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||||
|
registrations.forEach((registration) => registration.update());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Initialize the video modal (components/video-modal.html)
|
// 1. Initialize the video modal (components/video-modal.html)
|
||||||
await this.initModal();
|
await this.initModal();
|
||||||
this.updateModalElements();
|
this.updateModalElements();
|
||||||
@@ -693,51 +722,58 @@ class bitvidApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to new videos & render them.
|
* Subscribe to videos (older + new) and render them as they come in.
|
||||||
*/
|
*/
|
||||||
// js/app.js
|
|
||||||
|
|
||||||
async loadVideos() {
|
async loadVideos() {
|
||||||
console.log("Starting loadVideos...");
|
console.log("Starting loadVideos...");
|
||||||
|
|
||||||
// 1) If there's an existing subscription, unsubscribe it
|
// We do NOT decode initialEventBlacklist here.
|
||||||
if (this.videoSubscription) {
|
// That happens once in the constructor, creating this.blacklistedEventIds.
|
||||||
this.videoSubscription.unsub();
|
|
||||||
this.videoSubscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Show "Loading..." message
|
if (!this.videoSubscription) {
|
||||||
if (this.videoList) {
|
if (this.videoList) {
|
||||||
this.videoList.innerHTML = `
|
this.videoList.innerHTML = `
|
||||||
<p class="text-center text-gray-500">
|
<p class="text-center text-gray-500">
|
||||||
Loading videos...
|
Loading videos as they arrive...
|
||||||
</p>`;
|
</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Create a single subscription
|
||||||
// 3) Force a bulk fetch
|
this.videoSubscription = nostrClient.subscribeVideos(() => {
|
||||||
await nostrClient.fetchVideos();
|
const updatedAll = nostrClient.getActiveVideos();
|
||||||
|
|
||||||
// 4) Instead of reusing the entire fetched array,
|
// Filter out blacklisted authors & blacklisted event IDs
|
||||||
// use getActiveVideos() for the final display:
|
const filteredVideos = updatedAll.filter((video) => {
|
||||||
const newestActive = nostrClient.getActiveVideos();
|
// 1) If the event ID is in our blacklisted set, skip
|
||||||
this.renderVideoList(newestActive);
|
if (this.blacklistedEventIds.has(video.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 5) Subscribe for updates
|
// 2) Check author (if you’re also blacklisting authors by npub)
|
||||||
this.videoSubscription = nostrClient.subscribeVideos((video) => {
|
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||||
// Whenever we get a new or updated event, re-render the newest set:
|
if (initialBlacklist.includes(authorNpub)) {
|
||||||
const activeAll = nostrClient.getActiveVideos();
|
return false;
|
||||||
this.renderVideoList(activeAll);
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
console.error("Could not load videos:", err);
|
this.renderVideoList(filteredVideos);
|
||||||
this.showError("Could not load videos from relays.");
|
});
|
||||||
if (this.videoList) {
|
} else {
|
||||||
this.videoList.innerHTML = `
|
// Already subscribed: just show what's cached
|
||||||
<p class="text-center text-gray-500">
|
const allCached = nostrClient.getActiveVideos();
|
||||||
No videos available at this time.
|
|
||||||
</p>`;
|
const filteredCached = allCached.filter((video) => {
|
||||||
|
if (this.blacklistedEventIds.has(video.id)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||||
|
return !initialBlacklist.includes(authorNpub);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderVideoList(filteredCached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,7 +800,7 @@ class bitvidApp {
|
|||||||
|
|
||||||
if (!videos || videos.length === 0) {
|
if (!videos || videos.length === 0) {
|
||||||
this.videoList.innerHTML = `
|
this.videoList.innerHTML = `
|
||||||
<p class="text-center text-gray-500">
|
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
|
||||||
No public videos available yet. Be the first to upload one!
|
No public videos available yet. Be the first to upload one!
|
||||||
</p>`;
|
</p>`;
|
||||||
return;
|
return;
|
||||||
@@ -1017,26 +1053,27 @@ class bitvidApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up the video in our subscription map
|
// 1) Check local 'videosMap' or 'nostrClient.getActiveVideos()'
|
||||||
let matchedVideo = Array.from(this.videosMap.values()).find(
|
let matchedVideo = Array.from(this.videosMap.values()).find(
|
||||||
(v) => v.magnet === decodedMagnet
|
(v) => v.magnet === decodedMagnet
|
||||||
);
|
);
|
||||||
|
|
||||||
// If not found in the map, do a fallback fetch
|
|
||||||
if (!matchedVideo) {
|
if (!matchedVideo) {
|
||||||
const allVideos = await nostrClient.fetchVideos();
|
// Instead of forcing a full `fetchVideos()`,
|
||||||
matchedVideo = allVideos.find((v) => v.magnet === decodedMagnet);
|
// try looking in the activeVideos from local cache:
|
||||||
|
const activeVideos = nostrClient.getActiveVideos();
|
||||||
|
matchedVideo = activeVideos.find((v) => v.magnet === decodedMagnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If still not found, you can do a single event-based approach or just show an error:
|
||||||
if (!matchedVideo) {
|
if (!matchedVideo) {
|
||||||
this.showError("No matching video found.");
|
this.showError("No matching video found in local cache.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update our tracking
|
// Update tracking
|
||||||
this.currentMagnetUri = decodedMagnet;
|
this.currentMagnetUri = decodedMagnet;
|
||||||
|
|
||||||
// Hand off to the method that already sets modal fields and streams
|
// Delegate to the main method
|
||||||
await this.playVideoByEventId(matchedVideo.id);
|
await this.playVideoByEventId(matchedVideo.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in playVideo:", error);
|
console.error("Error in playVideo:", error);
|
||||||
@@ -1172,6 +1209,10 @@ class bitvidApp {
|
|||||||
|
|
||||||
// 8) Refresh local UI
|
// 8) Refresh local UI
|
||||||
await this.loadVideos();
|
await this.loadVideos();
|
||||||
|
|
||||||
|
// 8.1) Purge the outdated cache
|
||||||
|
this.videosMap.clear();
|
||||||
|
|
||||||
this.showSuccess("Video updated successfully!");
|
this.showSuccess("Video updated successfully!");
|
||||||
|
|
||||||
// 9) Also refresh all profile caches so any new name/pic changes are reflected
|
// 9) Also refresh all profile caches so any new name/pic changes are reflected
|
||||||
@@ -1309,16 +1350,20 @@ class bitvidApp {
|
|||||||
* Helper to open a video by event ID (like ?v=...).
|
* Helper to open a video by event ID (like ?v=...).
|
||||||
*/
|
*/
|
||||||
async playVideoByEventId(eventId) {
|
async playVideoByEventId(eventId) {
|
||||||
|
// First, check if this event is blacklisted
|
||||||
|
if (this.blacklistedEventIds.has(eventId)) {
|
||||||
|
this.showError("This content has been removed or is not allowed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Check local subscription map
|
// 1) Check local subscription map
|
||||||
let video = this.videosMap.get(eventId);
|
let video = this.videosMap.get(eventId);
|
||||||
|
|
||||||
// 2) If not in local map, attempt fallback fetch from getOldEventById
|
// 2) If not in local map, attempt fallback fetch from getOldEventById
|
||||||
if (!video) {
|
if (!video) {
|
||||||
video = await this.getOldEventById(eventId);
|
video = await this.getOldEventById(eventId);
|
||||||
}
|
}
|
||||||
|
// 3) If still not found, show error and return
|
||||||
// 3) If still no luck, show error and return
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
this.showError("Video not found.");
|
this.showError("Video not found.");
|
||||||
return;
|
return;
|
||||||
@@ -1368,8 +1413,9 @@ class bitvidApp {
|
|||||||
|
|
||||||
// 8) Render video details in modal
|
// 8) Render video details in modal
|
||||||
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
|
||||||
if (this.videoTitle)
|
if (this.videoTitle) {
|
||||||
this.videoTitle.textContent = video.title || "Untitled";
|
this.videoTitle.textContent = video.title || "Untitled";
|
||||||
|
}
|
||||||
if (this.videoDescription) {
|
if (this.videoDescription) {
|
||||||
this.videoDescription.textContent =
|
this.videoDescription.textContent =
|
||||||
video.description || "No description available.";
|
video.description || "No description available.";
|
||||||
@@ -1391,14 +1437,18 @@ class bitvidApp {
|
|||||||
this.creatorAvatar.alt = creatorProfile.name;
|
this.creatorAvatar.alt = creatorProfile.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9) Stream torrent
|
// 9) Clean up any existing torrent instance before starting a new stream
|
||||||
this.log("Starting video stream with:", video.magnet);
|
await torrentClient.cleanup();
|
||||||
|
// 10) Append a cache-busting parameter to the magnet URI
|
||||||
|
const cacheBustedMagnet = video.magnet + "&ts=" + Date.now();
|
||||||
|
this.log("Starting video stream with:", cacheBustedMagnet);
|
||||||
|
|
||||||
const realTorrent = await torrentClient.streamVideo(
|
const realTorrent = await torrentClient.streamVideo(
|
||||||
video.magnet,
|
cacheBustedMagnet,
|
||||||
this.modalVideo
|
this.modalVideo
|
||||||
);
|
);
|
||||||
|
|
||||||
// 10) Start intervals to update stats
|
// 11) Start intervals to update stats
|
||||||
const updateInterval = setInterval(() => {
|
const updateInterval = setInterval(() => {
|
||||||
if (!document.body.contains(this.modalVideo)) {
|
if (!document.body.contains(this.modalVideo)) {
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
@@ -1465,27 +1515,31 @@ class bitvidApp {
|
|||||||
return video;
|
return video;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Bulk fetch from relays
|
// 2) Already in nostrClient.allEvents?
|
||||||
const allFromBulk = await nostrClient.fetchVideos();
|
// (assuming nostrClient.allEvents is a Map of id => video)
|
||||||
|
const fromAll = nostrClient.allEvents.get(eventId);
|
||||||
// 2a) Deduplicate so we only keep newest version per root
|
if (fromAll && !fromAll.deleted) {
|
||||||
const newestPerRoot = dedupeToNewestByRoot(allFromBulk);
|
this.videosMap.set(eventId, fromAll);
|
||||||
|
return fromAll;
|
||||||
// 2b) Find the requested ID within the deduplicated set
|
|
||||||
video = newestPerRoot.find((v) => v.id === eventId);
|
|
||||||
if (video) {
|
|
||||||
// Store it in our local map, so we can open it instantly next time
|
|
||||||
this.videosMap.set(video.id, video);
|
|
||||||
return video;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Final fallback: direct single-event fetch
|
// 3) Direct single-event fetch (fewer resources than full fetchVideos)
|
||||||
const single = await nostrClient.getEventById(eventId);
|
const single = await nostrClient.getEventById(eventId);
|
||||||
if (single && !single.deleted) {
|
if (single && !single.deleted) {
|
||||||
this.videosMap.set(single.id, single);
|
this.videosMap.set(single.id, single);
|
||||||
return 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
|
// Not found or was deleted
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
// js/lists.js
|
// js/lists.js
|
||||||
|
|
||||||
|
// Whitelist of npubs that can access the video upload functions
|
||||||
const npubs = [
|
const npubs = [
|
||||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe", // bitvid
|
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe", // bitvid
|
||||||
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx", // thePR0M3TH3AN
|
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx", // thePR0M3TH3AN
|
||||||
@@ -10,9 +11,14 @@ const npubs = [
|
|||||||
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
|
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
|
||||||
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
|
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
|
||||||
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
|
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
|
||||||
|
"npub13qexjtmajssuhz8gdchgx65dwsnr705drse294zz5vt4e78ya2vqzyg8lv", // SatoshiSignal
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
console.log("DEBUG: lists.js loaded, npubs:", npubs);
|
||||||
|
|
||||||
|
// Blacklist of npubs that events will not be displayed in the bitvid official client
|
||||||
export const initialWhitelist = npubs;
|
export const initialWhitelist = npubs;
|
||||||
export const initialBlacklist = [""];
|
export const initialBlacklist = [""];
|
||||||
|
|
||||||
|
// Block specific events with the nevent
|
||||||
|
export const initialEventBlacklist = [""];
|
||||||
|
312
src/js/nostr.js
312
src/js/nostr.js
@@ -3,15 +3,18 @@
|
|||||||
import { isDevMode } from "./config.js";
|
import { isDevMode } from "./config.js";
|
||||||
import { accessControl } from "./accessControl.js";
|
import { accessControl } from "./accessControl.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The usual relays
|
||||||
|
*/
|
||||||
const RELAY_URLS = [
|
const RELAY_URLS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://nos.lol",
|
"wss://nos.lol",
|
||||||
"wss://relay.snort.social",
|
"wss://relay.snort.social",
|
||||||
"wss://nostr.wine",
|
"wss://relay.primal.net",
|
||||||
"wss://relay.nostr.band",
|
"wss://relay.nostr.band",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Just a helper to keep error spam in check
|
// To limit error spam
|
||||||
let errorLogCount = 0;
|
let errorLogCount = 0;
|
||||||
const MAX_ERROR_LOGS = 100;
|
const MAX_ERROR_LOGS = 100;
|
||||||
function logErrorOnce(message, eventContent = null) {
|
function logErrorOnce(message, eventContent = null) {
|
||||||
@@ -31,7 +34,7 @@ function logErrorOnce(message, eventContent = null) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Example "encryption" that just reverses strings.
|
* Example "encryption" that just reverses strings.
|
||||||
* In real usage, swap with actual crypto.
|
* In real usage, replace with actual crypto.
|
||||||
*/
|
*/
|
||||||
function fakeEncrypt(magnet) {
|
function fakeEncrypt(magnet) {
|
||||||
return magnet.split("").reverse().join("");
|
return magnet.split("").reverse().join("");
|
||||||
@@ -42,39 +45,61 @@ function fakeDecrypt(encrypted) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a raw Nostr event => your "video" object.
|
* Convert a raw Nostr event => your "video" object.
|
||||||
|
* CHANGED: skip if version <2
|
||||||
*/
|
*/
|
||||||
function convertEventToVideo(event) {
|
function convertEventToVideo(event) {
|
||||||
|
try {
|
||||||
const content = JSON.parse(event.content || "{}");
|
const content = JSON.parse(event.content || "{}");
|
||||||
|
|
||||||
|
// Example checks:
|
||||||
|
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 {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
// If content.videoRootId is missing, use event.id as a fallback
|
|
||||||
videoRootId: content.videoRootId || event.id,
|
videoRootId: content.videoRootId || event.id,
|
||||||
version: content.version ?? 1,
|
version: content.version,
|
||||||
isPrivate: content.isPrivate ?? false,
|
isPrivate: content.isPrivate ?? false,
|
||||||
title: content.title || "",
|
title: content.title ?? "",
|
||||||
magnet: content.magnet || "",
|
magnet: content.magnet ?? "",
|
||||||
thumbnail: content.thumbnail || "",
|
thumbnail: content.thumbnail ?? "",
|
||||||
description: content.description || "",
|
description: content.description ?? "",
|
||||||
mode: content.mode || "live",
|
mode: content.mode ?? "live",
|
||||||
deleted: content.deleted === true,
|
deleted: content.deleted === true,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
created_at: event.created_at,
|
created_at: event.created_at,
|
||||||
tags: event.tags,
|
tags: event.tags,
|
||||||
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// JSON parse error
|
||||||
|
return { id: event.id, invalid: true, reason: "json parse error" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key each "active" video by its root ID => so you only store
|
* If the video has videoRootId => use that as the “group key”.
|
||||||
* the newest version for each root. But for older events w/o videoRootId,
|
* Otherwise fallback to (pubkey + dTag), or if no dTag => “LEGACY:id”
|
||||||
* or w/o 'd' tag, we handle fallback logic below.
|
|
||||||
*/
|
*/
|
||||||
function getActiveKey(video) {
|
function getActiveKey(video) {
|
||||||
// If it has a videoRootId, we use that
|
|
||||||
if (video.videoRootId) {
|
if (video.videoRootId) {
|
||||||
return `ROOT:${video.videoRootId}`;
|
return `ROOT:${video.videoRootId}`;
|
||||||
}
|
}
|
||||||
// Otherwise fallback to (pubkey + dTag) or if no dTag, fallback to event.id
|
|
||||||
// This is a fallback approach so older events appear in the "active map".
|
|
||||||
const dTag = video.tags?.find((t) => t[0] === "d");
|
const dTag = video.tags?.find((t) => t[0] === "d");
|
||||||
if (dTag) {
|
if (dTag) {
|
||||||
return `${video.pubkey}:${dTag[1]}`;
|
return `${video.pubkey}:${dTag[1]}`;
|
||||||
@@ -88,15 +113,15 @@ class NostrClient {
|
|||||||
this.pubkey = null;
|
this.pubkey = null;
|
||||||
this.relays = RELAY_URLS;
|
this.relays = RELAY_URLS;
|
||||||
|
|
||||||
// All events—old or new—so older share links still work
|
// Store all events so older links still work
|
||||||
this.allEvents = new Map();
|
this.allEvents = new Map();
|
||||||
|
|
||||||
// "activeMap" holds only the newest version for each root ID (or fallback).
|
// “activeMap” holds only the newest version for each root
|
||||||
this.activeMap = new Map();
|
this.activeMap = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to all configured relays
|
* Connect to the configured relays
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
if (isDevMode) console.log("Connecting to relays...");
|
if (isDevMode) console.log("Connecting to relays...");
|
||||||
@@ -107,7 +132,9 @@ class NostrClient {
|
|||||||
const successfulRelays = results
|
const successfulRelays = results
|
||||||
.filter((r) => r.success)
|
.filter((r) => r.success)
|
||||||
.map((r) => r.url);
|
.map((r) => r.url);
|
||||||
if (successfulRelays.length === 0) throw new Error("No relays connected");
|
if (successfulRelays.length === 0) {
|
||||||
|
throw new Error("No relays connected");
|
||||||
|
}
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
console.log(`Connected to ${successfulRelays.length} relay(s)`);
|
||||||
}
|
}
|
||||||
@@ -133,7 +160,6 @@ class NostrClient {
|
|||||||
sub.unsub();
|
sub.unsub();
|
||||||
resolve({ url, success: true });
|
resolve({ url, success: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
sub.on("event", succeed);
|
sub.on("event", succeed);
|
||||||
sub.on("eose", succeed);
|
sub.on("eose", succeed);
|
||||||
})
|
})
|
||||||
@@ -142,7 +168,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt Nostr extension login or abort
|
* Attempt login with a Nostr extension
|
||||||
*/
|
*/
|
||||||
async login() {
|
async login() {
|
||||||
try {
|
try {
|
||||||
@@ -152,7 +178,6 @@ class NostrClient {
|
|||||||
"Please install a Nostr extension (Alby, nos2x, etc.)."
|
"Please install a Nostr extension (Alby, nos2x, etc.)."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
const pubkey = await window.nostr.getPublicKey();
|
||||||
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
const npub = window.NostrTools.nip19.npubEncode(pubkey);
|
||||||
|
|
||||||
@@ -162,8 +187,7 @@ class NostrClient {
|
|||||||
console.log("Whitelist:", accessControl.getWhitelist());
|
console.log("Whitelist:", accessControl.getWhitelist());
|
||||||
console.log("Blacklist:", accessControl.getBlacklist());
|
console.log("Blacklist:", accessControl.getBlacklist());
|
||||||
}
|
}
|
||||||
|
// Access control
|
||||||
// Access control check
|
|
||||||
if (!accessControl.canAccess(npub)) {
|
if (!accessControl.canAccess(npub)) {
|
||||||
if (accessControl.isBlacklisted(npub)) {
|
if (accessControl.isBlacklisted(npub)) {
|
||||||
throw new Error("Your account has been blocked on this platform.");
|
throw new Error("Your account has been blocked on this platform.");
|
||||||
@@ -171,15 +195,14 @@ class NostrClient {
|
|||||||
throw new Error("Access restricted to whitelisted users only.");
|
throw new Error("Access restricted to whitelisted users only.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pubkey = pubkey;
|
this.pubkey = pubkey;
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
console.log("Logged in with extension. Pubkey:", this.pubkey);
|
console.log("Logged in with extension. Pubkey:", this.pubkey);
|
||||||
}
|
}
|
||||||
return this.pubkey;
|
return this.pubkey;
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error("Login error:", e);
|
console.error("Login error:", err);
|
||||||
throw e;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,17 +211,9 @@ class NostrClient {
|
|||||||
if (isDevMode) console.log("User logged out.");
|
if (isDevMode) console.log("User logged out.");
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeNsec(nsec) {
|
|
||||||
try {
|
|
||||||
const { data } = window.NostrTools.nip19.decode(nsec);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid NSEC key.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish a *new* video with a brand-new d tag & brand-new videoRootId
|
* Publish a new video
|
||||||
|
* CHANGED: Force version=2 for all new notes
|
||||||
*/
|
*/
|
||||||
async publishVideo(videoData, pubkey) {
|
async publishVideo(videoData, pubkey) {
|
||||||
if (!pubkey) throw new Error("Not logged in to publish video.");
|
if (!pubkey) throw new Error("Not logged in to publish video.");
|
||||||
@@ -212,13 +227,13 @@ class NostrClient {
|
|||||||
finalMagnet = fakeEncrypt(finalMagnet);
|
finalMagnet = fakeEncrypt(finalMagnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// new "videoRootId" ensures all future edits know they're from the same root
|
// brand-new root & d
|
||||||
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
const contentObject = {
|
const contentObject = {
|
||||||
videoRootId,
|
videoRootId,
|
||||||
version: videoData.version ?? 1,
|
version: 2, // forcibly set version=2
|
||||||
deleted: false,
|
deleted: false,
|
||||||
isPrivate: videoData.isPrivate ?? false,
|
isPrivate: videoData.isPrivate ?? false,
|
||||||
title: videoData.title || "",
|
title: videoData.title || "",
|
||||||
@@ -258,7 +273,6 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return signedEvent;
|
return signedEvent;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDevMode) console.error("Failed to sign/publish:", err);
|
if (isDevMode) console.error("Failed to sign/publish:", err);
|
||||||
@@ -270,71 +284,63 @@ class NostrClient {
|
|||||||
* Edits a video by creating a *new event* with a brand-new d tag,
|
* Edits a video by creating a *new event* with a brand-new d tag,
|
||||||
* but reuses the same videoRootId as the original.
|
* but reuses the same videoRootId as the original.
|
||||||
*
|
*
|
||||||
* => old link remains pinned to the old event, new link is a fresh ID.
|
* This version forces version=2 for the original note and uses
|
||||||
* => older version is overshadowed if your dedupe logic only shows newest.
|
* lowercase comparison for public keys.
|
||||||
*/
|
*/
|
||||||
async editVideo(originalEventStub, updatedData, pubkey) {
|
async editVideo(originalEventStub, updatedData, userPubkey) {
|
||||||
if (!pubkey) {
|
if (!userPubkey) {
|
||||||
throw new Error("Not logged in to edit.");
|
throw new Error("Not logged in to edit.");
|
||||||
}
|
}
|
||||||
if (!originalEventStub.pubkey || originalEventStub.pubkey !== pubkey) {
|
|
||||||
|
// Convert the provided pubkey to lowercase
|
||||||
|
const userPubkeyLower = userPubkey.toLowerCase();
|
||||||
|
|
||||||
|
// Use getEventById to fetch the full original event details
|
||||||
|
const baseEvent = await this.getEventById(originalEventStub.id);
|
||||||
|
if (!baseEvent) {
|
||||||
|
throw new Error("Could not retrieve the original event to edit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the original event is version 2 or higher
|
||||||
|
if (baseEvent.version < 2) {
|
||||||
|
throw new Error(
|
||||||
|
"This video is not in the supported version for editing."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ownership check (compare lowercase hex public keys)
|
||||||
|
if (
|
||||||
|
!baseEvent.pubkey ||
|
||||||
|
baseEvent.pubkey.toLowerCase() !== userPubkeyLower
|
||||||
|
) {
|
||||||
throw new Error("You do not own this video (pubkey mismatch).");
|
throw new Error("You do not own this video (pubkey mismatch).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Attempt to get the FULL old event details (especially videoRootId)
|
// Decrypt the old magnet if the note is private
|
||||||
let baseEvent = originalEventStub;
|
|
||||||
// If the caller didn't pass .videoRootId, fetch from local or relay:
|
|
||||||
if (!baseEvent.videoRootId) {
|
|
||||||
const fetched = await this.getEventById(originalEventStub.id);
|
|
||||||
if (!fetched) {
|
|
||||||
throw new Error("Could not retrieve the original event to edit.");
|
|
||||||
}
|
|
||||||
baseEvent = fetched;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) We now have baseEvent.videoRootId if it existed
|
|
||||||
let oldRootId = baseEvent.videoRootId || null;
|
|
||||||
|
|
||||||
// Decrypt the old magnet if it was private
|
|
||||||
let oldPlainMagnet = baseEvent.magnet || "";
|
let oldPlainMagnet = baseEvent.magnet || "";
|
||||||
if (baseEvent.isPrivate && oldPlainMagnet) {
|
if (baseEvent.isPrivate && oldPlainMagnet) {
|
||||||
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Decide new privacy
|
// Determine if the updated note should be private
|
||||||
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
|
||||||
|
|
||||||
// 4) Fallback to old magnet if none was provided
|
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
|
||||||
let finalPlainMagnet = (updatedData.magnet || "").trim();
|
let finalPlainMagnet = (updatedData.magnet || "").trim() || oldPlainMagnet;
|
||||||
if (!finalPlainMagnet) {
|
let finalMagnet = wantPrivate
|
||||||
finalPlainMagnet = oldPlainMagnet;
|
? fakeEncrypt(finalPlainMagnet)
|
||||||
}
|
: finalPlainMagnet;
|
||||||
|
|
||||||
// 5) Re-encrypt if user wants private
|
// Use the existing videoRootId (or fall back to the base event's ID)
|
||||||
let finalMagnet = finalPlainMagnet;
|
const oldRootId = baseEvent.videoRootId || baseEvent.id;
|
||||||
if (wantPrivate) {
|
|
||||||
finalMagnet = fakeEncrypt(finalPlainMagnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6) If there's no root yet (legacy), use the old event's own ID.
|
// Generate a new d-tag so that the edit gets its own share link
|
||||||
// Otherwise keep the existing rootId.
|
|
||||||
if (!oldRootId) {
|
|
||||||
oldRootId = baseEvent.id;
|
|
||||||
if (isDevMode) {
|
|
||||||
console.log(
|
|
||||||
"No existing root => using baseEvent.id as root:",
|
|
||||||
oldRootId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a brand-new d-tag so it doesn't overshadow the old share link
|
|
||||||
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
// 7) Build updated content
|
// Build the updated content object
|
||||||
const contentObject = {
|
const contentObject = {
|
||||||
videoRootId: oldRootId,
|
videoRootId: oldRootId,
|
||||||
version: updatedData.version ?? baseEvent.version ?? 1,
|
version: updatedData.version ?? baseEvent.version ?? 2,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
isPrivate: wantPrivate,
|
isPrivate: wantPrivate,
|
||||||
title: updatedData.title ?? baseEvent.title,
|
title: updatedData.title ?? baseEvent.title,
|
||||||
@@ -346,11 +352,12 @@ class NostrClient {
|
|||||||
|
|
||||||
const event = {
|
const event = {
|
||||||
kind: 30078,
|
kind: 30078,
|
||||||
pubkey,
|
// Use the provided userPubkey (or you can also force it to lowercase here if desired)
|
||||||
|
pubkey: userPubkeyLower,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [
|
tags: [
|
||||||
["t", "video"],
|
["t", "video"],
|
||||||
["d", newD], // new share link
|
["d", newD], // new share link tag
|
||||||
],
|
],
|
||||||
content: JSON.stringify(contentObject),
|
content: JSON.stringify(contentObject),
|
||||||
};
|
};
|
||||||
@@ -360,7 +367,6 @@ class NostrClient {
|
|||||||
console.log("Event content:", event.content);
|
console.log("Event content:", event.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8) Sign and publish the new event
|
|
||||||
try {
|
try {
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
const signedEvent = await window.nostr.signEvent(event);
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
@@ -381,6 +387,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return signedEvent;
|
return signedEvent;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Edit failed:", err);
|
console.error("Edit failed:", err);
|
||||||
@@ -389,7 +396,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Reverting" => we just mark the most recent content as {deleted:true} and blank out magnet/desc
|
* revertVideo => old style
|
||||||
*/
|
*/
|
||||||
async revertVideo(originalEvent, pubkey) {
|
async revertVideo(originalEvent, pubkey) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
@@ -399,7 +406,6 @@ class NostrClient {
|
|||||||
throw new Error("Not your event (pubkey mismatch).");
|
throw new Error("Not your event (pubkey mismatch).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If front-end didn't pass the tags array, load the full event:
|
|
||||||
let baseEvent = originalEvent;
|
let baseEvent = originalEvent;
|
||||||
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
|
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
|
||||||
const fetched = await this.getEventById(originalEvent.id);
|
const fetched = await this.getEventById(originalEvent.id);
|
||||||
@@ -423,7 +429,6 @@ class NostrClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check d-tag
|
|
||||||
const dTag = baseEvent.tags.find((t) => t[0] === "d");
|
const dTag = baseEvent.tags.find((t) => t[0] === "d");
|
||||||
if (!dTag) {
|
if (!dTag) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -435,17 +440,15 @@ class NostrClient {
|
|||||||
const oldContent = JSON.parse(baseEvent.content || "{}");
|
const oldContent = JSON.parse(baseEvent.content || "{}");
|
||||||
const oldVersion = oldContent.version ?? 1;
|
const oldVersion = oldContent.version ?? 1;
|
||||||
|
|
||||||
// If no root, fallback
|
|
||||||
let finalRootId = oldContent.videoRootId || null;
|
let finalRootId = oldContent.videoRootId || null;
|
||||||
if (!finalRootId) {
|
if (!finalRootId) {
|
||||||
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
|
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build “deleted: true” overshadow event => revert current version
|
|
||||||
const contentObject = {
|
const contentObject = {
|
||||||
videoRootId: finalRootId,
|
videoRootId: finalRootId,
|
||||||
version: oldVersion,
|
version: oldVersion,
|
||||||
deleted: true, // mark *this version* as deleted
|
deleted: true,
|
||||||
isPrivate: oldContent.isPrivate ?? false,
|
isPrivate: oldContent.isPrivate ?? false,
|
||||||
title: oldContent.title || "",
|
title: oldContent.title || "",
|
||||||
magnet: "",
|
magnet: "",
|
||||||
@@ -460,7 +463,7 @@ class NostrClient {
|
|||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [
|
tags: [
|
||||||
["t", "video"],
|
["t", "video"],
|
||||||
["d", existingD], // re-use same d => overshadow
|
["d", existingD],
|
||||||
],
|
],
|
||||||
content: JSON.stringify(contentObject),
|
content: JSON.stringify(contentObject),
|
||||||
};
|
};
|
||||||
@@ -471,9 +474,7 @@ class NostrClient {
|
|||||||
try {
|
try {
|
||||||
await this.pool.publish([url], signedEvent);
|
await this.pool.publish([url], signedEvent);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDevMode) {
|
if (isDevMode) console.error(`Failed to revert on ${url}`, err);
|
||||||
console.error(`Failed to revert on ${url}`, err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -482,14 +483,26 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Deleting" => we just mark all content with the same videoRootId as {deleted:true} and blank out magnet/desc
|
* "Deleting" => Mark all content with the same videoRootId as {deleted:true}
|
||||||
|
* and blank out magnet/desc.
|
||||||
|
*
|
||||||
|
* This version now asks for confirmation before proceeding.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async deleteAllVersions(videoRootId, pubkey) {
|
async deleteAllVersions(videoRootId, pubkey) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
throw new Error("Not logged in to delete all versions.");
|
throw new Error("Not logged in to delete all versions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask for confirmation before proceeding
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Are you sure you want to delete all versions of this video? This action cannot be undone."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log("Deletion cancelled by user.");
|
||||||
|
return null; // Cancel deletion if user clicks "Cancel"
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Find all events in our local allEvents that share the same root.
|
// 1) Find all events in our local allEvents that share the same root.
|
||||||
const matchingEvents = [];
|
const matchingEvents = [];
|
||||||
for (const [id, vid] of this.allEvents.entries()) {
|
for (const [id, vid] of this.allEvents.entries()) {
|
||||||
@@ -501,19 +514,15 @@ class NostrClient {
|
|||||||
matchingEvents.push(vid);
|
matchingEvents.push(vid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If you want to re-check the relay for older versions too,
|
|
||||||
// you can do a fallback query, but typically your local cache is enough.
|
|
||||||
|
|
||||||
if (!matchingEvents.length) {
|
if (!matchingEvents.length) {
|
||||||
throw new Error("No existing events found for that root.");
|
throw new Error("No existing events found for that root.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) For each event, create a "deleted: true" overshadow
|
// 2) For each event, create a "revert" event to mark it as deleted.
|
||||||
// by re-using the same d-tag so it cannot appear again.
|
// This will prompt the user (via the extension) to sign the deletion.
|
||||||
for (const vid of matchingEvents) {
|
for (const vid of matchingEvents) {
|
||||||
await this.revertVideo(
|
await this.revertVideo(
|
||||||
{
|
{
|
||||||
// re-using revertVideo logic
|
|
||||||
id: vid.id,
|
id: vid.id,
|
||||||
pubkey: vid.pubkey,
|
pubkey: vid.pubkey,
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
@@ -532,19 +541,22 @@ class NostrClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally return some status
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to *all* video events. We store them in this.allEvents so older
|
* subscribeVideos => old approach
|
||||||
* notes remain accessible by ID, plus we maintain this.activeMap for the newest
|
*/
|
||||||
* version of each root (or fallback).
|
/**
|
||||||
|
* Subscribe to *all* videos (old and new) with a single subscription,
|
||||||
|
* then call onVideo() each time a new or updated event arrives.
|
||||||
*/
|
*/
|
||||||
subscribeVideos(onVideo) {
|
subscribeVideos(onVideo) {
|
||||||
const filter = {
|
const filter = {
|
||||||
kinds: [30078],
|
kinds: [30078],
|
||||||
"#t": ["video"],
|
"#t": ["video"],
|
||||||
|
// Remove or adjust limit if you prefer,
|
||||||
|
// and set since=0 to retrieve historical events:
|
||||||
limit: 500,
|
limit: 500,
|
||||||
since: 0,
|
since: 0,
|
||||||
};
|
};
|
||||||
@@ -553,37 +565,31 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sub = this.pool.sub(this.relays, [filter]);
|
const sub = this.pool.sub(this.relays, [filter]);
|
||||||
|
const invalidDuringSub = [];
|
||||||
|
|
||||||
sub.on("event", (event) => {
|
sub.on("event", (event) => {
|
||||||
try {
|
try {
|
||||||
const video = convertEventToVideo(event);
|
const video = convertEventToVideo(event);
|
||||||
|
if (video.invalid) {
|
||||||
|
invalidDuringSub.push({ id: video.id, reason: video.reason });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Store in allEvents
|
||||||
this.allEvents.set(event.id, video);
|
this.allEvents.set(event.id, video);
|
||||||
|
|
||||||
// If it’s marked deleted, remove from active map if it’s the active version
|
// If it's a "deleted" note, remove from activeMap
|
||||||
// NEW CODE
|
|
||||||
if (video.deleted) {
|
if (video.deleted) {
|
||||||
const activeKey = getActiveKey(video);
|
const activeKey = getActiveKey(video);
|
||||||
// Don't compare IDs—just remove that key from the active map
|
|
||||||
this.activeMap.delete(activeKey);
|
this.activeMap.delete(activeKey);
|
||||||
|
|
||||||
// (Optional) If you want a debug log:
|
|
||||||
// console.log(`[DELETE] Removed activeKey=${activeKey}`);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not deleted => see if it’s the newest
|
// Otherwise, if it's newer than what we have, update activeMap
|
||||||
const activeKey = getActiveKey(video);
|
const activeKey = getActiveKey(video);
|
||||||
const prevActive = this.activeMap.get(activeKey);
|
const prevActive = this.activeMap.get(activeKey);
|
||||||
if (!prevActive) {
|
if (!prevActive || video.created_at > prevActive.created_at) {
|
||||||
// brand new => set it
|
|
||||||
this.activeMap.set(activeKey, video);
|
this.activeMap.set(activeKey, video);
|
||||||
onVideo(video);
|
onVideo(video); // trigger the callback that re-renders
|
||||||
} else {
|
|
||||||
// compare timestamps
|
|
||||||
if (video.created_at > prevActive.created_at) {
|
|
||||||
this.activeMap.set(activeKey, video);
|
|
||||||
onVideo(video);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
@@ -593,8 +599,16 @@ class NostrClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sub.on("eose", () => {
|
sub.on("eose", () => {
|
||||||
|
if (isDevMode && invalidDuringSub.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[subscribeVideos] found ${invalidDuringSub.length} invalid v2 notes:`,
|
||||||
|
invalidDuringSub
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
console.log("[subscribeVideos] Reached EOSE for all relays");
|
console.log(
|
||||||
|
"[subscribeVideos] Reached EOSE for all relays (historical load done)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -602,7 +616,7 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk fetch from all relays, store in allEvents, rebuild activeMap
|
* fetchVideos => old approach
|
||||||
*/
|
*/
|
||||||
async fetchVideos() {
|
async fetchVideos() {
|
||||||
const filter = {
|
const filter = {
|
||||||
@@ -613,39 +627,51 @@ class NostrClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const localAll = new Map();
|
const localAll = new Map();
|
||||||
|
// NEW: track invalid
|
||||||
|
const invalidNotes = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Fetch all events from each relay
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.relays.map(async (url) => {
|
this.relays.map(async (url) => {
|
||||||
const events = await this.pool.list([url], [filter]);
|
const events = await this.pool.list([url], [filter]);
|
||||||
for (const evt of events) {
|
for (const evt of events) {
|
||||||
const vid = convertEventToVideo(evt);
|
const vid = convertEventToVideo(evt);
|
||||||
|
if (vid.invalid) {
|
||||||
|
// Accumulate if invalid
|
||||||
|
invalidNotes.push({ id: vid.id, reason: vid.reason });
|
||||||
|
} else {
|
||||||
|
// Only add if good
|
||||||
localAll.set(evt.id, vid);
|
localAll.set(evt.id, vid);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2) Merge into this.allEvents
|
// Merge into allEvents
|
||||||
for (const [id, vid] of localAll.entries()) {
|
for (const [id, vid] of localAll.entries()) {
|
||||||
this.allEvents.set(id, vid);
|
this.allEvents.set(id, vid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Rebuild activeMap
|
// Rebuild activeMap
|
||||||
this.activeMap.clear();
|
this.activeMap.clear();
|
||||||
for (const [id, video] of this.allEvents.entries()) {
|
for (const [id, video] of this.allEvents.entries()) {
|
||||||
// Skip if the video is marked deleted
|
|
||||||
if (video.deleted) continue;
|
if (video.deleted) continue;
|
||||||
|
|
||||||
const activeKey = getActiveKey(video);
|
const activeKey = getActiveKey(video);
|
||||||
const existing = this.activeMap.get(activeKey);
|
const existing = this.activeMap.get(activeKey);
|
||||||
|
|
||||||
// If there's no existing entry or this is newer, set/replace
|
|
||||||
if (!existing || video.created_at > existing.created_at) {
|
if (!existing || video.created_at > existing.created_at) {
|
||||||
this.activeMap.set(activeKey, video);
|
this.activeMap.set(activeKey, video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Return newest version for each root in descending order
|
// OPTIONAL: Log invalid stats
|
||||||
|
if (invalidNotes.length > 0 && isDevMode) {
|
||||||
|
console.warn(
|
||||||
|
`Skipped ${invalidNotes.length} invalid v2 notes:\n`,
|
||||||
|
invalidNotes.map((n) => `${n.id.slice(0, 8)}.. => ${n.reason}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const activeVideos = Array.from(this.activeMap.values()).sort(
|
const activeVideos = Array.from(this.activeMap.values()).sort(
|
||||||
(a, b) => b.created_at - a.created_at
|
(a, b) => b.created_at - a.created_at
|
||||||
);
|
);
|
||||||
@@ -657,14 +683,13 @@ class NostrClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to fetch an event by ID from local cache, then from the relays
|
* getEventById => old approach
|
||||||
*/
|
*/
|
||||||
async getEventById(eventId) {
|
async getEventById(eventId) {
|
||||||
const local = this.allEvents.get(eventId);
|
const local = this.allEvents.get(eventId);
|
||||||
if (local) {
|
if (local) {
|
||||||
return local;
|
return local;
|
||||||
}
|
}
|
||||||
// direct fetch if missing
|
|
||||||
try {
|
try {
|
||||||
for (const url of this.relays) {
|
for (const url of this.relays) {
|
||||||
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
|
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
|
||||||
@@ -679,12 +704,9 @@ class NostrClient {
|
|||||||
console.error("getEventById direct fetch error:", err);
|
console.error("getEventById direct fetch error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // not found
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return newest versions from activeMap if you want to skip older events
|
|
||||||
*/
|
|
||||||
getActiveVideos() {
|
getActiveVideos() {
|
||||||
return Array.from(this.activeMap.values()).sort(
|
return Array.from(this.activeMap.values()).sort(
|
||||||
(a, b) => b.created_at - a.created_at
|
(a, b) => b.created_at - a.created_at
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
// js/webtorrent.js
|
|
||||||
|
|
||||||
import WebTorrent from "./webtorrent.min.js";
|
import WebTorrent from "./webtorrent.min.js";
|
||||||
|
|
||||||
export class TorrentClient {
|
export class TorrentClient {
|
||||||
@@ -7,8 +5,6 @@ export class TorrentClient {
|
|||||||
this.client = new WebTorrent();
|
this.client = new WebTorrent();
|
||||||
this.currentTorrent = null;
|
this.currentTorrent = null;
|
||||||
this.TIMEOUT_DURATION = 60000; // 60 seconds
|
this.TIMEOUT_DURATION = 60000; // 60 seconds
|
||||||
// We remove the “statsInterval” since we’re not using it here anymore
|
|
||||||
// this.statsInterval = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(msg) {
|
log(msg) {
|
||||||
@@ -71,7 +67,6 @@ export class TorrentClient {
|
|||||||
throw new Error("Service Worker not supported or disabled");
|
throw new Error("Service Worker not supported or disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional Brave config
|
|
||||||
if (isBraveBrowser) {
|
if (isBraveBrowser) {
|
||||||
this.log("Checking Brave configuration...");
|
this.log("Checking Brave configuration...");
|
||||||
if (!navigator.serviceWorker) {
|
if (!navigator.serviceWorker) {
|
||||||
@@ -84,23 +79,17 @@ export class TorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
for (const registration of registrations) {
|
for (const reg of registrations) {
|
||||||
await registration.unregister();
|
await reg.unregister();
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
this.log("Registering service worker at /sw.min.js...");
|
||||||
const basePath = currentPath.substring(
|
|
||||||
0,
|
|
||||||
currentPath.lastIndexOf("/") + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
this.log("Registering service worker...");
|
|
||||||
const registration = await navigator.serviceWorker.register(
|
const registration = await navigator.serviceWorker.register(
|
||||||
"./sw.min.js",
|
"./sw.min.js",
|
||||||
{
|
{
|
||||||
scope: basePath,
|
scope: "./",
|
||||||
updateViaCache: "none",
|
updateViaCache: "none",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -143,6 +132,9 @@ export class TorrentClient {
|
|||||||
throw new Error("Service worker not active after ready state");
|
throw new Error("Service worker not active after ready state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force the SW to check for updates
|
||||||
|
registration.update();
|
||||||
|
|
||||||
this.log("Service worker ready");
|
this.log("Service worker ready");
|
||||||
return registration;
|
return registration;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -151,83 +143,51 @@ export class TorrentClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formatBytes(bytes) {
|
// Minimal handleChromeTorrent
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams the magnet to the <video> element.
|
|
||||||
* No stats intervals here—just returns the torrent object.
|
|
||||||
*/
|
|
||||||
async streamVideo(magnetURI, videoElement) {
|
|
||||||
try {
|
|
||||||
// 1) Setup service worker
|
|
||||||
const registration = await this.setupServiceWorker();
|
|
||||||
if (!registration || !registration.active) {
|
|
||||||
throw new Error("Service worker setup failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Create WebTorrent server
|
|
||||||
this.client.createServer({ controller: registration });
|
|
||||||
this.log("WebTorrent server created");
|
|
||||||
|
|
||||||
const isFirefoxBrowser = this.isFirefox();
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (isFirefoxBrowser) {
|
|
||||||
this.log("Starting torrent download (Firefox path)");
|
|
||||||
this.client.add(
|
|
||||||
magnetURI,
|
|
||||||
{ strategy: "sequential", maxWebConns: 4 },
|
|
||||||
(torrent) => {
|
|
||||||
this.log("Torrent added (Firefox path):", torrent.name);
|
|
||||||
this.handleFirefoxTorrent(torrent, videoElement, resolve, reject);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.log("Starting torrent download (Chrome path)");
|
|
||||||
this.client.add(magnetURI, (torrent) => {
|
|
||||||
this.log("Torrent added (Chrome path):", torrent.name);
|
|
||||||
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.log("Failed to setup video streaming:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal handleChromeTorrent — no internal setInterval
|
|
||||||
handleChromeTorrent(torrent, videoElement, resolve, reject) {
|
handleChromeTorrent(torrent, videoElement, resolve, reject) {
|
||||||
const file = torrent.files.find((f) =>
|
torrent.on("warning", (err) => {
|
||||||
/\.(mp4|webm|mkv)$/.test(f.name.toLowerCase())
|
if (err && typeof err.message === "string") {
|
||||||
|
if (
|
||||||
|
err.message.includes("CORS") ||
|
||||||
|
err.message.includes("Access-Control-Allow-Origin")
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"CORS warning detected. Attempting to remove the failing webseed/tracker."
|
||||||
);
|
);
|
||||||
|
if (torrent._opts?.urlList?.length) {
|
||||||
|
torrent._opts.urlList = torrent._opts.urlList.filter((url) => {
|
||||||
|
return !url.includes("distribution.bbb3d.renderfarming.net");
|
||||||
|
});
|
||||||
|
console.warn("Cleaned up webseeds =>", torrent._opts.urlList);
|
||||||
|
}
|
||||||
|
if (torrent._opts?.announce?.length) {
|
||||||
|
torrent._opts.announce = torrent._opts.announce.filter((url) => {
|
||||||
|
return !url.includes("fastcast.nz");
|
||||||
|
});
|
||||||
|
console.warn("Cleaned up trackers =>", torrent._opts.announce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const file = torrent.files.find((f) => /\.(mp4|webm|mkv)$/i.test(f.name));
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return reject(new Error("No compatible video file found in torrent"));
|
return reject(new Error("No compatible video file found in torrent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mute & crossOrigin
|
|
||||||
videoElement.muted = true;
|
videoElement.muted = true;
|
||||||
videoElement.crossOrigin = "anonymous";
|
videoElement.crossOrigin = "anonymous";
|
||||||
|
|
||||||
// Catch video errors
|
|
||||||
videoElement.addEventListener("error", (e) => {
|
videoElement.addEventListener("error", (e) => {
|
||||||
this.log("Video error:", e.target.error);
|
this.log("Video error:", e.target.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attempt autoplay
|
|
||||||
videoElement.addEventListener("canplay", () => {
|
videoElement.addEventListener("canplay", () => {
|
||||||
videoElement.play().catch((err) => {
|
videoElement.play().catch((err) => {
|
||||||
this.log("Autoplay failed:", err);
|
this.log("Autoplay failed:", err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actually stream
|
|
||||||
try {
|
try {
|
||||||
file.streamTo(videoElement);
|
file.streamTo(videoElement);
|
||||||
this.currentTorrent = torrent;
|
this.currentTorrent = torrent;
|
||||||
@@ -237,7 +197,6 @@ export class TorrentClient {
|
|||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also handle torrent error events
|
|
||||||
torrent.on("error", (err) => {
|
torrent.on("error", (err) => {
|
||||||
this.log("Torrent error (Chrome path):", err);
|
this.log("Torrent error (Chrome path):", err);
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -282,11 +241,57 @@ export class TorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up
|
* Initiates streaming of a torrent magnet to a <video> element.
|
||||||
|
* Ensures the service worker is registered first.
|
||||||
|
*/
|
||||||
|
async streamVideo(magnetURI, videoElement) {
|
||||||
|
try {
|
||||||
|
// 1) Setup service worker
|
||||||
|
const registration = await this.setupServiceWorker();
|
||||||
|
if (!registration || !registration.active) {
|
||||||
|
throw new Error("Service worker setup failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the WebTorrent server with the registered service worker.
|
||||||
|
// Force the server to use '/webtorrent' as the URL prefix.
|
||||||
|
this.client.createServer({
|
||||||
|
controller: registration,
|
||||||
|
pathPrefix: "/webtorrent",
|
||||||
|
});
|
||||||
|
this.log("WebTorrent server created");
|
||||||
|
|
||||||
|
const isFirefoxBrowser = this.isFirefox();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (isFirefoxBrowser) {
|
||||||
|
this.log("Starting torrent download (Firefox path)");
|
||||||
|
this.client.add(
|
||||||
|
magnetURI,
|
||||||
|
{ strategy: "sequential", maxWebConns: 4 },
|
||||||
|
(torrent) => {
|
||||||
|
this.log("Torrent added (Firefox path):", torrent.name);
|
||||||
|
this.handleFirefoxTorrent(torrent, videoElement, resolve, reject);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.log("Starting torrent download (Chrome path)");
|
||||||
|
this.client.add(magnetURI, (torrent) => {
|
||||||
|
this.log("Torrent added (Chrome path):", torrent.name);
|
||||||
|
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.log("Failed to setup video streaming:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up resources.
|
||||||
*/
|
*/
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
try {
|
try {
|
||||||
// No local interval to clear here
|
|
||||||
if (this.currentTorrent) {
|
if (this.currentTorrent) {
|
||||||
this.currentTorrent.destroy();
|
this.currentTorrent.destroy();
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/index.html",
|
"start_url": "index.html",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0f172a",
|
"background_color": "#0f172a",
|
||||||
"theme_color": "#0f172a",
|
"theme_color": "#0f172a",
|
||||||
|
191
src/sw.min.js
vendored
191
src/sw.min.js
vendored
@@ -3,130 +3,159 @@
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
// Handle skip waiting message
|
// Handle messages from clients
|
||||||
self.addEventListener('message', event => {
|
self.addEventListener("message", (event) => {
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
self.skipWaiting()
|
self.skipWaiting();
|
||||||
}
|
}
|
||||||
})
|
if (event.data && event.data.type === "CLEAR_CACHES") {
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((cacheNames) =>
|
||||||
|
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Immediately install and activate
|
// Immediately install and skip waiting
|
||||||
self.addEventListener("install", () => {
|
self.addEventListener("install", (event) => {
|
||||||
self.skipWaiting()
|
self.skipWaiting();
|
||||||
})
|
});
|
||||||
|
|
||||||
// Claim clients on activation
|
// Claim clients on activation and clear caches
|
||||||
self.addEventListener('activate', event => {
|
self.addEventListener("activate", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
Promise.all([
|
Promise.all([
|
||||||
clients.claim(),
|
clients.claim(),
|
||||||
self.skipWaiting(),
|
self.skipWaiting(),
|
||||||
caches.keys().then(cacheNames =>
|
caches
|
||||||
Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)))
|
.keys()
|
||||||
)
|
.then((cacheNames) =>
|
||||||
|
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
|
||||||
|
),
|
||||||
])
|
])
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
// Handle fetch events
|
// Handle fetch events
|
||||||
self.addEventListener("fetch", s => {
|
self.addEventListener("fetch", (event) => {
|
||||||
const t = (s => {
|
const requestURL = event.request.url;
|
||||||
const { url: t } = s.request;
|
// Only handle WebTorrent streaming requests; let other requests proceed normally.
|
||||||
|
if (!requestURL.includes("/webtorrent/")) {
|
||||||
// Only handle webtorrent requests
|
return;
|
||||||
if (!t.includes(self.registration.scope + "webtorrent/")) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responsePromise = (async () => {
|
||||||
// Handle keepalive requests
|
// Handle keepalive requests
|
||||||
if (t.includes(self.registration.scope + "webtorrent/keepalive/")) {
|
if (requestURL.includes("/webtorrent/keepalive/")) {
|
||||||
return new Response();
|
return new Response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle cancel requests
|
// Handle cancel requests
|
||||||
if (t.includes(self.registration.scope + "webtorrent/cancel/")) {
|
if (requestURL.includes("/webtorrent/cancel/")) {
|
||||||
return new Response(new ReadableStream({
|
return new Response(
|
||||||
|
new ReadableStream({
|
||||||
cancel() {
|
cancel() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
}
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle streaming requests
|
// Handle streaming requests
|
||||||
return async function({ request: s }) {
|
return (async function ({ request }) {
|
||||||
const { url: t, method: n, headers: o, destination: a } = s;
|
const { url, method, headers, destination } = request;
|
||||||
|
const windowClients = await clients.matchAll({
|
||||||
// Get all window clients
|
|
||||||
const l = await clients.matchAll({
|
|
||||||
type: "window",
|
type: "window",
|
||||||
includeUncontrolled: true
|
includeUncontrolled: true,
|
||||||
});
|
});
|
||||||
|
const [clientResponse, port] = await new Promise((resolve) => {
|
||||||
// Create message channel and wait for response
|
for (const client of windowClients) {
|
||||||
const [r, i] = await new Promise(e => {
|
const channel = new MessageChannel();
|
||||||
for (const s of l) {
|
channel.port1.onmessage = ({ data }) => {
|
||||||
const l = new MessageChannel,
|
resolve([data, channel.port1]);
|
||||||
{ port1: r, port2: i } = l;
|
|
||||||
r.onmessage = ({ data: s }) => {
|
|
||||||
e([s, r])
|
|
||||||
};
|
};
|
||||||
s.postMessage({
|
client.postMessage(
|
||||||
url: t,
|
{
|
||||||
method: n,
|
url,
|
||||||
headers: Object.fromEntries(o.entries()),
|
method,
|
||||||
|
headers: Object.fromEntries(headers.entries()),
|
||||||
scope: self.registration.scope,
|
scope: self.registration.scope,
|
||||||
destination: a,
|
destination,
|
||||||
type: "webtorrent"
|
type: "webtorrent",
|
||||||
}, [i]);
|
},
|
||||||
|
[channel.port2]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let c = null;
|
let timeoutId = null;
|
||||||
|
const closeChannel = () => {
|
||||||
const d = () => {
|
port.postMessage(false);
|
||||||
i.postMessage(false);
|
clearTimeout(timeoutId);
|
||||||
clearTimeout(c);
|
port.onmessage = null;
|
||||||
i.onmessage = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle non-streaming response
|
// Clone and update headers to prevent caching.
|
||||||
if (r.body !== "STREAM") {
|
const responseHeaders = new Headers(clientResponse.headers);
|
||||||
d();
|
responseHeaders.set(
|
||||||
return new Response(r.body, r);
|
"Cache-Control",
|
||||||
|
"no-cache, no-store, must-revalidate, max-age=0"
|
||||||
|
);
|
||||||
|
responseHeaders.set("Pragma", "no-cache");
|
||||||
|
responseHeaders.set("Expires", "0");
|
||||||
|
|
||||||
|
// If the response is not a streaming request, return it directly.
|
||||||
|
if (clientResponse.body !== "STREAM") {
|
||||||
|
closeChannel();
|
||||||
|
return new Response(clientResponse.body, {
|
||||||
|
status: clientResponse.status,
|
||||||
|
statusText: clientResponse.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle streaming response
|
// Otherwise, stream the response via a ReadableStream.
|
||||||
return new Response(new ReadableStream({
|
return new Response(
|
||||||
pull: s => new Promise(t => {
|
new ReadableStream({
|
||||||
i.onmessage = ({ data: e }) => {
|
pull(controller) {
|
||||||
if (e) {
|
return new Promise((resolvePull) => {
|
||||||
s.enqueue(e);
|
port.onmessage = ({ data }) => {
|
||||||
|
if (data) {
|
||||||
|
controller.enqueue(data);
|
||||||
} else {
|
} else {
|
||||||
d();
|
closeChannel();
|
||||||
s.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
t();
|
resolvePull();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!cancelled && a !== "document") {
|
if (!cancelled && destination !== "document") {
|
||||||
clearTimeout(c);
|
clearTimeout(timeoutId);
|
||||||
c = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
d();
|
closeChannel();
|
||||||
t();
|
resolvePull();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
i.postMessage(true);
|
port.postMessage(true);
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
d();
|
closeChannel();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: clientResponse.status,
|
||||||
|
statusText: clientResponse.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
}
|
}
|
||||||
}), r);
|
);
|
||||||
}(s);
|
})(event);
|
||||||
})(s);
|
})();
|
||||||
|
|
||||||
if (t) {
|
if (responsePromise) {
|
||||||
s.respondWith(t);
|
event.respondWith(responsePromise);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
Reference in New Issue
Block a user