add boards

This commit is contained in:
smolgrrr 2024-01-06 00:44:39 +11:00
parent 4b2f7b213d
commit 0920abfcba
6 changed files with 254 additions and 6 deletions

View File

@ -6,9 +6,10 @@ import Thread from "./components/Thread";
import Header from "./components/Header/Header"; import Header from "./components/Header/Header";
import AddToHomeScreenPrompt from "./components/Modals/CheckMobile/CheckMobile"; import AddToHomeScreenPrompt from "./components/Modals/CheckMobile/CheckMobile";
import Notifications from "./components/Notifications"; import Notifications from "./components/Notifications";
import Board from "./components/Board";
import Boards from "./components/Boards";
function App() { function App() {
return ( return (
<Router> <Router>
<Header /> <Header />
@ -17,6 +18,8 @@ function App() {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/thread/:id" element={<Thread />} /> <Route path="/thread/:id" element={<Thread />} />
<Route path="/notifications" element={<Notifications />} /> <Route path="/notifications" element={<Notifications />} />
<Route path="/board/:id" element={<Board />} />
<Route path="/boards" element={<Boards />} />
</Routes> </Routes>
<AddToHomeScreenPrompt/> <AddToHomeScreenPrompt/>
</Router> </Router>

View File

@ -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 { subBoardFeed } 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 = 20;
const Board = () => {
const { id } = useParams();
const filterDifficulty = localStorage.getItem("filterDifficulty") || DEFAULT_DIFFICULTY;
const [sortByTime, setSortByTime] = useState<boolean>(localStorage.getItem('sortBy') !== 'false');
const [setAnon, setSetAnon] = useState<boolean>(localStorage.getItem('anonMode') !== 'false');
let decodeResult = nip19.decode(id as string);
let pubkey = decodeResult.data as string;
const [delayedSort, setDelayedSort] = useState(false)
const [events, setEvents] = useState<Event[]>([]);
useEffect(() => {
const onEvent = (event: Event) => setEvents((prevEvents) => [...prevEvents, event]);
console.log(events[events.length])
const unsubscribe = subBoardFeed(pubkey, onEvent);
return unsubscribe;
}, [pubkey]);
const uniqEvents = uniqBy(events, "id");
const noteEvents = uniqEvents.filter(event => event.kind === 1 || event.kind === 6);
const metadataEvents = uniqEvents.filter(event => event.kind === 0);
const postEvents: Event[] = noteEvents
.filter((event) =>
verifyPow(event) >= Number(filterDifficulty) &&
event.kind !== 0 &&
(event.kind !== 1 || !event.tags.some((tag) => tag[0] === "e"))
)
// Delayed filtering
useEffect(() => {
const timer = setTimeout(() => {
setDelayedSort(true);
}, 3000);
return () => clearTimeout(timer);
}, []);
let sortedEvents = [...postEvents]
.sort((a, b) =>
sortByTime ? b.created_at - a.created_at : verifyPow(b) - verifyPow(a)
)
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 (
<main className="text-white mb-20">
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<NewNoteCard board={id}/>
</div>
<OptionsBar sortByTime={sortByTime} setAnon={setAnon} toggleSort={toggleSort} toggleAnon={toggleAnon} />
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
{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>
</main>
);
};
export default Board;

View File

@ -0,0 +1,69 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Boards = () => {
const navigate = useNavigate();
const addedBoards = JSON.parse(localStorage.getItem('addedBoards') as string) || [];
const [boardName, setBoardName] = useState('');
const [boardPubkey, setboardPubkey] = useState('')
const DefaultBoards = [['bitcoin', 'npub19nrn4l0s39kpwww7pgk9jddj8lzekqxmtrll8r2a57chtq3zx6sq00vetn']];
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
addedBoards.push([boardName, boardPubkey])
localStorage.setItem('addedBoards', String(addedBoards));
};
return (
<div className="settings-page bg-black text-white p-8 flex flex-col h-full">
<h1 className="text-lg font-semibold mb-4">Boards</h1>
<div className="">
{/* Map over DefaultBoards and addedBoards and display them */}
<ul className='py-4'>
{DefaultBoards.map((board, index) => (
<li key={index}><a href={`/board/${board[1]}`}>/{board[0]}/</a></li>
))}
{addedBoards.map((board: string, index: number) => (
<li key={index}><a href={`/board/${board[1]}`}>/{board[0]}/</a></li>
))}
</ul>
<form onSubmit={handleSubmit}>
<div className="flex flex-wrap -mx-2 my-4">
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
<label className="block text-xs mb-2" htmlFor="difficulty">
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
Add Board
</span>
</label>
<div className="flex">
<input
id="BoardName"
type="string"
placeholder={'Board Name'}
onChange={e => setBoardName(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
<input
id="BoardPubkey"
type="string"
placeholder={'Board Pubkey'}
onChange={e => setboardPubkey(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
</div>
</div>
<button
type="submit"
className="bg-black border text-white font-bold py-2 px-4 rounded">
Add Board
</button>
</form>
</div>
</div>
);
};
export default Boards;

View File

@ -16,6 +16,7 @@ import "./Form.css";
interface FormProps { interface FormProps {
refEvent?: NostrEvent; refEvent?: NostrEvent;
tagType?: 'Reply' | 'Quote' | ''; tagType?: 'Reply' | 'Quote' | '';
board?: string;
} }
const tagMapping = { const tagMapping = {
@ -25,7 +26,8 @@ const tagMapping = {
const NewNoteCard = ({ const NewNoteCard = ({
refEvent, refEvent,
tagType tagType,
board
}: FormProps) => { }: FormProps) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
@ -49,8 +51,8 @@ const NewNoteCard = ({
const [uploadingFile, setUploadingFile] = useState(false); const [uploadingFile, setUploadingFile] = useState(false);
useEffect(() => { useEffect(() => {
if (refEvent && tagType && unsigned.tags.length === 0) { if (refEvent && tagType && unsigned.tags.length === 1) {
if (tagType === 'Reply' && unsigned.tags.length === 0) { if (tagType === 'Reply') {
unsigned.tags.push(['p', refEvent.pubkey]); unsigned.tags.push(['p', refEvent.pubkey]);
unsigned.tags.push(['e', refEvent.id, 'root']); unsigned.tags.push(['e', refEvent.id, 'root']);
} else { } else {
@ -66,6 +68,10 @@ const NewNoteCard = ({
} }
} }
if (board) {
unsigned.tags.push(['d', nip19.decode(board).data as string]);
}
const handleDifficultyChange = (event: Event) => { const handleDifficultyChange = (event: Event) => {
const customEvent = event as CustomEvent; const customEvent = event as CustomEvent;
const { difficulty } = customEvent.detail; const { difficulty } = customEvent.detail;

View File

@ -1,6 +1,7 @@
import { import {
Cog6ToothIcon, Cog6ToothIcon,
BellIcon BellIcon,
ArchiveBoxIcon
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
export default function Header() { export default function Header() {
@ -17,8 +18,16 @@ export default function Header() {
</a> </a>
<div> <div>
<a <a
href="/notifications" href="/boards"
className="text-neutral-300 inline-flex gap-4 items-center" className="text-neutral-300 inline-flex gap-4 items-center"
>
<button>
<ArchiveBoxIcon className="h-5 w-5" />
</button>
</a>
<a
href="/notifications"
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
> >
<button> <button>
<BellIcon className="h-5 w-5" /> <BellIcon className="h-5 w-5" />

View File

@ -295,3 +295,50 @@ export const subNotifications = (
unsub: true, unsub: true,
}); });
}; };
/** subscribe to global feed */
export const subBoardFeed = (
board: string,
onEvent: SubCallback,
) => {
console.info('subscribe to board');
unsubAll();
const now = Math.floor(Date.now() * 0.001);
const pubkeys = new Set<string>();
const notes = new Set<string>();
const prefix = Math.floor(16 / 4); // 4 bits in each '0' character
sub({ // get past events
cb: (evt, relay) => {
pubkeys.add(evt.pubkey);
notes.add(evt.id);
onEvent(evt, relay);
},
filter: {
...(prefix && { ids: ['0'.repeat(prefix)] }),
"#d": [board],
kinds: [1, 6],
since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)),
limit: 500,
},
unsub: true
});
// subscribe to future notes, reactions and profile updates
sub({
cb: (evt, relay) => {
onEvent(evt, relay);
if (
evt.kind !== 1
|| pubkeys.has(evt.pubkey)
) {
return;
}
},
filter: {
...(prefix && { ids: ['0'.repeat(prefix)] }),
"#d": [board],
kinds: [1],
since: now,
},
});
};