-
+
)
}
diff --git a/src/pages/primary/ProfilePage/index.tsx b/src/pages/primary/ProfilePage/index.tsx
index 9302a91f..245eb4cd 100644
--- a/src/pages/primary/ProfilePage/index.tsx
+++ b/src/pages/primary/ProfilePage/index.tsx
@@ -1,7 +1,9 @@
import Profile from '@/components/Profile'
+import { Button } from '@/components/ui/button'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
+import { usePrimaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
-import { UserRound } from 'lucide-react'
+import { Settings, UserRound } from 'lucide-react'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
@@ -26,11 +28,26 @@ export default ProfilePage
function ProfilePageTitlebar() {
const { t } = useTranslation()
+ const { pubkey } = useNostr()
+ const { navigate } = usePrimaryPage()
return (
-
-
-
{t('Profile')}
+
+
+ {pubkey ? (
+
+ ) : null}
)
}
diff --git a/src/pages/primary/SettingsPrimaryPage/index.tsx b/src/pages/primary/SettingsPrimaryPage/index.tsx
new file mode 100644
index 00000000..b88714fe
--- /dev/null
+++ b/src/pages/primary/SettingsPrimaryPage/index.tsx
@@ -0,0 +1,29 @@
+import SettingsMenuBody from '@/components/Settings/SettingsMenuBody'
+import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
+import { Settings } from 'lucide-react'
+import { forwardRef } from 'react'
+import { useTranslation } from 'react-i18next'
+
+const SettingsPrimaryPage = forwardRef
((_, ref) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t('Settings')}
+
+ }
+ displayScrollToTopButton
+ >
+
+
+
+
+ )
+})
+SettingsPrimaryPage.displayName = 'SettingsPrimaryPage'
+export default SettingsPrimaryPage
diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
index e761d50e..001217aa 100644
--- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
+++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
@@ -135,6 +135,20 @@ export function buildCalendarSpellFilter(): Filter {
}
}
+const FOLLOW_PACK_LIMIT = 100
+
+/** Kind 39089 follow/starter packs from fast read relays (same scope as the old Follow Packs page). */
+export function buildFollowPacksSubRequests(): TFeedSubRequest[] {
+ const urls = FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
+ if (!urls.length) return []
+ return [
+ {
+ urls,
+ filter: { kinds: [ExtendedKind.FOLLOW_PACK], limit: FOLLOW_PACK_LIMIT }
+ }
+ ]
+}
+
/** One subrequest per topic (OR). Uses same kind set as the main profile/favorites feed. */
export function buildInterestsSubRequests(
relayUrls: string[],
diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx
index 8f477b87..27c9d4e9 100644
--- a/src/pages/primary/SpellsPage/index.tsx
+++ b/src/pages/primary/SpellsPage/index.tsx
@@ -56,6 +56,7 @@ import {
ChevronDown,
Copy,
FileText,
+ Gift,
Hash,
Image as ImageIcon,
MessageSquare,
@@ -76,6 +77,7 @@ import {
buildBookmarksSubRequests,
buildCalendarSpellFilter,
buildDiscussionFilter,
+ buildFollowPacksSubRequests,
buildInterestsSubRequests,
buildMediaSpellFilter,
buildNotificationFilter,
@@ -206,6 +208,8 @@ function fauxSpellLabelKey(name: FauxSpellName): string {
return 'Discussions'
case 'following':
return 'Following'
+ case 'followPacks':
+ return 'Follow Packs'
case 'media':
return 'Media'
case 'interests':
@@ -223,6 +227,7 @@ const FAUX_SPELL_ICON: Record
= {
notifications: Bell,
discussions: MessageSquare,
following: Users,
+ followPacks: Gift,
media: ImageIcon,
interests: Hash,
bookmarks: Bookmark,
@@ -439,6 +444,9 @@ const SpellsPage = forwardRef(function SpellsPage(
const urls = notificationRelayUrls(relayList, favoriteRelays)
return buildBookmarksSubRequests(bookmarkListEvent, urls)
}
+ if (selectedFauxSpell === 'followPacks') {
+ return buildFollowPacksSubRequests()
+ }
return []
}, [
selectedFauxSpell,
@@ -557,6 +565,9 @@ const SpellsPage = forwardRef(function SpellsPage(
if (selectedFauxSpell === 'following') {
return kindFilterShowKinds
}
+ if (selectedFauxSpell === 'followPacks') {
+ return [ExtendedKind.FOLLOW_PACK]
+ }
if (selectedFauxSpell === 'media') {
return [...MEDIA_SPELL_KINDS]
}
diff --git a/src/pages/secondary/FollowPacksPage/index.tsx b/src/pages/secondary/FollowPacksPage/index.tsx
deleted file mode 100644
index 16f54de1..00000000
--- a/src/pages/secondary/FollowPacksPage/index.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import { Button } from '@/components/ui/button'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
-import { Skeleton } from '@/components/ui/skeleton'
-import { useFollowList } from '@/providers/FollowListProvider'
-import { useMuteList } from '@/providers/MuteListProvider'
-import { useNostr } from '@/providers/NostrProvider'
-import { getPubkeysFromPTags } from '@/lib/tag'
-import { Event } from 'nostr-tools'
-import { useEffect, useMemo, useState, forwardRef } from 'react'
-import { useTranslation } from 'react-i18next'
-import { toast } from 'sonner'
-import { queryService } from '@/services/client.service'
-import { FAST_READ_RELAY_URLS } from '@/constants'
-import { normalizeUrl } from '@/lib/url'
-import { Users } from 'lucide-react'
-import logger from '@/lib/logger'
-import ProfileSearchBar from '@/components/ui/ProfileSearchBar'
-import { SimpleUserAvatar } from '@/components/UserAvatar'
-import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
-
-const FollowPacksPage = forwardRef(
- ({ index, hideTitlebar = false }, ref) => {
- const { t } = useTranslation()
- const { pubkey } = useNostr()
- const { followings, follow } = useFollowList()
- const { mutePubkeySet } = useMuteList()
- const [packs, setPacks] = useState([])
- const [isLoading, setIsLoading] = useState(true)
- const [_followingPacks, setFollowingPacks] = useState>(new Set())
- const [searchQuery, setSearchQuery] = useState('')
-
- useEffect(() => {
- const fetchPacks = async () => {
- if (!pubkey) return
-
- setIsLoading(true)
- try {
- const relayUrls = FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
-
- // Fetch kind 39089 events (starter packs)
- const events = await queryService.fetchEvents(relayUrls, [{
- kinds: [39089],
- limit: 100
- }])
-
- // Sort by created_at descending
- events.sort((a, b) => b.created_at - a.created_at)
-
- setPacks(events)
-
- // Check which packs the user is already following all members of
- const followingSet = new Set(followings)
- const packsFollowingAll = new Set()
-
- events.forEach(pack => {
- const packPubkeys = getPubkeysFromPTags(pack.tags)
- if (packPubkeys.length > 0 && packPubkeys.every(p => followingSet.has(p))) {
- packsFollowingAll.add(pack.id)
- }
- })
-
- setFollowingPacks(packsFollowingAll)
- } catch (error) {
- logger.error('Failed to fetch follow packs', { error })
- toast.error(t('Failed to load follow packs'))
- } finally {
- setIsLoading(false)
- }
- }
-
- fetchPacks()
- }, [pubkey, followings])
-
- const handleFollowPack = async (pack: Event) => {
- if (!pubkey) {
- toast.error(t('Please log in to follow'))
- return
- }
-
- const packPubkeys = getPubkeysFromPTags(pack.tags)
- const followingSet = new Set(followings)
- // Filter out users that are already followed OR muted
- const toFollow = packPubkeys.filter(p => !followingSet.has(p) && !mutePubkeySet.has(p))
-
- if (toFollow.length === 0) {
- const mutedCount = packPubkeys.filter(p => mutePubkeySet.has(p) && !followingSet.has(p)).length
- if (mutedCount > 0) {
- toast.info(t('All available members are already followed or muted'))
- } else {
- toast.info(t('You are already following all members of this pack'))
- }
- return
- }
-
- try {
- // Follow all pubkeys in the pack (excluding muted users)
- for (const pubkeyToFollow of toFollow) {
- await follow(pubkeyToFollow)
- }
- toast.success(t('Followed {{count}} users', { count: toFollow.length }))
-
- // Update followingPacks if all non-muted members are now followed
- const nonMutedPackPubkeys = packPubkeys.filter(p => !mutePubkeySet.has(p))
- if (nonMutedPackPubkeys.length > 0 && nonMutedPackPubkeys.every(p => followingSet.has(p) || toFollow.includes(p))) {
- setFollowingPacks(prev => new Set([...prev, pack.id]))
- }
- } catch (error) {
- logger.error('Failed to follow pack', { error })
- toast.error(t('Failed to follow pack') + ': ' + (error as Error).message)
- }
- }
-
- const getPackTitle = (pack: Event): string => {
- const titleTag = pack.tags.find(tag => tag[0] === 'title' || tag[0] === 'name')
- return titleTag?.[1] || t('Follow Pack')
- }
-
- const getPackDescription = (pack: Event): string => {
- const descTag = pack.tags.find(tag => tag[0] === 'description' || tag[0] === 'd')
- return descTag?.[1] || ''
- }
-
- const filteredPacks = useMemo(() => {
- if (!searchQuery.trim()) {
- return packs
- }
- const query = searchQuery.toLowerCase().trim()
- return packs.filter(pack => {
- const titleTag = pack.tags.find(tag => tag[0] === 'title' || tag[0] === 'name')
- const title = (titleTag?.[1] || t('Follow Pack')).toLowerCase()
- const descTag = pack.tags.find(tag => tag[0] === 'description' || tag[0] === 'd')
- const description = (descTag?.[1] || '').toLowerCase()
- return title.includes(query) || description.includes(query)
- })
- }, [packs, searchQuery, t])
-
- if (!pubkey) {
- return (
-
-
-
{t('Please log in')}
-
{t('You need to be logged in to browse follow packs')}
-
-
- )
- }
-
- return (
-
-
- {!isLoading && packs.length > 0 && (
-
- )}
-
- {isLoading ? (
-
- {Array.from({ length: 6 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
- ))}
-
- ) : packs.length === 0 ? (
-
-
{t('No follow packs found')}
-
{t('There are no follow packs available at the moment')}
-
- ) : filteredPacks.length === 0 ? (
-
-
{t('No packs match your search')}
-
{t('Try a different search term')}
-
- ) : (
-
- {filteredPacks.map((pack) => {
- const packPubkeys = getPubkeysFromPTags(pack.tags)
- const followingSet = new Set(followings)
- // Exclude muted users from calculations
- const availablePubkeys = packPubkeys.filter(p => !mutePubkeySet.has(p))
- const alreadyFollowingAll = availablePubkeys.length > 0 && availablePubkeys.every(p => followingSet.has(p))
- const toFollowCount = availablePubkeys.filter(p => !followingSet.has(p)).length
-
- return (
-
-
- {getPackTitle(pack)}
- {getPackDescription(pack) && (
- {getPackDescription(pack)}
- )}
-
-
-
-
- {t('{{count}} profiles', { count: availablePubkeys.length })}
-
-
- {availablePubkeys.length > 0 && (
-
- {availablePubkeys.slice(0, 5).map((pubkey) => (
-
- ))}
- {availablePubkeys.length > 5 && (
-
- +{availablePubkeys.length - 5}
-
- )}
-
- )}
-
-
-
-
- )
- })}
-
- )}
-
-
- )
-})
-
-FollowPacksPage.displayName = 'FollowPacksPage'
-export default FollowPacksPage
-
diff --git a/src/pages/secondary/FollowPacksRedirect.tsx b/src/pages/secondary/FollowPacksRedirect.tsx
new file mode 100644
index 00000000..10a9f41d
--- /dev/null
+++ b/src/pages/secondary/FollowPacksRedirect.tsx
@@ -0,0 +1,11 @@
+import { useSecondaryPage } from '@/PageManager'
+import { useEffect } from 'react'
+
+/** Legacy `/follow-packs` opens Spells → Follow Packs faux feed. */
+export default function FollowPacksRedirect() {
+ const { navigateToPrimaryPage } = useSecondaryPage()
+ useEffect(() => {
+ navigateToPrimaryPage('spells', { spell: 'followPacks' })
+ }, [navigateToPrimaryPage])
+ return null
+}
diff --git a/src/pages/secondary/SettingsPage/index.tsx b/src/pages/secondary/SettingsPage/index.tsx
index 8e9ab892..94d2d806 100644
--- a/src/pages/secondary/SettingsPage/index.tsx
+++ b/src/pages/secondary/SettingsPage/index.tsx
@@ -1,170 +1,18 @@
-import AboutInfoDialog from '@/components/AboutInfoDialog'
+import SettingsMenuBody from '@/components/Settings/SettingsMenuBody'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
-import {
- toGeneralSettings,
- toPostSettings,
- toRelaySettings,
- toCacheSettings,
- toTranslation,
- toWallet,
- toRssFeedSettings
-} from '@/lib/link'
-import { cn } from '@/lib/utils'
-import { useSmartSettingsNavigation } from '@/PageManager'
-import { useNostr } from '@/providers/NostrProvider'
-import {
- Check,
- ChevronRight,
- Copy,
- Database,
- Info,
- KeyRound,
- Languages,
- PencilLine,
- Rss,
- Server,
- Settings2,
- Wallet
-} from 'lucide-react'
-import { forwardRef, HTMLProps, useState } from 'react'
+import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
-const SettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
- const { t } = useTranslation()
- const { pubkey, nsec, ncryptsec } = useNostr()
- const { navigateToSettings } = useSmartSettingsNavigation()
- const [copiedNsec, setCopiedNsec] = useState(false)
- const [copiedNcryptsec, setCopiedNcryptsec] = useState(false)
+const SettingsPage = forwardRef(
+ ({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
+ const { t } = useTranslation()
- return (
-
- navigateToSettings(toGeneralSettings())}>
-
-
-
- navigateToSettings(toRelaySettings())}>
-
-
-
{t('Relays and Storage Settings')}
-
-
-
- navigateToSettings(toCacheSettings())}>
-
-
-
{t('Cache & offline storage')}
-
-
-
- {!!pubkey && (
- navigateToSettings(toTranslation())}>
-
-
-
- )}
- {!!pubkey && (
- navigateToSettings(toWallet())}>
-
-
-
- )}
- {!!pubkey && (
- navigateToSettings(toPostSettings())}>
-
-
-
{t('Post settings')}
-
-
-
- )}
- {!!pubkey && (
- navigateToSettings(toRssFeedSettings())}>
-
-
-
{t('RSS Feed Settings')}
-
-
-
- )}
- {!!nsec && (
- {
- navigator.clipboard.writeText(nsec)
- setCopiedNsec(true)
- setTimeout(() => setCopiedNsec(false), 2000)
- }}
- >
-
-
-
{t('Copy private key')} (nsec)
-
- {copiedNsec ? : }
-
- )}
- {!!ncryptsec && (
- {
- navigator.clipboard.writeText(ncryptsec)
- setCopiedNcryptsec(true)
- setTimeout(() => setCopiedNcryptsec(false), 2000)
- }}
- >
-
-
-
{t('Copy private key')} (ncryptsec)
-
- {copiedNcryptsec ? : }
-
- )}
-
-
-
-
-
- v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT})
-
-
-
-
-
-
-
- )
-})
-SettingsPage.displayName = 'SettingsPage'
-export default SettingsPage
-
-const SettingItem = forwardRef>(
- ({ children, className, ...props }, ref) => {
return (
-
- {children}
-
+
+
+
)
}
)
-SettingItem.displayName = 'SettingItem'
+SettingsPage.displayName = 'SettingsPage'
+export default SettingsPage
diff --git a/src/routes.tsx b/src/routes.tsx
index c06af6bf..7d15641b 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -19,7 +19,7 @@ import SearchPage from './pages/secondary/SearchPage'
import SettingsPage from './pages/secondary/SettingsPage'
import TranslationPage from './pages/secondary/TranslationPage'
import WalletPage from './pages/secondary/WalletPage'
-import FollowPacksPage from './pages/secondary/FollowPacksPage'
+import FollowPacksRedirect from './pages/secondary/FollowPacksRedirect'
const ROUTES = [
{ path: '/notes', element: },
@@ -52,7 +52,7 @@ const ROUTES = [
{ path: '/settings/rss-feeds', element: },
{ path: '/profile-editor', element: },
{ path: '/mutes', element: },
- { path: '/follow-packs', element: }
+ { path: '/follow-packs', element: }
]
export const routes = ROUTES.map(({ path, element }) => ({