From dbcb48d599d00a635d5fa341d7746c2444d48668 Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 15 Dec 2025 22:46:04 +0800 Subject: [PATCH] feat: add special follow feed --- src/components/FeedSwitcher/index.tsx | 21 +++++++- .../Profile/SpecialFollowButton.tsx | 51 +++++++++++++++++++ src/components/Profile/index.tsx | 2 + src/pages/primary/NoteListPage/FeedButton.tsx | 13 ++++- src/pages/primary/NoteListPage/PinnedFeed.tsx | 34 +++++++++++++ src/pages/primary/NoteListPage/index.tsx | 6 +++ src/providers/FeedProvider.tsx | 19 +++++++ src/types/index.d.ts | 2 +- 8 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 src/components/Profile/SpecialFollowButton.tsx create mode 100644 src/pages/primary/NoteListPage/PinnedFeed.tsx diff --git a/src/components/FeedSwitcher/index.tsx b/src/components/FeedSwitcher/index.tsx index 6da3376..055fc3e 100644 --- a/src/components/FeedSwitcher/index.tsx +++ b/src/components/FeedSwitcher/index.tsx @@ -5,7 +5,8 @@ import { SecondaryPageLink } from '@/PageManager' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' -import { UsersRound } from 'lucide-react' +import { usePinnedUsers } from '@/providers/PinnedUsersProvider' +import { Star, UsersRound } from 'lucide-react' import { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' import RelaySetCard from '../RelaySetCard' @@ -15,6 +16,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { const { pubkey } = useNostr() const { relaySets, favoriteRelays } = useFavoriteRelays() const { feedInfo, switchFeed } = useFeed() + const { pinnedPubkeySet } = usePinnedUsers() return (
@@ -35,6 +37,23 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
+ { + if (!pubkey) return + switchFeed('pinned', { pubkey }) + close?.() + }} + > +
+
+ +
+
{t('Special Follow')}
+
+
+
isPinned(pubkey), [isPinned, pubkey]) + + if (!accountPubkey || (pubkey && pubkey === accountPubkey)) return null + + const onToggle = async (e: React.MouseEvent) => { + e.stopPropagation() + checkLogin(async () => { + setUpdating(true) + try { + await togglePin(pubkey) + } catch (error) { + if (pinned) { + toast.error(t('Unfollow failed') + ': ' + (error as Error).message) + } else { + toast.error(t('Follow failed') + ': ' + (error as Error).message) + } + } finally { + setUpdating(false) + } + }) + } + + return ( + + ) +} diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index c1ba795..eda5218 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -27,6 +27,7 @@ import FollowedBy from './FollowedBy' import Followings from './Followings' import ProfileFeed from './ProfileFeed' import Relays from './Relays' +import SpecialFollowButton from './SpecialFollowButton' export default function Profile({ id }: { id?: string }) { const { t } = useTranslation() @@ -133,6 +134,7 @@ export default function Profile({ id }: { id?: string }) { ) : ( <> {!!lightningAddress && } + )} diff --git a/src/pages/primary/NoteListPage/FeedButton.tsx b/src/pages/primary/NoteListPage/FeedButton.tsx index 9a63fea..a725605 100644 --- a/src/pages/primary/NoteListPage/FeedButton.tsx +++ b/src/pages/primary/NoteListPage/FeedButton.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' -import { ChevronDown, Server, UsersRound } from 'lucide-react' +import { ChevronDown, Server, Star, UsersRound } from 'lucide-react' import { forwardRef, HTMLAttributes, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -62,6 +62,9 @@ const FeedSwitcherTrigger = forwardRef { + if (feedInfo?.feedType === 'following') return + if (feedInfo?.feedType === 'pinned') return + return + }, [feedInfo]) + return (
- {feedInfo?.feedType === 'following' ? : } + {icon}
{title}
diff --git a/src/pages/primary/NoteListPage/PinnedFeed.tsx b/src/pages/primary/NoteListPage/PinnedFeed.tsx new file mode 100644 index 0000000..7469605 --- /dev/null +++ b/src/pages/primary/NoteListPage/PinnedFeed.tsx @@ -0,0 +1,34 @@ +import NormalFeed from '@/components/NormalFeed' +import { useFeed } from '@/providers/FeedProvider' +import { useNostr } from '@/providers/NostrProvider' +import { usePinnedUsers } from '@/providers/PinnedUsersProvider' +import client from '@/services/client.service' +import { TFeedSubRequest } from '@/types' +import { useEffect, useRef, useState } from 'react' + +export default function PinnedFeed() { + const { pubkey } = useNostr() + const { feedInfo } = useFeed() + const { pinnedPubkeySet } = usePinnedUsers() + const [subRequests, setSubRequests] = useState([]) + const initializedRef = useRef(false) + + useEffect(() => { + if (initializedRef.current) return + + async function init() { + if (feedInfo?.feedType !== 'pinned' || !pubkey || pinnedPubkeySet.size === 0) { + setSubRequests([]) + return + } + + initializedRef.current = true + const pinnedPubkeys = Array.from(pinnedPubkeySet) + setSubRequests(await client.generateSubRequestsForPubkeys(pinnedPubkeys, pubkey)) + } + + init() + }, [feedInfo?.feedType, pubkey, pinnedPubkeySet]) + + return +} diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 88ea41d..cc107cd 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -22,6 +22,7 @@ import { import { useTranslation } from 'react-i18next' import FeedButton from './FeedButton' import FollowingFeed from './FollowingFeed' +import PinnedFeed from './PinnedFeed' import RelaysFeed from './RelaysFeed' const NoteListPage = forwardRef((_, ref) => { @@ -59,8 +60,13 @@ const NoteListPage = forwardRef((_, ref) => { } else if (feedInfo.feedType === 'following' && !pubkey) { switchFeed(null) return null + } else if (feedInfo.feedType === 'pinned' && !pubkey) { + switchFeed(null) + return null } else if (feedInfo.feedType === 'following') { content = + } else if (feedInfo.feedType === 'pinned') { + content = } else { content = ( <> diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index a932a02..1369b52 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -65,6 +65,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { return await switchFeed('following', { pubkey }) } + // update pinned feed if pubkey changes + if (feedInfo?.feedType === 'pinned' && pubkey) { + return await switchFeed('pinned', { pubkey }) + } + setIsReady(true) } @@ -147,6 +152,20 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { setIsReady(true) return } + if (feedType === 'pinned') { + if (!options.pubkey) { + setIsReady(true) + return + } + const newFeedInfo = { feedType } + setFeedInfo(newFeedInfo) + feedInfoRef.current = newFeedInfo + storage.setFeedInfo(newFeedInfo, pubkey) + + setRelayUrls([]) + setIsReady(true) + return + } setIsReady(true) } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ad4ac8b..004169c 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -107,7 +107,7 @@ export type TAccount = { export type TAccountPointer = Pick -export type TFeedType = 'following' | 'relays' | 'relay' +export type TFeedType = 'following' | 'pinned' | 'relays' | 'relay' export type TFeedInfo = { feedType: TFeedType; id?: string } | null export type TLanguage = 'en' | 'zh' | 'pl'