mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 09:21:25 +00:00
commit
e149de5ef9
@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import Home from './components/Home';
|
||||
import Settings from './components/Settings';
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import Thread from './components/Thread/Thread';
|
||||
import Header from './components/Header/Header';
|
||||
import React from "react";
|
||||
import "./App.css";
|
||||
import Home from "./components/Home";
|
||||
import Settings from "./components/Settings";
|
||||
import SwipeableViews from "react-swipeable-views";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import Thread from "./components/Thread/Thread";
|
||||
import Header from "./components/Header/Header";
|
||||
|
||||
function App() {
|
||||
const [index, setIndex] = React.useState(1);
|
||||
|
||||
const handleChangeIndex = (index: number) => {
|
||||
console.log("Changed index to:", index); // Add a log to see if this function is called
|
||||
console.log("Changed index to:", index); // Add a log to see if this function is called
|
||||
setIndex(index);
|
||||
};
|
||||
|
||||
@ -21,17 +21,23 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path='/thread/:id' element={<Thread />} />
|
||||
<Route path="/" element={
|
||||
<SwipeableViews index={index} onChangeIndex={handleChangeIndex}>
|
||||
<Settings />
|
||||
<Home />
|
||||
</SwipeableViews>
|
||||
} />
|
||||
<Route path="/thread/:id" element={<Thread />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<SwipeableViews
|
||||
index={index}
|
||||
onChangeIndex={handleChangeIndex}
|
||||
className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<Settings />
|
||||
<Home />
|
||||
</SwipeableViews>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
|
@ -1,25 +1,32 @@
|
||||
import { Cog6ToothIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
Cog6ToothIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="hidden lg:block text-white p-4">
|
||||
<div className="container mx-auto flex justify-between items-center">
|
||||
<a href='/'>
|
||||
<div className="flex items-center">
|
||||
<img src="/tao.png" className="h-8" />
|
||||
<span className="font-bold">The Anon Operation</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href='/settings'>
|
||||
<button className="ml-auto pr-4">
|
||||
<QuestionMarkCircleIcon className="h-6 w-6 text-transperant" />
|
||||
</button>
|
||||
|
||||
<button className="">
|
||||
<Cog6ToothIcon className="h-6 w-6 text-transperant" />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
return (
|
||||
<header className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<a href="/">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/tao.png" className="h-8" alt="logo" />
|
||||
<span className="font-semibold text-white text-sm">
|
||||
The Anon Operation
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
className="text-neutral-300 inline-flex gap-4 items-center"
|
||||
>
|
||||
<button>
|
||||
<QuestionMarkCircleIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button>
|
||||
<Cog6ToothIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PostCard from './PostCard/PostCard';
|
||||
import NewThreadCard from './PostCard/NewThreadCard';
|
||||
import { getPow } from '../utils/mine';
|
||||
import { Event } from 'nostr-tools';
|
||||
import { subGlobalFeed } from '../utils/subscriptions';
|
||||
import { uniqBy } from '../utils/utils';
|
||||
import PWAInstallPopup from './Modals/PWACheckModal';
|
||||
import { useEffect, useState } from "react";
|
||||
import PostCard from "./PostCard/PostCard";
|
||||
import NewThreadCard from "./PostCard/NewThreadCard";
|
||||
import { getPow } from "../utils/mine";
|
||||
import { Event } from "nostr-tools";
|
||||
import { subGlobalFeed } from "../utils/subscriptions";
|
||||
import { uniqBy } from "../utils/utils";
|
||||
import PWAInstallPopup from "./Modals/PWACheckModal";
|
||||
|
||||
const Home = () => {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || '20');
|
||||
const [inBrowser, setInBrowser] = useState(false)
|
||||
const [filterDifficulty, setFilterDifficulty] = useState(
|
||||
localStorage.getItem("filterDifficulty") || "20"
|
||||
);
|
||||
const [inBrowser, setInBrowser] = useState(false);
|
||||
|
||||
const onEvent = (event: Event) => {
|
||||
setEvents((prevEvents) => [...prevEvents, event]);
|
||||
@ -33,43 +35,54 @@ const Home = () => {
|
||||
// setInBrowser(true)
|
||||
// }
|
||||
|
||||
window.addEventListener('difficultyChanged', handleDifficultyChange);
|
||||
window.addEventListener("difficultyChanged", handleDifficultyChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('difficultyChanged', handleDifficultyChange);
|
||||
window.removeEventListener("difficultyChanged", handleDifficultyChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const uniqEvents = events.length > 0 ? uniqBy(events, "id") : [];
|
||||
|
||||
const filteredAndSortedEvents = uniqEvents
|
||||
.filter(event =>
|
||||
getPow(event.id) > Number(filterDifficulty) &&
|
||||
event.kind === 1 &&
|
||||
!event.tags.some(tag => tag[0] === 'e')
|
||||
.filter(
|
||||
(event) =>
|
||||
getPow(event.id) > Number(filterDifficulty) &&
|
||||
event.kind === 1 &&
|
||||
!event.tags.some((tag) => tag[0] === "e")
|
||||
)
|
||||
.sort((a, b) => (b.created_at as any) - (a.created_at as any));
|
||||
|
||||
|
||||
const getMetadataEvent = (event: Event) => {
|
||||
const metadataEvent = uniqEvents.find(e => e.pubkey === event.pubkey && e.kind === 0);
|
||||
const metadataEvent = uniqEvents.find(
|
||||
(e) => e.pubkey === event.pubkey && e.kind === 0
|
||||
);
|
||||
if (metadataEvent) {
|
||||
return metadataEvent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const countReplies = (event: Event) => {
|
||||
return uniqEvents.filter(e => e.tags.some(tag => tag[0] === 'e' && tag[1] === event.id)).length;
|
||||
}
|
||||
return uniqEvents.filter((e) =>
|
||||
e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id)
|
||||
).length;
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="bg-black text-white min-h-screen">
|
||||
<main className="text-white mb-20">
|
||||
{/* {inBrowser && <PWAInstallPopup onClose={() => setInBrowser(false)} />} */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-16">
|
||||
<NewThreadCard />
|
||||
{filteredAndSortedEvents.map((event, index) => (
|
||||
<PostCard key={event.id} event={event} metadata={getMetadataEvent(event)} replyCount={countReplies(event)}/>
|
||||
</div>
|
||||
<div className="columns-1 md:columns-2 lg:columns-3 [column-fill:_balance] box-border gap-4">
|
||||
{filteredAndSortedEvents.map((event) => (
|
||||
<PostCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
metadata={getMetadataEvent(event)}
|
||||
replyCount={countReplies(event)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { parseContent } from '../../utils/content';
|
||||
import { Event } from 'nostr-tools';
|
||||
import { getMetadata, uniqBy } from '../../utils/utils';
|
||||
import ContentPreview from './TextModal';
|
||||
import { renderMedia } from '../../utils/FileUpload';
|
||||
import { parseContent } from "../../utils/content";
|
||||
import { Event } from "nostr-tools";
|
||||
import { getMetadata, uniqBy } from "../../utils/utils";
|
||||
import ContentPreview from "./TextModal";
|
||||
import { renderMedia } from "../../utils/FileUpload";
|
||||
|
||||
const colorCombos = [
|
||||
'from-red-400 to-yellow-500',
|
||||
'from-green-400 to-blue-500',
|
||||
'from-purple-400 to-pink-500',
|
||||
'from-yellow-400 to-orange-500',
|
||||
'from-indigo-400 to-purple-500',
|
||||
'from-pink-400 to-red-500',
|
||||
'from-blue-400 to-indigo-500',
|
||||
'from-orange-400 to-red-500',
|
||||
'from-teal-400 to-green-500',
|
||||
'from-cyan-400 to-teal-500',
|
||||
'from-lime-400 to-green-500',
|
||||
'from-amber-400 to-orange-500',
|
||||
'from-rose-400 to-pink-500',
|
||||
'from-violet-400 to-purple-500',
|
||||
'from-sky-400 to-cyan-500'
|
||||
"from-red-400 to-yellow-500",
|
||||
"from-green-400 to-blue-500",
|
||||
"from-purple-400 to-pink-500",
|
||||
"from-yellow-400 to-orange-500",
|
||||
"from-indigo-400 to-purple-500",
|
||||
"from-pink-400 to-red-500",
|
||||
"from-blue-400 to-indigo-500",
|
||||
"from-orange-400 to-red-500",
|
||||
"from-teal-400 to-green-500",
|
||||
"from-cyan-400 to-teal-500",
|
||||
"from-lime-400 to-green-500",
|
||||
"from-amber-400 to-orange-500",
|
||||
"from-rose-400 to-pink-500",
|
||||
"from-violet-400 to-purple-500",
|
||||
"from-sky-400 to-cyan-500",
|
||||
];
|
||||
|
||||
const getColorFromHash = (id: string, colors: string[]): string => {
|
||||
@ -35,7 +35,7 @@ const getColorFromHash = (id: string, colors: string[]): string => {
|
||||
};
|
||||
|
||||
const timeAgo = (unixTime: number) => {
|
||||
const seconds = Math.floor((new Date().getTime() / 1000) - unixTime);
|
||||
const seconds = Math.floor(new Date().getTime() / 1000 - unixTime);
|
||||
|
||||
if (seconds < 60) return `now`;
|
||||
|
||||
@ -52,38 +52,50 @@ const timeAgo = (unixTime: number) => {
|
||||
return `${weeks}w`;
|
||||
};
|
||||
|
||||
const QuoteEmbed = ({ event, metadata }: { event: Event, metadata: Event | null}) => {
|
||||
const { comment, file } = parseContent(event);
|
||||
const colorCombo = getColorFromHash(event.pubkey, colorCombos);
|
||||
const QuoteEmbed = ({
|
||||
event,
|
||||
metadata,
|
||||
}: {
|
||||
event: Event;
|
||||
metadata: Event | null;
|
||||
}) => {
|
||||
const { comment, file } = parseContent(event);
|
||||
const colorCombo = getColorFromHash(event.pubkey, colorCombos);
|
||||
|
||||
let metadataParsed = null;
|
||||
if (metadata !== null) {
|
||||
metadataParsed = getMetadata(metadata);
|
||||
}
|
||||
let metadataParsed = null;
|
||||
if (metadata !== null) {
|
||||
metadataParsed = getMetadata(metadata);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-1 bg-gradient-to-r from-black to-neutral-900 rounded-lg border border-neutral-800">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
{metadataParsed ?
|
||||
<div className="p-3 rounded-lg border border-neutral-700 bg-neutral-800">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{metadataParsed ? (
|
||||
<>
|
||||
<img className={`h-5 w-5 rounded-full`} src={metadataParsed.picture} />
|
||||
<div className="ml-2 text-sm font-semibold">{metadataParsed.name}</div>
|
||||
<img
|
||||
className={`h-5 w-5 rounded-full`}
|
||||
src={metadataParsed.picture}
|
||||
alt=""
|
||||
/>
|
||||
<div className="text-sm font-medium">{metadataParsed.name}</div>
|
||||
</>
|
||||
:
|
||||
) : (
|
||||
<>
|
||||
<div className={`h-5 w-5 bg-gradient-to-r ${colorCombo} rounded-full`} />
|
||||
<div className="ml-2 text-sm font-semibold">Anonymous</div>
|
||||
<div
|
||||
className={`h-5 w-5 bg-gradient-to-r ${colorCombo} rounded-full`}
|
||||
/>
|
||||
<div className="text-sm font-medium">Anonymous</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mr-2 flex flex-col break-words">
|
||||
<ContentPreview key={event.id} comment={comment} />
|
||||
</div>
|
||||
{renderMedia(file)}
|
||||
</div>
|
||||
<div className="flex flex-col break-words">
|
||||
<ContentPreview key={event.id} comment={comment} />
|
||||
</div>
|
||||
{renderMedia(file)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
import QuoteEmbed from "./QuoteEmbed";
|
||||
import { Event } from 'nostr-tools';
|
||||
import { Event } from "nostr-tools";
|
||||
import { useEffect, useState } from "react";
|
||||
import { subNoteOnce } from '../../utils/subscriptions';
|
||||
import { subNoteOnce } from "../../utils/subscriptions";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import LinkModal from "./LinkPreview";
|
||||
|
||||
const ContentPreview = ({ key, comment }: { key: string, comment: string }) => {
|
||||
const [finalComment, setFinalComment] = useState(comment)
|
||||
const ContentPreview = ({ key, comment }: { key: string; comment: string }) => {
|
||||
const [finalComment, setFinalComment] = useState(comment);
|
||||
const [quoteEvents, setQuoteEvents] = useState<Event[]>([]); // Initialize state
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [url, setUrl] = useState('')
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
// Define your callback function for subGlobalFeed
|
||||
const onEvent = (event: Event, relay: string) => {
|
||||
@ -19,8 +19,8 @@ const ContentPreview = ({ key, comment }: { key: string, comment: string }) => {
|
||||
useEffect(() => {
|
||||
const findUrl = comment.match(/\bhttps?:\/\/\S+/gi);
|
||||
if (findUrl && findUrl.length > 0) {
|
||||
setUrl(findUrl[0])
|
||||
setFinalComment(finalComment.replace(findUrl[0], '').trim())
|
||||
setUrl(findUrl[0]);
|
||||
setFinalComment(finalComment.replace(findUrl[0], "").trim());
|
||||
}
|
||||
|
||||
const match = comment.match(/\bnostr:([a-z0-9]+)/i);
|
||||
@ -28,39 +28,43 @@ const ContentPreview = ({ key, comment }: { key: string, comment: string }) => {
|
||||
if (nostrQuoteID && nostrQuoteID.length > 0) {
|
||||
let id_to_hex = String(nip19.decode(nostrQuoteID as string).data);
|
||||
subNoteOnce(id_to_hex, onEvent);
|
||||
setFinalComment(finalComment.replace('nostr:' + nostrQuoteID, '').trim())
|
||||
setFinalComment(finalComment.replace("nostr:" + nostrQuoteID, "").trim());
|
||||
}
|
||||
}, [comment, finalComment]);
|
||||
|
||||
const getMetadataEvent = (event: Event) => {
|
||||
const metadataEvent = quoteEvents.find(e => e.pubkey === event.pubkey && e.kind === 0);
|
||||
const metadataEvent = quoteEvents.find(
|
||||
(e) => e.pubkey === event.pubkey && e.kind === 0
|
||||
);
|
||||
if (metadataEvent) {
|
||||
return metadataEvent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mr-2 flex flex-col break-words text-sm">
|
||||
<div className="gap-2 flex flex-col break-words text-sm">
|
||||
{isExpanded ? finalComment : finalComment.slice(0, 240)}
|
||||
{finalComment.length > 240 && (
|
||||
<button
|
||||
className="text-gray-500 text-sm text-neutral-500"
|
||||
className="text-sm text-neutral-500"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? '...Read less' : '...Read more'}
|
||||
{isExpanded ? "...Read less" : "...Read more"}
|
||||
</button>
|
||||
)}
|
||||
{url !== '' && (
|
||||
<LinkModal key={key} url={url} />
|
||||
)}
|
||||
{url !== "" && <LinkModal key={key} url={url} />}
|
||||
{quoteEvents[0] && quoteEvents.length > 0 && (
|
||||
<a href={`/thread/${nip19.noteEncode(quoteEvents[0].id)}`}>
|
||||
<QuoteEmbed key={key} event={quoteEvents[0]} metadata={getMetadataEvent(quoteEvents[0])} />
|
||||
<QuoteEmbed
|
||||
key={key}
|
||||
event={quoteEvents[0]}
|
||||
metadata={getMetadataEvent(quoteEvents[0])}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ContentPreview;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export default function CardContainer({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="card bg-gradient-to-r from-black to-neutral-950 shadow-lg shadow-black">
|
||||
<div className="card-body p-4">{children}</div>
|
||||
<div className="card break-inside-avoid mb-4 bg-neutral-900 border border-transparent hover:border-neutral-800">
|
||||
<div className="card-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,22 +1,31 @@
|
||||
import CardContainer from './CardContainer';
|
||||
import { ArrowUpTrayIcon, CpuChipIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import { XCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { generatePrivateKey, getPublicKey, finishEvent } from 'nostr-tools';
|
||||
import { publish } from '../../utils/relays';
|
||||
import FileUpload from '../../utils/FileUpload';
|
||||
import { renderMedia } from '../../utils/FileUpload';
|
||||
import CardContainer from "./CardContainer";
|
||||
import {
|
||||
ArrowUpTrayIcon,
|
||||
CpuChipIcon,
|
||||
ArrowPathIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { generatePrivateKey, getPublicKey, finishEvent } from "nostr-tools";
|
||||
import { publish } from "../../utils/relays";
|
||||
import FileUpload from "../../utils/FileUpload";
|
||||
import { renderMedia } from "../../utils/FileUpload";
|
||||
|
||||
const NewThreadCard: React.FC = () => {
|
||||
const [comment, setComment] = useState("");
|
||||
const [file, setFile] = useState("");
|
||||
const [sk, setSk] = useState(generatePrivateKey());
|
||||
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || '21');
|
||||
const [difficulty, setDifficulty] = useState(
|
||||
localStorage.getItem("difficulty") || "21"
|
||||
);
|
||||
const [uploadingFile, setUploadingFile] = useState(false);
|
||||
const [messageFromWorker, setMessageFromWorker] = useState(null);
|
||||
const [doingWorkProp, setDoingWorkProp] = useState(false);
|
||||
// Initialize the worker outside of any effects
|
||||
const worker = useMemo(() => new Worker(new URL('../../powWorker', import.meta.url)), []);
|
||||
const worker = useMemo(
|
||||
() => new Worker(new URL("../../powWorker", import.meta.url)),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
worker.onmessage = (event) => {
|
||||
@ -29,10 +38,10 @@ const NewThreadCard: React.FC = () => {
|
||||
setDifficulty(difficulty);
|
||||
};
|
||||
|
||||
window.addEventListener('difficultyChanged', handleDifficultyChange);
|
||||
window.addEventListener("difficultyChanged", handleDifficultyChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('difficultyChanged', handleDifficultyChange);
|
||||
window.removeEventListener("difficultyChanged", handleDifficultyChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -46,12 +55,12 @@ const NewThreadCard: React.FC = () => {
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: getPublicKey(sk),
|
||||
},
|
||||
difficulty
|
||||
difficulty,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDoingWorkProp(false)
|
||||
setDoingWorkProp(false);
|
||||
if (messageFromWorker) {
|
||||
try {
|
||||
const signedEvent = finishEvent(messageFromWorker, sk);
|
||||
@ -66,17 +75,17 @@ const NewThreadCard: React.FC = () => {
|
||||
worker.terminate();
|
||||
};
|
||||
} catch (error) {
|
||||
setComment(error + ' ' + comment);
|
||||
setComment(error + " " + comment);
|
||||
}
|
||||
}
|
||||
}, [messageFromWorker]);
|
||||
|
||||
async function attachFile(file_input: File | null) {
|
||||
setUploadingFile(true); // start loading
|
||||
setUploadingFile(true); // start loading
|
||||
try {
|
||||
if (file_input) {
|
||||
const rx = await FileUpload(file_input);
|
||||
setUploadingFile(false); // stop loading
|
||||
setUploadingFile(false); // stop loading
|
||||
if (rx.url) {
|
||||
setFile(rx.url);
|
||||
} else if (rx?.error) {
|
||||
@ -84,86 +93,100 @@ const NewThreadCard: React.FC = () => {
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setUploadingFile(false); // stop loading
|
||||
setUploadingFile(false); // stop loading
|
||||
if (error instanceof Error) {
|
||||
setFile(error?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContainer>
|
||||
<form
|
||||
name="post"
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={(event) => {
|
||||
handleSubmit(event);
|
||||
setDoingWorkProp(true);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
|
||||
<div id="togglePostFormLink" className="text-lg font-semibold">
|
||||
Start a New Thread
|
||||
<form
|
||||
name="post"
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={(event) => {
|
||||
handleSubmit(event);
|
||||
setDoingWorkProp(true);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
|
||||
<div
|
||||
id="togglePostFormLink"
|
||||
className="text-lg text-neutral-700 text-center mb-2 font-semibold"
|
||||
>
|
||||
Start a New Thread
|
||||
</div>
|
||||
<div className="px-4 pt-4 flex flex-col bg-neutral-900 border border-neutral-800 rounded-lg">
|
||||
<textarea
|
||||
name="com"
|
||||
wrap="soft"
|
||||
className="shadow-lg w-full px-4 py-3 h-28 rounded-md outline-none focus:outline-none bg-neutral-800 border border-neutral-700 text-white placeholder:text-neutral-500"
|
||||
placeholder="Shitpost here..."
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
<div className="h-14 flex items-center justify-between">
|
||||
<div className="inline-flex items-center gap-2 bg-neutral-800 px-1.5 py-1 rounded-lg">
|
||||
<div className="inline-flex items-center gap-1.5 text-neutral-300">
|
||||
<CpuChipIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-neutral-400">
|
||||
{difficulty} POW
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
name="com"
|
||||
wrap="soft"
|
||||
className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none placeholder-blue-300"
|
||||
placeholder='Shitpost here...'
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
{file != '' &&
|
||||
<button onClick={() => setFile('')}><XCircleIcon className="h-10 w-10 absolute shadow z-100 text-blue-500" /></button>
|
||||
}
|
||||
{renderMedia(file)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<ArrowUpTrayIcon
|
||||
className="h-6 w-6 text-white cursor-pointer"
|
||||
onClick={() => document.getElementById('file_input')?.click()}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
name="file_input"
|
||||
id="file_input"
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => {
|
||||
const file_input = e.target.files?.[0];
|
||||
if (file_input) {
|
||||
attachFile(file_input);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{uploadingFile ? (
|
||||
<div className='flex animate-spin text-sm text-gray-300'>
|
||||
<ArrowPathIcon className="h-4 w-4 ml-auto" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="relative">
|
||||
{file !== "" && (
|
||||
<button onClick={() => setFile("")}>
|
||||
<XCircleIcon className="h-10 w-10 absolute shadow z-100 text-blue-500" />
|
||||
</button>
|
||||
)}
|
||||
{renderMedia(file)}
|
||||
</div>
|
||||
<span className="flex items-center"><CpuChipIcon className="h-6 w-6 text-white" />: {difficulty}</span>
|
||||
<button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
{doingWorkProp ? (
|
||||
<div className='flex animate-pulse text-sm text-gray-300'>
|
||||
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
||||
<span>Generating Proof-of-Work...</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center">
|
||||
<ArrowUpTrayIcon
|
||||
className="h-4 w-4 text-neutral-400 cursor-pointer"
|
||||
onClick={() => document.getElementById("file_input")?.click()}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
name="file_input"
|
||||
id="file_input"
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
const file_input = e.target.files?.[0];
|
||||
if (file_input) {
|
||||
attachFile(file_input);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{uploadingFile ? (
|
||||
<div className="flex animate-spin text-sm text-gray-300">
|
||||
<ArrowPathIcon className="h-4 w-4 ml-auto" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="h-9 inline-flex items-center justify-center px-4 bg-blue-500 hover:bg-blue-600 rounded-lg text-white font-medium text-sm"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
</form>
|
||||
</CardContainer>
|
||||
</>
|
||||
{doingWorkProp ? (
|
||||
<div className="flex animate-pulse text-sm text-gray-300">
|
||||
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
||||
<span>Generating Proof-of-Work...</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,28 +1,28 @@
|
||||
import CardContainer from './CardContainer';
|
||||
import { FolderIcon } from '@heroicons/react/24/outline';
|
||||
import { parseContent } from '../../utils/content';
|
||||
import { Event } from 'nostr-tools';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { getMetadata } from '../../utils/utils';
|
||||
import ContentPreview from'../Modals/TextModal';
|
||||
import { renderMedia } from '../../utils/FileUpload';
|
||||
import CardContainer from "./CardContainer";
|
||||
import { ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
import { parseContent } from "../../utils/content";
|
||||
import { Event } from "nostr-tools";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getMetadata } from "../../utils/utils";
|
||||
import ContentPreview from "../Modals/TextModal";
|
||||
import { renderMedia } from "../../utils/FileUpload";
|
||||
|
||||
const colorCombos = [
|
||||
'from-red-400 to-yellow-500',
|
||||
'from-green-400 to-blue-500',
|
||||
'from-purple-400 to-pink-500',
|
||||
'from-yellow-400 to-orange-500',
|
||||
'from-indigo-400 to-purple-500',
|
||||
'from-pink-400 to-red-500',
|
||||
'from-blue-400 to-indigo-500',
|
||||
'from-orange-400 to-red-500',
|
||||
'from-teal-400 to-green-500',
|
||||
'from-cyan-400 to-teal-500',
|
||||
'from-lime-400 to-green-500',
|
||||
'from-amber-400 to-orange-500',
|
||||
'from-rose-400 to-pink-500',
|
||||
'from-violet-400 to-purple-500',
|
||||
'from-sky-400 to-cyan-500'
|
||||
"from-red-400 to-yellow-500",
|
||||
"from-green-400 to-blue-500",
|
||||
"from-purple-400 to-pink-500",
|
||||
"from-yellow-400 to-orange-500",
|
||||
"from-indigo-400 to-purple-500",
|
||||
"from-pink-400 to-red-500",
|
||||
"from-blue-400 to-indigo-500",
|
||||
"from-orange-400 to-red-500",
|
||||
"from-teal-400 to-green-500",
|
||||
"from-cyan-400 to-teal-500",
|
||||
"from-lime-400 to-green-500",
|
||||
"from-amber-400 to-orange-500",
|
||||
"from-rose-400 to-pink-500",
|
||||
"from-violet-400 to-purple-500",
|
||||
"from-sky-400 to-cyan-500",
|
||||
];
|
||||
|
||||
const getColorFromHash = (id: string, colors: string[]): string => {
|
||||
@ -38,7 +38,7 @@ const getColorFromHash = (id: string, colors: string[]): string => {
|
||||
};
|
||||
|
||||
const timeAgo = (unixTime: number) => {
|
||||
const seconds = Math.floor((new Date().getTime() / 1000) - unixTime);
|
||||
const seconds = Math.floor(new Date().getTime() / 1000 - unixTime);
|
||||
|
||||
if (seconds < 60) return `now`;
|
||||
|
||||
@ -55,7 +55,17 @@ const timeAgo = (unixTime: number) => {
|
||||
return `${weeks}w`;
|
||||
};
|
||||
|
||||
const PostCard = ({ key, event, metadata, replyCount }: { key: string, event: Event, metadata: Event | null, replyCount: number }) => {
|
||||
const PostCard = ({
|
||||
key,
|
||||
event,
|
||||
metadata,
|
||||
replyCount,
|
||||
}: {
|
||||
key: string;
|
||||
event: Event;
|
||||
metadata: Event | null;
|
||||
replyCount: number;
|
||||
}) => {
|
||||
let { comment, file } = parseContent(event);
|
||||
const colorCombo = getColorFromHash(event.pubkey, colorCombos);
|
||||
|
||||
@ -65,39 +75,55 @@ const PostCard = ({ key, event, metadata, replyCount }: { key: string, event: Ev
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContainer>
|
||||
<a href={`/thread/${nip19.noteEncode(event.id)}`}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
{metadataParsed ?
|
||||
<>
|
||||
<img className={`h-8 w-8 rounded-full`} src={metadataParsed.picture} />
|
||||
<div className="ml-2 text-md font-semibold">{metadataParsed.name}</div>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<div className={`h-8 w-8 bg-gradient-to-r ${colorCombo} rounded-full`} />
|
||||
<div className="ml-2 text-md font-semibold">Anonymous</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center ml-auto">
|
||||
<div className="text-xs text-gray-500">{event.id.match(/^0*([^\0]{2})/)?.[0] || 0}</div>
|
||||
<div className="text-xs font-semibold text-gray-500 mr-2"> {timeAgo(event.created_at)}</div>
|
||||
<FolderIcon className="h-5 w-5 mr-1 text-gray-500" />
|
||||
<span className="text-xs text-gray-500">{replyCount}</span>
|
||||
</div>
|
||||
<CardContainer>
|
||||
<a href={`/thread/${nip19.noteEncode(event.id)}`}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{metadataParsed ? (
|
||||
<>
|
||||
<img
|
||||
className={`h-8 w-8 rounded-full`}
|
||||
src={metadataParsed.picture}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="text-md font-semibold">
|
||||
{metadataParsed.name}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`h-7 w-7 bg-gradient-to-r ${colorCombo} rounded-full`}
|
||||
/>
|
||||
<div className="text-sm font-semibold">Anonymous</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mr-2 flex flex-col break-words">
|
||||
<ContentPreview key={event.id} comment={comment} />
|
||||
<div className="flex items-center ml-auto gap-2.5">
|
||||
<div className="text-xs text-neutral-600">
|
||||
{event.id.match(/^0*([^\0]{2})/)?.[0] || 0}
|
||||
</div>
|
||||
<span className="text-neutral-700">·</span>
|
||||
<div className="text-xs font-semibold text-neutral-600">
|
||||
{timeAgo(event.created_at)}
|
||||
</div>
|
||||
<span className="text-neutral-700">·</span>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<ChatBubbleLeftEllipsisIcon className="h-4 w-4 text-neutral-600" />
|
||||
<span className="text-xs text-neutral-600">{replyCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{renderMedia(file)}
|
||||
</CardContainer>
|
||||
</>
|
||||
<div className="flex flex-col break-words">
|
||||
<ContentPreview key={event.id} comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{renderMedia(file)}
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -20,7 +20,7 @@ code {
|
||||
@layer components {
|
||||
.card {
|
||||
border-radius: theme('borderRadius.lg');
|
||||
padding: theme('spacing.6');
|
||||
padding: theme('spacing.4');
|
||||
}
|
||||
/* ... */
|
||||
}
|
@ -4,11 +4,11 @@ export interface UploadResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to void.cat
|
||||
* https://void.cat/swagger/index.html
|
||||
*/
|
||||
* Upload file to void.cat
|
||||
* https://void.cat/swagger/index.html
|
||||
*/
|
||||
|
||||
export default async function FileUpload(file: File ): Promise<UploadResult> {
|
||||
export default async function FileUpload(file: File): Promise<UploadResult> {
|
||||
const buf = await file.arrayBuffer();
|
||||
|
||||
const req = await fetch("https://void.cat/upload", {
|
||||
@ -23,10 +23,10 @@ export default async function FileUpload(file: File ): Promise<UploadResult> {
|
||||
},
|
||||
});
|
||||
if (req.ok) {
|
||||
let rsp: VoidUploadResponse = await req.json();
|
||||
const fileExtension = file.name.split('.').pop(); // Extracting the file extension
|
||||
const resultUrl = `https://void.cat/d/${rsp.file?.id}.${fileExtension}`;
|
||||
return {url: resultUrl};
|
||||
let rsp: VoidUploadResponse = await req.json();
|
||||
const fileExtension = file.name.split(".").pop(); // Extracting the file extension
|
||||
const resultUrl = `https://void.cat/d/${rsp.file?.id}.${fileExtension}`;
|
||||
return { url: resultUrl };
|
||||
}
|
||||
return {
|
||||
error: "Upload failed",
|
||||
@ -36,48 +36,56 @@ export default async function FileUpload(file: File ): Promise<UploadResult> {
|
||||
export const renderMedia = (file: string) => {
|
||||
if (file && (file.endsWith(".mp4") || file.endsWith(".webm"))) {
|
||||
return (
|
||||
<video controls className="thumb">
|
||||
<video
|
||||
controls
|
||||
muted
|
||||
preload="metadata"
|
||||
className="thumb mt-2 rounded-md w-full ring-1 ring-neutral-800"
|
||||
>
|
||||
<source src={file} type="video/mp4" />
|
||||
</video>
|
||||
);
|
||||
} else if (!file.includes("http")) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
return <></>;
|
||||
} else {
|
||||
return (
|
||||
<img alt="Invalid thread" loading="lazy" className="thumb" src={file} />
|
||||
<img
|
||||
alt="Invalid thread"
|
||||
loading="lazy"
|
||||
className="thumb mt-2 rounded-md w-full ring-1 ring-neutral-800"
|
||||
src={file}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export interface UploadResult {
|
||||
url?: string;
|
||||
error?: string;
|
||||
url?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type VoidUploadResponse = {
|
||||
ok: boolean,
|
||||
file?: VoidFile,
|
||||
errorMessage?: string,
|
||||
}
|
||||
ok: boolean;
|
||||
file?: VoidFile;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export type VoidFile = {
|
||||
id: string,
|
||||
meta?: VoidFileMeta
|
||||
}
|
||||
id: string;
|
||||
meta?: VoidFileMeta;
|
||||
};
|
||||
|
||||
export type VoidFileMeta = {
|
||||
version: number,
|
||||
id: string,
|
||||
name?: string,
|
||||
size: number,
|
||||
uploaded: Date,
|
||||
description?: string,
|
||||
mimeType?: string,
|
||||
digest?: string,
|
||||
url?: string,
|
||||
expires?: Date,
|
||||
storage?: string,
|
||||
encryptionParams?: string,
|
||||
}
|
||||
version: number;
|
||||
id: string;
|
||||
name?: string;
|
||||
size: number;
|
||||
uploaded: Date;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
digest?: string;
|
||||
url?: string;
|
||||
expires?: Date;
|
||||
storage?: string;
|
||||
encryptionParams?: string;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user