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 (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ })}
+
+ );
+};
+
+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 (
-
- );
- }
- })}
-
- );
-};
-
-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);