feat: trending notes

This commit is contained in:
Kieran 2023-05-10 12:32:09 +01:00
parent 1143cdfc88
commit 2ed38d1b97
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
7 changed files with 154 additions and 55 deletions

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from "react";
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
import { FormattedMessage } from "react-intl";
import PageSpinner from "Element/PageSpinner";
import Note from "Element/Note";
import NostrBandApi from "NostrBand";
export default function TrendingNotes() {
const [posts, setPosts] = useState<Array<RawEvent>>();
async function loadTrendingNotes() {
const api = new NostrBandApi();
const trending = await api.trendingNotes();
setPosts(trending.notes.map(a => a.event));
}
useEffect(() => {
loadTrendingNotes().catch(console.error);
}, []);
if (!posts) return <PageSpinner />;
return (
<>
<h3>
<FormattedMessage defaultMessage="Trending Notes" />
</h3>
{posts.map(e => (
<Note key={e.id} data={e as TaggedRawEvent} related={[]} depth={0} />
))}
</>
);
}

View File

@ -4,37 +4,20 @@ import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase"; import FollowListBase from "Element/FollowListBase";
import PageSpinner from "Element/PageSpinner"; import PageSpinner from "Element/PageSpinner";
import NostrBandApi from "NostrBand";
interface TrendingUser { export default function TrendingUsers() {
pubkey: HexKey;
}
interface TrendingUserResponse {
profiles: Array<TrendingUser>;
}
async function fetchTrendingUsers() {
try {
const res = await fetch(`https://api.nostr.band/v0/trending/profiles`);
if (res.ok) {
const data = (await res.json()) as TrendingUserResponse;
return data.profiles.map(a => a.pubkey);
}
} catch (e) {
console.warn(`Failed to load link preview`);
}
}
const TrendingUsers = () => {
const [userList, setUserList] = useState<HexKey[]>(); const [userList, setUserList] = useState<HexKey[]>();
async function loadTrendingUsers() {
const api = new NostrBandApi();
const users = await api.trendingProfiles();
const keys = users.profiles.map(a => a.pubkey);
setUserList(keys);
}
useEffect(() => { useEffect(() => {
(async () => { loadTrendingUsers().catch(console.error);
const data = await fetchTrendingUsers();
if (data) {
setUserList(data);
}
})();
}, []); }, []);
if (!userList) return <PageSpinner />; if (!userList) return <PageSpinner />;
@ -42,11 +25,9 @@ const TrendingUsers = () => {
return ( return (
<> <>
<h3> <h3>
<FormattedMessage defaultMessage="Trending Users" /> <FormattedMessage defaultMessage="Trending People" />
</h3> </h3>
<FollowListBase pubkeys={userList} showAbout={true} /> <FollowListBase pubkeys={userList} showAbout={true} />
</> </>
); );
}; }
export default TrendingUsers;

View File

@ -0,0 +1,52 @@
import { RawEvent } from "@snort/nostr";
export interface TrendingUser {
pubkey: string;
}
export interface TrendingUserResponse {
profiles: Array<TrendingUser>;
}
export interface TrendingNote {
event: RawEvent;
author: RawEvent; // kind0 event
}
export interface TrendingNoteResponse {
notes: Array<TrendingNote>;
}
export class NostrBandError extends Error {
body: string;
statusCode: number;
constructor(message: string, body: string, status: number) {
super(message);
this.body = body;
this.statusCode = status;
}
}
export default class NostrBandApi {
#url = "https://api.nostr.band";
async trendingProfiles() {
return await this.#json<TrendingUserResponse>("GET", "/v0/trending/profiles");
}
async trendingNotes() {
return await this.#json<TrendingNoteResponse>("GET", "/v0/trending/notes");
}
async #json<T>(method: string, path: string) {
const res = await fetch(`${this.#url}${path}`, {
method: method ?? "GET",
});
if (res.ok) {
return (await res.json()) as T;
} else {
throw new NostrBandError("Failed to load content from nostr.band", await res.text(), res.status);
}
}
}

View File

@ -8,11 +8,10 @@ import { router } from "index";
import { SearchRelays } from "Const"; import { SearchRelays } from "Const";
import { System } from "System"; import { System } from "System";
import TrendingUsers from "Element/TrendingUsers"; import TrendingUsers from "Element/TrendingUsers";
import useHorizontalScroll from "Hooks/useHorizontalScroll";
import messages from "./messages"; import TrendingNotes from "Element/TrendingPosts";
const POSTS = 0; const NOTES = 0;
const PROFILES = 1; const PROFILES = 1;
const SearchPage = () => { const SearchPage = () => {
@ -23,8 +22,8 @@ const SearchPage = () => {
const [sortPopular, setSortPopular] = useState<boolean>(true); const [sortPopular, setSortPopular] = useState<boolean>(true);
// tabs // tabs
const SearchTab = { const SearchTab = {
Posts: { text: formatMessage(messages.Posts), value: POSTS }, Posts: { text: formatMessage({ defaultMessage: "Notes" }), value: NOTES },
Profiles: { text: formatMessage(messages.People), value: PROFILES }, Profiles: { text: formatMessage({ defaultMessage: "People" }), value: PROFILES },
}; };
const [tab, setTab] = useState<Tab>(SearchTab.Posts); const [tab, setTab] = useState<Tab>(SearchTab.Posts);
@ -57,7 +56,16 @@ const SearchPage = () => {
}, []); }, []);
function tabContent() { function tabContent() {
if (!keyword) return null; if (!keyword) {
switch (tab.value) {
case PROFILES:
return <TrendingUsers />;
case NOTES:
return <TrendingNotes />;
}
return null;
}
const pf = tab.value == PROFILES; const pf = tab.value == PROFILES;
return ( return (
<> <>
@ -72,6 +80,7 @@ const SearchPage = () => {
postsOnly={false} postsOnly={false}
noSort={pf && sortPopular} noSort={pf && sortPopular}
method={"LIMIT_UNTIL"} method={"LIMIT_UNTIL"}
loadMore={false}
/> />
</> </>
); );
@ -102,20 +111,19 @@ const SearchPage = () => {
return ( return (
<div className="main-content"> <div className="main-content">
<h2> <h2>
<FormattedMessage {...messages.Search} /> <FormattedMessage defaultMessage="Search" />
</h2> </h2>
<div className="flex mb10"> <div className="flex mb10">
<input <input
type="text" type="text"
className="f-grow mr10" className="f-grow mr10"
placeholder={formatMessage(messages.SearchPlaceholder)} placeholder={formatMessage({ defaultMessage: "Search..." })}
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
autoFocus={true} autoFocus={true}
/> />
</div> </div>
<div className="tabs">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div> <div className="tabs">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div>
{!keyword && <TrendingUsers />}
{tabContent()} {tabContent()}
</div> </div>
); );

View File

@ -22,8 +22,6 @@ export default defineMessages({
Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" }, Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" },
Following: { defaultMessage: "Following {n}" }, Following: { defaultMessage: "Following {n}" },
Settings: { defaultMessage: "Settings" }, Settings: { defaultMessage: "Settings" },
Search: { defaultMessage: "Search" },
SearchPlaceholder: { defaultMessage: "Search..." },
Messages: { defaultMessage: "Messages" }, Messages: { defaultMessage: "Messages" },
MarkAllRead: { defaultMessage: "Mark All Read" }, MarkAllRead: { defaultMessage: "Mark All Read" },
GetVerified: { defaultMessage: "Get Verified" }, GetVerified: { defaultMessage: "Get Verified" },
@ -47,5 +45,4 @@ export default defineMessages({
Bookmarks: { defaultMessage: "Bookmarks" }, Bookmarks: { defaultMessage: "Bookmarks" },
BookmarksCount: { defaultMessage: "{n} Bookmarks" }, BookmarksCount: { defaultMessage: "{n} Bookmarks" },
KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex" }, KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex" },
People: { defaultMessage: "People" },
}); });

View File

@ -244,6 +244,9 @@
"B6+XJy": { "B6+XJy": {
"defaultMessage": "zapped" "defaultMessage": "zapped"
}, },
"B6H7eJ": {
"defaultMessage": "nsec, npub, nip-05, hex"
},
"BGCM48": { "BGCM48": {
"defaultMessage": "Write access to Snort relay, with 1 year of event retention" "defaultMessage": "Write access to Snort relay, with 1 year of event retention"
}, },
@ -268,6 +271,9 @@
"CHTbO3": { "CHTbO3": {
"defaultMessage": "Failed to load invoice" "defaultMessage": "Failed to load invoice"
}, },
"CVWeJ6": {
"defaultMessage": "Trending People"
},
"CmZ9ls": { "CmZ9ls": {
"defaultMessage": "{n} Muted" "defaultMessage": "{n} Muted"
}, },
@ -337,9 +343,6 @@
"FS3b54": { "FS3b54": {
"defaultMessage": "Done!" "defaultMessage": "Done!"
}, },
"FSYL8G": {
"defaultMessage": "Trending Users"
},
"FdhSU2": { "FdhSU2": {
"defaultMessage": "Claim Now" "defaultMessage": "Claim Now"
}, },
@ -413,6 +416,9 @@
"Iwm6o2": { "Iwm6o2": {
"defaultMessage": "NIP-05 Shop" "defaultMessage": "NIP-05 Shop"
}, },
"Ix8l+B": {
"defaultMessage": "Trending Notes"
},
"J+dIsA": { "J+dIsA": {
"defaultMessage": "Subscriptions" "defaultMessage": "Subscriptions"
}, },
@ -422,9 +428,6 @@
"JHEHCk": { "JHEHCk": {
"defaultMessage": "Zaps ({n})" "defaultMessage": "Zaps ({n})"
}, },
"JTgbT0": {
"defaultMessage": "Find Twitter follows"
},
"JXtsQW": { "JXtsQW": {
"defaultMessage": "Fast Zap Donation" "defaultMessage": "Fast Zap Donation"
}, },
@ -516,6 +519,9 @@
"NndBJE": { "NndBJE": {
"defaultMessage": "New users page" "defaultMessage": "New users page"
}, },
"O9GTIc": {
"defaultMessage": "Profile picture"
},
"OEW7yJ": { "OEW7yJ": {
"defaultMessage": "Zaps" "defaultMessage": "Zaps"
}, },
@ -589,12 +595,20 @@
"RhDAoS": { "RhDAoS": {
"defaultMessage": "Are you sure you want to delete {id}" "defaultMessage": "Are you sure you want to delete {id}"
}, },
"RjpoYG": {
"defaultMessage": "Recent",
"description": "Sort order name"
},
"RoOyAh": { "RoOyAh": {
"defaultMessage": "Relays" "defaultMessage": "Relays"
}, },
"Rs4kCE": { "Rs4kCE": {
"defaultMessage": "Bookmark" "defaultMessage": "Bookmark"
}, },
"RwFaYs": {
"defaultMessage": "Sort",
"description": "Label for sorting options for people search"
},
"SOqbe9": { "SOqbe9": {
"defaultMessage": "Update Lightning Address" "defaultMessage": "Update Lightning Address"
}, },
@ -618,6 +632,9 @@
"defaultMessage": "Hex Salt..", "defaultMessage": "Hex Salt..",
"description": "Hexidecimal 'salt' input for imgproxy" "description": "Hexidecimal 'salt' input for imgproxy"
}, },
"Tpy00S": {
"defaultMessage": "People"
},
"TwyMau": { "TwyMau": {
"defaultMessage": "Account" "defaultMessage": "Account"
}, },
@ -627,9 +644,6 @@
"ULotH9": { "ULotH9": {
"defaultMessage": "Amount: {amount} sats" "defaultMessage": "Amount: {amount} sats"
}, },
"UQ3pOC": {
"defaultMessage": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step."
},
"UUPFlt": { "UUPFlt": {
"defaultMessage": "Users must accept the content warning to show the content of your note." "defaultMessage": "Users must accept the content warning to show the content of your note."
}, },
@ -713,6 +727,9 @@
"ZUZedV": { "ZUZedV": {
"defaultMessage": "Lightning Donation:" "defaultMessage": "Lightning Donation:"
}, },
"Zr5TMx": {
"defaultMessage": "Setup profile"
},
"a5UPxh": { "a5UPxh": {
"defaultMessage": "Fund developers and platforms providing NIP-05 verification services" "defaultMessage": "Fund developers and platforms providing NIP-05 verification services"
}, },
@ -963,6 +980,10 @@
"mKhgP9": { "mKhgP9": {
"defaultMessage": "{n,plural,=0{} =1{zapped} other{zapped}}" "defaultMessage": "{n,plural,=0{} =1{zapped} other{zapped}}"
}, },
"mTJFgF": {
"defaultMessage": "Popular",
"description": "Sort order name"
},
"mfe8RW": { "mfe8RW": {
"defaultMessage": "Option: {n}" "defaultMessage": "Option: {n}"
}, },

View File

@ -79,6 +79,7 @@
"AyGauy": "Login", "AyGauy": "Login",
"B4C47Y": "name too short", "B4C47Y": "name too short",
"B6+XJy": "zapped", "B6+XJy": "zapped",
"B6H7eJ": "nsec, npub, nip-05, hex",
"BGCM48": "Write access to Snort relay, with 1 year of event retention", "BGCM48": "Write access to Snort relay, with 1 year of event retention",
"BOUMjw": "No nostr users found for {twitterUsername}", "BOUMjw": "No nostr users found for {twitterUsername}",
"BOr9z/": "Snort is an open source project built by passionate people in their free time", "BOr9z/": "Snort is an open source project built by passionate people in their free time",
@ -87,6 +88,7 @@
"C5xzTC": "Premium", "C5xzTC": "Premium",
"C81/uG": "Logout", "C81/uG": "Logout",
"CHTbO3": "Failed to load invoice", "CHTbO3": "Failed to load invoice",
"CVWeJ6": "Trending People",
"CmZ9ls": "{n} Muted", "CmZ9ls": "{n} Muted",
"Cu/K85": "Translated from {lang}", "Cu/K85": "Translated from {lang}",
"D+KzKd": "Automatically zap every note when loaded", "D+KzKd": "Automatically zap every note when loaded",
@ -110,7 +112,6 @@
"FDguSC": "{n} Zaps", "FDguSC": "{n} Zaps",
"FP+D3H": "LNURL to forward zaps to", "FP+D3H": "LNURL to forward zaps to",
"FS3b54": "Done!", "FS3b54": "Done!",
"FSYL8G": "Trending Users",
"FdhSU2": "Claim Now", "FdhSU2": "Claim Now",
"FfYsOb": "An error has occured!", "FfYsOb": "An error has occured!",
"FmXUJg": "follows you", "FmXUJg": "follows you",
@ -135,10 +136,10 @@
"INSqIz": "Twitter username...", "INSqIz": "Twitter username...",
"IUZC+0": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.", "IUZC+0": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.",
"Iwm6o2": "NIP-05 Shop", "Iwm6o2": "NIP-05 Shop",
"Ix8l+B": "Trending Notes",
"J+dIsA": "Subscriptions", "J+dIsA": "Subscriptions",
"JCIgkj": "Username", "JCIgkj": "Username",
"JHEHCk": "Zaps ({n})", "JHEHCk": "Zaps ({n})",
"JTgbT0": "Find Twitter follows",
"JXtsQW": "Fast Zap Donation", "JXtsQW": "Fast Zap Donation",
"JkLHGw": "Website", "JkLHGw": "Website",
"JymXbw": "Private Key", "JymXbw": "Private Key",
@ -169,6 +170,7 @@
"NepkXH": "Can't vote with {amount} sats, please set a different default zap amount", "NepkXH": "Can't vote with {amount} sats, please set a different default zap amount",
"NfNk2V": "Your private key", "NfNk2V": "Your private key",
"NndBJE": "New users page", "NndBJE": "New users page",
"O9GTIc": "Profile picture",
"OEW7yJ": "Zaps", "OEW7yJ": "Zaps",
"OKhRC6": "Share", "OKhRC6": "Share",
"OLEm6z": "Unknown login error", "OLEm6z": "Unknown login error",
@ -193,8 +195,10 @@
"RahCRH": "Expired", "RahCRH": "Expired",
"RfhLwC": "By: {author}", "RfhLwC": "By: {author}",
"RhDAoS": "Are you sure you want to delete {id}", "RhDAoS": "Are you sure you want to delete {id}",
"RjpoYG": "Recent",
"RoOyAh": "Relays", "RoOyAh": "Relays",
"Rs4kCE": "Bookmark", "Rs4kCE": "Bookmark",
"RwFaYs": "Sort",
"SOqbe9": "Update Lightning Address", "SOqbe9": "Update Lightning Address",
"SX58hM": "Copy", "SX58hM": "Copy",
"SYQtZ7": "LN Address Proxy", "SYQtZ7": "LN Address Proxy",
@ -202,10 +206,10 @@
"Ss0sWu": "Pay Now", "Ss0sWu": "Pay Now",
"TMfYfY": "Cashu token", "TMfYfY": "Cashu token",
"TpgeGw": "Hex Salt..", "TpgeGw": "Hex Salt..",
"Tpy00S": "People",
"TwyMau": "Account", "TwyMau": "Account",
"UDYlxu": "Pending Subscriptions", "UDYlxu": "Pending Subscriptions",
"ULotH9": "Amount: {amount} sats", "ULotH9": "Amount: {amount} sats",
"UQ3pOC": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
"UUPFlt": "Users must accept the content warning to show the content of your note.", "UUPFlt": "Users must accept the content warning to show the content of your note.",
"Up5U7K": "Block", "Up5U7K": "Block",
"VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.", "VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.",
@ -233,6 +237,7 @@
"ZKORll": "Activate Now", "ZKORll": "Activate Now",
"ZLmyG9": "Contributors", "ZLmyG9": "Contributors",
"ZUZedV": "Lightning Donation:", "ZUZedV": "Lightning Donation:",
"Zr5TMx": "Setup profile",
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services", "a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
"aWpBzj": "Show more", "aWpBzj": "Show more",
"b12Goz": "Mnemonic", "b12Goz": "Mnemonic",
@ -315,6 +320,7 @@
"mKAr6h": "Follow all", "mKAr6h": "Follow all",
"mKh2HS": "File upload service", "mKh2HS": "File upload service",
"mKhgP9": "{n,plural,=0{} =1{zapped} other{zapped}}", "mKhgP9": "{n,plural,=0{} =1{zapped} other{zapped}}",
"mTJFgF": "Popular",
"mfe8RW": "Option: {n}", "mfe8RW": "Option: {n}",
"n1Whvj": "Switch", "n1Whvj": "Switch",
"nDejmx": "Unblock", "nDejmx": "Unblock",