From 016a044b05f2ea3a260cc2ae5dd402027c52f793 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 18 Mar 2026 17:00:50 +0100 Subject: [PATCH] remove mobile RSS feed button implement spells --- src/PageManager.tsx | 7 +- .../BottomNavigationBar/SpellsButton.tsx | 16 ++ src/components/BottomNavigationBar/index.tsx | 2 + src/components/NoteList/index.tsx | 21 +- src/components/Sidebar/SpellsButton.tsx | 15 + src/components/Sidebar/index.tsx | 2 + src/constants.ts | 4 +- src/lib/draft-event.ts | 49 ++++ src/lib/link.ts | 1 + src/pages/primary/NoteListPage/index.tsx | 34 +-- .../primary/SpellsPage/CreateSpellDialog.tsx | 259 ++++++++++++++++++ src/pages/primary/SpellsPage/index.tsx | 211 ++++++++++++++ src/services/client.service.ts | 25 +- src/services/indexed-db.service.ts | 82 +++++- src/services/spell.service.ts | 177 ++++++++++++ 15 files changed, 855 insertions(+), 50 deletions(-) create mode 100644 src/components/BottomNavigationBar/SpellsButton.tsx create mode 100644 src/components/Sidebar/SpellsButton.tsx create mode 100644 src/pages/primary/SpellsPage/CreateSpellDialog.tsx create mode 100644 src/pages/primary/SpellsPage/index.tsx create mode 100644 src/services/spell.service.ts diff --git a/src/PageManager.tsx b/src/PageManager.tsx index fdaaf3d0..8722d1e1 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -49,6 +49,7 @@ import ProfilePage from './pages/primary/ProfilePage' import RelayPage from './pages/primary/RelayPage' import SearchPage from './pages/primary/SearchPage' import DiscussionsPage from './pages/primary/DiscussionsPage' +import SpellsPage from './pages/primary/SpellsPage' import { useScreenSize } from './providers/ScreenSizeProvider' import { routes } from './routes' import modalManager from './services/modal-manager.service' @@ -83,7 +84,8 @@ const PRIMARY_PAGE_REF_MAP = { profile: createRef(), relay: createRef(), search: createRef(), - discussions: createRef() + discussions: createRef(), + spells: createRef() } // Lazy function to create PRIMARY_PAGE_MAP to avoid circular dependency @@ -96,7 +98,8 @@ const getPrimaryPageMap = () => ({ profile: , relay: , search: , - discussions: + discussions: , + spells: }) // Type for primary page names - use the return type of getPrimaryPageMap diff --git a/src/components/BottomNavigationBar/SpellsButton.tsx b/src/components/BottomNavigationBar/SpellsButton.tsx new file mode 100644 index 00000000..ca51eec9 --- /dev/null +++ b/src/components/BottomNavigationBar/SpellsButton.tsx @@ -0,0 +1,16 @@ +import { usePrimaryPage } from '@/PageManager' +import { Wand2 } from 'lucide-react' +import BottomNavigationBarItem from './BottomNavigationBarItem' + +export default function SpellsButton() { + const { navigate, current, display } = usePrimaryPage() + + return ( + navigate('spells')} + > + + + ) +} diff --git a/src/components/BottomNavigationBar/index.tsx b/src/components/BottomNavigationBar/index.tsx index caf79993..0431700c 100644 --- a/src/components/BottomNavigationBar/index.tsx +++ b/src/components/BottomNavigationBar/index.tsx @@ -3,6 +3,7 @@ import HomeButton from './HomeButton' import NotificationsButton from './NotificationsButton' import DiscussionsButton from './DiscussionsButton' import SearchButton from './SearchButton' +import SpellsButton from './SpellsButton' import WriteButton from './WriteButton' export default function BottomNavigationBar() { @@ -19,6 +20,7 @@ export default function BottomNavigationBar() { + diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index d3f538aa..185eb1c3 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -52,7 +52,8 @@ const NoteList = forwardRef( hideUntrustedNotes = false, areAlgoRelays = false, showRelayCloseReason = false, - pinnedEventIds = [] + pinnedEventIds = [], + useFilterAsIs = false }: { subRequests: TFeedSubRequest[] showKinds: number[] @@ -65,6 +66,8 @@ const NoteList = forwardRef( areAlgoRelays?: boolean showRelayCloseReason?: boolean pinnedEventIds?: string[] + /** When true, use filter from subRequests as-is (kinds, limit) instead of showKinds. For spell feeds. */ + useFilterAsIs?: boolean }, ref ) => { @@ -219,11 +222,13 @@ const NoteList = forwardRef( const { closer, timelineKey } = await client.subscribeTimeline( subRequests.map(({ urls, filter }) => ({ urls, - filter: { - ...filter, - kinds: showKinds, - limit: areAlgoRelays ? ALGO_LIMIT : LIMIT - } + filter: useFilterAsIs + ? { ...filter, limit: filter.limit ?? (areAlgoRelays ? ALGO_LIMIT : LIMIT) } + : { + ...filter, + kinds: showKinds, + limit: areAlgoRelays ? ALGO_LIMIT : LIMIT + } })), { onEvents: (events, eosed) => { @@ -239,7 +244,7 @@ const NoteList = forwardRef( } }, onNew: (event) => { - if (!showKinds.includes(event.kind)) return + if (!useFilterAsIs && !showKinds.includes(event.kind)) return if (event.kind === kinds.ShortTextNote) { const isReply = isReplyNoteEvent(event) if (isReply && !showKind1Replies) return @@ -303,7 +308,7 @@ const NoteList = forwardRef( return () => { promise.then((closer) => closer()) } - }, [subRequestsKey, refreshCount, showKinds, showKind1OPs, showKind1Replies, showKind1111]) + }, [subRequestsKey, refreshCount, showKinds, showKind1OPs, showKind1Replies, showKind1111, useFilterAsIs]) useEffect(() => { const options = { diff --git a/src/components/Sidebar/SpellsButton.tsx b/src/components/Sidebar/SpellsButton.tsx new file mode 100644 index 00000000..58738136 --- /dev/null +++ b/src/components/Sidebar/SpellsButton.tsx @@ -0,0 +1,15 @@ +import { usePrimaryPage } from '@/PageManager' +import { Wand2 } from 'lucide-react' +import SidebarItem from './SidebarItem' + +export default function SpellsButton() { + const { navigate, current, display } = usePrimaryPage() + + const isActive = display && current === 'spells' + + return ( + navigate('spells')} active={isActive}> + + + ) +} diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 108f9a92..eba98a4e 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -9,6 +9,7 @@ import NotificationsButton from './NotificationButton' import PostButton from './PostButton' import ProfileButton from './ProfileButton' import RssButton from './RssButton' +import SpellsButton from './SpellsButton' import SearchButton from './SearchButton' import SettingsButton from './SettingsButton' import PaneModeToggle from './PaneModeToggle' @@ -38,6 +39,7 @@ export default function PrimaryPageSidebar() { {showRssFeed && } + diff --git a/src/constants.ts b/src/constants.ts index c09ea088..428fbb08 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -213,7 +213,9 @@ export const ExtendedKind = { /** NIP-52 Time-based calendar event */ CALENDAR_EVENT_TIME: 31923, /** NIP-52 Calendar event RSVP */ - CALENDAR_EVENT_RSVP: 31925 + CALENDAR_EVENT_RSVP: 31925, + /** NIP-A7 Spells: portable relay query filters (kind 777) */ + SPELL: 777 } /** NIP-52 calendar event kinds (addressable by d-tag); use in isReplaceableEvent. */ diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 3a5d6fe6..a880822c 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -589,6 +589,55 @@ export function createRelayListDraftEvent(mailboxRelays: TMailboxRelay[]): TDraf } } +/** NIP-A7 spell (kind 777) draft params from Create Spell form. */ +export type TSpellDraftParams = { + cmd: 'REQ' | 'COUNT' + content: string + name?: string + alt?: string + kinds: string[] // e.g. ['1', '6'] + authors: string[] + ids: string[] + tagFilters: { letter: string; values: string[] }[] // e.g. { letter: 't', values: ['bitcoin'] } + limit: string + since: string + until: string + search: string + relays: string[] + topics: string[] // t tags for spell categorization + closeOnEose: boolean +} + +export function createSpellDraftEvent(params: TSpellDraftParams): TDraftEvent { + const tags: string[][] = [['cmd', params.cmd]] + if (params.name?.trim()) tags.push(['name', params.name.trim()]) + if (params.alt?.trim()) tags.push(['alt', params.alt.trim()]) + params.kinds.filter((k) => k.trim()).forEach((k) => tags.push(['k', k.trim()])) + if (params.authors.length) tags.push(['authors', ...params.authors]) + if (params.ids.length) tags.push(['ids', ...params.ids]) + params.tagFilters.forEach(({ letter, values }) => { + if (letter?.trim() && values.some((v) => v?.trim())) { + tags.push(['tag', letter.trim(), ...values.map((v) => v.trim()).filter(Boolean)]) + } + }) + if (params.limit.trim()) { + const n = parseInt(params.limit, 10) + if (!Number.isNaN(n)) tags.push(['limit', String(n)]) + } + if (params.since.trim()) tags.push(['since', params.since.trim()]) + if (params.until.trim()) tags.push(['until', params.until.trim()]) + if (params.search.trim()) tags.push(['search', params.search.trim()]) + if (params.relays.length) tags.push(['relays', ...params.relays]) + params.topics.filter((t) => t?.trim()).forEach((t) => tags.push(['t', t.trim()])) + if (params.closeOnEose) tags.push(['close-on-eose']) + return { + kind: ExtendedKind.SPELL, + content: params.content?.trim() ?? '', + tags, + created_at: dayjs().unix() + } +} + export function createRssFeedListDraftEvent(feedUrls: string[]): TDraftEvent { // Validate and sanitize feed URLs const validUrls = feedUrls diff --git a/src/lib/link.ts b/src/lib/link.ts index 5f6820e0..48e9116e 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -77,6 +77,7 @@ export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}` export const toRelayReviews = (url: string) => `/relays/${encodeURIComponent(url)}/reviews` export const toMuteList = () => '/mutes' export const toFollowPacks = () => '/follow-packs' +export const toSpells = () => '/spells' export const toChachiChat = (relay: string, d: string) => { return `https://chachi.chat/${relay.replace(/^wss?:\/\//, '').replace(/\/$/, '')}/${d}` diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index b2e6e374..c2a01474 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -8,7 +8,7 @@ import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { TPageRef } from '@/types' -import { Info, Rss } from 'lucide-react' +import { Info } from 'lucide-react' import React, { Dispatch, forwardRef, @@ -26,8 +26,7 @@ import AccountButton from '@/components/Titlebar/AccountButton' import FollowingFeed from './FollowingFeed' import RelaysFeed from './RelaysFeed' import logger from '@/lib/logger' -import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager' -import storage from '@/services/local-storage.service' +import { usePrimaryNoteView } from '@/PageManager' const NoteListPage = forwardRef((_, ref) => { logger.debug('NoteListPage component rendering') @@ -147,25 +146,6 @@ function NoteListPageTitlebar({ }) { const { isSmallScreen } = useScreenSize() const { setPrimaryNoteView } = usePrimaryNoteView() - const { navigate, current } = usePrimaryPage() - const { primaryViewType } = usePrimaryNoteView() - const showRssFeed = storage.getShowRssFeed() - - const handleRssClick = (e: React.MouseEvent) => { - e.stopPropagation() - // Navigate to home if not already there - if (current !== 'home' || primaryViewType !== null) { - navigate('home') - // Wait a bit for navigation to complete, then switch to RSS - setTimeout(() => { - window.dispatchEvent(new CustomEvent('switchToRssFeed')) - }, 100) - } else { - // Already on home, just switch to RSS tab - window.dispatchEvent(new CustomEvent('switchToRssFeed')) - } - } - return (
@@ -188,16 +168,6 @@ function NoteListPageTitlebar({
)}
- {isSmallScreen && showRssFeed && ( - - )} {setShowRelayDetails && ( + +

+ {t('Spells are saved relay filters (NIP-A7). Fill in the filter fields below. Use $me for your pubkey and $contacts for your follow list when executing.')} +

+ +
+
+ + +

{t('REQ returns a feed; COUNT returns a number.')}

+
+ +
+ + setForm((f) => ({ ...f, name: e.target.value }))} + placeholder={t('Human-readable spell name')} + /> +
+ +
+ +