From 0ebe5150ef9e422f23627b23dc321c8bf9a19b25 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 21 Mar 2026 08:23:45 +0100 Subject: [PATCH] more refactoring --- README.md | 92 ++++--- src/PageManager.tsx | 87 ++---- .../BottomNavigationBar/AccountButton.tsx | 4 +- .../ContentPreview/FollowPackPreview.tsx | 149 ++++++---- .../Explore/ExploreFavoriteRelays.tsx | 117 ++++++++ src/components/Note/index.tsx | 2 +- src/components/Profile/index.tsx | 12 +- src/components/Settings/SettingsMenuBody.tsx | 171 ++++++++++++ src/components/Sidebar/AccountButton.tsx | 93 ++++--- src/components/Sidebar/SettingsButton.tsx | 26 -- src/components/Sidebar/index.tsx | 2 - src/components/Titlebar/AccountButton.tsx | 4 +- src/constants.ts | 1 + src/i18n/locales/de.ts | 14 + src/i18n/locales/en.ts | 14 + src/lib/link.ts | 1 - src/pages/primary/ExplorePage/index.tsx | 182 ++++++++++++- src/pages/primary/MePage/index.tsx | 11 +- src/pages/primary/ProfilePage/index.tsx | 25 +- .../primary/SettingsPrimaryPage/index.tsx | 29 ++ .../primary/SpellsPage/fauxSpellFeeds.ts | 14 + src/pages/primary/SpellsPage/index.tsx | 11 + src/pages/secondary/FollowPacksPage/index.tsx | 254 ------------------ src/pages/secondary/FollowPacksRedirect.tsx | 11 + src/pages/secondary/SettingsPage/index.tsx | 172 +----------- src/routes.tsx | 4 +- 26 files changed, 838 insertions(+), 664 deletions(-) create mode 100644 src/components/Explore/ExploreFavoriteRelays.tsx create mode 100644 src/components/Settings/SettingsMenuBody.tsx delete mode 100644 src/components/Sidebar/SettingsButton.tsx create mode 100644 src/pages/primary/SettingsPrimaryPage/index.tsx delete mode 100644 src/pages/secondary/FollowPacksPage/index.tsx create mode 100644 src/pages/secondary/FollowPacksRedirect.tsx diff --git a/README.md b/README.md index 164ca3d5..f8745370 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,56 @@

logo designed by Daniel David

-# Jumble +# Jumble — **ImWald Edition** -A user-friendly Nostr client focused on relay feed browsing and relay discovery +**Maintainer: [Silberengel](https://github.com/Silberengel)** · Hard fork of [Cody Tseng’s Jumble](https://github.com/CodyTseng/jumble) -## Features +A Nostr web client focused on relay feeds, discovery, and spells. This repository is the **ImWald** line: same core ideas as upstream, with a substantial navigation and information-architecture rewrite (see below). -- **Relay Feeds:** Explore content directly through relays without following specific users -- **Relay-Friendly Design:** Minimized and simplified requests ensure efficient communication with relays -- **Relay Sets:** Easily manage and switch between relay sets -- **Clean Interface:** Enjoy a minimalist design and intuitive interactions +--- + +## Major rewrite (this fork) + +High-level changes versus a “stock” Jumble-style layout: + +### Home vs feed + +- **Home** is the **Explore** experience: relay directory, **Following’s Favorites**, and related discovery — not a duplicate of your main timeline. +- **Feed** is a dedicated primary area for **favorite relays**, displaying their diverse social content as a feed: short text notes (microblogging), longform articles, wiki pages, media notes, calendar entries, etc. + +### RSS + +- **RSS** is a **separate primary page** with its own title bar, refresh, and filters +- Sidebar **RSS** opens that page directly when enabled in settings. + +### Spells & faux feeds + +- Built-in **faux spells** (notifications, discussions, following, follow packs, media, interests, bookmarks, calendar) all run through the **same `NoteList` path** as user-defined kind-777 spells. +- Sidebar **Notifications** and **Discussions** navigate to the correct faux feed with proper **active** states; primary page props are merged through the lazy `Suspense` boundary correctly. +- **Following** faux feed respects global kind filters and Notes/Replies mode; **bookmarks** faux uses classic **`e`-tag** ids from the bookmark list. + +### Profiles + +- **Pinned** notes (kind `10001` lists) appear first with a **pin** marker; the rest of the profile timeline uses **main-feed-style** kind and reply rules, with a clear split when pins exist. +- Profiles with **no pins** behave like a normal timeline (no empty pin chrome). + +### Explore quality-of-life + +- **Relay URL search** in the Explore title bar: paste `wss://…` or a host, submit, and open the relay page with the same navigation as the relay cards. While typing, **suggestions** come from the **NIP-66 monitoring (public lively) list** on partial or full URL/host matches; you can still submit any URL the app does not know. + +### Other + +- Sidebar layout tuned for **long translations** (e.g. German) so labels don’t sit on the divider. +- Branding in-app: **Im Wald**. + +--- + +## Features (still core to Jumble) + +- **Relay feeds:** Browse content through relays, sets, and favorites +- **Relay-friendly requests:** Efficient subscriptions where possible +- **Relay sets:** Switch between saved relay groups +- **Spells:** Portable filters (kind 777) plus built-in faux feeds above ## Screenshots @@ -27,46 +67,32 @@ A user-friendly Nostr client focused on relay feed browsing and relay discovery Jumble Screenshot 04 -## Forks - -> Some interesting forks of Jumble. +## Upstream & related forks -- [https://jumble.imwald.eu/](https://jumble.imwald.eu/) Repo: [Silberengel/jumble](https://github.com/Silberengel/jumble) - Discussions support -- [https://grouped-notes.dtonon.com/](https://grouped-notes.dtonon.com/) - "Grouped Notes" mode: organizes posts by user and timeframe for cleaner browsing and easier discovery -- [https://jumblekat.shakespeare.wtf/](https://jumblekat.shakespeare.wtf/) - Supports custom styles +- **Original project:** [CodyTseng/jumble](https://github.com/CodyTseng/jumble) — design, sponsorship, and donation links below still refer to Cody’s work where applicable. +- **This fork:** [Silberengel/jumble](https://github.com/Silberengel/jumble) — Im Wald / rewrite described above. +- Other public forks (examples): [grouped-notes.dtonon.com](https://grouped-notes.dtonon.com/), [jumblekat.shakespeare.wtf](https://jumblekat.shakespeare.wtf/). -## Run Locally +## Run locally ```bash -# Clone this repository -git clone https://github.com/CodyTseng/jumble.git - -# Go into the repository +git clone https://github.com/Silberengel/jumble.git cd jumble - -# Install dependencies npm install - -# Run the app npm run dev ``` -## Run Docker +## Run with Docker ```bash -# Clone this repository -git clone https://github.com/CodyTseng/jumble.git - -# Go into the repository +git clone https://github.com/Silberengel/jumble.git cd jumble - -# Run the docker compose docker compose up --build -d ``` -After finishing, access: http://localhost:8089 +Then open: http://localhost:8089 -## Sponsors +## Sponsors (original Jumble) open-sats-logo @@ -74,7 +100,7 @@ After finishing, access: http://localhost:8089 ## Donate -If you like this project, you can buy me a coffee :) +**Original author** — if you want to support the project Jumble was forked from: lightning: ⚡️ codytseng@getalby.com ⚡️ @@ -82,6 +108,8 @@ bitcoin: bc1qx8kvutghdhejx7vuvatmvw2ghypdungu0qm7ds geyser: https://geyser.fund/project/jumble +--- + ## License MIT diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 221cd319..3a37bd9d 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -7,14 +7,6 @@ import { NavigationService } from '@/services/navigation.service' import NoteListPage from '@/pages/primary/NoteListPage' import SecondaryNoteListPage from '@/pages/secondary/NoteListPage' // Page imports needed for primary note view -import SettingsPage from '@/pages/secondary/SettingsPage' -import RelaySettingsPage from '@/pages/secondary/RelaySettingsPage' -import WalletPage from '@/pages/secondary/WalletPage' -import PostSettingsPage from '@/pages/secondary/PostSettingsPage' -import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage' -import TranslationPage from '@/pages/secondary/TranslationPage' -import CacheSettingsPage from '@/pages/secondary/CacheSettingsPage' -import RssFeedSettingsPage from '@/pages/secondary/RssFeedSettingsPage' import NoteDrawer from '@/components/NoteDrawer' import SecondaryProfilePage from '@/pages/secondary/ProfilePage' import storage from '@/services/local-storage.service' @@ -56,6 +48,7 @@ import ProfilePage from './pages/primary/ProfilePage' import RelayPage from './pages/primary/RelayPage' import SearchPage from './pages/primary/SearchPage' import RssPage from './pages/primary/RssPage' +import SettingsPrimaryPage from './pages/primary/SettingsPrimaryPage' import { useScreenSize } from './providers/ScreenSizeProvider' /** Lazy-loaded so PageManager does not synchronously import SpellsPage (avoids HMR cycle: SpellsPage → PrimaryPageLayout → PageManager → SpellsPage). */ @@ -96,6 +89,7 @@ const PRIMARY_PAGE_REF_MAP = { relay: createRef(), search: createRef(), rss: createRef(), + settings: createRef(), spells: createRef() } @@ -109,6 +103,7 @@ const getPrimaryPageMap = () => ({ relay: , search: , rss: , + settings: , spells: ( { - const topUrl = getTopSecondaryUrl?.() - const settingsInRightPanel = topUrl === '/settings' - - // When the right panel is showing the settings list, push the sub-page so it opens in the panel instead of in the main area (behind the panel). - if (settingsInRightPanel && url !== '/settings') { - pushSecondaryPage(url) + const base = url.split('?')[0].split('#')[0] + if (base === '/settings') { + navigatePrimary('settings') return } - - // Otherwise use primary note view (main content area) - if (url === '/settings') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings') - } else if (url.startsWith('/settings/relays')) { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } else if (url === '/settings/cache') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } else if (url === '/settings/wallet') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } else if (url === '/settings/posts') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } else if (url === '/settings/general') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } else if (url === '/settings/translation') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } else if (url === '/settings/rss-feeds') { - window.history.pushState(null, '', url) - setPrimaryNoteView(, 'settings-sub') - } + pushSecondaryPage(url) } return { navigateToSettings } @@ -734,24 +700,6 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } } - const goBack = () => { - // Special handling for settings sub-pages - go back to main settings page - if (primaryViewType === 'settings-sub') { - window.history.pushState(null, '', '/settings') - setPrimaryNoteView(, 'settings') - } else if (primaryViewType === 'following' || primaryViewType === 'mute' || primaryViewType === 'others-relay-settings') { - // Special handling for profile sub-pages - go back to main profile page - const currentPath = window.location.pathname - const profileId = currentPath.replace('/users/', '').replace('/following', '').replace('/muted', '').replace('/relays', '') - const profileUrl = `/users/${profileId}` - window.history.pushState(null, '', profileUrl) - setPrimaryNoteView(, 'profile') - } else { - // Use browser's back functionality for other pages - window.history.back() - } - } - // Drawer handlers const openDrawer = useCallback((noteId: string) => { setDrawerNoteId(noteId) @@ -1094,7 +1042,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Double-pane mode: continue with stack creation } } - // Create a new stack item if it's a secondary route (e.g., /follow-packs, /mutes) + // Create a new stack item if it's a secondary route (e.g., /mutes) const { component, ref } = findAndCreateComponent(state.url, state.index) if (component) { newStack.push({ @@ -1246,6 +1194,21 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // NEVER scroll to top - feed should maintain scroll position at all times } + const goBack = () => { + if (primaryViewType === 'settings-sub') { + navigatePrimaryPage('settings') + return + } + if (primaryViewType === 'following' || primaryViewType === 'mute' || primaryViewType === 'others-relay-settings') { + const currentPath = window.location.pathname + const profileId = currentPath.replace('/users/', '').replace('/following', '').replace('/muted', '').replace('/relays', '') + const profileUrl = `/users/${profileId}` + window.history.pushState(null, '', profileUrl) + setPrimaryNoteView(, 'profile') + return + } + window.history.back() + } const pushSecondaryPage = (url: string, index?: number) => { logger.component('PageManager', 'pushSecondaryPage called', { url }) diff --git a/src/components/BottomNavigationBar/AccountButton.tsx b/src/components/BottomNavigationBar/AccountButton.tsx index 9087862d..8449667a 100644 --- a/src/components/BottomNavigationBar/AccountButton.tsx +++ b/src/components/BottomNavigationBar/AccountButton.tsx @@ -15,12 +15,12 @@ export default function AccountButton() { () => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''), [profile] ) - const active = useMemo(() => current === 'me' && display, [display, current]) + const active = useMemo(() => current === 'profile' && display, [display, current]) return ( { - navigate('me') + navigate(pubkey ? 'profile' : 'me') }} active={active} > diff --git a/src/components/ContentPreview/FollowPackPreview.tsx b/src/components/ContentPreview/FollowPackPreview.tsx index 444c3fff..fac67c65 100644 --- a/src/components/ContentPreview/FollowPackPreview.tsx +++ b/src/components/ContentPreview/FollowPackPreview.tsx @@ -1,13 +1,16 @@ import { getPubkeysFromPTags } from '@/lib/tag' +import logger from '@/lib/logger' import { cn } from '@/lib/utils' +import { useFollowList } from '@/providers/FollowListProvider' +import { useMuteList } from '@/providers/MuteListProvider' +import { useNostr } from '@/providers/NostrProvider' import { Event } from 'nostr-tools' -import { useMemo } from 'react' +import { Users } from 'lucide-react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import { SimpleUserAvatar } from '../UserAvatar' -import { Users, ExternalLink } from 'lucide-react' import { Button } from '../ui/button' -import { toFollowPacks } from '@/lib/link' -import { useSecondaryPage } from '@/PageManager' export default function FollowPackPreview({ event, @@ -17,76 +20,124 @@ export default function FollowPackPreview({ className?: string }) { const { t } = useTranslation() - const { push } = useSecondaryPage() - + const { pubkey } = useNostr() + const { followings, follow } = useFollowList() + const { mutePubkeySet } = useMuteList() + const [busy, setBusy] = useState(false) + const packPubkeys = useMemo(() => getPubkeysFromPTags(event.tags), [event.tags]) - + const getPackTitle = (pack: Event): string => { - const titleTag = pack.tags.find(tag => tag[0] === 'title' || tag[0] === 'name') + 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') + const descTag = pack.tags.find((tag) => tag[0] === 'description' || tag[0] === 'd') return descTag?.[1] || '' } const title = getPackTitle(event) const description = getPackDescription(event) - const handleOpenInViewer = (e: React.MouseEvent) => { - e.stopPropagation() - push(toFollowPacks()) - } + const followingSet = useMemo(() => new Set(followings), [followings]) + const availablePubkeys = useMemo( + () => packPubkeys.filter((p) => !mutePubkeySet.has(p)), + [packPubkeys, mutePubkeySet] + ) + const alreadyFollowingAll = + availablePubkeys.length > 0 && availablePubkeys.every((p) => followingSet.has(p)) + const toFollowCount = availablePubkeys.filter((p) => !followingSet.has(p)).length + + const handleFollowPack = useCallback( + async (e: React.MouseEvent) => { + e.stopPropagation() + if (!pubkey) { + toast.error(t('Please log in to follow')) + return + } + 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 + } + setBusy(true) + try { + for (const pubkeyToFollow of toFollow) { + await follow(pubkeyToFollow) + } + toast.success(t('Followed {{count}} users', { count: toFollow.length })) + } catch (error) { + logger.error('Failed to follow pack', { error }) + toast.error(t('Failed to follow pack') + ': ' + (error as Error).message) + } finally { + setBusy(false) + } + }, + [pubkey, packPubkeys, followingSet, mutePubkeySet, follow, t] + ) return ( -
-
- [{t('Follow Pack')}] - {title} +
+
+ [{t('Follow Pack')}] + {title}
- - {description && ( -
- {description} -
- )} - -
+ + {description ? ( +
{description}
+ ) : null} + +
- {t('{{count}} profiles', { count: packPubkeys.length })} + {t('{{count}} profiles', { count: availablePubkeys.length })}
- - {packPubkeys.length > 0 && ( + + {availablePubkeys.length > 0 ? (
- {packPubkeys.slice(0, 5).map((pubkey) => ( - ( + ))} - {packPubkeys.length > 5 && ( -
- +{packPubkeys.length - 5} + {availablePubkeys.length > 5 ? ( +
+ +{availablePubkeys.length - 5}
- )} + ) : null}
- )} + ) : null}
- - + + {!pubkey ? ( +

{t('Please log in to follow')}

+ ) : ( + + )}
) } - diff --git a/src/components/Explore/ExploreFavoriteRelays.tsx b/src/components/Explore/ExploreFavoriteRelays.tsx new file mode 100644 index 00000000..37c3270b --- /dev/null +++ b/src/components/Explore/ExploreFavoriteRelays.tsx @@ -0,0 +1,117 @@ +import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '@/components/RelaySimpleInfo' +import { Button } from '@/components/ui/button' +import { DEFAULT_FAVORITE_RELAYS } from '@/constants' +import { useFetchRelayInfo } from '@/hooks' +import { toRelay } from '@/lib/link' +import { normalizeUrl, simplifyUrl } from '@/lib/url' +import { usePrimaryPage, useSmartRelayNavigation } from '@/PageManager' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { cn } from '@/lib/utils' +import { Newspaper } from 'lucide-react' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +function FavoriteRelayCard({ url }: { url: string }) { + const { navigateToRelay } = useSmartRelayNavigation() + const { relayInfo, isFetching } = useFetchRelayInfo(url) + + if (isFetching) { + return ( + + ) + } + + if (!relayInfo) { + return ( + + ) + } + + return ( + { + e.stopPropagation() + navigateToRelay(toRelay(relayInfo.url)) + }} + /> + ) +} + +/** + * Horizontal strip of favorite relays (non-blocked), or {@link DEFAULT_FAVORITE_RELAYS} when none. + */ +export default function ExploreFavoriteRelays() { + const { t } = useTranslation() + const { navigate } = usePrimaryPage() + const { favoriteRelays, blockedRelays } = useFavoriteRelays() + + const blockedSet = useMemo( + () => new Set(blockedRelays.map((b) => normalizeUrl(b) || b)), + [blockedRelays] + ) + + const { urls, usingDefaults } = useMemo(() => { + const visible = favoriteRelays.filter((r) => { + const k = normalizeUrl(r) || r + return k && !blockedSet.has(k) + }) + if (visible.length > 0) { + return { urls: visible, usingDefaults: false } + } + const defaultsFiltered = DEFAULT_FAVORITE_RELAYS.filter((r) => { + const k = normalizeUrl(r) || r + return k && !blockedSet.has(k) + }) + return { + urls: defaultsFiltered.length > 0 ? defaultsFiltered : DEFAULT_FAVORITE_RELAYS, + usingDefaults: true + } + }, [favoriteRelays, blockedSet]) + + if (urls.length === 0) return null + + return ( +
+
+
+

{t('Favorite Relays')}

+ +
+ {usingDefaults ? ( + {t('Using app default relays')} + ) : null} +
+
+ {urls.map((url) => ( +
+ +
+ ))} +
+
+ ) +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index edb2e5b3..712423f0 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -126,7 +126,7 @@ export default function Note({ ExtendedKind.ZAP_REQUEST, ExtendedKind.ZAP_RECEIPT, ExtendedKind.PUBLICATION_CONTENT, // Only for rendering embedded content, not in feeds - ExtendedKind.FOLLOW_PACK, // Only for rendering embedded content, not in feeds + ExtendedKind.FOLLOW_PACK, // Follow-pack feed + embedded previews ExtendedKind.CITATION_INTERNAL, // Citations for rendering ExtendedKind.CITATION_EXTERNAL, ExtendedKind.CITATION_HARDCOPY, diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 44a77d8f..1b10d215 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -16,7 +16,7 @@ import { kinds, type NostrEvent } from 'nostr-tools' import { getPaymentInfoFromEvent } from '@/lib/event-metadata' import { toProfileEditor } from '@/lib/link' import { generateImageByPubkey } from '@/lib/pubkey' -import { useSecondaryPage } from '@/PageManager' +import { usePrimaryPage, useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { replaceableEventService } from '@/services/client.service' @@ -27,7 +27,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { Link, Copy, Ellipsis, Calendar, MapPin, Pencil, SatelliteDish, Code } from 'lucide-react' +import { Copy, Ellipsis, Calendar, MapPin, Pencil, SatelliteDish, Code, Gift, Link } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -38,7 +38,6 @@ import ProfileFeedWithPins from './ProfileFeedWithPins' import SmartFollowings from './SmartFollowings' import SmartMuteLink from './SmartMuteLink' import SmartRelays from './SmartRelays' -import { toFollowPacks } from '@/lib/link' import ZapDialog from '@/components/ZapDialog' import PaytoLink from '@/components/PaytoLink' import PostEditor from '@/components/PostEditor' @@ -157,6 +156,7 @@ function mergePaymentMethods( export default function Profile({ id }: { id?: string }) { const { t } = useTranslation() const { push } = useSecondaryPage() + const { navigate: navigatePrimary } = usePrimaryPage() const { profile, isFetching } = useFetchProfile(id) const { pubkey: accountPubkey } = useNostr() const [paymentInfo, setPaymentInfo] = useState | null>(null) @@ -399,9 +399,9 @@ export default function Profile({ id }: { id?: string }) { {t('Schedule in-person meeting')} - push(toFollowPacks())}> - - {t('Browse follow packs')} + navigatePrimary('spells', { spell: 'followPacks' })}> + + {t('Follow Packs')} push(toProfileEditor())}> diff --git a/src/components/Settings/SettingsMenuBody.tsx b/src/components/Settings/SettingsMenuBody.tsx new file mode 100644 index 00000000..77f3f12f --- /dev/null +++ b/src/components/Settings/SettingsMenuBody.tsx @@ -0,0 +1,171 @@ +import AboutInfoDialog from '@/components/AboutInfoDialog' +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 { useTranslation } from 'react-i18next' + +/** + * Shared settings index rows (General, Relays, …). Used by the primary Settings page and + * the secondary /settings route for deep links / stack restores. + */ +export default function SettingsMenuBody({ className }: { className?: string }) { + const { t } = useTranslation() + const { pubkey, nsec, ncryptsec } = useNostr() + const { navigateToSettings } = useSmartSettingsNavigation() + const [copiedNsec, setCopiedNsec] = useState(false) + const [copiedNcryptsec, setCopiedNcryptsec] = useState(false) + + return ( +
+ navigateToSettings(toGeneralSettings())}> +
+ +
{t('General')}
+
+ +
+ navigateToSettings(toRelaySettings())}> +
+ +
{t('Relays and Storage Settings')}
+
+ +
+ navigateToSettings(toCacheSettings())}> +
+ +
{t('Cache & offline storage')}
+
+ +
+ {!!pubkey && ( + navigateToSettings(toTranslation())}> +
+ +
{t('Translation')}
+
+ +
+ )} + {!!pubkey && ( + navigateToSettings(toWallet())}> +
+ +
{t('Wallet')}
+
+ +
+ )} + {!!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 ? : } +
+ )} + + +
+ +
{t('About')}
+
+
+
+ v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT}) +
+ +
+
+
+
+
Jumble
+
Im Wald
+
+
+ ) +} + +const SettingItem = forwardRef>( + ({ children, className, ...props }, ref) => { + return ( +
+ {children} +
+ ) + } +) +SettingItem.displayName = 'SettingItem' diff --git a/src/components/Sidebar/AccountButton.tsx b/src/components/Sidebar/AccountButton.tsx index 38dc64ea..fc62ad46 100644 --- a/src/components/Sidebar/AccountButton.tsx +++ b/src/components/Sidebar/AccountButton.tsx @@ -11,7 +11,7 @@ import { toWallet } from '@/lib/link' import { formatPubkey, generateImageByPubkey, pubkeyToNpub, formatNpub } from '@/lib/pubkey' import { usePrimaryPage, useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { ArrowDownUp, LogIn, LogOut, UserRound, Wallet } from 'lucide-react' +import { ArrowDownUp, LogIn, LogOut, MoreVertical, Wallet } from 'lucide-react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import LoginDialog from '../LoginDialog' @@ -39,55 +39,62 @@ function ProfileButton() { if (!pubkey) return null const defaultAvatar = generateImageByPubkey(pubkey) - // Fallback to formatted npub if no profile const npub = pubkeyToNpub(pubkey) const fallbackUsername = npub ? formatNpub(npub) : formatPubkey(pubkey) const { username, avatar } = profile || { username: fallbackUsername, avatar: defaultAvatar } return ( - - - - - - navigate('profile')}> - - {t('Profile')} - - - push(toWallet())}> - - {t('Wallet')} - - - setLoginDialogOpen(true)}> - - {t('Switch account')} - - setLogoutDialogOpen(true)} - > - - {t('Logout')} - - +
+ + + + + + + push(toWallet())}> + + {t('Wallet')} + + + setLoginDialogOpen(true)}> + + {t('Switch account')} + + setLogoutDialogOpen(true)} + > + + {t('Logout')} + + + - +
) } diff --git a/src/components/Sidebar/SettingsButton.tsx b/src/components/Sidebar/SettingsButton.tsx deleted file mode 100644 index 7a875101..00000000 --- a/src/components/Sidebar/SettingsButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { toSettings } from '@/lib/link' -import { useSmartSettingsNavigation, usePrimaryNoteView } from '@/PageManager' -// DEPRECATED: useUserPreferences removed - double-panel functionality disabled -import { Settings } from 'lucide-react' -import SidebarItem from './SidebarItem' - -export default function SettingsButton() { - const { navigateToSettings } = useSmartSettingsNavigation() - const { primaryViewType } = usePrimaryNoteView() - // DEPRECATED: showRecommendedRelaysPanel removed - double-panel functionality disabled - - // Settings is active when: - // 1. primaryViewType is 'settings' or 'settings-sub' (when side panel is off) - // 2. OR we're on a /settings URL (when side panel is on) - const url = window.location.pathname - const isActive = - primaryViewType === 'settings' || - primaryViewType === 'settings-sub' || - url.startsWith('/settings') - - return ( - navigateToSettings(toSettings())} active={isActive}> - - - ) -} diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 822caabe..20123c5c 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -10,7 +10,6 @@ import NotificationButton from './NotificationButton' import PostButton from './PostButton' import RssButton from './RssButton' import SearchButton from './SearchButton' -import SettingsButton from './SettingsButton' import SpellsButton from './SpellsButton' import PaneModeToggle from './PaneModeToggle' @@ -37,7 +36,6 @@ export default function PrimaryPageSidebar() { -
diff --git a/src/components/Titlebar/AccountButton.tsx b/src/components/Titlebar/AccountButton.tsx index d5b7d048..849c0b15 100644 --- a/src/components/Titlebar/AccountButton.tsx +++ b/src/components/Titlebar/AccountButton.tsx @@ -15,13 +15,13 @@ export default function AccountButton() { () => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''), [profile] ) - const active = useMemo(() => current === 'me' && display, [display, current]) + const active = useMemo(() => current === 'profile' && display, [display, current]) return (
@@ -68,17 +120,129 @@ const ExplorePage = forwardRef((_, ref) => { ExplorePage.displayName = 'ExplorePage' export default ExplorePage -function ExplorePageTitlebar({ t }: { t: (key: string) => string }) { +function ExplorePageTitlebar() { + const { t } = useTranslation() + const { navigateToRelay } = useSmartRelayNavigation() + const [relayQuery, setRelayQuery] = useState('') + const [monitoringRelays, setMonitoringRelays] = useState([]) + const [suggestOpen, setSuggestOpen] = useState(false) + const blurCloseTimer = useRef | null>(null) + + useEffect(() => { + nip66Service.getPublicLivelyRelayUrls().then((urls) => { + setMonitoringRelays(dedupeNormalizedRelayUrls(urls ?? [])) + }) + }, []) + + useEffect(() => { + return () => { + if (blurCloseTimer.current != null) clearTimeout(blurCloseTimer.current) + } + }, []) + + const relaySuggestions = useMemo( + () => filterMonitoringRelaySuggestions(monitoringRelays, relayQuery), + [monitoringRelays, relayQuery] + ) + + const clearBlurTimer = () => { + if (blurCloseTimer.current != null) { + clearTimeout(blurCloseTimer.current) + blurCloseTimer.current = null + } + } + + const openRelayAndReset = (normalizedUrl: string) => { + navigateToRelay(toRelay(normalizedUrl)) + setRelayQuery('') + setSuggestOpen(false) + } + + const tryOpenRelay = () => { + const trimmed = relayQuery.trim() + if (!trimmed) return + const normalized = normalizeUrl(trimmed) + if (!normalized || !isWebsocketUrl(normalized)) { + toast.error(t('invalid relay URL')) + return + } + openRelayAndReset(normalized) + } + + const onSubmitRelay = (e: FormEvent) => { + e.preventDefault() + tryOpenRelay() + } + return ( -
-
- +
+
+
{t('Explore')}
+
+
+ setRelayQuery(e.target.value)} + aria-label={t('Relay URL…')} + aria-autocomplete="list" + aria-expanded={suggestOpen && relaySuggestions.length > 0} + aria-controls="explore-relay-suggestions" + role="combobox" + onFocus={() => { + clearBlurTimer() + setSuggestOpen(true) + }} + onBlur={() => { + clearBlurTimer() + blurCloseTimer.current = setTimeout(() => setSuggestOpen(false), 200) + }} + /> + +
+ {suggestOpen && relaySuggestions.length > 0 ? ( +
    e.preventDefault()} + > + {relaySuggestions.map((url) => ( +
  • + +
  • + ))} +
+ ) : null} +
+
+
{t('YouTabName')}
) } 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')}
+
+
+ +
{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())}> -
- -
{t('General')}
-
- -
- navigateToSettings(toRelaySettings())}> -
- -
{t('Relays and Storage Settings')}
-
- -
- navigateToSettings(toCacheSettings())}> -
- -
{t('Cache & offline storage')}
-
- -
- {!!pubkey && ( - navigateToSettings(toTranslation())}> -
- -
{t('Translation')}
-
- -
- )} - {!!pubkey && ( - navigateToSettings(toWallet())}> -
- -
{t('Wallet')}
-
- -
- )} - {!!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 ? : } -
- )} - - -
- -
{t('About')}
-
-
-
- v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT}) -
- -
-
-
-
-
Jumble
-
Im Wald
-
-
- ) -}) -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 }) => ({