mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 01:11:25 +00:00
add media back
This commit is contained in:
parent
d712dc99bb
commit
50b6e49934
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@ -25,3 +25,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
bun.lockb
|
||||
.env
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 && <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" />,
|
||||
<div className="text-xs text-gray-300 pl-1">
|
||||
~{timeToGoEst(difficulty, hashrate)}
|
||||
~{timeToGoEst(difficulty, hashrate)} total
|
||||
</div>)
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -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 = ({
|
||||
<div className="flex flex-col break-words">
|
||||
<ContentPreview key={event.id} eventdata={event} />
|
||||
</div>
|
||||
<RenderMedia files={files} />
|
||||
<div className="flex justify-between items-center">
|
||||
{metadataParsed ?
|
||||
<img
|
||||
|
@ -50,12 +50,6 @@ const ContentPreview = ({ key, eventdata }: { key: string; eventdata: Event }) =
|
||||
};
|
||||
|
||||
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 nostrURI = match && match[1];
|
||||
if (nostrURI && quoteEvents.length === 0) {
|
||||
|
113
client/src/components/modals/MediaRender.tsx
Normal file
113
client/src/components/modals/MediaRender.tsx
Normal 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;
|
@ -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<Event[]>([]);
|
||||
@ -65,13 +65,13 @@ const PostCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<CardContainer>
|
||||
<div className={`flex flex-col gap-2`} key={key}>
|
||||
<div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
|
||||
<ContentPreview key={parsedEvent.id} eventdata={parsedEvent} />
|
||||
</div>
|
||||
<RenderMedia files={files} />
|
||||
{repliedTo && <div className="flex items-center mt-1" >
|
||||
<span className="text-xs text-gray-500">Reply to: </span>
|
||||
{uniqBy(repliedTo, 'pubkey').map((parsedEvent, index) => {
|
||||
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
@ -1 +1 @@
|
||||
export const DEFAULT_DIFFICULTY = 21;
|
||||
export const DEFAULT_DIFFICULTY = 22;
|
@ -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;
|
||||
}
|
@ -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(),
|
||||
|
@ -13,7 +13,7 @@ export const subGlobalFeed = (onEvent: SubCallback, age: number) => {
|
||||
const now = Math.floor(Date.now() * 0.001);
|
||||
const pubkeys = 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
|
||||
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);
|
||||
|
Loading…
Reference in New Issue
Block a user