feat: trending hashtags
This commit is contained in:
parent
ee0865d9af
commit
95b7cca4cb
@ -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`,
|
||||
|
35
packages/app/src/Element/TrendingHashtags.tsx
Normal file
35
packages/app/src/Element/TrendingHashtags.tsx
Normal 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" />)}
|
||||
</>
|
||||
}
|
16
packages/app/src/External/NostrBand.ts
vendored
16
packages/app/src/External/NostrBand.ts
vendored
@ -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}`, {
|
||||
|
@ -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,9 +61,19 @@ export function HashTagHeader({ tag }: { tag: string }) {
|
||||
const pubkeys = dedupe((followsTag.data ?? []).map(a => a.pubkey));
|
||||
|
||||
return (
|
||||
<div className={classNames("flex flex-col", className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col g8">
|
||||
<h2>#{tag}</h2>
|
||||
<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} />
|
||||
@ -74,15 +85,5 @@ export function HashTagHeader({ tag }: { tag: string }) {
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -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: (
|
||||
|
Loading…
Reference in New Issue
Block a user