From 553459afe8a2d0855a5136aca153d545cbd48b5e Mon Sep 17 00:00:00 2001 From: smolgrrr Date: Tue, 13 Aug 2024 16:13:40 +1000 Subject: [PATCH] hashtag view --- client/src/App.tsx | 8 +- client/src/components/Forms/PostFormCard.tsx | 12 +- client/src/components/Forms/RepostNote.tsx | 4 +- client/src/components/HashtagPage.tsx | 114 +++++++++++++++++++ client/src/components/Hashtags.tsx | 71 ++++++++++++ client/src/components/Header/Header.tsx | 8 +- client/src/utils/subscriptions.ts | 79 +++++++++++++ 7 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 client/src/components/HashtagPage.tsx create mode 100644 client/src/components/Hashtags.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 43e5340..3791b40 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,7 +6,9 @@ import Thread from "./components/Thread"; import Header from "./components/Header/Header"; import AddToHomeScreenPrompt from "./components/Modals/CheckMobile/CheckMobile"; import Notifications from "./components/Notifications"; -import TestUI from "./components/TestUI"; +// import TestUI from "./components/TestUI"; +import Hashtags from "./components/Hashtags"; +import HashtagPage from "./components/HashtagPage"; function App() { return ( @@ -17,7 +19,9 @@ function App() { } /> } /> } /> - } /> + } /> + } /> + {/* } /> */} diff --git a/client/src/components/Forms/PostFormCard.tsx b/client/src/components/Forms/PostFormCard.tsx index 3aec30d..1913362 100644 --- a/client/src/components/Forms/PostFormCard.tsx +++ b/client/src/components/Forms/PostFormCard.tsx @@ -16,11 +16,13 @@ import "./Form.css"; interface FormProps { refEvent?: NostrEvent; tagType?: 'Reply' | 'Quote' | ''; + hashtag?: string; } const NewNoteCard = ({ refEvent, - tagType + tagType, + hashtag, }: FormProps) => { const ref = useRef(null); const [comment, setComment] = useState(""); @@ -44,6 +46,10 @@ const NewNoteCard = ({ const [uploadingFile, setUploadingFile] = useState(false); useEffect(() => { + if (hashtag) { + unsigned.tags.push(['t', hashtag as string]); + } + if (refEvent && tagType) { unsigned.tags = Array.from(new Set(unsigned.tags.concat(refEvent.tags))); unsigned.tags.push(['p', refEvent.pubkey]); @@ -197,8 +203,8 @@ const NewNoteCard = ({ {doingWorkProp ? (
- Generating Proof-of-Work. - {doingWorkProgress && Current iteration {doingWorkProgress}} + Doing Work: + {doingWorkProgress && {doingWorkProgress} hashes}
) : null}
diff --git a/client/src/components/Forms/RepostNote.tsx b/client/src/components/Forms/RepostNote.tsx index d9236a0..151917e 100644 --- a/client/src/components/Forms/RepostNote.tsx +++ b/client/src/components/Forms/RepostNote.tsx @@ -74,8 +74,8 @@ const RepostNote = ({ {doingWorkProp ? (
- Generating Proof-of-Work. - {doingWorkProgress && Current iteration {doingWorkProgress}} + Doing Work: + {doingWorkProgress && {doingWorkProgress} hashes}
) : null}
diff --git a/client/src/components/HashtagPage.tsx b/client/src/components/HashtagPage.tsx new file mode 100644 index 0000000..764c513 --- /dev/null +++ b/client/src/components/HashtagPage.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState, useCallback } from "react"; +import PostCard from "./Modals/NoteCard"; +import { uniqBy } from "../utils/otherUtils"; // Assume getPow is a correct import now +import { subHashtagFeed, subProfile} from "../utils/subscriptions"; +import { verifyPow } from "../utils/mine"; +import { Event, nip19 } from "nostr-tools"; +import NewNoteCard from "./Forms/PostFormCard"; +import RepostCard from "./Modals/RepostCard"; +import OptionsBar from "./Modals/OptionsBar"; +import { useParams } from "react-router-dom"; + +const DEFAULT_DIFFICULTY = 0; + +const useUniqEvents = (hashtag: string) => { + const [events, setEvents] = useState([]); + const age = Number(localStorage.getItem("age")) || 24; + + useEffect(() => { + const onEvent = (event: Event) => setEvents((prevEvents) => [...prevEvents, event]); + console.log(events) + const unsubscribe = subHashtagFeed(hashtag, onEvent, age); + + return unsubscribe; + }, [hashtag]); + + const uniqEvents = uniqBy(events, "id"); + + const noteEvents = uniqEvents.filter(event => event.kind === 1 || event.kind === 6); + const metadataEvents = uniqEvents.filter(event => event.kind === 0); + + return { noteEvents, metadataEvents }; +}; + +const HashtagPage = () => { + const { id } = useParams(); + const filterDifficulty = localStorage.getItem("filterHashtagDifficulty") || DEFAULT_DIFFICULTY; + const [sortByTime, setSortByTime] = useState(localStorage.getItem('sortBy') !== 'true'); + const [setAnon, setSetAnon] = useState(localStorage.getItem('anonMode') !== 'false'); + + const {noteEvents, metadataEvents } = useUniqEvents(id as string); + + const [delayedSort, setDelayedSort] = useState(false) + + const postEvents: Event[] = noteEvents + .filter((event) => + verifyPow(event) >= Number(filterDifficulty) && + event.kind !== 0 && + (event.kind !== 1 || !event.tags.some((tag) => tag[0] === "e")) + ) + + let sortedEvents = [...postEvents] + .sort((a, b) => { + // Sort by PoW in descending order + const powDiff = verifyPow(b) - verifyPow(a); + if (powDiff !== 0) return powDiff; + + // If PoW is the same, sort by created_at in descending order + return b.created_at - a.created_at; + }); + + if (delayedSort) { + sortedEvents = sortedEvents.filter( + !setAnon ? (e) => !metadataEvents.some((metadataEvent) => metadataEvent.pubkey === e.pubkey) : () => true + ); + } else { + sortedEvents = sortedEvents.filter((e) => setAnon || e.tags.some((tag) => tag[0] === "client" && tag[1] === 'getwired.app')); + } + + const toggleSort = useCallback(() => { + setSortByTime(prev => { + const newValue = !prev; + localStorage.setItem('sortBy', String(newValue)); + return newValue; + }); + }, []); + + const toggleAnon = useCallback(() => { + setSetAnon(prev => { + const newValue = !prev; + localStorage.setItem('anonMode', String(newValue)); + return newValue; + }); + }, []); + + const countReplies = (event: Event) => { + return noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id)).length; + }; + + // Render the component + return ( +
+
+ +
+ +
+ {sortedEvents.map((event) => ( + event.kind === 1 ? + e.pubkey === event.pubkey && e.kind === 0) || null} + replyCount={countReplies(event)} + /> + : + + ))} +
+
+ ); +}; + +export default HashtagPage; \ No newline at end of file diff --git a/client/src/components/Hashtags.tsx b/client/src/components/Hashtags.tsx new file mode 100644 index 0000000..85b2a04 --- /dev/null +++ b/client/src/components/Hashtags.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; + +export const DefaultHashtags = ['asknostr', 'politics', 'technology', 'bitcoin', 'wired']; + +const Hashtags = () => { + const [addedHashtags, setAddedHashtags] = useState(JSON.parse(localStorage.getItem('hashtags') as string) || []); + const [newHashtag, setNewHashtag] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const newHashtagArray = [...addedHashtags, newHashtag]; + setAddedHashtags(newHashtagArray); + localStorage.setItem('addedBoards', JSON.stringify(newHashtagArray)); + }; + + const clearBoards = () => { + localStorage.setItem('hashtags', JSON.stringify([])); + setAddedHashtags([]); + }; + + return ( +
+

Saved hashtags

+
+ {/* Map over DefaultBoards and addedBoards and display them */} +
    + {DefaultHashtags.map((hashtag, index) => ( +
  • #{hashtag}
  • + ))} + {addedHashtags.map((hashtag, index: number) => ( +
  • #{hashtag}
  • + ))} +
+ +
+
+
+ +
+ setNewHashtag(e.target.value)} + className="w-full px-3 py-2 border rounded-md bg-black" + /> +
+
+
+ + +
+
+
+ ); +}; + +export default Hashtags; \ No newline at end of file diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 16fc599..7888aba 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,7 +1,7 @@ import { Cog6ToothIcon, BellIcon, - ArchiveBoxIcon + HashtagIcon } from "@heroicons/react/24/outline"; export default function Header() { @@ -18,11 +18,11 @@ export default function Header() {
{ + console.info('subscribe to hashtag feed'); + unsubAll(); + const now = Math.floor(Date.now() * 0.001); + const pubkeys = new Set(); + const notes = new Set(); + sub({ // get past events + cb: (evt, relay) => { + pubkeys.add(evt.pubkey); + notes.add(evt.id); + onEvent(evt, relay); + }, + filter: { + "#t": [hashtag], + kinds: [1, 6], + since: Math.floor((Date.now() * 0.001) - (age * 60 * 60)), + limit: 50, + }, + unsub: true + }); + + setTimeout(() => { + // get profile info + sub({ + cb: onEvent, + filter: { + authors: Array.from(pubkeys), + kinds: [0], + limit: pubkeys.size, + }, + unsub: true, + }); + pubkeys.clear(); + + sub({ + cb: onEvent, + filter: { + '#e': Array.from(notes), + kinds: [1], + }, + unsub: true, + }); + + notes.clear(); + }, 2000); + + // subscribe to future notes, reactions and profile updates + sub({ + cb: (evt, relay) => { + onEvent(evt, relay); + if ( + evt.kind !== 1 + || pubkeys.has(evt.pubkey) + ) { + return; + } + subOnce({ // get profile data + relay, + cb: onEvent, + filter: { + authors: [evt.pubkey], + kinds: [0], + limit: 1, + } + }); + }, + filter: { + "#t": [hashtag], + kinds: [1], + since: now, + }, + }); }; \ No newline at end of file