Merge pull request #15 from smolgrrr/notifications

Notifications
This commit is contained in:
smolgrrr 2023-12-19 16:18:08 +11:00 committed by GitHub
commit 21275087d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 255 additions and 26 deletions

View File

@ -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() {
<Route path="/settings" element={<Settings />} />
<Route path="/" element={<Home />} />
<Route path="/thread/:id" element={<Thread />} />
<Route path="/notifications" element={<Notifications />} />
</Routes>
<AddToHomeScreenPrompt/>
</Router>

View File

@ -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<UnsignedEvent>()
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(() => {

View File

@ -1,5 +1,6 @@
import {
Cog6ToothIcon
Cog6ToothIcon,
BellIcon
} from "@heroicons/react/24/outline";
export default function Header() {
@ -14,15 +15,25 @@ export default function Header() {
</span>
</div>
</a>
<div>
<a
href="/notifications"
className="text-neutral-300 inline-flex gap-4 items-center"
>
<button>
<BellIcon className="h-5 w-5" />
</button>
</a>
<a
href="/settings"
className="text-neutral-300 inline-flex gap-4 items-center"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
>
<button>
<Cog6ToothIcon className="h-5 w-5" />
</button>
</a>
</div>
</div>
</header>
);
}

View File

@ -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<Event[]>([]);
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<boolean>(localStorage.getItem('sortBy') !== 'false');
const [setAnon, setSetAnon] = useState<boolean>(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 (
<main className="text-white mb-20">
<OptionsBar sortByTime={sortByTime} setAnon={setAnon} toggleSort={toggleSort} toggleAnon={toggleAnon} />
<div className="block sm:hidden">
<label htmlFor="toggleC" className="p-4 flex items-center cursor-pointer">
<div className="relative">
<input
id="toggleC"
type="checkbox"
className="sr-only"
checked={notifsView}
onChange={toggleNotifs}
/>
<div className="block bg-gray-600 w-8 h-4 rounded-full"></div>
<div className={`dot absolute left-1 top-0.5 bg-white w-3 h-3 rounded-full transition ${notifsView ? 'transform translate-x-full bg-blue-400' : ''}`} ></div>
</div>
<div className={`ml-2 text-neutral-500 text-sm ${notifsView ? 'text-neutral-500' : ''}`}>
{notifsView ? 'Mentions' : 'Prev Posts'}
</div>
</label>
</div>
<div className="flex">
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? 'hidden sm:block' : ''}`}>
<span>Your Recent Posts</span>
{sortedEvents.map((event) => (
event.kind === 1 ?
<PostCard
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replyCount={countReplies(event)}
/>
:
<RepostCard
event={event}
/>
))}
</div>
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? '' : 'hidden sm:block'}`}>
<span>Mentions</span>
{sortedMentions.map((event) => (
event.kind === 1 ?
<PostCard
event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replyCount={countReplies(event)}
/>
:
<RepostCard
event={event}
/>
))}
</div>
</div>
</main>
);
};
export default Notifications;

View File

@ -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,
@ -235,3 +212,86 @@ export const subNotesOnce = (
pubkeys.clear();
}, 2000);
};
// /** quick subscribe to a note id (nip-19) */
// export const subNotifications = (
// pubkeys: string[],
// onEvent: SubCallback,
// ) => {
// const replyPubkeys = new Set<string>();
// 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,
});
};