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