@@ -2441,6 +2496,7 @@ const NoteList = forwardRef(
className="w-full"
event={event}
filterMutedNotes={filterMutedNotes}
+ bottomNoteLabel={eventReasonLabelMap.get(event.id)}
/>
))}
{listSourceEvents.length === 0 &&
diff --git a/src/components/Sidebar/FavoritesButton.tsx b/src/components/Sidebar/FavoritesButton.tsx
new file mode 100644
index 00000000..7fb64754
--- /dev/null
+++ b/src/components/Sidebar/FavoritesButton.tsx
@@ -0,0 +1,29 @@
+import { usePrimaryPage } from '@/contexts/primary-page-context'
+import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
+import { useNostr } from '@/providers/NostrProvider'
+import { Star } from 'lucide-react'
+import SidebarItem from './SidebarItem'
+
+export default function FavoritesButton() {
+ const { navigate, current, currentPageProps, display } = usePrimaryPage()
+ const { primaryViewType } = usePrimaryNoteView()
+ const { pubkey } = useNostr()
+ const spell = (currentPageProps as { spell?: string } | undefined)?.spell
+
+ if (!pubkey) return null
+
+ return (
+
navigate('spells', { spell: 'favorites' })}
+ active={
+ display &&
+ current === 'spells' &&
+ primaryViewType === null &&
+ spell === 'favorites'
+ }
+ >
+
+
+ )
+}
diff --git a/src/components/Sidebar/FollowsLatestButton.tsx b/src/components/Sidebar/FollowsLatestButton.tsx
index 94700581..8bbb526a 100644
--- a/src/components/Sidebar/FollowsLatestButton.tsx
+++ b/src/components/Sidebar/FollowsLatestButton.tsx
@@ -1,5 +1,6 @@
import { usePrimaryPage } from '@/contexts/primary-page-context'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
+import { useNostr } from '@/providers/NostrProvider'
import { UsersRound } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import SidebarItem from './SidebarItem'
@@ -8,6 +9,9 @@ export default function FollowsLatestButton() {
const { t } = useTranslation()
const { navigate, current, display } = usePrimaryPage()
const { primaryViewType } = usePrimaryNoteView()
+ const { pubkey } = useNostr()
+
+ if (!pubkey) return null
return (
checkLogin(() => navigate('spells', { spell: 'notifications' }))}
+ onClick={() => navigate('spells', { spell: 'notifications' })}
active={
display &&
current === 'spells' &&
diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx
index 8179ffb1..8f390b11 100644
--- a/src/components/Sidebar/index.tsx
+++ b/src/components/Sidebar/index.tsx
@@ -10,6 +10,7 @@ import PostButton from './PostButton'
import RssButton from './RssButton'
import SearchButton from './SearchButton'
import FollowsLatestButton from './FollowsLatestButton'
+import FavoritesButton from './FavoritesButton'
import SpellsButton from './SpellsButton'
import { FavoriteRelaysActiveStripSidebar } from '@/components/FavoriteRelaysActiveStrip'
import PaneModeToggle from './PaneModeToggle'
@@ -41,6 +42,7 @@ export default function PrimaryPageSidebar() {
+
diff --git a/src/constants.ts b/src/constants.ts
index 3cf6e3d7..922a1330 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -245,7 +245,6 @@ export const READ_ONLY_RELAY_URLS = [
'wss://relay.noswhere.com',
'wss://search.nos.today',
'wss://trending.nostr.wine',
- 'wss://sendit.nosflare.com',
'wss://relay.nip46.com'
]
@@ -344,6 +343,10 @@ export const PROFILE_RELAY_URLS = [
'wss://thecitadel.nostr1.com'
]
+ export const FOLLOWS_HISTORY_RELAY_URLS = [
+ 'wss://hist.nostr.land'
+ ]
+
// Combined relay URLs for profile fetching - includes both FAST_READ_RELAY_URLS and SEARCHABLE_RELAY_URLS
export const PROFILE_FETCH_RELAY_URLS = [...SEARCHABLE_RELAY_URLS, ...FAST_READ_RELAY_URLS, ...PROFILE_RELAY_URLS]
@@ -606,6 +609,7 @@ export const FAUX_SPELL_ORDER = [
'notifications',
'discussions',
'following',
+ 'favorites',
'followPacks',
'media',
'interests',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index a0d877ac..9770313c 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -797,12 +797,19 @@ export default {
'No articles or publications match your search',
'articles and publications': 'articles and publications',
Interests: 'Interests',
+ Favorites: 'Favorites',
Calendar: 'Calendar',
'No subscribed interests yet.':
'No subscribed interests yet. Add topics in settings to see them here.',
'No bookmarked notes with id tags yet.':
'No bookmarked notes with id tags yet. Only classic (e-tag) bookmarks load in this feed.',
'No follows or relays to load yet.': 'No follows or relays to load yet.',
+ 'No favorites yet.': 'No favorites yet. Add follows, follow sets, interests, or bookmarks.',
+ 'Added from interests': 'Added from interests',
+ 'Added from bookmarks list': 'Added from bookmarks list',
+ 'Added from your web bookmarks': 'Added from your web bookmarks',
+ 'Added from follows and contact lists': 'Added from follows and contact lists',
+ 'Added from follows web bookmarks': 'Added from follows web bookmarks',
'Nothing to load for this feed.': 'Nothing to load for this feed.',
'No posts loaded for this feed. Try refreshing.':
'No posts loaded for this feed. Try refreshing.',
@@ -1505,6 +1512,7 @@ export default {
'Pin note': 'Pin note',
'Plain text description of the query': 'Plain text description of the query',
'Please login to view bookmarks': 'Please login to view bookmarks',
+ 'Please login to view favorites': 'Please login to view favorites',
'Please select a group': 'Please select a group',
'Please select at least one relay': 'Please select at least one relay',
'Please set a start date': 'Please set a start date',
diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx
index 54163fd9..d63524a8 100644
--- a/src/pages/primary/NoteListPage/index.tsx
+++ b/src/pages/primary/NoteListPage/index.tsx
@@ -10,7 +10,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import type { TNoteListRef } from '@/components/NoteList'
import { NoteCardLoadingSkeleton } from '@/components/NoteCard'
import { TPageRef } from '@/types'
-import { Compass, Info, UsersRound } from 'lucide-react'
+import { Compass, Info, Star, UsersRound } from 'lucide-react'
import React, {
Dispatch,
forwardRef,
@@ -219,10 +219,14 @@ function NoteListPageTitlebar({
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
- const { navigate, current, display } = usePrimaryPage()
+ const { navigate, current, currentPageProps, display } = usePrimaryPage()
const { primaryViewType, setPrimaryNoteView } = usePrimaryNoteView()
+ const { pubkey } = useNostr()
+ const spell = (currentPageProps as { spell?: string } | undefined)?.spell
const exploreActive = display && current === 'explore' && primaryViewType === null
const followsLatestActive = display && current === 'follows-latest' && primaryViewType === null
+ const favoritesActive =
+ display && current === 'spells' && spell === 'favorites' && primaryViewType === null
return (
@@ -246,22 +250,42 @@ function NoteListPageTitlebar({
>
-
+ {pubkey ? (
+ <>
+
+
+ >
+ ) : null}
>
)}
diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
index 29ac38d2..9787510e 100644
--- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
+++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
@@ -48,7 +48,7 @@ export function applyFauxSpellCapsToSubRequests(requests: TFeedSubRequest[]): TF
if (Array.isArray(f.ids) && f.ids.length > FAUX_SPELL_EVENT_LIMIT) {
f.ids = f.ids.slice(0, FAUX_SPELL_EVENT_LIMIT)
}
- return { urls, filter: f }
+ return { ...r, urls, filter: f }
})
}
@@ -68,9 +68,9 @@ export const NOTIFICATION_SPELL_LOADING_SAFETY_MS = 90_000
const INTERESTS_MAX_TOPICS = 80
/**
- * Max distinct `t` tag values in one filter after singular/plural expansion.
+ * Max distinct `t` tag values in one filter after case + singular/plural expansion.
*/
-const INTERESTS_MAX_TOPIC_TAG_VALUES = INTERESTS_MAX_TOPICS * 2
+const INTERESTS_MAX_TOPIC_TAG_VALUES = INTERESTS_MAX_TOPICS * 4
/**
* Put {@link READ_ONLY_RELAY_URLS} (e.g. aggr) **first**, then curated relays. Faux spells cap URL count
@@ -173,6 +173,10 @@ function pluralizeTopic(topic: string): string {
return `${topic}s`
}
+function canonicalizeRawTopicTagValue(topic: string): string {
+ return topic.trim().replace(/^#+/u, '').replace(/\s+/g, '-')
+}
+
/**
* One subrequest for all interests: NIP-01 treats multiple `#t` values as OR (any topic matches).
* Expand every topic to singular+plural so feeds match either spelling on relays.
@@ -183,19 +187,21 @@ export function buildInterestsSubRequests(
kindsList: number[] = DEFAULT_FEED_SHOW_KINDS
): TFeedSubRequest[] {
if (!relayUrls.length || !rawTopics.length || !kindsList.length) return []
- const baseTopics = Array.from(
+ const normalizedBaseTopics = Array.from(
new Set(rawTopics.map((t) => normalizeTopic(t)).filter((t) => t.length > 0))
).slice(0, INTERESTS_MAX_TOPICS)
- if (!baseTopics.length) return []
- const topics = Array.from(
- new Set(
- baseTopics.flatMap((topic) => {
- const singular = normalizeTopic(topic)
- const plural = pluralizeTopic(singular)
- return [singular, plural]
- })
- )
- ).slice(0, INTERESTS_MAX_TOPIC_TAG_VALUES)
+ const rawCasedTopics = Array.from(
+ new Set(rawTopics.map((t) => canonicalizeRawTopicTagValue(t)).filter((t) => t.length > 0))
+ ).slice(0, INTERESTS_MAX_TOPICS)
+ if (!normalizedBaseTopics.length && !rawCasedTopics.length) return []
+ const topics = Array.from(new Set([
+ ...normalizedBaseTopics.flatMap((topic) => {
+ const singular = normalizeTopic(topic)
+ const plural = pluralizeTopic(singular)
+ return [singular, plural]
+ }),
+ ...rawCasedTopics.flatMap((topic) => [topic, pluralizeTopic(topic)])
+ ])).slice(0, INTERESTS_MAX_TOPIC_TAG_VALUES)
if (!topics.length) return []
return [
{
diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx
index 29721a3d..fa21311c 100644
--- a/src/pages/primary/SpellsPage/index.tsx
+++ b/src/pages/primary/SpellsPage/index.tsx
@@ -101,7 +101,6 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRe
import { useTranslation } from 'react-i18next'
import CreateSpellDialog from './CreateSpellDialog'
import {
- appendCuratedReadOnlyRelays,
applyFauxSpellCapsToSubRequests,
buildBookmarksSubRequests,
buildWebBookmarksSpellSubRequests,
@@ -280,6 +279,8 @@ function fauxSpellLabelKey(name: FauxSpellName): string {
return 'Discussions'
case 'following':
return 'Following'
+ case 'favorites':
+ return 'Favorites'
case 'followPacks':
return 'Follow Packs'
case 'media':
@@ -299,6 +300,7 @@ const FAUX_SPELL_ICON: Record = {
notifications: Bell,
discussions: MessageSquare,
following: Users,
+ favorites: Star,
followPacks: Gift,
media: ImageIcon,
interests: Hash,
@@ -398,6 +400,8 @@ const SpellsPage = forwardRef(function SpellsPage(
const [followingSubRequests, setFollowingSubRequests] = useState([])
const [followingFeedLoading, setFollowingFeedLoading] = useState(false)
+ const [favoritesSubRequests, setFavoritesSubRequests] = useState([])
+ const [favoritesFeedLoading, setFavoritesFeedLoading] = useState(false)
const loadSpells = useCallback(async () => {
const [events, ids] = await Promise.all([
@@ -482,13 +486,12 @@ const SpellsPage = forwardRef(function SpellsPage(
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
)
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- if (!urls.length) {
+ if (!feedUrls.length) {
if (!cancelled) setFollowSetListEvents([])
return
}
const events = await queryService.fetchEvents(
- urls,
+ feedUrls,
{ authors: [pubkey], kinds: [ExtendedKind.FOLLOW_SET], limit: 500 },
{ eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false }
)
@@ -774,11 +777,7 @@ const SpellsPage = forwardRef(function SpellsPage(
relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] }
)
- const withReadOnly = merged.map((r) => ({
- ...r,
- urls: appendCuratedReadOnlyRelays(r.urls, blockedRelays)
- }))
- if (!cancelled) setFollowingSubRequests(withReadOnly)
+ if (!cancelled) setFollowingSubRequests(merged)
} catch {
if (!cancelled) setFollowingSubRequests([])
} finally {
@@ -798,6 +797,114 @@ const SpellsPage = forwardRef(function SpellsPage(
followSetListStableKey
])
+ const favoritesShowKinds = useMemo(() => {
+ const out = [...kindFilterShowKinds]
+ if (!out.includes(nostrKinds.Repost)) out.push(nostrKinds.Repost)
+ if (!out.includes(ExtendedKind.GENERIC_REPOST)) out.push(ExtendedKind.GENERIC_REPOST)
+ if (!out.includes(ExtendedKind.WEB_BOOKMARK)) out.push(ExtendedKind.WEB_BOOKMARK)
+ return out.sort((a, b) => a - b)
+ }, [kindFilterShowKinds])
+
+ const favoritesShowKindsKey = useMemo(() => JSON.stringify(favoritesShowKinds), [favoritesShowKinds])
+
+ useEffect(() => {
+ if (selectedFauxSpell !== 'favorites' || !pubkey) {
+ setFavoritesSubRequests([])
+ setFavoritesFeedLoading(false)
+ return
+ }
+
+ let cancelled = false
+ setFavoritesFeedLoading(true)
+ void (async () => {
+ try {
+ const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
+ favoriteRelays,
+ blockedRelays,
+ relayList?.read ?? [],
+ {
+ userWriteRelays: relayList?.write ?? [],
+ applySocialKindBlockedFilter: false
+ }
+ )
+ const topics = interestListEvent?.tags.filter((tag) => tag[0] === 't' && tag[1]).map((tag) => tag[1]!) ?? []
+ const interestReqs = buildInterestsSubRequests(feedUrls, topics, favoritesShowKinds).map((r) => ({
+ ...r,
+ reasonLabel: t('Added from interests')
+ }))
+ const idReqs = buildBookmarksSubRequests(bookmarkListEvent, feedUrls).map((r) => ({
+ ...r,
+ reasonLabel: t('Added from bookmarks list')
+ }))
+ const ownWebReqs = buildWebBookmarksSpellSubRequests(pubkey, feedUrls).map((r) => ({
+ ...r,
+ reasonLabel: t('Added from your web bookmarks')
+ }))
+
+ const authorSet = new Set([pubkey, ...contacts])
+ for (const ev of followSetListEvents) {
+ if (ev.pubkey !== pubkey) continue
+ for (const author of pubkeysFromFollowSetEvent(ev)) authorSet.add(author)
+ }
+
+ const authorPubkeys = [...authorSet]
+ const followAndContactReqs = authorPubkeys.length
+ ? await client.generateSubRequestsForPubkeys(authorPubkeys, pubkey)
+ : []
+ const followAndContactAugmented = augmentSubRequestsWithFavoritesFastReadAndInbox(
+ followAndContactReqs,
+ favoriteRelays,
+ blockedRelays,
+ relayList?.read ?? [],
+ { userWriteRelays: relayList?.write ?? [] }
+ ).map((r) => ({ ...r, reasonLabel: t('Added from follows and contact lists') }))
+
+ const followsWebBookmarkReqs: TFeedSubRequest[] = authorPubkeys.length
+ ? [
+ {
+ urls: feedUrls,
+ filter: {
+ authors: authorPubkeys,
+ kinds: [ExtendedKind.WEB_BOOKMARK],
+ limit: FAUX_SPELL_EVENT_LIMIT
+ },
+ reasonLabel: t('Added from follows web bookmarks')
+ }
+ ]
+ : []
+
+ if (!cancelled) {
+ setFavoritesSubRequests([
+ ...interestReqs,
+ ...idReqs,
+ ...ownWebReqs,
+ ...followsWebBookmarkReqs,
+ ...followAndContactAugmented
+ ])
+ }
+ } catch {
+ if (!cancelled) setFavoritesSubRequests([])
+ } finally {
+ if (!cancelled) setFavoritesFeedLoading(false)
+ }
+ })()
+
+ return () => {
+ cancelled = true
+ }
+ }, [
+ selectedFauxSpell,
+ pubkey,
+ contactsSyncKey,
+ followSetListStableKey,
+ sortedFavoriteRelaysKey,
+ sortedBlockedRelaysKey,
+ relayMailboxStableKey,
+ interestListEvent?.id,
+ bookmarkListEvent?.id,
+ favoritesShowKindsKey
+ ])
+
const interestTagsStableKey = interestListEvent
? JSON.stringify(
[...interestListEvent.tags].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
@@ -822,7 +929,7 @@ const SpellsPage = forwardRef(function SpellsPage(
].join('\0')
const syncFauxSubRequests = useMemo(() => {
- if (!selectedFauxSpell || isFollowFeedFauxSpellId(selectedFauxSpell)) return []
+ if (!selectedFauxSpell || isFollowFeedFauxSpellId(selectedFauxSpell) || selectedFauxSpell === 'favorites') return []
/** Widen relay pool: these faux spells do not target social kinds (1 / 11 / 1111); skipping strip keeps fast-read mirrors in the stack. */
const fauxSpellSkipSocialKindBlocked =
selectedFauxSpell === 'calendar' ||
@@ -845,40 +952,33 @@ const SpellsPage = forwardRef(function SpellsPage(
return buildNotificationsSpellSubRequests(feedUrls, notificationsFeedPubkey)
}
if (selectedFauxSpell === 'discussions') {
- // Read-only prepended in appendCuratedReadOnlyRelays so FAUX_SPELL_MAX_RELAYS still includes aggr.
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- if (!urls.length) return []
- return [{ urls, filter: buildDiscussionFilter() }]
+ if (!feedUrls.length) return []
+ return [{ urls: feedUrls, filter: buildDiscussionFilter() }]
}
if (selectedFauxSpell === 'media') {
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- if (!urls.length) return []
- return [{ urls, filter: buildMediaSpellFilter() }]
+ if (!feedUrls.length) return []
+ return [{ urls: feedUrls, filter: buildMediaSpellFilter() }]
}
if (selectedFauxSpell === 'calendar') {
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- if (!urls.length) return []
- return [{ urls, filter: buildCalendarSpellFilter() }]
+ if (!feedUrls.length) return []
+ return [{ urls: feedUrls, filter: buildCalendarSpellFilter() }]
}
if (selectedFauxSpell === 'interests') {
if (!pubkey || !interestListEvent) return []
const topics = interestListEvent.tags.filter((tag) => tag[0] === 't' && tag[1]).map((tag) => tag[1]!)
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- return buildInterestsSubRequests(urls, topics, DEFAULT_FEED_SHOW_KINDS)
+ return buildInterestsSubRequests(feedUrls, topics, DEFAULT_FEED_SHOW_KINDS)
}
if (selectedFauxSpell === 'bookmarks') {
if (!pubkey) return []
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- const idReqs = buildBookmarksSubRequests(bookmarkListEvent, urls)
- const webReqs = buildWebBookmarksSpellSubRequests(pubkey, urls)
+ const idReqs = buildBookmarksSubRequests(bookmarkListEvent, feedUrls)
+ const webReqs = buildWebBookmarksSpellSubRequests(pubkey, feedUrls)
return [...idReqs, ...webReqs]
}
if (selectedFauxSpell === 'followPacks') {
- const urls = appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
- if (!urls.length) return []
+ if (!feedUrls.length) return []
return [
{
- urls,
+ urls: feedUrls,
filter: { kinds: [ExtendedKind.FOLLOW_PACK], limit: FAUX_SPELL_EVENT_LIMIT }
}
]
@@ -887,11 +987,14 @@ const SpellsPage = forwardRef(function SpellsPage(
}, [selectedFauxSpell, pubkey, notificationsFeedPubkey, fauxFeedRelaysDepsKey, relayMailboxStableKey])
const fauxSubRequests = useMemo(() => {
- const base = isFollowFeedFauxSpellId(selectedFauxSpell ?? '')
- ? followingSubRequests
- : syncFauxSubRequests
+ const base =
+ selectedFauxSpell === 'favorites'
+ ? favoritesSubRequests
+ : isFollowFeedFauxSpellId(selectedFauxSpell ?? '')
+ ? followingSubRequests
+ : syncFauxSubRequests
return applyFauxSpellCapsToSubRequests(base)
- }, [selectedFauxSpell, followingSubRequests, syncFauxSubRequests])
+ }, [selectedFauxSpell, favoritesSubRequests, followingSubRequests, syncFauxSubRequests])
const spellSubRequests = useMemo(() => {
if (!selectedSpell) return []
@@ -1097,6 +1200,9 @@ const SpellsPage = forwardRef(function SpellsPage(
if (selectedFauxSpell === 'interests') {
return [...DEFAULT_FEED_SHOW_KINDS]
}
+ if (selectedFauxSpell === 'favorites') {
+ return favoritesShowKinds
+ }
if (selectedFauxSpell === 'bookmarks') {
const out = [...DEFAULT_FEED_SHOW_KINDS]
if (!out.includes(ExtendedKind.WEB_BOOKMARK)) out.push(ExtendedKind.WEB_BOOKMARK)
@@ -1108,7 +1214,7 @@ const SpellsPage = forwardRef(function SpellsPage(
.map((tag) => parseInt(tag[1], 10))
.filter((n) => !Number.isNaN(n))
return kinds.length ? kinds : [1]
- }, [selectedFauxSpell, selectedSpell?.id, showKindsTagKey, followingShowKindsKey])
+ }, [selectedFauxSpell, selectedSpell?.id, showKindsTagKey, followingShowKindsKey, favoritesShowKindsKey])
const spellMenuLabel = useCallback(
(spell: Event) =>
@@ -1225,17 +1331,19 @@ const SpellsPage = forwardRef(function SpellsPage(
if (selectedFauxSpell === 'interests') return t('No subscribed interests yet.')
if (selectedFauxSpell === 'bookmarks')
return t('No NIP-51 bookmarks or web bookmarks yet.')
+ if (selectedFauxSpell === 'favorites') return t('No favorites yet.')
if (selectedFauxSpell === 'following') return t('No follows or relays to load yet.')
if (isFollowSetSpellId(selectedFauxSpell)) return t('Follow set feed empty')
return t('Nothing to load for this feed.')
}, [selectedFauxSpell, fauxSubRequests.length, t])
- const showFollowFeedLoading = !!(
+ const showAsyncFauxFeedLoading = !!(
pubkey &&
selectedFauxSpell &&
- isFollowFeedFauxSpellId(selectedFauxSpell) &&
- (followingFeedLoading ||
- (isFollowSetSpellId(selectedFauxSpell) && followSetCatalogLoading))
+ (selectedFauxSpell === 'favorites'
+ ? favoritesFeedLoading
+ : isFollowFeedFauxSpellId(selectedFauxSpell) &&
+ (followingFeedLoading || (isFollowSetSpellId(selectedFauxSpell) && followSetCatalogLoading)))
)
const spellStarAddTitle = t('Spell star add title')
@@ -1269,6 +1377,7 @@ const SpellsPage = forwardRef(function SpellsPage(
if (
(name === 'notifications' ||
name === 'following' ||
+ name === 'favorites' ||
name === 'bookmarks' ||
name === 'interests') &&
!pubkey
@@ -1675,7 +1784,11 @@ const SpellsPage = forwardRef(function SpellsPage(
{t('Please login to view bookmarks')}
- ) : showFollowFeedLoading ? (
+ ) : selectedFauxSpell === 'favorites' && !pubkey ? (
+
+ {t('Please login to view favorites')}
+
+ ) : showAsyncFauxFeedLoading ? (
{t('loading...')}
) : selectedFauxSpell && fauxSubRequests.length === 0 ? (
{fauxFeedEmptyMessage}
@@ -1706,7 +1819,9 @@ const SpellsPage = forwardRef(function SpellsPage(
: undefined
}
clientSideKindFilter={
- selectedFauxSpell === 'notifications' || selectedFauxSpell === 'bookmarks'
+ selectedFauxSpell === 'notifications' ||
+ selectedFauxSpell === 'bookmarks' ||
+ selectedFauxSpell === 'favorites'
}
useFilterAsIs={fauxNoteListUseFilterAsIs}
oneShotFetch={false}
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index 6572a31e..b538abf4 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -6,6 +6,8 @@ export type TSubRequestFilter = Omit & { limit: numbe
export type TFeedSubRequest = {
urls: string[]
filter: Omit
+ /** Optional UI hint used by feed UIs (e.g. Favorites) to explain why an event was included. */
+ reasonLabel?: string
}
export type TProfile = {