mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 09:21:25 +00:00
hashtag view
This commit is contained in:
parent
01212705b9
commit
553459afe8
@ -6,7 +6,9 @@ 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 TestUI from "./components/TestUI";
|
// import TestUI from "./components/TestUI";
|
||||||
|
import Hashtags from "./components/Hashtags";
|
||||||
|
import HashtagPage from "./components/HashtagPage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -17,7 +19,9 @@ 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="/test" element={<TestUI />} />
|
<Route path="/hashtags" element={<Hashtags />} />
|
||||||
|
<Route path="/hashtag/:id" element={<HashtagPage />} />
|
||||||
|
{/* <Route path="/test" element={<TestUI />} /> */}
|
||||||
</Routes>
|
</Routes>
|
||||||
<AddToHomeScreenPrompt/>
|
<AddToHomeScreenPrompt/>
|
||||||
</Router>
|
</Router>
|
||||||
|
@ -16,11 +16,13 @@ import "./Form.css";
|
|||||||
interface FormProps {
|
interface FormProps {
|
||||||
refEvent?: NostrEvent;
|
refEvent?: NostrEvent;
|
||||||
tagType?: 'Reply' | 'Quote' | '';
|
tagType?: 'Reply' | 'Quote' | '';
|
||||||
|
hashtag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewNoteCard = ({
|
const NewNoteCard = ({
|
||||||
refEvent,
|
refEvent,
|
||||||
tagType
|
tagType,
|
||||||
|
hashtag,
|
||||||
}: FormProps) => {
|
}: FormProps) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
@ -44,6 +46,10 @@ const NewNoteCard = ({
|
|||||||
const [uploadingFile, setUploadingFile] = useState(false);
|
const [uploadingFile, setUploadingFile] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (hashtag) {
|
||||||
|
unsigned.tags.push(['t', hashtag as string]);
|
||||||
|
}
|
||||||
|
|
||||||
if (refEvent && tagType) {
|
if (refEvent && tagType) {
|
||||||
unsigned.tags = Array.from(new Set(unsigned.tags.concat(refEvent.tags)));
|
unsigned.tags = Array.from(new Set(unsigned.tags.concat(refEvent.tags)));
|
||||||
unsigned.tags.push(['p', refEvent.pubkey]);
|
unsigned.tags.push(['p', refEvent.pubkey]);
|
||||||
@ -197,8 +203,8 @@ const NewNoteCard = ({
|
|||||||
{doingWorkProp ? (
|
{doingWorkProp ? (
|
||||||
<div className="flex animate-pulse text-sm text-gray-300">
|
<div className="flex animate-pulse text-sm text-gray-300">
|
||||||
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
||||||
<span>Generating Proof-of-Work.</span>
|
<span>Doing Work:</span>
|
||||||
{doingWorkProgress && <span>Current iteration {doingWorkProgress}</span>}
|
{doingWorkProgress && <span>{doingWorkProgress} hashes</span>}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div id="postFormError" className="text-red-500" />
|
<div id="postFormError" className="text-red-500" />
|
||||||
|
@ -74,8 +74,8 @@ const RepostNote = ({
|
|||||||
{doingWorkProp ? (
|
{doingWorkProp ? (
|
||||||
<div className="flex animate-pulse text-sm text-gray-300">
|
<div className="flex animate-pulse text-sm text-gray-300">
|
||||||
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
||||||
<span>Generating Proof-of-Work.</span>
|
<span>Doing Work:</span>
|
||||||
{doingWorkProgress && <span>Current iteration {doingWorkProgress}</span>}
|
{doingWorkProgress && <span>{doingWorkProgress} hashes</span>}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div id="postFormError" className="text-red-500" />
|
<div id="postFormError" className="text-red-500" />
|
||||||
|
114
client/src/components/HashtagPage.tsx
Normal file
114
client/src/components/HashtagPage.tsx
Normal 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 { 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<Event[]>([]);
|
||||||
|
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<boolean>(localStorage.getItem('sortBy') !== 'true');
|
||||||
|
const [setAnon, setSetAnon] = useState<boolean>(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 (
|
||||||
|
<main className="text-white mb-20">
|
||||||
|
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
|
||||||
|
<NewNoteCard hashtag={id as string}/>
|
||||||
|
</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 HashtagPage;
|
71
client/src/components/Hashtags.tsx
Normal file
71
client/src/components/Hashtags.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
export const DefaultHashtags = ['asknostr', 'politics', 'technology', 'bitcoin', 'wired'];
|
||||||
|
|
||||||
|
const Hashtags = () => {
|
||||||
|
const [addedHashtags, setAddedHashtags] = useState<string[]>(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 (
|
||||||
|
<div className="settings-page bg-black text-white p-8 flex flex-col h-full">
|
||||||
|
<h1 className="text-lg font-semibold mb-4">Saved hashtags</h1>
|
||||||
|
<div className="">
|
||||||
|
{/* Map over DefaultBoards and addedBoards and display them */}
|
||||||
|
<ul className='py-4'>
|
||||||
|
{DefaultHashtags.map((hashtag, index) => (
|
||||||
|
<li key={index}><a href={`/hashtag/${hashtag}`} className='hover:underline'>#{hashtag}</a></li>
|
||||||
|
))}
|
||||||
|
{addedHashtags.map((hashtag, index: number) => (
|
||||||
|
<li key={index}><a href={`/hashtag/${hashtag}`} className='hover:underline'>#{hashtag}</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 Hashtag
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex">
|
||||||
|
<input
|
||||||
|
id="hashtag"
|
||||||
|
type="string"
|
||||||
|
placeholder={'Hashtag'}
|
||||||
|
onChange={e => setNewHashtag(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">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearBoards}
|
||||||
|
className="bg-black border text-white font-bold py-2 px-4 rounded mx-4">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hashtags;
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Cog6ToothIcon,
|
Cog6ToothIcon,
|
||||||
BellIcon,
|
BellIcon,
|
||||||
ArchiveBoxIcon
|
HashtagIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
@ -18,11 +18,11 @@ export default function Header() {
|
|||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="/boards"
|
href="/hashtags"
|
||||||
className="text-neutral-300 inline-flex gap-4 items-center"
|
className="text-neutral-300 inline-flex gap-4 items-center pl-4"
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
<ArchiveBoxIcon className="h-5 w-5" />
|
<HashtagIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
@ -279,4 +279,83 @@ export const subProfile = (
|
|||||||
},
|
},
|
||||||
unsub: true,
|
unsub: true,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** subscribe to global feed */
|
||||||
|
export const subHashtagFeed = (
|
||||||
|
hashtag: string,
|
||||||
|
onEvent: SubCallback,
|
||||||
|
age: number
|
||||||
|
) => {
|
||||||
|
console.info('subscribe to hashtag feed');
|
||||||
|
unsubAll();
|
||||||
|
const now = Math.floor(Date.now() * 0.001);
|
||||||
|
const pubkeys = new Set<string>();
|
||||||
|
const notes = new Set<string>();
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
Loading…
Reference in New Issue
Block a user