mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 09:21:25 +00:00
card modal and feed clean-up
This commit is contained in:
parent
3a1ca92d30
commit
26bf02e771
@ -4,7 +4,7 @@ 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 Thread from "./components/Thread";
|
||||
import Header from "./components/Header/Header";
|
||||
|
||||
function App() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import CardContainer from "./CardContainer";
|
||||
import CardContainer from "../Modals/CardContainer";
|
||||
import {
|
||||
ArrowUpTrayIcon,
|
||||
CpuChipIcon,
|
@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import PostCard from "./PostCard/PostCard";
|
||||
import NewThreadCard from "./PostCard/NewThreadCard";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import PostCard from "./Modals/Card";
|
||||
import NewThreadCard from "./Forms/NewThreadCard";
|
||||
import { uniqBy } from "../utils/utils"; // Assume getPow is a correct import now
|
||||
import { subGlobalFeed } from "../utils/subscriptions";
|
||||
import { verifyPow } from "../utils/mine";
|
||||
import { Event } from "nostr-tools";
|
||||
|
||||
const DEFAULT_DIFFICULTY = 20;
|
||||
|
||||
const useUniqEvents = () => {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
|
||||
@ -20,7 +22,7 @@ const useUniqEvents = () => {
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
const filterDifficulty = localStorage.getItem("filterDifficulty") || 20;
|
||||
const filterDifficulty = localStorage.getItem("filterDifficulty") || DEFAULT_DIFFICULTY;
|
||||
const [sortByTime, setSortByTime] = useState(true);
|
||||
const uniqEvents = useUniqEvents();
|
||||
|
||||
@ -35,9 +37,9 @@ const Home = () => {
|
||||
sortByTime ? b.created_at - a.created_at : verifyPow(b) - verifyPow(a)
|
||||
);
|
||||
|
||||
const toggleSort = () => {
|
||||
const toggleSort = useCallback(() => {
|
||||
setSortByTime(prev => !prev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getMetadataEvent = (event: Event) => {
|
||||
return uniqEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null;
|
||||
|
116
client/src/components/Modals/Card.tsx
Normal file
116
client/src/components/Modals/Card.tsx
Normal 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;
|
24
client/src/components/Modals/Placeholder.tsx
Normal file
24
client/src/components/Modals/Placeholder.tsx
Normal 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;
|
@ -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;
|
@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
// import {publish} from './relays';
|
||||
import { addRelay } from '../utils/relays';
|
||||
import { CpuChipIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
const Settings = () => {
|
||||
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20);
|
||||
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 21);
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useState } from "react";
|
||||
import { Event, nip19 } from "nostr-tools"
|
||||
import { subNote, subNotesOnce } from '../../utils/subscriptions';
|
||||
import { subNote, subNotesOnce } from '../utils/subscriptions';
|
||||
import { useEffect } from 'react';
|
||||
import { uniqBy } from '../../utils/utils';
|
||||
import { uniqBy } from '../utils/utils';
|
||||
import { DocumentTextIcon, FolderPlusIcon } from '@heroicons/react/24/outline';
|
||||
import { generatePrivateKey, getPublicKey, finishEvent } from 'nostr-tools';
|
||||
import { getPow } from '../../utils/mine';
|
||||
import { publish } from '../../utils/relays';
|
||||
import ThreadPost from './ThreadPost';
|
||||
import ReplyCard from './ReplyCard';
|
||||
import OPCard from './OPCard';
|
||||
import { getPow } from '../utils/mine';
|
||||
import ThreadPost from './Forms/ThreadPost';
|
||||
import PostCard from './Modals/Card';
|
||||
import Placeholder from './Modals/Placeholder';
|
||||
|
||||
|
||||
const difficulty = 20
|
||||
@ -18,13 +16,16 @@ const difficulty = 20
|
||||
const Thread = () => {
|
||||
const { id } = useParams();
|
||||
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 [postType, setPostType] = useState("");
|
||||
const [hasRun, setHasRun] = useState(false);
|
||||
const [preOPEvents, setPreOPEvents] = useState(['']);
|
||||
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
|
||||
const onEvent = (event: Event, relay: string) => {
|
||||
@ -32,29 +33,31 @@ const Thread = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setHasRun(false)
|
||||
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
|
||||
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 () => {
|
||||
// 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") : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasRun && events.length > 0) {
|
||||
let OPNoteEvents = events[0].tags.filter(tag => tag[0] === 'e').map(tag => tag[1]);
|
||||
console.log(OPNoteEvents);
|
||||
let OPEvent = events.find(e => e.id === hexID);
|
||||
|
||||
if (OPEvent) {
|
||||
setOPEvent(OPEvent);
|
||||
let OPNoteEvents = OPEvent.tags.filter(tag => tag[0] === 'e').map(tag => tag[1]);
|
||||
setHasRun(true);
|
||||
setPreOPEvents(OPNoteEvents)
|
||||
subNotesOnce(OPNoteEvents, onEvent)
|
||||
}
|
||||
}
|
||||
}, [uniqEvents, hasRun]);
|
||||
|
||||
const getMetadataEvent = (event: Event) => {
|
||||
@ -97,25 +100,7 @@ const Thread = () => {
|
||||
|
||||
if (!uniqEvents[0]) {
|
||||
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>
|
||||
</>
|
||||
<Placeholder />
|
||||
);
|
||||
}
|
||||
return (
|
||||
@ -125,9 +110,9 @@ const Thread = () => {
|
||||
{earlierEvents
|
||||
.filter(event => event.kind === 1)
|
||||
.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 className="col-span-full flex justify-center space-x-36">
|
||||
<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="col-span-full h-0.5 bg-neutral-900"></div> {/* This is the white line separator */}
|
||||
{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>
|
||||
</main>
|
@ -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;
|
@ -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;
|
Loading…
Reference in New Issue
Block a user