diff --git a/client/src/App.tsx b/client/src/App.tsx index c189d16..586d1ae 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import Thread from "./components/Thread"; import Header from "./components/Header/Header"; import AddToHomeScreenPrompt from "./components/Modals/CheckMobile/CheckMobile"; +import Notifications from "./components/Notifications"; function App() { @@ -15,6 +16,7 @@ function App() { } /> } /> } /> + } /> diff --git a/client/src/components/Forms/handleSubmit.ts b/client/src/components/Forms/handleSubmit.ts index 2f2b215..25a2591 100644 --- a/client/src/components/Forms/handleSubmit.ts +++ b/client/src/components/Forms/handleSubmit.ts @@ -39,6 +39,7 @@ export const useSubmitForm = (unsigned: UnsignedEvent, difficulty: string) => { const unsignedWithPubkey = { ...unsigned, pubkey: getPublicKey(sk) }; const powServer = useState(localStorage.getItem('powserver') || ''); const [unsignedPoWEvent, setUnsignedPoWEvent] = useState() + let storedKeys = JSON.parse(localStorage.getItem('usedKeys') || '[]'); // Initialize the worker outside of any effects const numCores = navigator.hardwareConcurrency || 4; @@ -85,6 +86,11 @@ export const useSubmitForm = (unsigned: UnsignedEvent, difficulty: string) => { } else { startWork(); } + + // Add the logic here + storedKeys.push([sk, getPublicKey(sk)]); + // Stringify the array and store it back to localStorage + localStorage.setItem('usedKeys', JSON.stringify(storedKeys)); }; useEffect(() => { diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 0b434b1..d145ad5 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,5 +1,6 @@ import { - Cog6ToothIcon + Cog6ToothIcon, + BellIcon } from "@heroicons/react/24/outline"; export default function Header() { @@ -14,14 +15,24 @@ export default function Header() { + ); diff --git a/client/src/components/Notifications.tsx b/client/src/components/Notifications.tsx new file mode 100644 index 0000000..05519d8 --- /dev/null +++ b/client/src/components/Notifications.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState, useCallback } from "react"; +import PostCard from "./Modals/NoteCard"; +import { uniqBy } from "../utils/otherUtils"; // Assume getPow is a correct import now +import { subGlobalFeed } from "../utils/subscriptions"; +import { verifyPow } from "../utils/mine"; +import { Event } from "nostr-tools"; +import NewNoteCard from "./Forms/PostFormCard"; +import RepostCard from "./Modals/RepostCard"; +import OptionsBar from "./Modals/OptionsBar"; +import { subNotifications } from "../utils/subscriptions"; + +const useUniqEvents = () => { + const [events, setEvents] = useState([]); + let storedKeys = JSON.parse(localStorage.getItem('usedKeys') || '[]'); + let storedPubkeys = storedKeys.map((key: any[]) => key[1]); + + useEffect(() => { + const onEvent = (event: Event) => setEvents((prevEvents) => [...prevEvents, event]); + const unsubscribe = subNotifications(storedPubkeys, onEvent); + + return unsubscribe; + }, []); + + 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 Notifications = () => { + const [sortByTime, setSortByTime] = useState(localStorage.getItem('sortBy') !== 'false'); + const [setAnon, setSetAnon] = useState(localStorage.getItem('anonMode') !== 'false'); + const [notifsView, setNotifsView] = useState(false); + const { noteEvents, metadataEvents } = useUniqEvents(); + const storedKeys = JSON.parse(localStorage.getItem('usedKeys') || '[]'); + const storedPubkeys = storedKeys.map((key: any[]) => key[1]); + + const postEvents = noteEvents + .filter((event) => + event.kind !== 0 && + storedPubkeys.includes(event.pubkey) + ) + + const sortedEvents = [...postEvents] + .sort((a, b) => + sortByTime ? b.created_at - a.created_at : verifyPow(b) - verifyPow(a) + ) + .filter( + !setAnon ? (e) => !metadataEvents.some((metadataEvent) => metadataEvent.pubkey === e.pubkey) : () => true + ); + + const mentions = noteEvents + .filter((event) => + event.kind !== 0 && + event.tags.some((tag) => tag[0] === "p" && storedPubkeys.includes(tag[1])) + ) + + const sortedMentions = [...mentions] + .sort((a, b) => + sortByTime ? b.created_at - a.created_at : verifyPow(b) - verifyPow(a) + ) + .filter( + !setAnon ? (e) => !metadataEvents.some((metadataEvent) => metadataEvent.pubkey === e.pubkey) : () => true + ); + + 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 toggleNotifs = useCallback(() => { + setNotifsView(prev => !prev); + }, []); + + const countReplies = (event: Event) => { + return noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id)).length; + }; + + // Render the component + return ( +
+ +
+ +
+
+
+ Your Recent Posts + {sortedEvents.map((event) => ( + event.kind === 1 ? + e.pubkey === event.pubkey && e.kind === 0) || null} + replyCount={countReplies(event)} + /> + : + + ))} +
+
+ Mentions + {sortedMentions.map((event) => ( + event.kind === 1 ? + e.pubkey === event.pubkey && e.kind === 0) || null} + replyCount={countReplies(event)} + /> + : + + ))} +
+
+
+ ); +}; + +export default Notifications; diff --git a/client/src/utils/subscriptions.ts b/client/src/utils/subscriptions.ts index 24c8473..12bbcc7 100644 --- a/client/src/utils/subscriptions.ts +++ b/client/src/utils/subscriptions.ts @@ -29,15 +29,6 @@ export const subGlobalFeed = (onEvent: SubCallback) => { unsub: true }); - // // New Callback to only add events that pass the PoW requirement - // const powFilteredCallback = (evt: Event, relay: string) => { - // if (getPow(evt.id) > 2) { // Replace '5' with your actual PoW requirement - // pubkeys.add(evt.pubkey); - // notes.add(evt.id); - // onEvent(evt, relay); - // } - // }; - setTimeout(() => { // get profile info sub({ @@ -91,20 +82,6 @@ export const subGlobalFeed = (onEvent: SubCallback) => { }); }; -/** subscribe to global feed */ -export const simpleSub24hFeed = (onEvent: SubCallback) => { - unsubAll(); - sub({ - cb: onEvent, - filter: { - kinds: [1], - //until: Math.floor(Date.now() * 0.001), - since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)), - limit: 1, - } - }); -}; - /** subscribe to a note id (nip-19) */ export const subNote = ( eventId: string, @@ -234,4 +211,87 @@ export const subNotesOnce = ( }); pubkeys.clear(); }, 2000); -}; \ No newline at end of file +}; + +// /** quick subscribe to a note id (nip-19) */ +// export const subNotifications = ( +// pubkeys: string[], +// onEvent: SubCallback, +// ) => { +// const replyPubkeys = new Set(); +// sub({ +// cb: (evt, relay) => { +// replyPubkeys.add(evt.pubkey); +// onEvent(evt, relay); +// }, +// filter: { +// "#p": pubkeys, +// kinds: [1], +// limit: 50, +// }, +// unsub: true, +// }); + +// setTimeout(() => { +// // get profile info +// sub({ +// cb: onEvent, +// filter: { +// authors: Array.from(replyPubkeys), +// kinds: [0], +// limit: replyPubkeys.size, +// }, +// unsub: true, +// }); +// replyPubkeys.clear(); +// }, 2000); +// }; + +const hasEventTag = (tag: string[]) => tag[0] === 'e'; +const isReply = ([tag, , , marker]: string[]) => tag === 'e' && marker !== 'mention'; + +export const getReplyTo = (evt: Event): string | null => { + const eventTags = evt.tags.filter(isReply); + const withReplyMarker = eventTags.filter(([, , , marker]) => marker === 'reply'); + if (withReplyMarker.length === 1) { + return withReplyMarker[0][1]; + } + const withRootMarker = eventTags.filter(([, , , marker]) => marker === 'root'); + if (withReplyMarker.length === 0 && withRootMarker.length === 1) { + return withRootMarker[0][1]; + } + // fallback to deprecated positional 'e' tags (nip-10) + const lastTag = eventTags.at(-1); + return lastTag ? lastTag[1] : null; +}; + +export const subNotifications = ( + pubkeys: string[], + onEvent: SubCallback, +) => { + unsubAll(); + + sub({ + cb: (evt, relay) => { + onEvent(evt, relay); + }, + filter: { + authors: pubkeys, + kinds: [1, 7], + limit: 25, + }, + unsub: true, + }); + + sub({ + cb: (evt, relay) => { + onEvent(evt, relay); + }, + filter: { + '#p': pubkeys, + kinds: [1], + limit: 50, + }, + unsub: true, + }); +};