-
- {t('Loading more…')}
+
+
) : null}
diff --git a/src/components/MailboxSetting/DiscoveredRelays.tsx b/src/components/MailboxSetting/DiscoveredRelays.tsx
index 6c0c1fd2..60352169 100644
--- a/src/components/MailboxSetting/DiscoveredRelays.tsx
+++ b/src/components/MailboxSetting/DiscoveredRelays.tsx
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import { Checkbox } from '@/components/ui/checkbox'
import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url'
import { getRelaysFromNip07Extension, verifyNip05 } from '@/lib/nip05'
@@ -158,9 +159,11 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
return (
{t('Discovered Relays')}
-
-
- {t('Discovering relays...')}
+
+
{t('Discovering relays...')}
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
)
@@ -223,7 +226,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
>
{isAdding ? (
<>
-
+
{t('Adding...')}
>
) : (
diff --git a/src/components/MailboxSetting/SaveButton.tsx b/src/components/MailboxSetting/SaveButton.tsx
index 69793a7c..a5b83d1b 100644
--- a/src/components/MailboxSetting/SaveButton.tsx
+++ b/src/components/MailboxSetting/SaveButton.tsx
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import { createRelayListDraftEvent } from '@/lib/draft-event'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import { useNostr } from '@/providers/NostrProvider'
@@ -73,7 +74,7 @@ export default function SaveButton({
return (
)
diff --git a/src/components/MuteButton/index.tsx b/src/components/MuteButton/index.tsx
index 5a8f86b8..9c8af6b2 100644
--- a/src/components/MuteButton/index.tsx
+++ b/src/components/MuteButton/index.tsx
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import {
DropdownMenu,
@@ -69,7 +70,11 @@ export default function MuteButton({ pubkey }: { pubkey: string }) {
onClick={handleUnmute}
disabled={updating || changing}
>
- {updating ?
:
{t('Unmute')}}
+ {updating ? (
+
+ ) : (
+
{t('Unmute')}
+ )}
)
}
@@ -80,7 +85,7 @@ export default function MuteButton({ pubkey }: { pubkey: string }) {
className="w-20 min-w-20 rounded-full"
disabled={updating || changing}
>
- {updating ?
: t('Mute')}
+ {updating ?
: t('Mute')}
)
@@ -96,7 +101,7 @@ export default function MuteButton({ pubkey }: { pubkey: string }) {
onClick={(e) => handleMute(e, true)}
disabled={updating || changing}
>
- {updating ?
: t('Mute user privately')}
+ {updating ?
: t('Mute user privately')}
diff --git a/src/components/Note/Poll.tsx b/src/components/Note/Poll.tsx
index 792e4741..e30c6249 100644
--- a/src/components/Note/Poll.tsx
+++ b/src/components/Note/Poll.tsx
@@ -7,7 +7,8 @@ import { cn, isPartiallyInViewport } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import pollResultsService from '@/services/poll-results.service'
import dayjs from 'dayjs'
-import { CheckCircle2, Loader2 } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
+import { CheckCircle2 } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -253,7 +254,7 @@ export default function Poll({ event, className }: { event: Event; className?: s
disabled={!selectedOptionIds.length || isVoting}
className="w-full"
>
- {isVoting &&
}
+ {isVoting &&
}
{t('Vote')}
)}
diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx
index 0affb438..f38ec15a 100644
--- a/src/components/NoteList/index.tsx
+++ b/src/components/NoteList/index.tsx
@@ -41,7 +41,6 @@ import PullToRefresh from 'react-simple-pull-to-refresh'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext'
import type { TProfile } from '@/types'
-import { Loader2 } from 'lucide-react'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
const LIMIT = 100 // Increased from 200 to load more events per request
@@ -101,7 +100,11 @@ const NoteList = forwardRef(
* {@link client.subscribeTimeline}. No live stream or `loadMore` timeline pagination; use for faux spells
* (except Following). Refresh re-fetches.
*/
- oneShotFetch = false
+ oneShotFetch = false,
+ /** Max events kept after merging one-shot REQ batches (default 100). */
+ oneShotMergedCap,
+ /** Initial visible rows and each “reveal more” step when scrolling cached events (default first {@link SHOW_COUNT}, then 2× per step). */
+ revealBatchSize
}: {
subRequests: TFeedSubRequest[]
showKinds: number[]
@@ -124,6 +127,8 @@ const NoteList = forwardRef(
spellFeedInstrumentToken?: number
onSpellFeedFirstPaint?: (detail: { eventCount: number; firstEventId: string }) => void
oneShotFetch?: boolean
+ oneShotMergedCap?: number
+ revealBatchSize?: number
},
ref
) => {
@@ -484,6 +489,7 @@ const NoteList = forwardRef(
if (!keepExistingTimelineEvents) {
setEvents([])
setNewEvents([])
+ setShowCount(revealBatchSize ?? SHOW_COUNT)
}
setHasMore(true)
consecutiveEmptyRef.current = 0 // Reset counter on refresh
@@ -549,9 +555,10 @@ const NoteList = forwardRef(
byId.set(ev.id, ev)
}
}
+ const cap = oneShotMergedCap ?? ONE_SHOT_MERGED_CAP
const merged = [...byId.values()]
.sort((a, b) => b.created_at - a.created_at)
- .slice(0, ONE_SHOT_MERGED_CAP)
+ .slice(0, cap)
setEvents(merged)
lastEventsForTimelinePrefetchRef.current = merged
} catch {
@@ -734,7 +741,9 @@ const NoteList = forwardRef(
showKind1111,
useFilterAsIs,
areAlgoRelays,
- oneShotFetch
+ oneShotFetch,
+ oneShotMergedCap,
+ revealBatchSize
])
useEffect(() => {
@@ -803,9 +812,9 @@ const NoteList = forwardRef(
// Show more events immediately if we have them cached
if (currentShowCount < currentEvents.length) {
- // Show more aggressively: increase by SHOW_COUNT, but also check if we should show even more
const remaining = currentEvents.length - currentShowCount
- const increment = Math.min(SHOW_COUNT * 2, remaining) // Show up to 2x SHOW_COUNT if available
+ const step = revealBatchSize ?? SHOW_COUNT * 2
+ const increment = Math.min(step, remaining)
setShowCount((prev) => prev + increment)
// Only preload more if we have plenty cached (more than 3/4 of LIMIT)
// BUT: Always try to load more if we have very few events (might be due to filtering)
@@ -819,7 +828,8 @@ const NoteList = forwardRef(
}
}
- if (!currentTimelineKey || currentLoading || !currentHasMore) return
+ const canLoadFromTimeline = !!currentTimelineKey && currentHasMore
+ if (currentLoading || (!canLoadFromTimeline && currentShowCount >= currentEvents.length)) return
// Schedule loadMore with a small delay to throttle rapid calls
loadMoreTimeoutRef.current = setTimeout(async () => {
@@ -948,8 +958,10 @@ const NoteList = forwardRef(
}
const observerInstance = new IntersectionObserver((entries) => {
- if (entries[0].isIntersecting && hasMoreRef.current && !loadingRef.current) {
- // Throttle: only trigger if not already loading and not already scheduled
+ if (!entries[0].isIntersecting || loadingRef.current) return
+ const ev = eventsRef.current
+ const sc = showCountRef.current
+ if (sc < ev.length || hasMoreRef.current) {
loadMore()
}
}, options)
@@ -1120,13 +1132,14 @@ const NoteList = forwardRef(
{events.length === 0 && loading ? (
-
-
{t('Loading...')}
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
) : events.length > 0 && (hasMore || loading) ? (
diff --git a/src/components/NoteOptions/ReportDialog.tsx b/src/components/NoteOptions/ReportDialog.tsx
index 0194f6f2..dbcc9bf4 100644
--- a/src/components/NoteOptions/ReportDialog.tsx
+++ b/src/components/NoteOptions/ReportDialog.tsx
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import {
Dialog,
DialogContent,
@@ -18,7 +19,6 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { createReportDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
-import { Loader } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -137,7 +137,7 @@ function ReportContent({ event, closeDialog }: { event: NostrEvent; closeDialog:
handleReport()
}}
>
- {reporting && }
+ {reporting && }
{t('Report')}
diff --git a/src/components/NoteStats/LikeButton.tsx b/src/components/NoteStats/LikeButton.tsx
index 536537d6..08d4695c 100644
--- a/src/components/NoteStats/LikeButton.tsx
+++ b/src/components/NoteStats/LikeButton.tsx
@@ -4,6 +4,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
+import { Skeleton } from '@/components/ui/skeleton'
import { ExtendedKind } from '@/constants'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { shouldHideInteractions } from '@/lib/event-filtering'
@@ -15,7 +16,7 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
import { eventService } from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import { TEmoji } from '@/types'
-import { Loader, SmilePlus } from 'lucide-react'
+import { SmilePlus } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import logger from '@/lib/logger'
@@ -189,7 +190,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
}}
>
{liking ? (
-
+
) : myLastEmoji ? (
<>
@@ -224,7 +225,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
}}
>
{liking && index === 0 ? (
-
+
) : (
<>
{emoji}
diff --git a/src/components/NoteStats/Likes.tsx b/src/components/NoteStats/Likes.tsx
index 6109000a..5d87984e 100644
--- a/src/components/NoteStats/Likes.tsx
+++ b/src/components/NoteStats/Likes.tsx
@@ -1,4 +1,5 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
+import { Skeleton } from '@/components/ui/skeleton'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { ExtendedKind } from '@/constants'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
@@ -175,7 +176,7 @@ export default function Likes({ event }: { event: Event }) {
)}
{liking === key ? (
-
+
) : (
- {reposting ?
:
}
+ {reposting ?
:
}
{!hideCount && !!repostCount &&
{formatCount(repostCount)}
}
)
diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx
index 3f76ef32..eb239b23 100644
--- a/src/components/NoteStats/ZapButton.tsx
+++ b/src/components/NoteStats/ZapButton.tsx
@@ -1,3 +1,4 @@
+import { Skeleton } from '@/components/ui/skeleton'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { getLightningAddressFromProfile } from '@/lib/lightning'
import { cn } from '@/lib/utils'
@@ -8,7 +9,7 @@ import { getProfileFromEvent } from '@/lib/event-metadata'
import { kinds } from 'nostr-tools'
import lightning from '@/services/lightning.service'
import noteStatsService from '@/services/note-stats.service'
-import { Loader, Zap } from 'lucide-react'
+import { Zap } from 'lucide-react'
import { Event } from 'nostr-tools'
import { MouseEvent, TouchEvent, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -146,7 +147,7 @@ export default function ZapButton({ event, hideCount = false }: { event: Event;
onTouchEnd={handleClickEnd}
>
{zapping ? (
-
+
) : (
)}
diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx
index d1b8de05..1f6c679d 100644
--- a/src/components/PostEditor/PostContent.tsx
+++ b/src/components/PostEditor/PostContent.tsx
@@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
+import { Skeleton } from '@/components/ui/skeleton'
import {
DropdownMenu,
DropdownMenuContent,
@@ -45,7 +46,6 @@ import { TPollCreateData } from '@/types'
import {
ImageUp,
ListTodo,
- LoaderCircle,
MessageCircle,
MessagesSquare,
Settings,
@@ -2346,7 +2346,9 @@ export default function PostContent({
{t('Cancel')}
@@ -2384,7 +2386,9 @@ export default function PostContent({
{t('Cancel')}
diff --git a/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx b/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx
index 84dc458a..a815af2d 100644
--- a/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx
+++ b/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx
@@ -17,7 +17,8 @@ import { SimpleUsername } from '@/components/Username'
import { nip19, type Event as NEvent } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { Loader2, Search } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
+import { Search } from 'lucide-react'
import type { Editor } from '@tiptap/core'
import { OPEN_NEVENT_PICKER_EVENT, extendMentionRangeToEndOfWord } from './suggestion'
@@ -136,8 +137,10 @@ export function NeventNaddrPickerDialog({
{loading && (
-
-
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
)}
{!loading && debouncedQuery && events.length === 0 && (
diff --git a/src/components/Profile/Followings.tsx b/src/components/Profile/Followings.tsx
index 07dca48b..ea28fadb 100644
--- a/src/components/Profile/Followings.tsx
+++ b/src/components/Profile/Followings.tsx
@@ -3,7 +3,7 @@ import { toFollowingList } from '@/lib/link'
import { SecondaryPageLink } from '@/PageManager'
import { useFollowList } from '@/providers/FollowListProvider'
import { useNostr } from '@/providers/NostrProvider'
-import { Loader } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
import { useTranslation } from 'react-i18next'
export default function Followings({ pubkey }: { pubkey: string }) {
@@ -20,7 +20,7 @@ export default function Followings({ pubkey }: { pubkey: string }) {
{accountPubkey === pubkey ? (
selfFollowings.length
) : isFetching ? (
-
+
) : (
followings.length
)}
diff --git a/src/components/Profile/ProfileFeedWithPins.tsx b/src/components/Profile/ProfileFeedWithPins.tsx
index 104c87b2..8eea4270 100644
--- a/src/components/Profile/ProfileFeedWithPins.tsx
+++ b/src/components/Profile/ProfileFeedWithPins.tsx
@@ -117,7 +117,18 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
const mergedDisplay = useMemo(() => [...filteredPins, ...filteredRest], [filteredPins, filteredRest])
- const pinnedDisplayIds = useMemo(() => new Set(filteredPins.map((e) => e.id)), [filteredPins])
+ /** Pins always occupy the top of the profile; `showCount` caps total visible rows (pins + posts). */
+ const displayedPins = useMemo(() => {
+ if (filteredPins.length <= showCount) return filteredPins
+ return filteredPins.slice(0, showCount)
+ }, [filteredPins, showCount])
+
+ const displayedFeed = useMemo(
+ () => filteredRest.slice(0, Math.max(0, showCount - displayedPins.length)),
+ [filteredRest, showCount, displayedPins.length]
+ )
+
+ const totalVisible = displayedPins.length + displayedFeed.length
useEffect(() => {
setShowCount(INITIAL_SHOW_COUNT)
@@ -138,16 +149,11 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
useImperativeHandle(ref, () => ({ refresh: refreshAll }), [refreshAll])
- const displayedEvents = useMemo(
- () => mergedDisplay.slice(0, showCount),
- [mergedDisplay, showCount]
- )
-
useEffect(() => {
- if (!bottomRef.current || displayedEvents.length >= mergedDisplay.length) return
+ if (!bottomRef.current || totalVisible >= mergedDisplay.length) return
const observer = new IntersectionObserver(
(entries) => {
- if (entries[0]?.isIntersecting && displayedEvents.length < mergedDisplay.length) {
+ if (entries[0]?.isIntersecting && totalVisible < mergedDisplay.length) {
setShowCount((prev) => Math.min(prev + LOAD_MORE_COUNT, mergedDisplay.length))
}
},
@@ -155,7 +161,7 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
)
observer.observe(bottomRef.current)
return () => observer.disconnect()
- }, [displayedEvents.length, mergedDisplay.length])
+ }, [totalVisible, mergedDisplay.length])
const loading = (loadingPins || loadingTimeline) && mergedDisplay.length === 0
@@ -210,29 +216,45 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
{searchQuery.trim() && (
{t('Showing {{filtered}} of {{total}} items', {
- filtered: displayedEvents.length,
+ filtered: totalVisible,
total: mergedDisplay.length
})}
)}
- {displayedEvents.map((event, index) => (
-
- {index === filteredPins.length && filteredPins.length > 0 && filteredRest.length > 0 && (
-
- {t('Feed')}
-
- )}
-
+ {displayedPins.length > 0 && (
+
+ {displayedPins.map((event) => (
+
+ ))}
+
+ )}
+ {displayedPins.length > 0 && displayedFeed.length > 0 && (
+
+ {t('Feed')}
+
+ )}
+ {displayedFeed.length > 0 && (
+
+ {displayedFeed.map((event) => (
+
+ ))}
- ))}
+ )}
- {displayedEvents.length < mergedDisplay.length && (
+ {totalVisible < mergedDisplay.length && (
diff --git a/src/components/Profile/ProfileMediaFeed.tsx b/src/components/Profile/ProfileMediaFeed.tsx
new file mode 100644
index 00000000..310899d2
--- /dev/null
+++ b/src/components/Profile/ProfileMediaFeed.tsx
@@ -0,0 +1,128 @@
+import NoteList, { type TNoteListRef } from '@/components/NoteList'
+import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
+import { computeSpellSubRequestsIdentityKey } from '@/lib/spell-feed-request-identity'
+import {
+ applyFauxSpellCapsToSubRequests,
+ appendCuratedReadOnlyRelays,
+ buildProfileMediaSpellFilter,
+ MEDIA_SPELL_KINDS,
+ PROFILE_MEDIA_REQ_LIMIT
+} from '@/pages/primary/SpellsPage/fauxSpellFeeds'
+import { normalizeUrl } from '@/lib/url'
+import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
+import client from '@/services/client.service'
+import { NoteCardLoadingSkeleton } from '@/components/NoteCard'
+import { forwardRef, useEffect, useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string {
+ const fav = [...favoriteRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
+ const blk = [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
+ return `${fav}\u0000${blk}`
+}
+
+const ProfileMediaFeed = forwardRef
(({ pubkey }, ref) => {
+ const { t } = useTranslation()
+ const { favoriteRelays, blockedRelays } = useFavoriteRelays()
+ const relayListsKey = useMemo(
+ () => relayListsContentKey(favoriteRelays, blockedRelays),
+ [favoriteRelays, blockedRelays]
+ )
+
+ /** `null` = still resolving viewed profile NIP-65 + merged relay stack (same as pins / main profile feed). */
+ const [profileRelayUrls, setProfileRelayUrls] = useState(null)
+
+ useEffect(() => {
+ const pk = pubkey?.trim()
+ if (!pk) {
+ setProfileRelayUrls([])
+ return
+ }
+ let cancelled = false
+ setProfileRelayUrls(null)
+ void (async () => {
+ const authorRl = await client.fetchRelayList(pk).catch(() => ({
+ read: [] as string[],
+ write: [] as string[]
+ }))
+ if (cancelled) return
+ setProfileRelayUrls(
+ buildProfilePageReadRelayUrls(favoriteRelays, blockedRelays, authorRl, false)
+ )
+ })()
+ return () => {
+ cancelled = true
+ }
+ }, [pubkey, relayListsKey, favoriteRelays, blockedRelays])
+
+ const subRequests = useMemo(() => {
+ const pk = pubkey?.trim()
+ if (!pk || profileRelayUrls === null) return []
+ const urls = appendCuratedReadOnlyRelays(profileRelayUrls, blockedRelays)
+ if (!urls.length) return []
+ return applyFauxSpellCapsToSubRequests([
+ { urls, filter: buildProfileMediaSpellFilter(pk) }
+ ])
+ }, [pubkey, profileRelayUrls, blockedRelays])
+
+ const feedSubscriptionKey = useMemo(
+ () => computeSpellSubRequestsIdentityKey(subRequests),
+ [subRequests]
+ )
+
+ const showKinds = useMemo(() => [...MEDIA_SPELL_KINDS], [])
+
+ if (!pubkey?.trim()) {
+ return (
+
+ {t('Nothing to load for this feed.')}
+
+ )
+ }
+
+ if (profileRelayUrls === null) {
+ return (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ )
+ }
+
+ if (!subRequests.length) {
+ return (
+
+ {t('Nothing to load for this feed.')}
+
+ )
+ }
+
+ return (
+
+
+
+ )
+})
+
+ProfileMediaFeed.displayName = 'ProfileMediaFeed'
+
+export default ProfileMediaFeed
diff --git a/src/components/Profile/Relays.tsx b/src/components/Profile/Relays.tsx
index d4772a4d..d7dbdbd5 100644
--- a/src/components/Profile/Relays.tsx
+++ b/src/components/Profile/Relays.tsx
@@ -1,8 +1,8 @@
import { useFetchRelayList } from '@/hooks'
import { toOthersRelaySettings, toRelaySettings } from '@/lib/link'
import { SecondaryPageLink } from '@/PageManager'
+import { Skeleton } from '@/components/ui/skeleton'
import { useNostr } from '@/providers/NostrProvider'
-import { Loader } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function Relays({ pubkey }: { pubkey: string }) {
@@ -15,7 +15,7 @@ export default function Relays({ pubkey }: { pubkey: string }) {
to={accountPubkey === pubkey ? toRelaySettings('mailbox') : toOthersRelaySettings(pubkey)}
className="flex gap-1 hover:underline w-fit items-center"
>
- {isFetching ? : relayList.originalRelays.length}
+ {isFetching ? : relayList.originalRelays.length}
{t('Relays')}
)
diff --git a/src/components/Profile/SmartFollowings.tsx b/src/components/Profile/SmartFollowings.tsx
index 73b4ad0f..d255c19c 100644
--- a/src/components/Profile/SmartFollowings.tsx
+++ b/src/components/Profile/SmartFollowings.tsx
@@ -3,7 +3,7 @@ import { toFollowingList } from '@/lib/link'
import { useSmartFollowingListNavigation } from '@/PageManager'
import { useFollowList } from '@/providers/FollowListProvider'
import { useNostr } from '@/providers/NostrProvider'
-import { Loader } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
import { useTranslation } from 'react-i18next'
export default function SmartFollowings({ pubkey }: { pubkey: string }) {
@@ -25,7 +25,7 @@ export default function SmartFollowings({ pubkey }: { pubkey: string }) {
{accountPubkey === pubkey ? (
selfFollowings.length
) : isFetching ? (
-
+
) : (
followings.length
)}
diff --git a/src/components/Profile/SmartRelays.tsx b/src/components/Profile/SmartRelays.tsx
index 30a384e4..67d301fe 100644
--- a/src/components/Profile/SmartRelays.tsx
+++ b/src/components/Profile/SmartRelays.tsx
@@ -1,7 +1,7 @@
import { useFetchRelayList } from '@/hooks'
import { toOthersRelaySettings } from '@/lib/link'
import { useSmartOthersRelaySettingsNavigation } from '@/PageManager'
-import { Loader } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
import { useTranslation } from 'react-i18next'
export default function SmartRelays({ pubkey }: { pubkey: string }) {
@@ -19,7 +19,7 @@ export default function SmartRelays({ pubkey }: { pubkey: string }) {
className="flex gap-1 hover:underline w-fit items-center cursor-pointer"
onClick={handleClick}
>
- {isFetching ? : relayList.originalRelays.length}
+ {isFetching ? : relayList.originalRelays.length}
{t('Relays')}
)
diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx
index 4ff4f4cb..75f3b91a 100644
--- a/src/components/Profile/index.tsx
+++ b/src/components/Profile/index.tsx
@@ -28,13 +28,24 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Copy, Ellipsis, Calendar, MapPin, Pencil, SatelliteDish, Code, Gift, Link } from 'lucide-react'
-import { useEffect, useMemo, useRef, useState, type Ref } from 'react'
+import {
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+ type MutableRefObject,
+ type Ref
+} from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import logger from '@/lib/logger'
import NotFound from '../NotFound'
import FollowedBy from './FollowedBy'
import ProfileFeedWithPins from './ProfileFeedWithPins'
+import ProfileMediaFeed from './ProfileMediaFeed'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import type { TNoteListRef } from '@/components/NoteList'
import SmartFollowings from './SmartFollowings'
import SmartMuteLink from './SmartMuteLink'
import SmartRelays from './SmartRelays'
@@ -166,6 +177,8 @@ export default function Profile({
const { navigate: navigatePrimary } = usePrimaryPage()
const internalFeedRef = useRef<{ refresh: () => void }>(null)
const profileFeedRef = feedRef ?? internalFeedRef
+ const postsFeedRef = useRef<{ refresh: () => void }>(null)
+ const mediaFeedRef = useRef(null)
const { profile, isFetching } = useFetchProfile(id)
const { pubkey: accountPubkey } = useNostr()
@@ -323,6 +336,21 @@ export default function Profile({
})
}
+ useLayoutEffect(() => {
+ const r = profileFeedRef
+ if (typeof r === 'function') return
+ const m = r as MutableRefObject<{ refresh: () => void } | null>
+ m.current = {
+ refresh: () => {
+ postsFeedRef.current?.refresh()
+ mediaFeedRef.current?.refresh()
+ }
+ }
+ return () => {
+ m.current = null
+ }
+ }, [])
+
useEffect(() => {
if (!profile?.pubkey) return
@@ -361,14 +389,7 @@ export default function Profile({
if (!profile) return null // TypeScript guard - should never reach here but satisfies type checker
const { banner, username, about, avatar, pubkey, website, websiteList, nip05List } = profile
-
- logger.component('Profile', 'Profile data loaded', {
- pubkey,
- username,
- hasProfile: !!profile,
- isFetching,
- id
- })
+
return (
<>
@@ -572,7 +593,18 @@ export default function Profile({
-
+
+
+ {t('Posts')}
+ {t('Media')}
+
+
+
+
+
+
+
+
{openPublicMessageTo && (
- {submitting && }
+ {submitting && }
{t('Submit')}
diff --git a/src/components/RssFeedList/index.tsx b/src/components/RssFeedList/index.tsx
index 8385937f..1de159bc 100644
--- a/src/components/RssFeedList/index.tsx
+++ b/src/components/RssFeedList/index.tsx
@@ -4,7 +4,8 @@ import { useNostr } from '@/providers/NostrProvider'
import rssFeedService, { RssFeedItem as TRssFeedItem } from '@/services/rss-feed.service'
import { DEFAULT_RSS_FEEDS } from '@/constants'
import RssFeedItem from '../RssFeedItem'
-import { Loader, AlertCircle, Search, Plus } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
+import { AlertCircle, Search, Plus } from 'lucide-react'
import logger from '@/lib/logger'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
@@ -511,9 +512,11 @@ export default function RssFeedList() {
if (loading) {
return (
-
-
-
{t('Loading RSS feeds...')}
+
+
{t('Loading RSS feeds...')}
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
)
}
@@ -651,9 +654,9 @@ export default function RssFeedList() {
{refreshing && (
-
-
-
{t('Refreshing feeds...')}
+
+
+
)}
@@ -672,8 +675,8 @@ export default function RssFeedList() {
))}
{/* Bottom ref for infinite scroll */}
{displayedItems.length < filteredItems.length && (
-
-
+
+
)}
>
diff --git a/src/components/SaveRelayDropdownMenu/index.tsx b/src/components/SaveRelayDropdownMenu/index.tsx
index 5017a728..8a7fd0fa 100644
--- a/src/components/SaveRelayDropdownMenu/index.tsx
+++ b/src/components/SaveRelayDropdownMenu/index.tsx
@@ -15,12 +15,13 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Separator } from '@/components/ui/separator'
+import { Skeleton } from '@/components/ui/skeleton'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { TRelaySet } from '@/types'
-import { Ban, Check, FolderPlus, Loader2, Plus, Star } from 'lucide-react'
+import { Ban, Check, FolderPlus, Plus, Star } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import DrawerMenuItem from '../DrawerMenuItem'
@@ -268,7 +269,7 @@ function BlockRelayItem({ urls }: { urls: string[] }) {
onClick={isLoading ? undefined : handleClick}
className={isLoading ? 'opacity-50 cursor-not-allowed' : ''}
>
- {isLoading ?
:
}
+ {isLoading ?
:
}
{isLoading ? t('Processing...') : blocked ? t('Unblock') : t('Block')}
)
@@ -276,7 +277,7 @@ function BlockRelayItem({ urls }: { urls: string[] }) {
return (
- {isLoading ? : }
+ {isLoading ? : }
{isLoading ? t('Processing...') : blocked ? t('Unblock') : t('Block')}
)
diff --git a/src/components/StartupSessionBanner.tsx b/src/components/StartupSessionBanner.tsx
index 470e8a2a..90094960 100644
--- a/src/components/StartupSessionBanner.tsx
+++ b/src/components/StartupSessionBanner.tsx
@@ -1,6 +1,6 @@
import { useNostr } from '@/providers/NostrProvider'
import { cn } from '@/lib/utils'
-import { Loader2 } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -36,7 +36,7 @@ export default function StartupSessionBanner() {
'bg-background px-3 py-2 text-center text-sm text-muted-foreground'
)}
>
-
+
{t('startupSessionHydrating', {
defaultValue: 'Syncing your relays and profile from the network…'
diff --git a/src/components/TopicSubscribeButton/index.tsx b/src/components/TopicSubscribeButton/index.tsx
index c051cd4a..ab1fcd9d 100644
--- a/src/components/TopicSubscribeButton/index.tsx
+++ b/src/components/TopicSubscribeButton/index.tsx
@@ -1,7 +1,8 @@
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import { useInterestList } from '@/providers/InterestListProvider'
import { useNostr } from '@/providers/NostrProvider'
-import { Bell, BellOff, Loader2 } from 'lucide-react'
+import { Bell, BellOff } from 'lucide-react'
import { useTranslation } from 'react-i18next'
interface TopicSubscribeButtonProps {
@@ -50,7 +51,7 @@ export default function TopicSubscribeButton({
title={subscribed ? t('Unsubscribe') : t('Subscribe')}
>
{changing ? (
-
+
) : subscribed ? (
) : (
@@ -70,7 +71,7 @@ export default function TopicSubscribeButton({
>
{changing ? (
<>
-
+
{subscribed ? t('Unsubscribing...') : t('Subscribing...')}
>
) : subscribed ? (
diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx
index e8eb02a8..e4535f4e 100644
--- a/src/components/TrendingNotes/index.tsx
+++ b/src/components/TrendingNotes/index.tsx
@@ -15,7 +15,8 @@ import noteStatsService from '@/services/note-stats.service'
import { FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
-import { ChevronDown, Loader2 } from 'lucide-react'
+import { Skeleton } from '@/components/ui/skeleton'
+import { ChevronDown } from 'lucide-react'
const SHOW_COUNT = 25
const CACHE_DURATION = 30 * 60 * 1000 // 30 minutes
@@ -395,7 +396,7 @@ export default function TrendingNotes({ variant = 'page' }: { variant?: Trending
{headerTitle}
{cacheLoading && cacheEvents.length === 0 ? (
-
+
) : null}
{isUpdating ? (
<>
-
+
{t('Updating...')}
>
) : (
diff --git a/src/components/ZapDialog/index.tsx b/src/components/ZapDialog/index.tsx
index 624d0b9a..abd4355f 100644
--- a/src/components/ZapDialog/index.tsx
+++ b/src/components/ZapDialog/index.tsx
@@ -14,13 +14,13 @@ import {
DrawerTitle
} from '@/components/ui/drawer'
import { Input } from '@/components/ui/input'
+import { Skeleton } from '@/components/ui/skeleton'
import { Label } from '@/components/ui/label'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useZap } from '@/providers/ZapProvider'
import lightning from '@/services/lightning.service'
import noteStatsService from '@/services/note-stats.service'
-import { Loader } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -262,7 +262,8 @@ function ZapDialogContent({
{/* Zap button - fixed at bottom */}
diff --git a/src/hooks/useProfilePins.tsx b/src/hooks/useProfilePins.tsx
index 63e8a204..c9ab0f22 100644
--- a/src/hooks/useProfilePins.tsx
+++ b/src/hooks/useProfilePins.tsx
@@ -1,13 +1,14 @@
-import { useCallback, useEffect, useState } from 'react'
import { Event } from 'nostr-tools'
import {
buildProfilePageReadRelayUrls,
PROFILE_PAGE_PINS_RESOLVE_LIMIT
} from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
+import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'
import { queryService } from '@/services/client.service'
+import { useCallback, useEffect, useMemo, useState } from 'react'
const CACHE_DURATION = 5 * 60 * 1000
@@ -57,8 +58,18 @@ function orderPinEvents(pinList: Event, eventsById: Map
): Event[]
return ordered
}
+function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string {
+ const fav = [...favoriteRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
+ const blk = [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
+ return `${fav}\u0000${blk}`
+}
+
export function useProfilePins(pubkey: string | undefined) {
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
+ const relayListsKey = useMemo(
+ () => relayListsContentKey(favoriteRelays, blockedRelays),
+ [favoriteRelays, blockedRelays]
+ )
const [pinEvents, setPinEvents] = useState([])
const [loadingPins, setLoadingPins] = useState(false)
@@ -84,6 +95,8 @@ export function useProfilePins(pubkey: string | undefined) {
read: [] as string[],
write: [] as string[]
}))
+ // Same stack as profile feed: viewed npub NIP-65 read+write → your favorites → FAST_READ_RELAY_URLS,
+ // deduped, blocked stripped, max PROFILE_PAGE_FEED_MAX_RELAYS (6). Relays here accept `#d` on REQ.
const profileRelays = buildProfilePageReadRelayUrls(
favoriteRelays,
blockedRelays,
@@ -125,17 +138,18 @@ export function useProfilePins(pubkey: string | undefined) {
)
}
if (aTags.length > 0) {
- const aTagFetches = aTags.map(async (aTag) => {
- const parts = aTag.split(':')
+ const aTagFetches = aTags.map(async (aTagRaw) => {
+ const parts = aTagRaw.trim().split(':')
if (parts.length < 2) return null
const kind = parseInt(parts[0], 10)
- const author = parts[1]
- const d = parts[2] || ''
+ const author = parts[1]?.trim().toLowerCase()
+ if (!Number.isFinite(kind) || !author || !/^[0-9a-f]{64}$/.test(author)) return null
+ const d = parts.slice(2).join(':')
const filter = d
? { authors: [author], kinds: [kind], limit: 1, '#d': [d] as [string] }
: { authors: [author], kinds: [kind], limit: 1 }
- const events = await queryService.fetchEvents(profileRelays, [filter])
- return events[0] || null
+ const events = await queryService.fetchEvents(profileRelays, filter)
+ return events[0] ?? null
})
eventPromises.push(
Promise.all(aTagFetches).then((events) => events.filter((e): e is Event => e !== null))
@@ -151,7 +165,7 @@ export function useProfilePins(pubkey: string | undefined) {
byId.set(e.id, e)
}
- const ordered = orderPinEvents(pinList, byId)
+ const ordered = orderPinEvents(pinList, byId).slice(0, PROFILE_PAGE_PINS_RESOLVE_LIMIT)
setPinEvents(ordered)
pinsCache.set(cacheKey, { events: ordered, lastUpdated: Date.now() })
} catch (e) {
@@ -161,7 +175,7 @@ export function useProfilePins(pubkey: string | undefined) {
setLoadingPins(false)
}
},
- [pubkey, favoriteRelays, blockedRelays]
+ [pubkey, relayListsKey, favoriteRelays, blockedRelays]
)
useEffect(() => {
diff --git a/src/hooks/useProfileTimeline.tsx b/src/hooks/useProfileTimeline.tsx
index 907e6bda..e9f3e3ee 100644
--- a/src/hooks/useProfileTimeline.tsx
+++ b/src/hooks/useProfileTimeline.tsx
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event } from 'nostr-tools'
import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
+import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
type ProfileTimelineMemoryEntry = {
@@ -82,6 +83,12 @@ function postProcessEvents(
return events.slice(0, limit)
}
+function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string {
+ const fav = [...favoriteRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
+ const blk = [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
+ return `${fav}\u0000${blk}`
+}
+
export function useProfileTimeline({
pubkey,
cacheKey,
@@ -90,6 +97,10 @@ export function useProfileTimeline({
filterPredicate
}: UseProfileTimelineOptions): UseProfileTimelineResult {
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
+ const relayListsKey = useMemo(
+ () => relayListsContentKey(favoriteRelays, blockedRelays),
+ [favoriteRelays, blockedRelays]
+ )
const { isEventDeleted, tombstoneEpoch } = useDeletedEvent()
const isEventDeletedRef = useRef(isEventDeleted)
isEventDeletedRef.current = isEventDeleted
@@ -216,16 +227,7 @@ export function useProfileTimeline({
subscriptionRef.current()
subscriptionRef.current = () => {}
}
- }, [
- pubkey,
- cacheKey,
- JSON.stringify(kinds),
- limit,
- filterPredicate,
- refreshToken,
- favoriteRelays,
- blockedRelays
- ])
+ }, [pubkey, cacheKey, JSON.stringify(kinds), limit, refreshToken, relayListsKey])
const refresh = useCallback(() => {
subscriptionRef.current()
diff --git a/src/lib/favorites-feed-relays.ts b/src/lib/favorites-feed-relays.ts
index e378deda..9b9d1d96 100644
--- a/src/lib/favorites-feed-relays.ts
+++ b/src/lib/favorites-feed-relays.ts
@@ -111,7 +111,10 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox(
})
}
-/** Profile page pins + feed: author's NIP-65 read/write, then favorites, then fast-read defaults, capped. */
+/**
+ * Profile page pins + feed: viewed author's NIP-65 read + write (REQ tier 1), then logged-in user's favorites,
+ * then fast-read defaults from constants, deduped and blocked-stripped, capped at this count.
+ */
export const PROFILE_PAGE_FEED_MAX_RELAYS = 6
export const PROFILE_PAGE_PINS_RESOLVE_LIMIT = 10
diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx
index fb5a30e4..9784e90d 100644
--- a/src/pages/primary/NoteListPage/index.tsx
+++ b/src/pages/primary/NoteListPage/index.tsx
@@ -9,8 +9,9 @@ import { useFeed } from '@/providers/FeedProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import type { TNoteListRef } from '@/components/NoteList'
+import { NoteCardLoadingSkeleton } from '@/components/NoteCard'
import { TPageRef } from '@/types'
-import { Compass, Info, Loader2 } from 'lucide-react'
+import { Compass, Info } from 'lucide-react'
import React, {
Dispatch,
forwardRef,
@@ -85,17 +86,19 @@ const NoteListPage = forwardRef((_, ref) => {
if (!isReady) {
content = (
-
-
+
{t('feedStarting', {
defaultValue: 'Starting feeds and relays… This can take a few seconds after login.'
})}
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
)
} else if (feedInfo.feedType === 'following' && !pubkey) {
diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
index fd8cbd07..9d90acd7 100644
--- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
+++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts
@@ -18,6 +18,9 @@ import { type Event, type Filter, kinds } from 'nostr-tools'
export const FAUX_SPELL_MAX_RELAYS = 6
export const FAUX_SPELL_EVENT_LIMIT = 200
+/** Profile Media tab: single REQ `limit` (matches merged cap in NoteList one-shot). */
+export const PROFILE_MEDIA_REQ_LIMIT = 200
+
/**
* Trim relay lists and filter limits (and bookmark `ids`) so faux feeds stay cheap to open.
*/
@@ -110,6 +113,16 @@ export function buildMediaSpellFilter(): Filter {
return { kinds: [...MEDIA_SPELL_KINDS], limit: FAUX_SPELL_EVENT_LIMIT }
}
+/** Media kinds for a single profile (same as {@link MEDIA_SPELL_KINDS}, scoped by `authors`). */
+export function buildProfileMediaSpellFilter(pubkey: string): Filter {
+ const pk = /^[0-9a-f]{64}$/i.test(pubkey.trim()) ? pubkey.trim().toLowerCase() : pubkey.trim()
+ return {
+ authors: [pk],
+ kinds: [...MEDIA_SPELL_KINDS],
+ limit: PROFILE_MEDIA_REQ_LIMIT
+ }
+}
+
export function buildCalendarSpellFilter(): Filter {
return {
kinds: [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME],
diff --git a/src/pages/secondary/MuteListPage/index.tsx b/src/pages/secondary/MuteListPage/index.tsx
index a0d0882c..cf91dc72 100644
--- a/src/pages/secondary/MuteListPage/index.tsx
+++ b/src/pages/secondary/MuteListPage/index.tsx
@@ -2,6 +2,7 @@ import MuteButton from '@/components/MuteButton'
import Nip05 from '@/components/Nip05'
import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import UserAvatar from '@/components/UserAvatar'
import Username from '@/components/Username'
import { useFetchProfile } from '@/hooks'
@@ -113,7 +114,7 @@ function UserItem({ pubkey }: { pubkey: string }) {
{switching ? (
) : muteType === 'private' ? (
) : (
{t('Preferred')}
@@ -159,7 +160,7 @@ export default function BlossomServerListSetting() {
title={t('Remove')}
disabled={removingIndex >= 0 || adding || movingIndex >= 0}
>
- {removingIndex === idx ?
:
}
+ {removingIndex === idx ?
:
}
@@ -180,7 +181,7 @@ export default function BlossomServerListSetting() {
}}
title={t('Add')}
>
- {adding &&
}
+ {adding &&
}
{t('Add')}
diff --git a/src/pages/secondary/ProfileEditorPage/index.tsx b/src/pages/secondary/ProfileEditorPage/index.tsx
index d45be64b..f5a471e1 100644
--- a/src/pages/secondary/ProfileEditorPage/index.tsx
+++ b/src/pages/secondary/ProfileEditorPage/index.tsx
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
+import { Skeleton } from '@/components/ui/skeleton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { createPaymentInfoDraftEvent, createProfileDraftEvent } from '@/lib/draft-event'
import { generateImageByPubkey } from '@/lib/pubkey'
@@ -24,7 +25,7 @@ import { isEmail } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
-import { ChevronDown, Loader, Pencil, Plus, RefreshCw, Trash2, Upload } from 'lucide-react'
+import { ChevronDown, Pencil, Plus, RefreshCw, Trash2, Upload } from 'lucide-react'
import type { Event } from 'nostr-tools'
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -299,11 +300,11 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
className="gap-1.5"
title={t('Force-refresh profile and payment info from relays')}
>
- {refreshingCache ?
:
}
+ {refreshingCache ?
:
}
{t('Refresh cache')}
)
@@ -319,7 +320,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
>
- {uploadingBanner ? : }
+ {uploadingBanner ? : }
{
- {uploadingAvatar ? : }
+ {uploadingAvatar ? : }
@@ -495,7 +496,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
disabled={savingFullProfile || !hasChanged}
className="gap-2"
>
- {savingFullProfile &&
}
+ {savingFullProfile &&
}
{savingFullProfile ? t('Saving…') : t('Save full profile')}
@@ -644,7 +645,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
{t('Cancel')}
diff --git a/src/pages/secondary/RssFeedSettingsPage/index.tsx b/src/pages/secondary/RssFeedSettingsPage/index.tsx
index ad0536e8..561db8c3 100644
--- a/src/pages/secondary/RssFeedSettingsPage/index.tsx
+++ b/src/pages/secondary/RssFeedSettingsPage/index.tsx
@@ -7,6 +7,7 @@ import { forwardRef, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
@@ -637,7 +638,7 @@ const RssFeedSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index
disabled={pushing || !hasChange}
onClick={handleSave}
>
- {pushing ?
:
}
+ {pushing ?
:
}
{t('Save')}
diff --git a/src/pages/secondary/WalletPage/LightningAddressInput.tsx b/src/pages/secondary/WalletPage/LightningAddressInput.tsx
index 6fb6022f..4240b7bf 100644
--- a/src/pages/secondary/WalletPage/LightningAddressInput.tsx
+++ b/src/pages/secondary/WalletPage/LightningAddressInput.tsx
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { createProfileDraftEvent } from '@/lib/draft-event'
@@ -64,7 +65,7 @@ export default function LightningAddressInput() {
}}
/>