card modal and feed clean-up

This commit is contained in:
smolgrrr 2023-11-08 12:54:46 +11:00
parent 3a1ca92d30
commit 26bf02e771
12 changed files with 179 additions and 327 deletions

View File

@ -4,7 +4,7 @@ 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";
import Header from "./components/Header/Header"; import Header from "./components/Header/Header";
function App() { function App() {

View File

@ -1,4 +1,4 @@
import CardContainer from "./CardContainer"; import CardContainer from "../Modals/CardContainer";
import { import {
ArrowUpTrayIcon, ArrowUpTrayIcon,
CpuChipIcon, CpuChipIcon,

View File

@ -1,11 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
import PostCard from "./PostCard/PostCard"; import PostCard from "./Modals/Card";
import NewThreadCard from "./PostCard/NewThreadCard"; import NewThreadCard from "./Forms/NewThreadCard";
import { uniqBy } from "../utils/utils"; // Assume getPow is a correct import now import { uniqBy } from "../utils/utils"; // Assume getPow is a correct import now
import { subGlobalFeed } from "../utils/subscriptions"; import { subGlobalFeed } from "../utils/subscriptions";
import { verifyPow } from "../utils/mine"; import { verifyPow } from "../utils/mine";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
const DEFAULT_DIFFICULTY = 20;
const useUniqEvents = () => { const useUniqEvents = () => {
const [events, setEvents] = useState<Event[]>([]); const [events, setEvents] = useState<Event[]>([]);
@ -20,7 +22,7 @@ const useUniqEvents = () => {
}; };
const Home = () => { const Home = () => {
const filterDifficulty = localStorage.getItem("filterDifficulty") || 20; const filterDifficulty = localStorage.getItem("filterDifficulty") || DEFAULT_DIFFICULTY;
const [sortByTime, setSortByTime] = useState(true); const [sortByTime, setSortByTime] = useState(true);
const uniqEvents = useUniqEvents(); const uniqEvents = useUniqEvents();
@ -35,9 +37,9 @@ const Home = () => {
sortByTime ? b.created_at - a.created_at : verifyPow(b) - verifyPow(a) sortByTime ? b.created_at - a.created_at : verifyPow(b) - verifyPow(a)
); );
const toggleSort = () => { const toggleSort = useCallback(() => {
setSortByTime(prev => !prev); setSortByTime(prev => !prev);
}; }, []);
const getMetadataEvent = (event: Event) => { const getMetadataEvent = (event: Event) => {
return uniqEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null; return uniqEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null;

View File

@ -0,0 +1,116 @@
import CardContainer from "./CardContainer";
import { FolderIcon, CpuChipIcon } from "@heroicons/react/24/outline";
import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools";
import { getMetadata } from "../../utils/utils";
import ContentPreview from "./TextModal";
import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash } from "../../utils/deterministicProfileIcon";
import { verifyPow } from "../../utils/mine";
import { uniqBy } from "../../utils/utils";
import { useNavigate } from 'react-router-dom';
const timeUnits = [
{ unit: 'w', value: 60 * 60 * 24 * 7 },
{ unit: 'd', value: 60 * 60 * 24 },
{ unit: 'h', value: 60 * 60 },
{ unit: 'm', value: 60 },
];
const timeAgo = (unixTime: number) => {
let seconds = Math.floor(new Date().getTime() / 1000 - unixTime);
if (seconds < 60) return `now`;
for (let unit of timeUnits) {
if (seconds >= unit.value) {
return `${Math.floor(seconds / unit.value)}${unit.unit}`;
}
seconds %= unit.value;
}
};
interface CardProps {
key?: string | number;
event: Event;
metadata: Event | null;
replyCount: number;
repliedTo?: Event[]
type?: 'OP' | 'Reply' | 'Post';
}
const PostCard = ({
key,
event,
metadata,
replyCount,
repliedTo,
type
}: CardProps) => {
const { comment, file } = parseContent(event);
const icon = getIconFromHash(event.pubkey);
const metadataParsed = metadata ? getMetadata(metadata) : null;
const navigate = useNavigate();
const handleClick = () => {
if (type !== "OP") {
navigate(`/thread/${nip19.noteEncode(event.id)}`);
}
};
return (
<CardContainer>
<div className={`flex flex-col gap-2 ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
<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 ?? icon}
alt=""
loading="lazy"
decoding="async"/>
:
<div className={`h-7 w-7 ${icon} rounded-full`} />
}
<div className="text-md font-semibold">
{metadataParsed?.name ?? 'Anonymous'}
</div>
</div>
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(event)}
</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">
<FolderIcon className="h-4 w-4 text-neutral-600" />
<span className="text-xs text-neutral-600">{replyCount}</span>
</div>
</div>
</div>
{repliedTo && <div className="flex items-center my-1" >
<span className="text-xs text-gray-500">Reply to: </span>
{uniqBy(repliedTo, 'pubkey').map((event, index) => (
<div key={index}>
{event.kind == 0 ? (
<img className={`h-5 w-5 rounded-full`} src={getMetadata(event)?.picture} />
) : (
<div className={`h-5 w-5 ${getIconFromHash(event.pubkey)} rounded-full`} />
)}
</div>
))}
</div>}
<div className="flex flex-col break-words">
<ContentPreview key={event.id} comment={comment} />
</div>
</div>
{renderMedia(file)}
</CardContainer>
);
};
export default PostCard;

View File

@ -0,0 +1,24 @@
const Placeholder = () => {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
<div className="border border-blue-300 shadow rounded-md p-4 max-w-sm w-full mx-auto">
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-slate-700 h-10 w-10"></div>
<div className="flex-1 space-y-6 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
</div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Placeholder;

View File

@ -1,101 +0,0 @@
import CardContainer from "./CardContainer";
import { FolderIcon, CpuChipIcon } from "@heroicons/react/24/outline";
import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools";
import { getMetadata } from "../../utils/utils";
import ContentPreview from "../Modals/TextModal";
import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash } from "../../utils/deterministicProfileIcon";
import { verifyPow } from "../../utils/mine";
const timeAgo = (unixTime: number) => {
const seconds = Math.floor(new Date().getTime() / 1000 - unixTime);
if (seconds < 60) return `now`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}d`;
const weeks = Math.floor(days / 7);
return `${weeks}w`;
};
const PostCard = ({
key,
event,
metadata,
replyCount,
}: {
key: string;
event: Event;
metadata: Event | null;
replyCount: number;
}) => {
let { comment, file } = parseContent(event);
const icon = getIconFromHash(event.pubkey);
let metadataParsed = null;
if (metadata !== null) {
metadataParsed = getMetadata(metadata);
}
return (
<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 ${icon} rounded-full`}
/>
<div className="text-sm font-semibold">Anonymous</div>
</>
)}
</div>
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(event)}
</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">
<FolderIcon className="h-4 w-4 text-neutral-600" />
<span className="text-xs text-neutral-600">{replyCount}</span>
</div>
</div>
</div>
<div className="flex flex-col break-words">
<ContentPreview key={event.id} comment={comment} />
</div>
</div>
</a>
{renderMedia(file)}
</CardContainer>
);
};
export default PostCard;

View File

@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
// import {publish} from './relays'; // import {publish} from './relays';
import { addRelay } from '../utils/relays'; import { addRelay } from '../utils/relays';
import { CpuChipIcon } from '@heroicons/react/24/outline'; import { CpuChipIcon } from '@heroicons/react/24/outline';
const Settings = () => { const Settings = () => {
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20); const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20);
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 21); const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 21);

View File

@ -1,16 +1,14 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useState } from "react"; import { useState } from "react";
import { Event, nip19 } from "nostr-tools" import { Event, nip19 } from "nostr-tools"
import { subNote, subNotesOnce } from '../../utils/subscriptions'; import { subNote, subNotesOnce } from '../utils/subscriptions';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { uniqBy } from '../../utils/utils'; import { uniqBy } from '../utils/utils';
import { DocumentTextIcon, FolderPlusIcon } from '@heroicons/react/24/outline'; import { DocumentTextIcon, FolderPlusIcon } from '@heroicons/react/24/outline';
import { generatePrivateKey, getPublicKey, finishEvent } from 'nostr-tools'; import { getPow } from '../utils/mine';
import { getPow } from '../../utils/mine'; import ThreadPost from './Forms/ThreadPost';
import { publish } from '../../utils/relays'; import PostCard from './Modals/Card';
import ThreadPost from './ThreadPost'; import Placeholder from './Modals/Placeholder';
import ReplyCard from './ReplyCard';
import OPCard from './OPCard';
const difficulty = 20 const difficulty = 20
@ -18,13 +16,16 @@ const difficulty = 20
const Thread = () => { const Thread = () => {
const { id } = useParams(); const { id } = useParams();
const [events, setEvents] = useState<Event[]>([]); // Initialize state const [events, setEvents] = useState<Event[]>([]); // Initialize state
let decodeResult = nip19.decode(id as string); const [OPEvent, setOPEvent] = useState<Event>()
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [postType, setPostType] = useState(""); const [postType, setPostType] = useState("");
const [hasRun, setHasRun] = useState(false); const [hasRun, setHasRun] = useState(false);
const [preOPEvents, setPreOPEvents] = useState(['']); const [preOPEvents, setPreOPEvents] = useState(['']);
const [sortByTime, setSortByTime] = useState(true); const [sortByTime, setSortByTime] = useState(true);
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem("filterDifficulty") || "20"); const filterDifficulty = useState(localStorage.getItem("filterDifficulty") || "20");
let decodeResult = nip19.decode(id as string);
let hexID = decodeResult.data as string;
// Define your callback function for subGlobalFeed // Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => { const onEvent = (event: Event, relay: string) => {
@ -32,28 +33,30 @@ const Thread = () => {
}; };
useEffect(() => { useEffect(() => {
setHasRun(false)
if (decodeResult.type === 'note') { if (decodeResult.type === 'note') {
let id_to_hex: string = decodeResult.data;
// Call your subNote function or do whatever you need to do with id_to_hex // Call your subNote function or do whatever you need to do with id_to_hex
subNote(id_to_hex, onEvent); subNote(hexID, onEvent);
} }
// Subscribe to global feed when the component mounts
// Optionally, return a cleanup function to unsubscribe when the component unmounts
return () => { return () => {
// Your cleanup code here // Your cleanup code here
}; };
}, []); // Empty dependency array means this useEffect runs once when the component mounts }, [id]); // Empty dependency array means this useEffect runs once when the component mounts
const uniqEvents = events.length > 0 ? uniqBy(events, "id") : []; const uniqEvents = events.length > 0 ? uniqBy(events, "id") : [];
useEffect(() => { useEffect(() => {
if (!hasRun && events.length > 0) { if (!hasRun && events.length > 0) {
let OPNoteEvents = events[0].tags.filter(tag => tag[0] === 'e').map(tag => tag[1]); let OPEvent = events.find(e => e.id === hexID);
console.log(OPNoteEvents);
if (OPEvent) {
setOPEvent(OPEvent);
let OPNoteEvents = OPEvent.tags.filter(tag => tag[0] === 'e').map(tag => tag[1]);
setHasRun(true); setHasRun(true);
setPreOPEvents(OPNoteEvents) setPreOPEvents(OPNoteEvents)
subNotesOnce(OPNoteEvents, onEvent) subNotesOnce(OPNoteEvents, onEvent)
}
} }
}, [uniqEvents, hasRun]); }, [uniqEvents, hasRun]);
@ -88,34 +91,16 @@ const Thread = () => {
// Events sorted by PoW (assuming `getPow` returns a numerical representation of the PoW) // Events sorted by PoW (assuming `getPow` returns a numerical representation of the PoW)
const eventsSortedByPow = [...uniqEvents].slice(1) const eventsSortedByPow = [...uniqEvents].slice(1)
.filter((event) => .filter((event) =>
getPow(event.id) > Number(filterDifficulty) && getPow(event.id) > Number(filterDifficulty) &&
event.kind === 1 event.kind === 1
).sort((a, b) => getPow(b.id) - getPow(a.id)); ).sort((a, b) => getPow(b.id) - getPow(a.id));
const displayedEvents = sortByTime ? eventsSortedByTime : eventsSortedByPow; const displayedEvents = sortByTime ? eventsSortedByTime : eventsSortedByPow;
if (!uniqEvents[0]) { if (!uniqEvents[0]) {
return ( return (
<> <Placeholder />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
<div className="border border-blue-300 shadow rounded-md p-4 max-w-sm w-full mx-auto">
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-slate-700 h-10 w-10"></div>
<div className="flex-1 space-y-6 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
</div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
</div>
</div>
</div>
</>
); );
} }
return ( return (
@ -125,9 +110,9 @@ const Thread = () => {
{earlierEvents {earlierEvents
.filter(event => event.kind === 1) .filter(event => event.kind === 1)
.sort((a, b) => a.created_at - b.created_at).map((event, index) => ( .sort((a, b) => a.created_at - b.created_at).map((event, index) => (
<OPCard event={event} metadata={getMetadataEvent(event)} replyCount={countReplies(event)} /> <PostCard event={event} metadata={getMetadataEvent(event)} replyCount={countReplies(event)} />
))} ))}
<OPCard event={uniqEvents[0]} metadata={getMetadataEvent(uniqEvents[0])} replyCount={countReplies(uniqEvents[0])} /> {OPEvent && <PostCard event={OPEvent} metadata={getMetadataEvent(OPEvent)} replyCount={countReplies(OPEvent)} type={'OP'}/>}
</div> </div>
<div className="col-span-full flex justify-center space-x-36"> <div className="col-span-full flex justify-center space-x-36">
<DocumentTextIcon <DocumentTextIcon
@ -173,7 +158,7 @@ const Thread = () => {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
<div className="col-span-full h-0.5 bg-neutral-900"></div> {/* This is the white line separator */} <div className="col-span-full h-0.5 bg-neutral-900"></div> {/* This is the white line separator */}
{displayedEvents.map((event, index) => ( {displayedEvents.map((event, index) => (
<ReplyCard key={index} event={event} metadata={getMetadataEvent(event)} replyCount={countReplies(event)} repliedTo={repliedList(event)} /> <PostCard key={index} event={event} metadata={getMetadataEvent(event)} replyCount={countReplies(event)} repliedTo={repliedList(event)} />
))} ))}
</div> </div>
</main> </main>

View File

@ -1,80 +0,0 @@
import CardContainer from '../PostCard/CardContainer';
import { FolderIcon } from '@heroicons/react/24/outline';
import { parseContent } from '../../utils/content';
import { Event } from 'nostr-tools';
import { getMetadata } from '../../utils/utils';
import ContentPreview from '../Modals/TextModal';
import { renderMedia } from '../../utils/FileUpload';
import { getIconFromHash } from '../../utils/deterministicProfileIcon';
const timeAgo = (unixTime: number) => {
const seconds = Math.floor((new Date().getTime() / 1000) - unixTime);
if (seconds < 60) return `now`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}d`;
const weeks = Math.floor(days / 7);
return `${weeks}w`;
};
const OPCard = ({ event, metadata, replyCount }: { event: Event, metadata: Event | null, replyCount: number}) => {
const { comment, file } = parseContent(event);
const icon = getIconFromHash(event.pubkey);
let metadataParsed = null;
if (metadata !== null) {
metadataParsed = getMetadata(metadata);
}
return (
<>
<CardContainer>
<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 ${icon} rounded-full`} />
<div className="ml-2 text-md font-semibold">Anonymous</div>
</>
}
</div>
<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">
<FolderIcon className="h-4 w-4 text-neutral-600" />
<span className="text-xs text-neutral-600">{replyCount}</span>
</div>
</div>
</div>
<div className="mr-2 flex flex-col break-words">
<ContentPreview key={event.id} comment={comment} />
</div>
{renderMedia(file)}
</div>
</CardContainer>
</>
);
};
export default OPCard;

View File

@ -1,95 +0,0 @@
import CardContainer from '../PostCard/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, uniqBy } from '../../utils/utils';
import ContentPreview from '../Modals/TextModal';
import { renderMedia } from '../../utils/FileUpload';
import { getIconFromHash } from '../../utils/deterministicProfileIcon';
const timeAgo = (unixTime: number) => {
const seconds = Math.floor((new Date().getTime() / 1000) - unixTime);
if (seconds < 60) return `now`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}d`;
const weeks = Math.floor(days / 7);
return `${weeks}w`;
};
const ReplyCard = ({ event, metadata, replyCount, repliedTo }: { event: Event, metadata: Event | null, replyCount: number, repliedTo: Event[] }) => {
const { comment, file } = parseContent(event);
const icon = getIconFromHash(event.pubkey);
// const [events, setEvents] = useState<Event[]>([]);
let metadataParsed = null;
if (metadata !== null) {
metadataParsed = getMetadata(metadata);
}
// const replyPubkeys = event.tags.filter(tag => tag[0] === 'p');
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 ${icon} rounded-full`} />
<div className="ml-2 text-md font-semibold">Anonymous</div>
</>
}
</div>
<div className="flex items-center ml-auto gap-2.5">
<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">
<FolderIcon className="h-4 w-4 text-neutral-600" />
<span className="text-xs text-neutral-600">{replyCount}</span>
</div>
</div>
</div>
<div className="flex items-center my-1" >
<span className="text-xs text-gray-500">Reply to: </span>
{uniqBy(repliedTo, 'pubkey').map((event, index) => (
<div key={index}>
{event.kind == 0 ? (
<img className={`h-5 w-5 rounded-full`} src={getMetadata(event)?.picture} />
) : (
<div className={`h-5 w-5 ${getIconFromHash(event.pubkey)} rounded-full`} />
)}
</div>
))}
</div>
<div className="mr-2 flex flex-col break-words">
<ContentPreview key={event.id} comment={comment} />
</div>
{renderMedia(file)}
</div>
</a>
</CardContainer>
</>
);
};
export default ReplyCard;