diff --git a/client/.gitignore b/client/.gitignore index 3ee3508..e729568 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -24,4 +24,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -bun.lockb \ No newline at end of file +bun.lockb +.env \ No newline at end of file diff --git a/client/package.json b/client/package.json index a69bf84..2de7fd7 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,9 @@ "@types/node": "^17.0.45", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "axios": "^1.7.5", "link-preview-js": "^3.0.5", + "lodash": "^4.17.21", "nostr-tools": "2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -59,6 +61,7 @@ }, "devDependencies": { "@tailwindcss/forms": "^0.5.6", + "@types/lodash": "^4.17.7", "typescript": "^5.2.2" } } diff --git a/client/src/components/forms/PostFormCard.tsx b/client/src/components/forms/PostFormCard.tsx index 7d73bcb..0b14e8d 100644 --- a/client/src/components/forms/PostFormCard.tsx +++ b/client/src/components/forms/PostFormCard.tsx @@ -122,13 +122,13 @@ const NewNoteCard = ({ const { handleSubmit: originalHandleSubmit, doingWorkProp, hashrate, bestPow, signedPoWEvent } = useSubmitForm(unsigned, difficulty); const handleSubmit = async (event: React.FormEvent) => { - await originalHandleSubmit(event); - // Check if tagType is 'Quote' and update comment if (tagType === 'Quote' && refEvent) { setComment(prevComment => prevComment + '\nnostr:' + nip19.noteEncode(refEvent.id)); } + await originalHandleSubmit(event); + setComment(""); setUnsigned(prevUnsigned => ({ ...prevUnsigned, @@ -222,7 +222,7 @@ const NewNoteCard = ({ {hashrate && {hashrate > 100000 ? `${(hashrate / 1000).toFixed(0)}k` : hashrate}}H/s (PB:{bestPow},
- ~{timeToGoEst(difficulty, hashrate)} + ~{timeToGoEst(difficulty, hashrate)} total
) ) : null} diff --git a/client/src/components/modals/CardModals/QuoteEmbed.tsx b/client/src/components/modals/CardModals/QuoteEmbed.tsx index 149ce8d..ae01a12 100644 --- a/client/src/components/modals/CardModals/QuoteEmbed.tsx +++ b/client/src/components/modals/CardModals/QuoteEmbed.tsx @@ -2,7 +2,7 @@ import { parseContent } from "../../../utils/content"; import { Event } from "nostr-tools"; import { getMetadata } from "../../../utils/getMetadata"; import ContentPreview from "./TextModal"; -import { renderMedia } from "../../../utils/FileUpload"; +import RenderMedia from "../MediaRender"; import { getIconFromHash, timeAgo } from "../../../utils/cardUtils"; import { CpuChipIcon } from "@heroicons/react/24/outline"; import { verifyPow } from "../../../utils/mine"; @@ -30,6 +30,7 @@ const QuoteEmbed = ({
+
{metadataParsed ? { - 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 nostrURI = match && match[1]; if (nostrURI && quoteEvents.length === 0) { diff --git a/client/src/components/modals/MediaRender.tsx b/client/src/components/modals/MediaRender.tsx new file mode 100644 index 0000000..7fdc986 --- /dev/null +++ b/client/src/components/modals/MediaRender.tsx @@ -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>({}); + + // Function to toggle blur on click + const toggleBlur = (event: React.MouseEvent) => { + 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 ( +
+ {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 ( +
+

Attached media has been flagged as not safe for work.

+
+ ); + } + + if (file && (file.endsWith(".mp4") || file.endsWith(".webm")) && mediaCheckResult && mediaCheckResult.predictedLabel === 'neutral') { + return ( + + ); + } else if (file && mediaCheckResult && mediaCheckResult.predictedLabel === 'neutral') { + return ( + Invalid thread + ); + } else { + return ( +
+

Checking media...

+
+ ); + } + })} +
+ ); +}; + +export default RenderMedia; \ No newline at end of file diff --git a/client/src/components/modals/PostCard.tsx b/client/src/components/modals/PostCard.tsx index 5566dda..a6f257a 100644 --- a/client/src/components/modals/PostCard.tsx +++ b/client/src/components/modals/PostCard.tsx @@ -1,14 +1,14 @@ 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 { getMetadata } from "../../utils/getMetadata"; -// import { renderMedia } from "../../utils/FileUpload"; import { getIconFromHash, timeAgo } from "../../utils/cardUtils"; import { verifyPow } from "../../utils/mine"; import { uniqBy } from "../../utils/otherUtils"; import ContentPreview from "./CardModals/TextModal"; import CardContainer from "./CardContainer"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo} from "react"; +import RenderMedia from "./MediaRender"; interface CardProps { key?: string | number; @@ -27,7 +27,7 @@ const PostCard = ({ repliedTo, type }: CardProps) => { - // const { files } = parseContent(event); + const { files } = parseContent(event); const icon = getIconFromHash(event.pubkey); const metadataParsed = metadata ? getMetadata(metadata) : null; const [relatedEvents, setRelatedEvents] = useState([]); @@ -65,13 +65,13 @@ const PostCard = ({ } }; - return (
+ {repliedTo &&
Reply to: {uniqBy(repliedTo, 'pubkey').map((parsedEvent, index) => { diff --git a/client/src/components/routes/Settings.tsx b/client/src/components/routes/Settings.tsx index 9f75e74..214ca96 100644 --- a/client/src/components/routes/Settings.tsx +++ b/client/src/components/routes/Settings.tsx @@ -79,7 +79,7 @@ const Settings = () => { type="number" value={filterDifficulty} onChange={e => setFilterDifficulty(e.target.value)} - min={21} + min={22} className="w-full px-3 py-2 border rounded-md bg-black" />
@@ -94,7 +94,7 @@ const Settings = () => { type="number" value={difficulty} onChange={e => setDifficulty(e.target.value)} - min={21} + min={22} className="w-full px-3 py-2 border rounded-md bg-black" />
diff --git a/client/src/config.ts b/client/src/config.ts index fa207b3..2d0742f 100644 --- a/client/src/config.ts +++ b/client/src/config.ts @@ -1 +1 @@ -export const DEFAULT_DIFFICULTY = 21; \ No newline at end of file +export const DEFAULT_DIFFICULTY = 22; \ No newline at end of file diff --git a/client/src/utils/FileUpload.tsx b/client/src/utils/FileUpload.tsx deleted file mode 100644 index 2be9544..0000000 --- a/client/src/utils/FileUpload.tsx +++ /dev/null @@ -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 { - 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) => { - event.currentTarget.classList.toggle('no-blur'); - }; - - return ( -
- {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 ( - - ); - } else if (!file.includes("http")) { - return null; - } else { - return ( - Invalid thread - ); - } - })} -
- ); -}; - -export async function attachFile(file_input: File | null): Promise { - 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; -} diff --git a/client/src/utils/content.ts b/client/src/utils/content.ts index aa49831..2bd941b 100644 --- a/client/src/utils/content.ts +++ b/client/src/utils/content.ts @@ -7,12 +7,12 @@ function extractMediaUrls(content: string): string[] { } export function parseContent(event: Event) { - const files: string[] = []; // extractMediaUrls(event.content); + const files = extractMediaUrls(event.content); let contentWithoutFiles = event.content; - // files.forEach(file => { - // contentWithoutFiles = contentWithoutFiles.replace(file, ''); - // }); + files.forEach(file => { + contentWithoutFiles = contentWithoutFiles.replace(file, ''); + }); return { comment: contentWithoutFiles.trim(), diff --git a/client/src/utils/subscriptions.ts b/client/src/utils/subscriptions.ts index e1cee58..b4d7784 100644 --- a/client/src/utils/subscriptions.ts +++ b/client/src/utils/subscriptions.ts @@ -13,7 +13,7 @@ export const subGlobalFeed = (onEvent: SubCallback, age: number) => { const now = Math.floor(Date.now() * 0.001); const pubkeys = new Set(); const notes = new Set(); - const prefix = 4; // 4 bits in each '0' character + const prefix = 6; // 4 bits in each '0' character sub({ // get past events cb: (evt, relay) => { pubkeys.add(evt.pubkey); @@ -300,7 +300,7 @@ export const subHashtagFeed = ( unsub: true }); - const prefix = 4; // 4 bits in each '0' character + const prefix = 6; // 4 bits in each '0' character sub({ // get past events cb: (evt, relay) => { pubkeys.add(evt.pubkey);