feat: trending hashtags

This commit is contained in:
Kieran 2023-11-16 14:01:12 +00:00
parent ee0865d9af
commit 95b7cca4cb
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
5 changed files with 88 additions and 26 deletions

View File

@ -78,6 +78,17 @@ export function RootTabs({ base }: { base?: string }) {
</>
),
},
{
tab: "trending-hashtags",
path: `${base}/trending/hashtags`,
show: true,
element: (
<>
<Icon name="hash" />
<FormattedMessage defaultMessage="Trending Hashtags" />
</>
),
},
{
tab: "global",
path: `${base}/global`,

View File

@ -0,0 +1,35 @@
import { ReactNode, useEffect, useState } from "react";
import PageSpinner from "Element/PageSpinner";
import NostrBandApi from "External/NostrBand";
import { ErrorOrOffline } from "./ErrorOrOffline";
import { HashTagHeader } from "Pages/HashTagsPage";
import { useLocale } from "IntlProvider";
export default function TrendingHashtags({ title }: { title?: ReactNode }) {
const [hashtags, setHashtags] = useState<string[]>();
const [error, setError] = useState<Error>();
const { lang } = useLocale();
async function loadTrendingHashtags() {
const api = new NostrBandApi();
const rsp = await api.trendingHashtags(lang);
setHashtags(rsp.hashtags);
}
useEffect(() => {
loadTrendingHashtags().catch(e => {
if (e instanceof Error) {
setError(e);
}
});
}, []);
if (error) return <ErrorOrOffline error={error} onRetry={loadTrendingHashtags} className="p" />;
if (!hashtags) return <PageSpinner />;
return <>
{title}
{hashtags.map(a => <HashTagHeader tag={a} className="bb p" />)}
</>
}

View File

@ -18,6 +18,10 @@ export interface TrendingNoteResponse {
notes: Array<TrendingNote>;
}
export interface TrendingHashtagsResponse {
hashtags: Array<string>
}
export interface SuggestedFollow {
pubkey: string;
}
@ -39,14 +43,13 @@ export class NostrBandError extends Error {
export default class NostrBandApi {
readonly #url = "https://api.nostr.band";
readonly #supportedLangs = ["en", "de", "ja", "zh", "th", "pt", "es", "fr"];
async trendingProfiles() {
return await this.#json<TrendingUserResponse>("GET", "/v0/trending/profiles");
}
async trendingNotes(lang?: string) {
const supportedLangs = ["en", "de", "ja", "zh", "th", "pt", "es", "fr"];
if (lang && supportedLangs.includes(lang)) {
if (lang && this.#supportedLangs.includes(lang)) {
return await this.#json<TrendingNoteResponse>("GET", `/v0/trending/notes?lang=${lang}`);
}
return await this.#json<TrendingNoteResponse>("GET", "/v0/trending/notes");
@ -56,6 +59,13 @@ export default class NostrBandApi {
return await this.#json<SuggestedFollowsResponse>("GET", `/v0/suggested/profiles/${pubkey}`);
}
async trendingHashtags(lang?: string) {
if (lang && this.#supportedLangs.includes(lang)) {
return await this.#json<TrendingHashtagsResponse>("GET", `/v0/trending/hashtags?lang=${lang}`);
}
return await this.#json<TrendingHashtagsResponse>("GET", "/v0/trending/hashtags");
}
async #json<T>(method: string, path: string) {
throwIfOffline();
const res = await fetch(`${this.#url}${path}`, {

View File

@ -11,6 +11,7 @@ import useLogin from "Hooks/useLogin";
import { setTags } from "Login";
import AsyncButton from "Element/AsyncButton";
import ProfileImage from "Element/User/ProfileImage";
import classNames from "classnames";
const HashTagsPage = () => {
const params = useParams();
@ -33,7 +34,7 @@ const HashTagsPage = () => {
export default HashTagsPage;
export function HashTagHeader({ tag }: { tag: string }) {
export function HashTagHeader({ tag, className }: { tag: string, className?: string }) {
const login = useLogin();
const isFollowing = useMemo(() => {
return login.tags.item.includes(tag);
@ -60,29 +61,29 @@ export function HashTagHeader({ tag }: { tag: string }) {
const pubkeys = dedupe((followsTag.data ?? []).map(a => a.pubkey));
return (
<div className="flex items-center justify-between">
<div className="flex flex-col g8">
<h2>#{tag}</h2>
<div className="flex">
{pubkeys.slice(0, 5).map(a => (
<ProfileImage pubkey={a} showUsername={false} link={""} showFollowDistance={false} size={40} />
))}
{pubkeys.length > 5 && (
<span>
+<FormattedNumber value={pubkeys.length - 5} />
</span>
)}
</div>
<div className={classNames("flex flex-col", className)}>
<div className="flex items-center justify-between">
<b className="text-lg">#{tag}</b>
{isFollowing ? (
<AsyncButton className="secondary" onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
<FormattedMessage defaultMessage="Unfollow" />
</AsyncButton>
) : (
<AsyncButton onClick={() => followTags(login.tags.item.concat([tag]))}>
<FormattedMessage defaultMessage="Follow" />
</AsyncButton>
)}
</div>
<div className="flex">
{pubkeys.slice(0, 5).map(a => (
<ProfileImage pubkey={a} showUsername={false} link={""} showFollowDistance={false} size={40} />
))}
{pubkeys.length > 5 && (
<span>
+<FormattedNumber value={pubkeys.length - 5} />
</span>
)}
</div>
{isFollowing ? (
<AsyncButton className="secondary" onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
<FormattedMessage defaultMessage="Unfollow" />
</AsyncButton>
) : (
<AsyncButton onClick={() => followTags(login.tags.item.concat([tag]))}>
<FormattedMessage defaultMessage="Follow" />
</AsyncButton>
)}
</div>
);
}

View File

@ -19,6 +19,7 @@ import TimelineFollows from "Element/Feed/TimelineFollows";
import { RootTabs } from "Element/RootTabs";
import { DeckContext } from "Pages/DeckLayout";
import { TopicsPage } from "./TopicsPage";
import TrendingHashtags from "Element/TrendingHashtags";
import messages from "./messages";
@ -228,6 +229,10 @@ export const RootTabRoutes = [
</div>
),
},
{
path: "trending/hashtags",
element: <TrendingHashtags />,
},
{
path: "suggested",
element: (