add media back

This commit is contained in:
smolgrrr 2024-08-29 16:04:49 +10:00
parent d712dc99bb
commit 50b6e49934
12 changed files with 137 additions and 153 deletions

1
client/.gitignore vendored
View File

@ -25,3 +25,4 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
bun.lockb bun.lockb
.env

View File

@ -9,7 +9,9 @@
"@types/node": "^17.0.45", "@types/node": "^17.0.45",
"@types/react": "^18.2.21", "@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"axios": "^1.7.5",
"link-preview-js": "^3.0.5", "link-preview-js": "^3.0.5",
"lodash": "^4.17.21",
"nostr-tools": "2.5.1", "nostr-tools": "2.5.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -59,6 +61,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.6", "@tailwindcss/forms": "^0.5.6",
"@types/lodash": "^4.17.7",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
} }

View File

@ -122,13 +122,13 @@ const NewNoteCard = ({
const { handleSubmit: originalHandleSubmit, doingWorkProp, hashrate, bestPow, signedPoWEvent } = useSubmitForm(unsigned, difficulty); const { handleSubmit: originalHandleSubmit, doingWorkProp, hashrate, bestPow, signedPoWEvent } = useSubmitForm(unsigned, difficulty);
const handleSubmit = async (event: React.FormEvent) => { const handleSubmit = async (event: React.FormEvent) => {
await originalHandleSubmit(event);
// Check if tagType is 'Quote' and update comment // Check if tagType is 'Quote' and update comment
if (tagType === 'Quote' && refEvent) { if (tagType === 'Quote' && refEvent) {
setComment(prevComment => prevComment + '\nnostr:' + nip19.noteEncode(refEvent.id)); setComment(prevComment => prevComment + '\nnostr:' + nip19.noteEncode(refEvent.id));
} }
await originalHandleSubmit(event);
setComment(""); setComment("");
setUnsigned(prevUnsigned => ({ setUnsigned(prevUnsigned => ({
...prevUnsigned, ...prevUnsigned,
@ -222,7 +222,7 @@ const NewNoteCard = ({
{hashrate && <span>{hashrate > 100000 ? `${(hashrate / 1000).toFixed(0)}k` : hashrate}</span>}H/s {hashrate && <span>{hashrate > 100000 ? `${(hashrate / 1000).toFixed(0)}k` : hashrate}</span>}H/s
<span className="pl-1"> (PB:{bestPow}</span><CpuChipIcon className="h-4 w-4" />, <span className="pl-1"> (PB:{bestPow}</span><CpuChipIcon className="h-4 w-4" />,
<div className="text-xs text-gray-300 pl-1"> <div className="text-xs text-gray-300 pl-1">
~{timeToGoEst(difficulty, hashrate)} ~{timeToGoEst(difficulty, hashrate)} total
</div>) </div>)
</div> </div>
) : null} ) : null}

View File

@ -2,7 +2,7 @@ import { parseContent } from "../../../utils/content";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
import { getMetadata } from "../../../utils/getMetadata"; import { getMetadata } from "../../../utils/getMetadata";
import ContentPreview from "./TextModal"; import ContentPreview from "./TextModal";
import { renderMedia } from "../../../utils/FileUpload"; import RenderMedia from "../MediaRender";
import { getIconFromHash, timeAgo } from "../../../utils/cardUtils"; import { getIconFromHash, timeAgo } from "../../../utils/cardUtils";
import { CpuChipIcon } from "@heroicons/react/24/outline"; import { CpuChipIcon } from "@heroicons/react/24/outline";
import { verifyPow } from "../../../utils/mine"; import { verifyPow } from "../../../utils/mine";
@ -30,6 +30,7 @@ const QuoteEmbed = ({
<div className="flex flex-col break-words"> <div className="flex flex-col break-words">
<ContentPreview key={event.id} eventdata={event} /> <ContentPreview key={event.id} eventdata={event} />
</div> </div>
<RenderMedia files={files} />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{metadataParsed ? {metadataParsed ?
<img <img

View File

@ -50,12 +50,6 @@ const ContentPreview = ({ key, eventdata }: { key: string; eventdata: Event }) =
}; };
useEffect(() => { useEffect(() => {
const findUrl = comment.match(/\bhttps?:\/\/\S+/gi);
if (findUrl && findUrl.length > 0) {
setUrl(findUrl[0]);
setFinalComment(finalComment.replace(findUrl[0], "").trim());
}
const match = comment.match(/\bnostr:(?:nevent1|note1)([a-z0-9]+)/i); const match = comment.match(/\bnostr:(?:nevent1|note1)([a-z0-9]+)/i);
const nostrURI = match && match[1]; const nostrURI = match && match[1];
if (nostrURI && quoteEvents.length === 0) { if (nostrURI && quoteEvents.length === 0) {

View File

@ -0,0 +1,113 @@
import { useEffect, useState } from "react";
// Function to check media against the API
const checkMedia = async (url: string) => {
try {
const token = process.env.REACT_APP_NSFW_TOKEN;
if (!token) {
console.error("NSFW token is not set in environment variables");
return null;
}
const response = await fetch('https://nsfw-detector-api-latest.onrender.com/predict', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ url }),
});
return await response.json();
} catch (error) {
console.error("Error checking media:", error);
return null;
}
};
const RenderMedia = ({ files }: { files: string[] }) => {
const gridTemplateColumns = files.length > 1 ? 'repeat(2, 1fr)' : 'repeat(1, 1fr)';
const gridTemplateRows = files.length > 2 ? 'repeat(2, 1fr)' : 'repeat(1, 1fr)';
const whitelistImageURL = ["nostr.build", "void.cat", "blossom.oxtr", "image.nostr.build"];
const [mediaCheckResults, setMediaCheckResults] = useState<Record<string, any>>({});
// Function to toggle blur on click
const toggleBlur = (event: React.MouseEvent<HTMLImageElement>) => {
event.currentTarget.classList.toggle('no-blur');
};
useEffect(() => {
const performMediaChecks = async () => {
for (const file of files) {
const result = await checkMedia(file);
console.log(`Result for ${file}:`, result);
if (result && result.data && result.data.predictedLabel) {
setMediaCheckResults(prev => ({
...prev,
[file]: { predictedLabel: result.data.predictedLabel }
}));
console.error(`Unexpected result structure for ${file}:`, result.data.predictedLabel);
} else {
console.error(`Unexpected result structure for ${file}:`, result);
}
}
};
if (Object.keys(mediaCheckResults).length === 0) {
performMediaChecks();
}
}, []);
return (
<div style={{ display: 'grid', gridTemplateColumns, gridTemplateRows, gap: '2px' }}>
{files.map((file, index) => {
// Check if the file is from allowed domains
const isFromAllowedDomain = whitelistImageURL.some(domain => file.includes(domain));
const mediaCheckResult = mediaCheckResults[file];
// Only render if predictedLabel is neutral
if (mediaCheckResult && mediaCheckResult.predictedLabel !== 'neutral') {
return (
<div>
<p className="text-center text-red-500 text-xs">Attached media has been flagged as not safe for work.</p>
</div>
);
}
if (file && (file.endsWith(".mp4") || file.endsWith(".webm")) && mediaCheckResult && mediaCheckResult.predictedLabel === 'neutral') {
return (
<video
key={index}
controls
muted
src={file + "#t=0.1"}
preload="metadata"
className="thumb mt-1 rounded-md w-full"
>
<source src={file} type="video/mp4" />
</video>
);
} else if (file && mediaCheckResult && mediaCheckResult.predictedLabel === 'neutral') {
return (
<img
key={index}
alt="Invalid thread"
loading="lazy"
className={`thumb mt-2 max-w-64 min-h-64 mx-auto rounded-md`}
src={file}
onClick={isFromAllowedDomain ? undefined : toggleBlur} // Only add onClick if blur is applied
/>
);
} else {
return (
<div>
<p className="text-center text-white-500 text-xs">Checking media...</p>
</div>
);
}
})}
</div>
);
};
export default RenderMedia;

View File

@ -1,14 +1,14 @@
import { FolderIcon, CpuChipIcon } from "@heroicons/react/24/outline"; import { FolderIcon, CpuChipIcon } from "@heroicons/react/24/outline";
// import { parseContent } from "../../utils/content"; import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools"; import { Event, nip19 } from "nostr-tools";
import { getMetadata } from "../../utils/getMetadata"; import { getMetadata } from "../../utils/getMetadata";
// import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash, timeAgo } from "../../utils/cardUtils"; import { getIconFromHash, timeAgo } from "../../utils/cardUtils";
import { verifyPow } from "../../utils/mine"; import { verifyPow } from "../../utils/mine";
import { uniqBy } from "../../utils/otherUtils"; import { uniqBy } from "../../utils/otherUtils";
import ContentPreview from "./CardModals/TextModal"; import ContentPreview from "./CardModals/TextModal";
import CardContainer from "./CardContainer"; import CardContainer from "./CardContainer";
import { useState, useEffect } from "react"; import React, { useState, useEffect, useMemo} from "react";
import RenderMedia from "./MediaRender";
interface CardProps { interface CardProps {
key?: string | number; key?: string | number;
@ -27,7 +27,7 @@ const PostCard = ({
repliedTo, repliedTo,
type type
}: CardProps) => { }: CardProps) => {
// const { files } = parseContent(event); const { files } = parseContent(event);
const icon = getIconFromHash(event.pubkey); const icon = getIconFromHash(event.pubkey);
const metadataParsed = metadata ? getMetadata(metadata) : null; const metadataParsed = metadata ? getMetadata(metadata) : null;
const [relatedEvents, setRelatedEvents] = useState<Event[]>([]); const [relatedEvents, setRelatedEvents] = useState<Event[]>([]);
@ -65,13 +65,13 @@ const PostCard = ({
} }
}; };
return ( return (
<CardContainer> <CardContainer>
<div className={`flex flex-col gap-2`} key={key}> <div className={`flex flex-col gap-2`} key={key}>
<div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}> <div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
<ContentPreview key={parsedEvent.id} eventdata={parsedEvent} /> <ContentPreview key={parsedEvent.id} eventdata={parsedEvent} />
</div> </div>
<RenderMedia files={files} />
{repliedTo && <div className="flex items-center mt-1" > {repliedTo && <div className="flex items-center mt-1" >
<span className="text-xs text-gray-500">Reply to: </span> <span className="text-xs text-gray-500">Reply to: </span>
{uniqBy(repliedTo, 'pubkey').map((parsedEvent, index) => { {uniqBy(repliedTo, 'pubkey').map((parsedEvent, index) => {

View File

@ -79,7 +79,7 @@ const Settings = () => {
type="number" type="number"
value={filterDifficulty} value={filterDifficulty}
onChange={e => setFilterDifficulty(e.target.value)} onChange={e => setFilterDifficulty(e.target.value)}
min={21} min={22}
className="w-full px-3 py-2 border rounded-md bg-black" className="w-full px-3 py-2 border rounded-md bg-black"
/> />
</div> </div>
@ -94,7 +94,7 @@ const Settings = () => {
type="number" type="number"
value={difficulty} value={difficulty}
onChange={e => setDifficulty(e.target.value)} onChange={e => setDifficulty(e.target.value)}
min={21} min={22}
className="w-full px-3 py-2 border rounded-md bg-black" className="w-full px-3 py-2 border rounded-md bg-black"
/> />
</div> </div>

View File

@ -1 +1 @@
export const DEFAULT_DIFFICULTY = 21; export const DEFAULT_DIFFICULTY = 22;

View File

@ -1,128 +0,0 @@
import { generateSecretKey, getPublicKey, finalizeEvent } from "nostr-tools";
import { base64 } from "@scure/base";
export interface UploadResult {
url?: string;
error?: string;
}
const whitelistImageURL = ["nostr.build", "void.cat", "blossom.oxtr"];
/**
* Upload file to void.cat
* https://void.cat/swagger/index.html
*/
export default async function FileUpload(file: File): Promise<UploadResult> {
const sk = generateSecretKey();
const fileBuffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', fileBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
const auth = async () => {
const authEvent = {
kind: 24242,
content: "Upload " + file.name + " from getwired.app",
tags: [
["t", "upload"],
["x", hashHex],
["expiration", (Math.floor(Date.now() / 1000) + 24 * 60 * 60).toString()]
],
created_at: Math.floor(Date.now() / 1000),
pubkey: getPublicKey(sk),
}
const authString = JSON.stringify(finalizeEvent(authEvent, sk));
const authBase64 = base64.encode(new TextEncoder().encode(authString));
return `Nostr ${authBase64}`;
};
const req = await fetch("https://blossom.oxtr.dev/upload", {
body: file,
method: "PUT",
headers: {
"authorization": await auth() // Use the encoded authorization header
},
});
if (req.ok) {
const fileExtension = file.name.split(".").pop(); // Extracting the file extension
const resultUrl = `https://blossom.oxtr.dev/${hashHex}.${fileExtension}`;
return { url: resultUrl };
}
return {
error: "Upload failed",
};
}
export const renderMedia = (files: string[]) => {
const gridTemplateColumns = files.length > 1 ? 'repeat(2, 1fr)' : 'repeat(1, 1fr)';
const gridTemplateRows = files.length > 2 ? 'repeat(2, 1fr)' : 'repeat(1, 1fr)';
// Function to toggle blur on click
const toggleBlur = (event: React.MouseEvent<HTMLImageElement>) => {
event.currentTarget.classList.toggle('no-blur');
};
return (
<div style={{ display: 'grid', gridTemplateColumns, gridTemplateRows, gap: '2px' }}>
{files.map((file, index) => {
// Check if the file is from allowed domains
const isFromAllowedDomain = whitelistImageURL.some(domain => file.includes(domain));
if (file && (file.endsWith(".mp4") || file.endsWith(".webm"))) {
return (
<video
key={index}
controls
muted
src={file + "#t=0.1"}
preload="metadata"
className="thumb mt-1 rounded-md w-full"
>
<source src={file} type="video/mp4" />
</video>
);
} else if (!file.includes("http")) {
return null;
} else {
return (
<img
key={index}
alt="Invalid thread"
loading="lazy"
className={`thumb mt-2 rounded-md w-full ${!isFromAllowedDomain ? "blur" : ""}`}
src={file}
onClick={isFromAllowedDomain ? undefined : toggleBlur} // Only add onClick if blur is applied
/>
);
}
})}
</div>
);
};
export async function attachFile(file_input: File | null): Promise<string> {
if (!file_input) {
throw new Error("No file provided");
}
try {
const rx = await FileUpload(file_input);
if (rx.error) {
throw new Error(rx.error);
}
return rx.url || "No URL returned from FileUpload";
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(`File upload failed: ${error.message}`);
}
throw new Error("Unknown error occurred during file upload");
}
}
export interface UploadResult {
url?: string;
error?: string;
}

View File

@ -7,12 +7,12 @@ function extractMediaUrls(content: string): string[] {
} }
export function parseContent(event: Event) { export function parseContent(event: Event) {
const files: string[] = []; // extractMediaUrls(event.content); const files = extractMediaUrls(event.content);
let contentWithoutFiles = event.content; let contentWithoutFiles = event.content;
// files.forEach(file => { files.forEach(file => {
// contentWithoutFiles = contentWithoutFiles.replace(file, ''); contentWithoutFiles = contentWithoutFiles.replace(file, '');
// }); });
return { return {
comment: contentWithoutFiles.trim(), comment: contentWithoutFiles.trim(),

View File

@ -13,7 +13,7 @@ export const subGlobalFeed = (onEvent: SubCallback, age: number) => {
const now = Math.floor(Date.now() * 0.001); const now = Math.floor(Date.now() * 0.001);
const pubkeys = new Set<string>(); const pubkeys = new Set<string>();
const notes = new Set<string>(); const notes = new Set<string>();
const prefix = 4; // 4 bits in each '0' character const prefix = 6; // 4 bits in each '0' character
sub({ // get past events sub({ // get past events
cb: (evt, relay) => { cb: (evt, relay) => {
pubkeys.add(evt.pubkey); pubkeys.add(evt.pubkey);
@ -300,7 +300,7 @@ export const subHashtagFeed = (
unsub: true unsub: true
}); });
const prefix = 4; // 4 bits in each '0' character const prefix = 6; // 4 bits in each '0' character
sub({ // get past events sub({ // get past events
cb: (evt, relay) => { cb: (evt, relay) => {
pubkeys.add(evt.pubkey); pubkeys.add(evt.pubkey);