Browse Source

Fully implement logger

imwald
Silberengel 4 months ago
parent
commit
1ca85bd660
  1. 3
      src/components/AccountManager/NostrConnectionLogin.tsx
  2. 3
      src/components/AudioPlayer/index.tsx
  3. 19
      src/components/CacheRelaysSetting/index.tsx
  4. 3
      src/components/ClientSelect/index.tsx
  5. 29
      src/components/Content/index.tsx
  6. 12
      src/components/Embedded/EmbeddedNote.tsx
  7. 3
      src/components/ErrorBoundary.tsx
  8. 3
      src/components/FavoriteRelaysSetting/AddBlockedRelay.tsx
  9. 3
      src/components/FavoriteRelaysSetting/AddNewRelay.tsx
  10. 3
      src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx
  11. 3
      src/components/FavoriteRelaysSetting/BlockedRelayItem.tsx
  12. 5
      src/components/FavoriteRelaysSetting/RelayUrl.tsx
  13. 3
      src/components/FeedSwitcher/index.tsx
  14. 7
      src/components/Image/index.tsx
  15. 3
      src/components/KindFilter/index.tsx
  16. 9
      src/components/MailboxSetting/DiscoveredRelays.tsx
  17. 3
      src/components/MailboxSetting/SaveButton.tsx
  18. 3
      src/components/MediaErrorBoundary.tsx
  19. 17
      src/components/Note/Article/index.tsx
  20. 17
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  21. 3
      src/components/Note/Highlight/index.tsx
  22. 3
      src/components/Note/LongFormArticle/NostrNode.tsx
  23. 3
      src/components/Note/MarkdownArticle/NostrNode.tsx
  24. 5
      src/components/Note/Poll.tsx
  25. 3
      src/components/NoteOptions/RawEventDialog.tsx
  26. 2
      src/components/NoteOptions/useMenuActions.tsx
  27. 11
      src/components/NoteStats/LikeButton.tsx
  28. 3
      src/components/NoteStats/Likes.tsx
  29. 3
      src/components/NoteStats/RepostButton.tsx
  30. 3
      src/components/NoteStats/VoteButtons.tsx
  31. 3
      src/components/ParentNotePreview/index.tsx
  32. 7
      src/components/PostEditor/HighlightEditor.tsx
  33. 3
      src/components/PostEditor/Mentions.tsx
  34. 9
      src/components/PostEditor/PostContent.tsx
  35. 5
      src/components/PostEditor/PostRelaySelector.tsx
  36. 3
      src/components/PostEditor/PostTextarea/ClipboardAndDropHandler.ts
  37. 3
      src/components/PostEditor/Uploader.tsx
  38. 275
      src/components/Profile/ProfileArticles.tsx
  39. 15
      src/components/Profile/ProfileBookmarksAndHashtags.tsx
  40. 251
      src/components/Profile/ProfileFeed.tsx
  41. 146
      src/components/Profile/ProfileMedia.tsx
  42. 103
      src/components/Profile/index.tsx
  43. 3
      src/components/RelayInfo/ReviewEditor.tsx
  44. 5
      src/components/SaveRelayDropdownMenu/index.tsx
  45. 131
      src/components/TrendingNotes/index.tsx
  46. 29
      src/components/UniversalContent/EnhancedContent.tsx
  47. 5
      src/components/UniversalContent/HighlightSourcePreview.tsx
  48. 3
      src/components/VersionUpdateBanner/index.tsx
  49. 3
      src/components/VideoPlayer/index.tsx
  50. 34
      src/components/WebPreview/index.tsx
  51. 3
      src/components/YoutubeEmbeddedPlayer/index.tsx
  52. 3
      src/hooks/useFetchRelayInfo.tsx
  53. 3
      src/hooks/useFetchRelayInfos.tsx
  54. 3
      src/hooks/useFetchRelayList.tsx
  55. 202
      src/hooks/useProfileTimeline.tsx
  56. 6
      src/lib/debug-utils.ts
  57. 15
      src/lib/draft-event.ts
  58. 3
      src/lib/event-metadata.ts
  59. 5
      src/lib/nip05.ts
  60. 3
      src/lib/nostr-parser.tsx
  61. 3
      src/lib/pubkey.ts
  62. 16
      src/lib/url.ts
  63. 2
      src/pages/primary/NoteListPage/index.tsx
  64. 3
      src/pages/secondary/HomePage/index.tsx
  65. 5
      src/pages/secondary/NotePage/NotFound.tsx
  66. 7
      src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx
  67. 3
      src/providers/MuteListProvider.tsx
  68. 4
      src/providers/NotificationProvider.tsx
  69. 21
      src/services/client.service.ts
  70. 11
      src/services/content-parser.service.ts
  71. 24
      src/services/indexed-db.service.ts
  72. 3
      src/services/lightning.service.ts
  73. 3
      src/services/media-manager.service.ts
  74. 5
      src/services/relay-info.service.ts
  75. 17
      src/services/relay-selection.service.ts

3
src/components/AccountManager/NostrConnectionLogin.tsx

@ -10,6 +10,7 @@ import QrScanner from 'qr-scanner' @@ -10,6 +10,7 @@ import QrScanner from 'qr-scanner'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import QrCode from '../QrCode'
import logger from '@/lib/logger'
export default function NostrConnectLogin({
back,
@ -95,7 +96,7 @@ export default function NostrConnectLogin({ @@ -95,7 +96,7 @@ export default function NostrConnectLogin({
nostrConnectionLogin(loginDetails.privKey, loginDetails.connectionString)
.then(() => onLoginSuccess())
.catch((err) => {
console.error('NostrConnectionLogin Error:', err)
logger.error('NostrConnectionLogin error', { error: err })
setNostrConnectionErrMsg(
err.message ? `${err.message}. Please reload.` : 'Connection failed. Please reload.'
)

3
src/components/AudioPlayer/index.tsx

@ -6,6 +6,7 @@ import { Pause, Play } from 'lucide-react' @@ -6,6 +6,7 @@ import { Pause, Play } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink'
import { MediaErrorBoundary } from '../MediaErrorBoundary'
import logger from '@/lib/logger'
interface AudioPlayerProps {
src: string
@ -91,7 +92,7 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) { @@ -91,7 +92,7 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) {
onError={(error) => {
// Don't log expected media errors
if (error.name !== 'AbortError' && !error.message.includes('play() request was interrupted')) {
console.warn('Audio player error:', error)
logger.warn('Audio player error', error)
}
setError(true)
}}

19
src/components/CacheRelaysSetting/index.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button'
import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url'
import logger from '@/lib/logger'
import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay, TMailboxRelayScope } from '@/types'
import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
@ -187,7 +188,7 @@ export default function CacheRelaysSetting() { @@ -187,7 +188,7 @@ export default function CacheRelaysSetting() {
const info = await indexedDb.getStoreInfo()
setCacheInfo(info)
} catch (error) {
console.error('Failed to load cache info:', error)
logger.error('Failed to load cache info', { error })
}
}
@ -209,7 +210,7 @@ export default function CacheRelaysSetting() { @@ -209,7 +210,7 @@ export default function CacheRelaysSetting() {
try {
window.localStorage.removeItem(key)
} catch (e) {
console.warn(`Failed to remove ${key} from localStorage:`, e)
logger.warn(`Failed to remove ${key} from localStorage`, e as Error)
}
})
@ -231,7 +232,7 @@ export default function CacheRelaysSetting() { @@ -231,7 +232,7 @@ export default function CacheRelaysSetting() {
toast.success(t('Cache cleared successfully'))
} catch (error) {
console.error('Failed to clear cache:', error)
logger.error('Failed to clear cache', { error })
toast.error(t('Failed to clear cache'))
}
}
@ -246,7 +247,7 @@ export default function CacheRelaysSetting() { @@ -246,7 +247,7 @@ export default function CacheRelaysSetting() {
toast.success(t('Cache refreshed successfully'))
} catch (error) {
console.error('Failed to refresh cache:', error)
logger.error('Failed to refresh cache', { error })
toast.error(t('Failed to refresh cache'))
}
}
@ -270,7 +271,7 @@ export default function CacheRelaysSetting() { @@ -270,7 +271,7 @@ export default function CacheRelaysSetting() {
: await indexedDb.getStoreItems(storeName)
setStoreItems(items)
} catch (error) {
console.error('Failed to load store items:', error)
logger.error('Failed to load store items', { error })
toast.error(t('Failed to load store items'))
setStoreItems([])
} finally {
@ -335,7 +336,7 @@ export default function CacheRelaysSetting() { @@ -335,7 +336,7 @@ export default function CacheRelaysSetting() {
// Update cache info
loadCacheInfo()
} catch (error) {
console.error('Failed to delete item:', error)
logger.error('Failed to delete item', { error })
toast.error(t('Failed to delete item'))
}
}
@ -354,7 +355,7 @@ export default function CacheRelaysSetting() { @@ -354,7 +355,7 @@ export default function CacheRelaysSetting() {
loadCacheInfo()
toast.success(t('All items deleted successfully'))
} catch (error) {
console.error('Failed to delete all items:', error)
logger.error('Failed to delete all items', { error })
toast.error(t('Failed to delete all items'))
}
}
@ -391,7 +392,7 @@ export default function CacheRelaysSetting() { @@ -391,7 +392,7 @@ export default function CacheRelaysSetting() {
toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}}', { deleted: result.deleted, kept: result.kept }))
}
} catch (error) {
console.error('Failed to cleanup duplicates:', error)
logger.error('Failed to cleanup duplicates', { error })
if (error instanceof Error && error.message === 'Not a replaceable event store') {
toast.error(t('This store does not contain replaceable events'))
} else {
@ -476,7 +477,7 @@ export default function CacheRelaysSetting() { @@ -476,7 +477,7 @@ export default function CacheRelaysSetting() {
} catch (error) {
// Reset flag on error
justSavedRef.current = false
console.error('Failed to save cache relays:', error)
logger.error('Failed to save cache relays', { error })
// Show error feedback
if (error instanceof Error && (error as any).relayStatuses) {
showPublishingFeedback({

3
src/components/ClientSelect/index.tsx

@ -10,6 +10,7 @@ import clientService from '@/services/client.service' @@ -10,6 +10,7 @@ import clientService from '@/services/client.service'
import { ExternalLink } from 'lucide-react'
import { Event, kinds, nip19 } from 'nostr-tools'
import { Dispatch, SetStateAction, useMemo, useState } from 'react'
import logger from '@/lib/logger'
import { useTranslation } from 'react-i18next'
const clients: Record<string, { name: string; getUrl: (id: string) => string }> = {
@ -94,7 +95,7 @@ export default function ClientSelect({ @@ -94,7 +95,7 @@ export default function ClientSelect({
kind = pointer.data.kind
}
} catch (error) {
console.error('Failed to decode NIP-19 pointer:', error)
logger.error('Failed to decode NIP-19 pointer', { error, originalNoteId })
return ['njump']
}
}

29
src/components/Content/index.tsx

@ -28,6 +28,33 @@ import Emoji from '../Emoji' @@ -28,6 +28,33 @@ import Emoji from '../Emoji'
import ImageGallery from '../ImageGallery'
import MediaPlayer from '../MediaPlayer'
import YoutubeEmbeddedPlayer from '../YoutubeEmbeddedPlayer'
import { toNote } from '@/lib/link'
const REDIRECT_REGEX = /Read (naddr1[a-z0-9]+) instead\./i
function renderRedirectText(text: string, key: number) {
const match = text.match(REDIRECT_REGEX)
if (!match) {
return text
}
const [fullMatch, naddr] = match
const [prefix, suffix] = text.split(fullMatch)
const href = toNote(naddr)
return (
<span key={`redirect-${key}`}>
{prefix}
Read{' '}
<a
className="text-primary hover:underline"
href={href}
onClick={(e) => e.stopPropagation()}
>
{naddr}
</a>{' '}
instead.{suffix}
</span>
)
}
export default function Content({
event,
@ -168,7 +195,7 @@ export default function Content({ @@ -168,7 +195,7 @@ export default function Content({
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
return renderRedirectText(node.data, index)
}
// Skip image nodes - they're rendered in the carousel at the top
if (node.type === 'image' || node.type === 'images') {

12
src/components/Embedded/EmbeddedNote.tsx

@ -11,6 +11,7 @@ import ClientSelect from '../ClientSelect' @@ -11,6 +11,7 @@ import ClientSelect from '../ClientSelect'
import MainNoteCard from '../NoteCard/MainNoteCard'
import { Button } from '../ui/button'
import { Search } from 'lucide-react'
import logger from '@/lib/logger'
export function EmbeddedNote({ noteId, className }: { noteId: string; className?: string }) {
const { event, isFetching } = useFetchEvent(noteId)
@ -32,7 +33,12 @@ export function EmbeddedNote({ noteId, className }: { noteId: string; className? @@ -32,7 +33,12 @@ export function EmbeddedNote({ noteId, className }: { noteId: string; className?
}
})
.catch((error: any) => {
console.warn(`Retry ${retryCount + 1}/${maxRetries} failed for event:`, noteId, error)
logger.warn('EmbeddedNote retry failed', {
attempt: retryCount + 1,
maxRetries,
noteId,
error
})
})
.finally(() => {
setIsRetrying(false)
@ -125,7 +131,7 @@ function EmbeddedNoteNotFound({ @@ -125,7 +131,7 @@ function EmbeddedNoteNotFound({
relays = relays.map(url => normalizeUrl(url) || url)
relays = Array.from(new Set(relays))
} catch (err) {
console.error('Failed to parse external relays:', err)
logger.error('Failed to parse external relays', { error: err, noteId })
}
} else {
extractedHexEventId = noteId
@ -167,7 +173,7 @@ function EmbeddedNoteNotFound({ @@ -167,7 +173,7 @@ function EmbeddedNoteNotFound({
onEventFound(event)
}
} catch (error) {
console.error('External relay fetch failed:', error)
logger.error('External relay fetch failed', { error, noteId })
} finally {
setIsSearchingExternal(false)
setTriedExternal(true)

3
src/components/ErrorBoundary.tsx

@ -4,6 +4,7 @@ import { SILBERENGEL_PUBKEY } from '@/constants' @@ -4,6 +4,7 @@ import { SILBERENGEL_PUBKEY } from '@/constants'
import { MessageCircle, RotateCw } from 'lucide-react'
import React, { Component, ReactNode } from 'react'
import { toast } from 'sonner'
import logger from '@/lib/logger'
interface ErrorBoundaryProps {
children: ReactNode
@ -26,7 +27,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt @@ -26,7 +27,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo)
logger.error('ErrorBoundary caught an error', { error, errorInfo })
}
render() {

3
src/components/FavoriteRelaysSetting/AddBlockedRelay.tsx

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Loader2, Check } from 'lucide-react'
import logger from '@/lib/logger'
export default function AddBlockedRelay() {
const { t } = useTranslation()
@ -38,7 +39,7 @@ export default function AddBlockedRelay() { @@ -38,7 +39,7 @@ export default function AddBlockedRelay() {
setSuccessMsg(t('Relay blocked successfully'))
setTimeout(() => setSuccessMsg(''), 3000)
} catch (error) {
console.error('Failed to block relay:', error)
logger.error('Failed to block relay', { error, relay: normalizedUrl })
setErrorMsg(t('Failed to block relay. Please try again.'))
} finally {
setIsLoading(false)

3
src/components/FavoriteRelaysSetting/AddNewRelay.tsx

@ -4,6 +4,7 @@ import { normalizeUrl } from '@/lib/url' @@ -4,6 +4,7 @@ import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function AddNewRelay() {
const { t } = useTranslation()
@ -31,7 +32,7 @@ export default function AddNewRelay() { @@ -31,7 +32,7 @@ export default function AddNewRelay() {
await addFavoriteRelays([normalizedUrl])
setInput('')
} catch (error) {
console.error('Failed to add favorite relay:', error)
logger.error('Failed to add favorite relay', { error, relay: normalizedUrl })
setErrorMsg(t('Failed to add relay. Please try again.'))
} finally {
setIsLoading(false)

3
src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx

@ -3,6 +3,7 @@ import { Input } from '@/components/ui/input' @@ -3,6 +3,7 @@ import { Input } from '@/components/ui/input'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function AddNewRelaySet() {
const { t } = useTranslation()
@ -21,7 +22,7 @@ export default function AddNewRelaySet() { @@ -21,7 +22,7 @@ export default function AddNewRelaySet() {
await createRelaySet(newRelaySetName)
setNewRelaySetName('')
} catch (error) {
console.error('Failed to create relay set:', error)
logger.error('Failed to create relay set', { error, name: newRelaySetName })
setErrorMsg(t('Failed to create relay set. Please try again.'))
} finally {
setIsLoading(false)

3
src/components/FavoriteRelaysSetting/BlockedRelayItem.tsx

@ -5,6 +5,7 @@ import { X, Loader2 } from 'lucide-react' @@ -5,6 +5,7 @@ import { X, Loader2 } from 'lucide-react'
import { useState } from 'react'
import RelayIcon from '../RelayIcon'
import { Button } from '../ui/button'
import logger from '@/lib/logger'
export default function BlockedRelayItem({ relay }: { relay: string }) {
const { push } = useSecondaryPage()
@ -19,7 +20,7 @@ export default function BlockedRelayItem({ relay }: { relay: string }) { @@ -19,7 +20,7 @@ export default function BlockedRelayItem({ relay }: { relay: string }) {
try {
await deleteBlockedRelays([relay])
} catch (error) {
console.error('Failed to unblock relay:', error)
logger.error('Failed to unblock relay', { error, relay })
} finally {
setIsLoading(false)
}

5
src/components/FavoriteRelaysSetting/RelayUrl.tsx

@ -8,6 +8,7 @@ import { CircleX } from 'lucide-react' @@ -8,6 +8,7 @@ import { CircleX } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
import logger from '@/lib/logger'
export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
const { t } = useTranslation()
@ -29,7 +30,7 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) { @@ -29,7 +30,7 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
relayUrls: relaySet.relayUrls.filter((u) => u !== url)
})
} catch (error) {
console.error('Failed to remove relay from set:', error)
logger.error('Failed to remove relay from set', { error, relaySetId, url })
}
}
@ -54,7 +55,7 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) { @@ -54,7 +55,7 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
await updateRelaySet({ ...relaySet, relayUrls: newRelayUrls })
setNewRelayUrl('')
} catch (error) {
console.error('Failed to update relay set:', error)
logger.error('Failed to update relay set', { error, relaySetId, url: normalizedUrl })
setNewRelayUrlError(t('Failed to add relay. Please try again.'))
} finally {
setIsLoading(false)

3
src/components/FeedSwitcher/index.tsx

@ -8,6 +8,7 @@ import { BookmarkIcon, UsersRound, Server } from 'lucide-react' @@ -8,6 +8,7 @@ import { BookmarkIcon, UsersRound, Server } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
import RelaySetCard from '../RelaySetCard'
import logger from '@/lib/logger'
export default function FeedSwitcher({ close }: { close?: () => void }) {
const { t } = useTranslation()
@ -60,7 +61,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { @@ -60,7 +61,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
<FeedSwitcherItem
isActive={feedInfo.feedType === 'all-favorites'}
onClick={() => {
console.log('FeedSwitcher: Switching to all-favorites')
logger.debug('FeedSwitcher: Switching to all-favorites')
switchFeed('all-favorites')
close?.()
}}

7
src/components/Image/index.tsx

@ -6,6 +6,7 @@ import { getHashFromURL } from 'blossom-client-sdk' @@ -6,6 +6,7 @@ import { getHashFromURL } from 'blossom-client-sdk'
import { decode } from 'blurhash'
import { ImageOff } from 'lucide-react'
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'
import logger from '@/lib/logger'
export default function Image({
image: { url, blurHash, pubkey, dim, alt: imetaAlt, fallback },
@ -62,7 +63,7 @@ export default function Image({ @@ -62,7 +63,7 @@ export default function Image({
oldImageUrl = new URL(imageUrl)
hash = getHashFromURL(oldImageUrl)
} catch (error) {
console.error('Invalid image URL:', error)
logger.error('Invalid image URL', { error, imageUrl })
}
if (!pubkey || !hash || !oldImageUrl) {
setIsLoading(false)
@ -79,7 +80,7 @@ export default function Image({ @@ -79,7 +80,7 @@ export default function Image({
try {
return new URL(server)
} catch (error) {
console.error('Invalid Blossom server URL:', server, error)
logger.error('Invalid Blossom server URL', { server, error })
return undefined
}
})
@ -167,7 +168,7 @@ function BlurHashCanvas({ blurHash, className = '' }: { blurHash: string; classN @@ -167,7 +168,7 @@ function BlurHashCanvas({ blurHash, className = '' }: { blurHash: string; classN
try {
return decode(blurHash, blurHashWidth, blurHashHeight)
} catch (error) {
console.warn('Failed to decode blurhash:', error)
logger.warn('Failed to decode blurhash', error as Error)
return null
}
}, [blurHash])

3
src/components/KindFilter/index.tsx

@ -17,7 +17,7 @@ const KIND_FILTER_OPTIONS = [ @@ -17,7 +17,7 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [kinds.Repost], label: 'Reposts' },
{ kindGroup: [kinds.LongFormArticle], label: 'Articles' },
{ kindGroup: [ExtendedKind.PUBLICATION], label: 'Publications' },
{ kindGroup: [ExtendedKind.WIKI_ARTICLE], label: 'Wiki Articles' },
{ kindGroup: [ExtendedKind.WIKI_ARTICLE, ExtendedKind.WIKI_ARTICLE_MARKDOWN], label: 'Wiki Articles' },
{ kindGroup: [kinds.Highlights], label: 'Highlights' },
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' },
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
@ -109,7 +109,6 @@ export default function KindFilter({ @@ -109,7 +109,6 @@ export default function KindFilter({
checked ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => {
console.log(checked)
if (!checked) {
// add all kinds in this group
setTemporaryShowKinds((prev) => Array.from(new Set([...prev, ...kindGroup])))

9
src/components/MailboxSetting/DiscoveredRelays.tsx

@ -8,6 +8,7 @@ import { Loader2, Check, AlertCircle } from 'lucide-react' @@ -8,6 +8,7 @@ import { Loader2, Check, AlertCircle } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
import logger from '@/lib/logger'
interface DiscoveredRelay {
url: string
@ -53,7 +54,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: @@ -53,7 +54,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
})
}
} catch (error) {
console.log('Could not fetch relays from NIP-05:', error)
logger.warn('Could not fetch relays from NIP-05', error as Error)
}
}
@ -72,7 +73,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: @@ -72,7 +73,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
}
})
} catch (error) {
console.log('Could not fetch relays from NIP-07 extension:', error)
logger.warn('Could not fetch relays from NIP-07 extension', error as Error)
}
}
@ -87,7 +88,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: @@ -87,7 +88,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
setDiscoveredRelays(discoveredArray)
} catch (error) {
console.error('Error discovering relays:', error)
logger.error('Error discovering relays', { error })
setErrorMsg(t('Failed to discover relays'))
} finally {
setIsLoading(false)
@ -131,7 +132,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: @@ -131,7 +132,7 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd:
// Clear discovered relays after adding
setDiscoveredRelays([])
} catch (error) {
console.error('Failed to add relays:', error)
logger.error('Failed to add relays', { error })
setErrorMsg(t('Failed to add relays'))
} finally {
setIsAdding(false)

3
src/components/MailboxSetting/SaveButton.tsx

@ -6,6 +6,7 @@ import { TMailboxRelay } from '@/types' @@ -6,6 +6,7 @@ import { TMailboxRelay } from '@/types'
import { CloudUpload, Loader } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function SaveButton({
mailboxRelays,
@ -49,7 +50,7 @@ export default function SaveButton({ @@ -49,7 +50,7 @@ export default function SaveButton({
showSimplePublishSuccess(t('Mailbox relays saved'))
}
} catch (error) {
console.error('Failed to save relay list:', error)
logger.error('Failed to save relay list', { error })
// Show error feedback with relay statuses if available
if (error instanceof Error && (error as any).relayStatuses) {
const errorRelayStatuses = (error as any).relayStatuses

3
src/components/MediaErrorBoundary.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import React, { Component, ReactNode } from 'react'
import { AlertTriangle } from 'lucide-react'
import logger from '@/lib/logger'
interface MediaErrorBoundaryProps {
children: ReactNode
@ -31,7 +32,7 @@ export class MediaErrorBoundary extends Component<MediaErrorBoundaryProps, Media @@ -31,7 +32,7 @@ export class MediaErrorBoundary extends Component<MediaErrorBoundaryProps, Media
}
// Log unexpected errors
console.warn('Media error boundary caught error:', error, errorInfo)
logger.warn('Media error boundary caught error', { error, errorInfo })
this.props.onError?.(error)
}

17
src/components/Note/Article/index.tsx

@ -248,7 +248,7 @@ export default function Article({ @@ -248,7 +248,7 @@ export default function Article({
/>
{/* Collapsible Article Info - only for article-type events */}
{isArticleType && (parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
{isArticleType && (parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
<Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
@ -258,21 +258,6 @@ export default function Article({ @@ -258,21 +258,6 @@ export default function Article({
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2">
{/* Nostr links summary */}
{parsedContent?.nostrLinks?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-2">Nostr references:</h4>
<div className="space-y-1">
{parsedContent?.nostrLinks?.map((link, index) => (
<div key={index} className="text-sm">
<span className="font-mono text-blue-600">{link.type}:</span>{' '}
<span className="font-mono">{link.id}</span>
</div>
))}
</div>
</div>
)}
{/* Highlight sources */}
{parsedContent?.highlightSources?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">

17
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -425,7 +425,7 @@ export default function AsciidocArticle({ @@ -425,7 +425,7 @@ export default function AsciidocArticle({
)}
{/* Collapsible Article Info - only for article-type events */}
{!hideImagesAndInfo && isArticleType && (parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
{!hideImagesAndInfo && isArticleType && (parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
<Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
@ -435,21 +435,6 @@ export default function AsciidocArticle({ @@ -435,21 +435,6 @@ export default function AsciidocArticle({
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2">
{/* Nostr links summary */}
{parsedContent?.nostrLinks?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-2">Nostr references:</h4>
<div className="space-y-1">
{parsedContent?.nostrLinks?.map((link, index) => (
<div key={index} className="text-sm">
<span className="font-mono text-blue-600">{link.type}:</span>{' '}
<span className="font-mono">{link.id}</span>
</div>
))}
</div>
</div>
)}
{/* Highlight sources */}
{parsedContent?.highlightSources?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">

3
src/components/Note/Highlight/index.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { Event } from 'nostr-tools'
import { Highlighter } from 'lucide-react'
import { nip19 } from 'nostr-tools'
import logger from '@/lib/logger'
import HighlightSourcePreview from '@/components/UniversalContent/HighlightSourcePreview'
export default function Highlight({
@ -123,7 +124,7 @@ export default function Highlight({ @@ -123,7 +124,7 @@ export default function Highlight({
</div>
)
} catch (error) {
console.error('Highlight component error:', error)
logger.error('Highlight component error', { error, eventId: event.id })
return (
<div className={`relative border-l-4 border-red-500 bg-red-50/50 dark:bg-red-950/20 rounded-r-lg p-4 ${className || ''}`}>
<div className="flex items-start gap-3">

3
src/components/Note/LongFormArticle/NostrNode.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { EmbeddedMention, EmbeddedNote } from '@/components/Embedded'
import { nip19 } from 'nostr-tools'
import { useMemo } from 'react'
import logger from '@/lib/logger'
interface NostrNodeProps {
rawText: string
@ -19,7 +20,7 @@ export default function NostrNode({ rawText, bech32Id }: NostrNodeProps) { @@ -19,7 +20,7 @@ export default function NostrNode({ rawText, bech32Id }: NostrNodeProps) {
return { type: 'note', id: bech32Id }
}
} catch (error) {
console.error('Invalid bech32 ID:', bech32Id, error)
logger.error('Invalid bech32 ID', { bech32Id, error })
}
return { type: 'invalid', id: '' }
}, [bech32Id])

3
src/components/Note/MarkdownArticle/NostrNode.tsx

@ -2,6 +2,7 @@ import { EmbeddedMention, EmbeddedNote } from '@/components/Embedded' @@ -2,6 +2,7 @@ import { EmbeddedMention, EmbeddedNote } from '@/components/Embedded'
import { nip19 } from 'nostr-tools'
import { ComponentProps, useMemo } from 'react'
import { Components } from './types'
import logger from '@/lib/logger'
export default function NostrNode({ rawText, bech32Id }: ComponentProps<Components['nostr']>) {
const { type, id } = useMemo(() => {
@ -15,7 +16,7 @@ export default function NostrNode({ rawText, bech32Id }: ComponentProps<Componen @@ -15,7 +16,7 @@ export default function NostrNode({ rawText, bech32Id }: ComponentProps<Componen
return { type: 'note', id: bech32Id }
}
} catch (error) {
console.error('Invalid bech32 ID:', bech32Id, error)
logger.error('Invalid bech32 ID', { bech32Id, error })
}
return { type: 'invalid', id: '' }
}, [bech32Id])

5
src/components/Note/Poll.tsx

@ -13,6 +13,7 @@ import { Event } from 'nostr-tools' @@ -13,6 +13,7 @@ import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import logger from '@/lib/logger'
export default function Poll({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
@ -80,7 +81,7 @@ export default function Poll({ event, className }: { event: Event; className?: s @@ -80,7 +81,7 @@ export default function Poll({ event, className }: { event: Event; className?: s
poll.endsAt
)
} catch (error) {
console.error('Failed to fetch poll results:', error)
logger.error('Failed to fetch poll results', { error, eventId: event.id })
toast.error('Failed to fetch poll results: ' + (error as Error).message)
} finally {
setIsLoadingResults(false)
@ -125,7 +126,7 @@ export default function Poll({ event, className }: { event: Event; className?: s @@ -125,7 +126,7 @@ export default function Poll({ event, className }: { event: Event; className?: s
setSelectedOptionIds([])
pollResultsService.addPollResponse(event.id, pubkey, selectedOptionIds)
} catch (error) {
console.error('Failed to vote:', error)
logger.error('Failed to vote', { error, eventId: event.id })
toast.error('Failed to vote: ' + (error as Error).message)
} finally {
setIsVoting(false)

3
src/components/NoteOptions/RawEventDialog.tsx

@ -11,6 +11,7 @@ import { Event } from 'nostr-tools' @@ -11,6 +11,7 @@ import { Event } from 'nostr-tools'
import { WrapText, Copy, Check } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function RawEventDialog({
event,
@ -31,7 +32,7 @@ export default function RawEventDialog({ @@ -31,7 +32,7 @@ export default function RawEventDialog({
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
logger.error('Failed to copy raw event', { error: err, eventId: event.id })
}
}

2
src/components/NoteOptions/useMenuActions.tsx

@ -342,7 +342,7 @@ export function useMenuActions({ @@ -342,7 +342,7 @@ export function useMenuActions({
relays: relays.length > 0 ? relays : undefined
})
} catch (error) {
console.error('Error generating naddr:', error)
logger.error('Error generating naddr', { error })
return ''
}
}, [isArticleType, event, dTag])

11
src/components/NoteStats/LikeButton.tsx

@ -18,6 +18,7 @@ import { TEmoji } from '@/types' @@ -18,6 +18,7 @@ import { TEmoji } from '@/types'
import { Loader, SmilePlus } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import logger from '@/lib/logger'
import { useTranslation } from 'react-i18next'
import Emoji from '../Emoji'
import EmojiPicker from '../EmojiPicker'
@ -88,7 +89,13 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -88,7 +89,13 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
const myLastEmojiString = typeof myLastEmoji === 'string' ? myLastEmoji : typeof myLastEmoji === 'object' ? myLastEmoji.shortcode : undefined
const isTogglingOff = myLastEmojiString === emojiString
console.log('Toggle check:', { myLastEmoji, myLastEmojiString, emojiString, isTogglingOff, myLikes: noteStats?.likes?.filter(l => l.pubkey === pubkey) })
logger.debug('Like toggle check', {
myLastEmoji,
myLastEmojiString,
emojiString,
isTogglingOff,
myLikes: noteStats?.likes?.filter(like => like.pubkey === pubkey)
})
if (isTogglingOff) {
// User wants to toggle off - find their previous reaction and delete it
@ -117,7 +124,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -117,7 +124,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
noteStatsService.updateNoteStatsByEvents([evt])
}
} catch (error) {
console.error('like failed', error)
logger.error('Like failed', { error, eventId: event.id })
} finally {
setLiking(false)
clearTimeout(timer)

3
src/components/NoteStats/Likes.tsx

@ -10,6 +10,7 @@ import { Loader } from 'lucide-react' @@ -10,6 +10,7 @@ import { Loader } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useRef, useState } from 'react'
import Emoji from '../Emoji'
import logger from '@/lib/logger'
export default function Likes({ event }: { event: Event }) {
const inQuietMode = shouldHideInteractions(event)
@ -58,7 +59,7 @@ export default function Likes({ event }: { event: Event }) { @@ -58,7 +59,7 @@ export default function Likes({ event }: { event: Event }) {
const evt = await publish(reaction)
noteStatsService.updateNoteStatsByEvents([evt])
} catch (error) {
console.error('like failed', error)
logger.error('Like failed', { error, eventId: event.id })
} finally {
setLiking(null)
clearTimeout(timer)

3
src/components/NoteStats/RepostButton.tsx

@ -18,6 +18,7 @@ import { Loader, PencilLine, Repeat } from 'lucide-react' @@ -18,6 +18,7 @@ import { Loader, PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
import PostEditor from '../PostEditor'
import { formatCount } from './utils'
@ -60,7 +61,7 @@ export default function RepostButton({ event, hideCount = false }: { event: Even @@ -60,7 +61,7 @@ export default function RepostButton({ event, hideCount = false }: { event: Even
const evt = await publish(repost)
noteStatsService.updateNoteStatsByEvents([evt])
} catch (error) {
console.error('repost failed', error)
logger.error('Repost failed', { error, eventId: event.id })
} finally {
setReposting(false)
clearTimeout(timer)

3
src/components/NoteStats/VoteButtons.tsx

@ -6,6 +6,7 @@ import { Event } from 'nostr-tools' @@ -6,6 +6,7 @@ import { Event } from 'nostr-tools'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import logger from '@/lib/logger'
export default function VoteButtons({ event }: { event: Event }) {
const { pubkey, publish, checkLogin } = useNostr()
@ -76,7 +77,7 @@ export default function VoteButtons({ event }: { event: Event }) { @@ -76,7 +77,7 @@ export default function VoteButtons({ event }: { event: Event }) {
noteStatsService.updateNoteStatsByEvents([evt])
}
} catch (error) {
console.error('vote failed', error)
logger.error('Vote failed', { error, eventId: event.id })
} finally {
setVoting(null)
clearTimeout(timer)

3
src/components/ParentNotePreview/index.tsx

@ -8,6 +8,7 @@ import { useCallback, useEffect, useState } from 'react' @@ -8,6 +8,7 @@ import { useCallback, useEffect, useState } from 'react'
import { Event, nip19 } from 'nostr-tools'
import ContentPreview from '../ContentPreview'
import UserAvatar from '../UserAvatar'
import logger from '@/lib/logger'
export default function ParentNotePreview({
eventId,
@ -55,7 +56,7 @@ export default function ParentNotePreview({ @@ -55,7 +56,7 @@ export default function ParentNotePreview({
setFallbackEvent(foundEvent)
}
} catch (error) {
console.warn('Fallback fetch from searchable relays failed:', error)
logger.warn('Fallback fetch from searchable relays failed', error as Error)
} finally {
setIsFetchingFallback(false)
}

7
src/components/PostEditor/HighlightEditor.tsx

@ -148,12 +148,11 @@ export default function HighlightEditor({ @@ -148,12 +148,11 @@ export default function HighlightEditor({
id="highlight-context"
value={context}
onChange={(e) => setContext(e.target.value)}
placeholder={t('Enter the full text that you are highlighting from...')}
rows={2}
maxLength={500}
placeholder={t('Paste the entire original passage that contains your highlight')}
rows={3}
/>
<p className="text-xs text-muted-foreground">
{context.length}/500 {t('characters')}
{t('The main editor above should contain only the text you want to highlight. This field should contain the full quote or paragraph for context.')}
</p>
</div>

3
src/components/PostEditor/Mentions.tsx

@ -3,6 +3,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover @@ -3,6 +3,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import logger from '@/lib/logger'
import { Check } from 'lucide-react'
import { Event, nip19 } from 'nostr-tools'
import { HTMLAttributes, useEffect, useState } from 'react'
@ -166,7 +167,7 @@ export async function extractMentions(content: string, parentEvent?: Event) { @@ -166,7 +167,7 @@ export async function extractMentions(content: string, parentEvent?: Event) {
}
}
} catch (e) {
console.error(e)
logger.error('Failed to decode mention', { error: e, match: m })
}
}

9
src/components/PostEditor/PostContent.tsx

@ -16,6 +16,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -16,6 +16,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider'
import { normalizeUrl, cleanUrl } from '@/lib/url'
import logger from '@/lib/logger'
import postEditorCache from '@/services/post-editor-cache.service'
import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types'
@ -148,7 +149,7 @@ export default function PostContent({ @@ -148,7 +149,7 @@ export default function PostContent({
// In a real implementation, you'd also resolve @ mentions to pubkeys
setExtractedMentions(nostrPubkeys)
} catch (error) {
console.error('Error extracting mentions:', error)
logger.error('Error extracting mentions', { error })
setExtractedMentions([])
}
}, [])
@ -173,7 +174,7 @@ export default function PostContent({ @@ -173,7 +174,7 @@ export default function PostContent({
e?.stopPropagation()
checkLogin(async () => {
if (!canPost) {
console.log('❌ Cannot post - canPost is false')
logger.warn('Attempted to post while canPost is false')
return
}
@ -326,8 +327,8 @@ export default function PostContent({ @@ -326,8 +327,8 @@ export default function PostContent({
addReplies([cleanEvent])
close()
} catch (error) {
console.error('Publishing error:', error)
console.error('Error details:', {
logger.error('Publishing error', { error })
logger.error('Publishing error details', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined

5
src/components/PostEditor/PostRelaySelector.tsx

@ -12,6 +12,7 @@ import relaySelectionService from '@/services/relay-selection.service' @@ -12,6 +12,7 @@ import relaySelectionService from '@/services/relay-selection.service'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import logger from '@/lib/logger'
export default function PostRelaySelector({
parentEvent: _parentEvent,
@ -109,7 +110,7 @@ export default function PostRelaySelector({ @@ -109,7 +110,7 @@ export default function PostRelaySelector({
}
} catch (error) {
console.error('Failed to update relay selection:', error)
logger.error('Failed to update relay selection', { error })
setSelectableRelays([])
if (!hasManualSelection) {
setSelectedRelayUrls([])
@ -168,7 +169,7 @@ export default function PostRelaySelector({ @@ -168,7 +169,7 @@ export default function PostRelaySelector({
}
} catch (error) {
console.error('Failed to update relay selection:', error)
logger.error('Failed to update relay selection', { error })
} finally {
setIsLoading(false)
}

3
src/components/PostEditor/PostTextarea/ClipboardAndDropHandler.ts

@ -2,6 +2,7 @@ import mediaUpload from '@/services/media-upload.service' @@ -2,6 +2,7 @@ import mediaUpload from '@/services/media-upload.service'
import { Extension } from '@tiptap/core'
import { EditorView } from '@tiptap/pm/view'
import { Plugin, TextSelection } from 'prosemirror-state'
import logger from '@/lib/logger'
const DRAGOVER_CLASS_LIST = [
'outline-2',
@ -177,7 +178,7 @@ async function uploadFiles( @@ -177,7 +178,7 @@ async function uploadFiles(
}
})
.catch((error) => {
console.error('Upload failed:', error)
logger.error('Clipboard/drop upload failed', { error, file: file.name })
options.onUploadEnd?.(file)
const tr = view.state.tr

3
src/components/PostEditor/Uploader.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import mediaUpload, { UPLOAD_ABORTED_ERROR_MSG } from '@/services/media-upload.service'
import { useRef } from 'react'
import { toast } from 'sonner'
import logger from '@/lib/logger'
export default function Uploader({
children,
@ -42,7 +43,7 @@ export default function Uploader({ @@ -42,7 +43,7 @@ export default function Uploader({
onUploadSuccess(result)
onUploadEnd?.(file)
} catch (error) {
console.error('Error uploading file', error)
logger.error('Error uploading file', { error, file: file.name })
const message = (error as Error).message
if (message !== UPLOAD_ABORTED_ERROR_MSG) {
toast.error(`Failed to upload file: ${message}`)

275
src/components/Profile/ProfileArticles.tsx

@ -1,12 +1,9 @@ @@ -1,12 +1,9 @@
import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useState, forwardRef, useImperativeHandle, useMemo } from 'react'
import { ExtendedKind } from '@/constants'
import NoteCard from '@/components/NoteCard'
import { Skeleton } from '@/components/ui/skeleton'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useProfileTimeline } from '@/hooks/useProfileTimeline'
import { Event, kinds } from 'nostr-tools'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
interface ProfileArticlesProps {
pubkey: string
@ -16,195 +13,81 @@ interface ProfileArticlesProps { @@ -16,195 +13,81 @@ interface ProfileArticlesProps {
onEventsChange?: (events: Event[]) => void
}
const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event[] }, ProfileArticlesProps>(({ pubkey, topSpace, searchQuery = '', kindFilter = 'all', onEventsChange }, ref) => {
const [events, setEvents] = useState<Event[]>([])
const [isLoading, setIsLoading] = useState(true)
const [retryCount, setRetryCount] = useState(0)
const [isRetrying, setIsRetrying] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
const { favoriteRelays } = useFavoriteRelays()
const maxRetries = 3
// Build comprehensive relay list including user's personal relays
const buildComprehensiveRelayList = useCallback(async () => {
try {
// Get user's relay list (kind 10002)
const userRelayList = await client.fetchRelayList(pubkey)
// Get all relays: user's + fast read + favorite relays
const allRelays = [
...(userRelayList.read || []), // User's read relays
...(userRelayList.write || []), // User's write relays
...FAST_READ_RELAY_URLS, // Fast read relays
...(favoriteRelays || []) // User's favorite relays
]
// Normalize URLs and remove duplicates
const normalizedRelays = allRelays
.map(url => normalizeUrl(url))
.filter((url): url is string => !!url)
const uniqueRelays = Array.from(new Set(normalizedRelays))
return uniqueRelays
} catch (error) {
return FAST_READ_RELAY_URLS
}
}, [pubkey, favoriteRelays])
const fetchArticles = useCallback(async (isRetry = false, isRefresh = false) => {
if (!pubkey) {
setEvents([])
setIsLoading(false)
return
}
try {
if (!isRetry && !isRefresh) {
setIsLoading(true)
setRetryCount(0)
} else if (isRetry) {
setIsRetrying(true)
} else if (isRefresh) {
setIsRefreshing(true)
}
// Build comprehensive relay list including user's personal relays
const comprehensiveRelays = await buildComprehensiveRelayList()
// Fetch longform articles (kind 30023), wiki articles (kinds 30817, 30818), publications (kind 30040), and highlights (kind 9802)
const allEvents = await client.fetchEvents(comprehensiveRelays, {
authors: [pubkey],
kinds: [kinds.LongFormArticle, 30817, 30818, 30040, kinds.Highlights], // LongFormArticle, WikiArticle (markdown), WikiArticle (asciidoc), Publication, and Highlights
limit: 100
})
const eventsToShow = allEvents
const ARTICLE_KINDS = [
kinds.LongFormArticle,
ExtendedKind.WIKI_ARTICLE_MARKDOWN,
ExtendedKind.WIKI_ARTICLE,
ExtendedKind.PUBLICATION,
kinds.Highlights
]
// Sort by creation time (newest first)
eventsToShow.sort((a, b) => b.created_at - a.created_at)
const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event[] }, ProfileArticlesProps>(
({ pubkey, topSpace, searchQuery = '', kindFilter = 'all', onEventsChange }, ref) => {
const [isRefreshing, setIsRefreshing] = useState(false)
// If initial load returns 0 events but it's not a retry, wait and retry once
// This handles cases where relays return "too many concurrent REQS" and return empty results
if (!isRetry && !isRefresh && eventsToShow.length === 0 && retryCount === 0) {
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchArticles(true)
}, 2000) // Wait 2 seconds before retry to let relays recover
return
}
const cacheKey = useMemo(() => `${pubkey}-articles`, [pubkey])
if (isRefresh) {
// For refresh, append new events and deduplicate
setEvents(prevEvents => {
const existingIds = new Set(prevEvents.map(e => e.id))
const newEvents = eventsToShow.filter(event => !existingIds.has(event.id))
const combinedEvents = [...newEvents, ...prevEvents]
// Re-sort the combined events
return combinedEvents.sort((a, b) => b.created_at - a.created_at)
const { events: timelineEvents, isLoading, refresh } = useProfileTimeline({
pubkey,
cacheKey,
kinds: ARTICLE_KINDS,
limit: 200
})
} else {
// For initial load or retry, replace events
setEvents(eventsToShow)
}
// Reset retry count on successful fetch
if (isRetry) {
setRetryCount(0)
}
} catch (error) {
console.error('[ProfileArticles] Error fetching events:', error)
logger.component('ProfileArticles', 'Initialization failed', { pubkey, error: (error as Error).message, retryCount: isRetry ? retryCount + 1 : 0 })
useEffect(() => {
onEventsChange?.(timelineEvents)
}, [timelineEvents, onEventsChange])
// If this is not a retry and we haven't exceeded max retries, schedule a retry
if (!isRetry && retryCount < maxRetries) {
// Use shorter delays for initial retries, then exponential backoff
const delay = retryCount === 0 ? 1000 : retryCount === 1 ? 2000 : 3000
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchArticles(true)
}, delay)
} else {
setEvents([])
}
} finally {
setIsLoading(false)
setIsRetrying(false)
useEffect(() => {
if (!isLoading) {
setIsRefreshing(false)
}
}, [pubkey, buildComprehensiveRelayList, maxRetries])
}, [isLoading])
// Expose refresh function to parent component
const refresh = useCallback(() => {
setRetryCount(0)
useImperativeHandle(
ref,
() => ({
refresh: () => {
setIsRefreshing(true)
fetchArticles(false, true) // isRetry = false, isRefresh = true
}, [fetchArticles])
useImperativeHandle(ref, () => ({
refresh,
getEvents: () => events
}), [refresh, events])
refresh()
},
getEvents: () => timelineEvents
}),
[refresh, timelineEvents]
)
// Notify parent of events changes
useEffect(() => {
if (onEventsChange) {
onEventsChange(events)
const eventsFilteredByKind = useMemo(() => {
if (kindFilter === 'all') {
return timelineEvents
}
const kindNumber = parseInt(kindFilter, 10)
if (Number.isNaN(kindNumber)) {
return timelineEvents
}
}, [events, onEventsChange])
return timelineEvents.filter((event) => event.kind === kindNumber)
}, [timelineEvents, kindFilter])
// Filter events based on search query and kind filter
const filteredEvents = useMemo(() => {
let filtered = events
// Filter by kind first
if (kindFilter && kindFilter !== 'all') {
const kindFilterNum = parseInt(kindFilter)
if (!isNaN(kindFilterNum)) {
filtered = filtered.filter(event => event.kind === kindFilterNum)
if (!searchQuery.trim()) {
return eventsFilteredByKind
}
}
// Then filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter(event =>
return eventsFilteredByKind.filter(
(event) =>
event.content.toLowerCase().includes(query) ||
event.tags.some(tag =>
tag.length > 1 && tag[1]?.toLowerCase().includes(query)
event.tags.some((tag) => tag.length > 1 && tag[1]?.toLowerCase().includes(query))
)
)
}
}, [eventsFilteredByKind, searchQuery])
return filtered
}, [events, searchQuery, kindFilter])
// Separate effect for initial fetch only with a small delay
// Separate effect for initial fetch only - delay slightly to avoid race conditions with other tabs
useEffect(() => {
if (pubkey) {
// Small delay to stagger initial fetches across tabs and allow relay list cache to populate
const timeoutId = setTimeout(() => {
fetchArticles()
}, 150) // 150ms delay (slightly longer than posts) to allow previous fetches to populate cache
return () => clearTimeout(timeoutId)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pubkey]) // Only depend on pubkey - fetchArticles is stable from useCallback
if (isLoading || isRetrying) {
return (
<div className="space-y-2">
{isRetrying && retryCount > 0 && (
<div className="text-center py-2 text-sm text-muted-foreground">
Retrying... ({retryCount}/{maxRetries})
</div>
)}
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
const getKindLabel = (kindValue: string) => {
if (!kindValue || kindValue === 'all') return 'articles, publications, or highlights'
const kindNum = parseInt(kindValue, 10)
if (kindNum === kinds.LongFormArticle) return 'long form articles'
if (kindNum === ExtendedKind.WIKI_ARTICLE_MARKDOWN) return 'wiki articles (markdown)'
if (kindNum === ExtendedKind.WIKI_ARTICLE) return 'wiki articles (asciidoc)'
if (kindNum === ExtendedKind.PUBLICATION) return 'publications'
if (kindNum === kinds.Highlights) return 'highlights'
return 'items'
}
if (!pubkey) {
@ -215,27 +98,17 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event @@ -215,27 +98,17 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event
)
}
if (events.length === 0) {
if (isLoading && timelineEvents.length === 0) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No articles, publications, or highlights found</div>
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
// Get kind label for display
const getKindLabel = (kindValue: string) => {
if (!kindValue || kindValue === 'all') return 'articles, publications, or highlights'
const kindNum = parseInt(kindValue)
if (kindNum === kinds.LongFormArticle) return 'long form articles'
if (kindNum === ExtendedKind.WIKI_ARTICLE_MARKDOWN) return 'wiki articles (markdown)'
if (kindNum === ExtendedKind.WIKI_ARTICLE) return 'wiki articles (asciidoc)'
if (kindNum === ExtendedKind.PUBLICATION) return 'publications'
if (kindNum === kinds.Highlights) return 'highlights'
return 'items'
}
if (filteredEvents.length === 0 && (searchQuery.trim() || (kindFilter && kindFilter !== 'all'))) {
if (!filteredEvents.length && !isLoading) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">
@ -250,28 +123,22 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event @@ -250,28 +123,22 @@ const ProfileArticles = forwardRef<{ refresh: () => void; getEvents: () => Event
return (
<div style={{ marginTop: topSpace || 0 }}>
{isRefreshing && (
<div className="px-4 py-2 text-sm text-green-500 text-center">
🔄 Refreshing articles...
</div>
<div className="px-4 py-2 text-sm text-green-500 text-center">🔄 Refreshing articles...</div>
)}
{(searchQuery.trim() || (kindFilter && kindFilter !== 'all')) && (
<div className="px-4 py-2 text-sm text-muted-foreground">
{filteredEvents.length} of {events.length} {getKindLabel(kindFilter)}
{filteredEvents.length} of {eventsFilteredByKind.length} {getKindLabel(kindFilter)}
</div>
)}
<div className="space-y-2">
{filteredEvents.map((event) => (
<NoteCard
key={event.id}
className="w-full"
event={event}
filterMutedNotes={false}
/>
<NoteCard key={event.id} className="w-full" event={event} filterMutedNotes={false} />
))}
</div>
</div>
)
})
}
)
ProfileArticles.displayName = 'ProfileArticles'

15
src/components/Profile/ProfileBookmarksAndHashtags.tsx

@ -182,7 +182,10 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, { @@ -182,7 +182,10 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, {
// If this is not a retry and we haven't exceeded max retries, schedule a retry
if (!isRetry && retryCountBookmarks < maxRetries) {
console.log('[ProfileBookmarksAndHashtags] Scheduling bookmark retry', retryCountBookmarks + 1, 'of', maxRetries)
logger.debug('[ProfileBookmarksAndHashtags] Scheduling bookmark retry', {
attempt: retryCountBookmarks + 1,
maxRetries
})
// Use shorter delays for initial retries, then exponential backoff
const delay = retryCountBookmarks === 0 ? 1000 : retryCountBookmarks === 1 ? 2000 : 3000
setTimeout(() => {
@ -280,7 +283,10 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, { @@ -280,7 +283,10 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, {
// If this is not a retry and we haven't exceeded max retries, schedule a retry
if (!isRetry && retryCountHashtags < maxRetries) {
console.log('[ProfileBookmarksAndHashtags] Scheduling hashtag retry', retryCountHashtags + 1, 'of', maxRetries)
logger.debug('[ProfileBookmarksAndHashtags] Scheduling hashtag retry', {
attempt: retryCountHashtags + 1,
maxRetries
})
// Use shorter delays for initial retries, then exponential backoff
const delay = retryCountHashtags === 0 ? 1000 : retryCountHashtags === 1 ? 2000 : 3000
setTimeout(() => {
@ -421,7 +427,10 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, { @@ -421,7 +427,10 @@ const ProfileBookmarksAndHashtags = forwardRef<{ refresh: () => void }, {
// If this is not a retry and we haven't exceeded max retries, schedule a retry
if (!isRetry && retryCountPins < maxRetries) {
console.log('[ProfileBookmarksAndHashtags] Scheduling pin retry', retryCountPins + 1, 'of', maxRetries)
logger.debug('[ProfileBookmarksAndHashtags] Scheduling pin retry', {
attempt: retryCountPins + 1,
maxRetries
})
// Use shorter delays for initial retries, then exponential backoff
const delay = retryCountPins === 0 ? 1000 : retryCountPins === 1 ? 2000 : 3000
setTimeout(() => {

251
src/components/Profile/ProfileFeed.tsx

@ -1,189 +1,100 @@ @@ -1,189 +1,100 @@
import { FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import { Event } from 'nostr-tools'
import { useCallback, useEffect, useState, forwardRef, useImperativeHandle, useMemo } from 'react'
import { ExtendedKind } from '@/constants'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import NoteCard from '@/components/NoteCard'
import { Skeleton } from '@/components/ui/skeleton'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { kinds, Event } from 'nostr-tools'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { useProfileTimeline } from '@/hooks/useProfileTimeline'
import { useZap } from '@/providers/ZapProvider'
interface ProfileFeedProps {
pubkey: string
topSpace?: number
searchQuery?: string
kindFilter?: string
onEventsChange?: (events: Event[]) => void
}
const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pubkey, topSpace, searchQuery = '' }, ref) => {
const [events, setEvents] = useState<Event[]>([])
const [isLoading, setIsLoading] = useState(true)
const [retryCount, setRetryCount] = useState(0)
const [isRetrying, setIsRetrying] = useState(false)
const POST_KIND_LIST = [
kinds.ShortTextNote,
kinds.Repost,
ExtendedKind.COMMENT,
ExtendedKind.DISCUSSION,
ExtendedKind.POLL,
ExtendedKind.ZAP_RECEIPT
]
const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(
({ pubkey, topSpace, searchQuery = '', kindFilter = 'all', onEventsChange }, ref) => {
const { zapReplyThreshold } = useZap()
const [isRefreshing, setIsRefreshing] = useState(false)
const { favoriteRelays } = useFavoriteRelays()
const maxRetries = 3
// Build comprehensive relay list including user's personal relays
const buildComprehensiveRelayList = useCallback(async () => {
try {
// Get user's relay list (kind 10002)
const userRelayList = await client.fetchRelayList(pubkey)
// Get all relays: user's + fast read + favorite relays
const allRelays = [
...(userRelayList.read || []), // User's read relays
...(userRelayList.write || []), // User's write relays
...FAST_READ_RELAY_URLS, // Fast read relays
...(favoriteRelays || []) // User's favorite relays
]
// Normalize URLs and remove duplicates
const normalizedRelays = allRelays
.map(url => normalizeUrl(url))
.filter((url): url is string => !!url)
const uniqueRelays = Array.from(new Set(normalizedRelays))
return uniqueRelays
} catch (error) {
return FAST_READ_RELAY_URLS
}
}, [pubkey, favoriteRelays])
const fetchPosts = useCallback(async (isRetry = false, isRefresh = false) => {
if (!pubkey) {
setEvents([])
setIsLoading(false)
return
const filterPredicate = useMemo(
() => (event: Event) => {
if (event.kind === ExtendedKind.ZAP_RECEIPT) {
const zapInfo = getZapInfoFromEvent(event)
if (!zapInfo?.amount || zapInfo.amount < zapReplyThreshold) {
return false
}
try {
if (!isRetry && !isRefresh) {
setIsLoading(true)
setRetryCount(0)
} else if (isRetry) {
setIsRetrying(true)
} else if (isRefresh) {
setIsRefreshing(true)
}
return true
},
[zapReplyThreshold]
)
// Build comprehensive relay list including user's personal relays
const comprehensiveRelays = await buildComprehensiveRelayList()
const cacheKey = useMemo(() => `${pubkey}-posts-${zapReplyThreshold}`, [pubkey, zapReplyThreshold])
// Now try to fetch text notes specifically
const allEvents = await client.fetchEvents(comprehensiveRelays, {
authors: [pubkey],
kinds: [1], // Text notes only
limit: 100
const { events: timelineEvents, isLoading, refresh } = useProfileTimeline({
pubkey,
cacheKey,
kinds: POST_KIND_LIST,
limit: 200,
filterPredicate
})
const eventsToShow = allEvents
// Sort by creation time (newest first)
eventsToShow.sort((a, b) => b.created_at - a.created_at)
useEffect(() => {
onEventsChange?.(timelineEvents)
}, [timelineEvents, onEventsChange])
// If initial load returns 0 events but it's not a retry, wait and retry once
// This handles cases where relays return "too many concurrent REQS" and return empty results
if (!isRetry && !isRefresh && eventsToShow.length === 0 && retryCount === 0) {
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchPosts(true)
}, 2000) // Wait 2 seconds before retry to let relays recover
return
useEffect(() => {
if (!isLoading) {
setIsRefreshing(false)
}
}, [isLoading])
if (isRefresh) {
// For refresh, append new events and deduplicate
setEvents(prevEvents => {
const existingIds = new Set(prevEvents.map(e => e.id))
const newEvents = eventsToShow.filter(event => !existingIds.has(event.id))
const combinedEvents = [...newEvents, ...prevEvents]
// Re-sort the combined events
return combinedEvents.sort((a, b) => b.created_at - a.created_at)
})
} else {
// For initial load or retry, replace events
setEvents(eventsToShow)
useImperativeHandle(
ref,
() => ({
refresh: () => {
setIsRefreshing(true)
refresh()
}
}),
[refresh]
)
// Reset retry count on successful fetch
if (isRetry) {
setRetryCount(0)
}
} catch (error) {
console.error('[ProfileFeed] Error fetching events:', error)
logger.component('ProfileFeed', 'Initialization failed', { pubkey, error: (error as Error).message, retryCount: isRetry ? retryCount + 1 : 0 })
// If this is not a retry and we haven't exceeded max retries, schedule a retry
if (!isRetry && retryCount < maxRetries) {
// Use shorter delays for initial retries, then exponential backoff
const delay = retryCount === 0 ? 1000 : retryCount === 1 ? 2000 : 3000
setTimeout(() => {
setRetryCount(prev => prev + 1)
fetchPosts(true)
}, delay)
} else {
setEvents([])
const eventsFilteredByKind = useMemo(() => {
if (kindFilter === 'all') {
return timelineEvents
}
} finally {
setIsLoading(false)
setIsRetrying(false)
setIsRefreshing(false)
const kindNumber = parseInt(kindFilter, 10)
if (Number.isNaN(kindNumber)) {
return timelineEvents
}
}, [pubkey, buildComprehensiveRelayList, maxRetries])
// Expose refresh function to parent component
const refresh = useCallback(() => {
setRetryCount(0)
setIsRefreshing(true)
fetchPosts(false, true) // isRetry = false, isRefresh = true
}, [fetchPosts])
return timelineEvents.filter((event) => event.kind === kindNumber)
}, [timelineEvents, kindFilter])
useImperativeHandle(ref, () => ({
refresh
}), [refresh])
// Filter events based on search query
const filteredEvents = useMemo(() => {
if (!searchQuery.trim()) {
return events
return eventsFilteredByKind
}
const query = searchQuery.toLowerCase()
return events.filter(event =>
return eventsFilteredByKind.filter(
(event) =>
event.content.toLowerCase().includes(query) ||
event.tags.some(tag =>
tag.length > 1 && tag[1]?.toLowerCase().includes(query)
)
event.tags.some((tag) => tag.length > 1 && tag[1]?.toLowerCase().includes(query))
)
}, [events, searchQuery])
// Separate effect for initial fetch only - delay slightly to avoid race conditions with other tabs
useEffect(() => {
if (pubkey) {
// Small delay to stagger initial fetches across tabs and allow relay list cache to populate
const timeoutId = setTimeout(() => {
fetchPosts()
}, 100) // 100ms delay to allow previous fetches to populate cache
return () => clearTimeout(timeoutId)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pubkey]) // Only depend on pubkey - fetchPosts is stable from useCallback
if (isLoading || isRetrying) {
return (
<div className="space-y-2">
{isRetrying && retryCount > 0 && (
<div className="text-center py-2 text-sm text-muted-foreground">
Retrying... ({retryCount}/{maxRetries})
</div>
)}
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
}, [eventsFilteredByKind, searchQuery])
if (!pubkey) {
return (
@ -193,18 +104,22 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub @@ -193,18 +104,22 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub
)
}
if (events.length === 0) {
if (isLoading && timelineEvents.length === 0) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No posts found</div>
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
if (filteredEvents.length === 0 && searchQuery.trim()) {
if (!filteredEvents.length && !isLoading) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No posts match your search</div>
<div className="text-sm text-muted-foreground">
{searchQuery.trim() ? 'No posts match your search' : 'No posts found'}
</div>
</div>
)
}
@ -212,28 +127,22 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub @@ -212,28 +127,22 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, ProfileFeedProps>(({ pub
return (
<div style={{ marginTop: topSpace || 0 }}>
{isRefreshing && (
<div className="px-4 py-2 text-sm text-green-500 text-center">
🔄 Refreshing posts...
</div>
<div className="px-4 py-2 text-sm text-green-500 text-center">🔄 Refreshing posts...</div>
)}
{searchQuery.trim() && (
<div className="px-4 py-2 text-sm text-muted-foreground">
{filteredEvents.length} of {events.length} posts
{filteredEvents.length} of {eventsFilteredByKind.length} posts
</div>
)}
<div className="space-y-2">
{filteredEvents.map((event) => (
<NoteCard
key={event.id}
className="w-full"
event={event}
filterMutedNotes={false}
/>
<NoteCard key={event.id} className="w-full" event={event} filterMutedNotes={false} />
))}
</div>
</div>
)
})
}
)
ProfileFeed.displayName = 'ProfileFeed'

146
src/components/Profile/ProfileMedia.tsx

@ -0,0 +1,146 @@ @@ -0,0 +1,146 @@
import NoteCard from '@/components/NoteCard'
import { Skeleton } from '@/components/ui/skeleton'
import { useProfileTimeline } from '@/hooks/useProfileTimeline'
import { Event } from 'nostr-tools'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { ExtendedKind } from '@/constants'
interface ProfileMediaProps {
pubkey: string
topSpace?: number
searchQuery?: string
kindFilter?: string
onEventsChange?: (events: Event[]) => void
}
const MEDIA_KIND_LIST = [
ExtendedKind.PICTURE,
ExtendedKind.VIDEO,
ExtendedKind.SHORT_VIDEO,
ExtendedKind.VOICE,
ExtendedKind.VOICE_COMMENT
]
const ProfileMedia = forwardRef<{ refresh: () => void; getEvents: () => Event[] }, ProfileMediaProps>(
({ pubkey, topSpace, searchQuery = '', kindFilter = 'all', onEventsChange }, ref) => {
const [isRefreshing, setIsRefreshing] = useState(false)
const cacheKey = useMemo(() => `${pubkey}-media`, [pubkey])
const { events: timelineEvents, isLoading, refresh } = useProfileTimeline({
pubkey,
cacheKey,
kinds: MEDIA_KIND_LIST,
limit: 200
})
useEffect(() => {
onEventsChange?.(timelineEvents)
}, [timelineEvents, onEventsChange])
useEffect(() => {
if (!isLoading) {
setIsRefreshing(false)
}
}, [isLoading])
useImperativeHandle(
ref,
() => ({
refresh: () => {
setIsRefreshing(true)
refresh()
},
getEvents: () => timelineEvents
}),
[refresh, timelineEvents]
)
const eventsFilteredByKind = useMemo(() => {
if (kindFilter === 'all') {
return timelineEvents
}
const kindNumber = parseInt(kindFilter, 10)
if (Number.isNaN(kindNumber)) {
return timelineEvents
}
return timelineEvents.filter((event) => event.kind === kindNumber)
}, [timelineEvents, kindFilter])
const filteredEvents = useMemo(() => {
if (!searchQuery.trim()) {
return eventsFilteredByKind
}
const query = searchQuery.toLowerCase()
return eventsFilteredByKind.filter(
(event) =>
event.content.toLowerCase().includes(query) ||
event.tags.some((tag) => tag.length > 1 && tag[1]?.toLowerCase().includes(query))
)
}, [eventsFilteredByKind, searchQuery])
const getKindLabel = (kindValue: string) => {
if (!kindValue || kindValue === 'all') return 'media items'
const kindNum = parseInt(kindValue, 10)
if (kindNum === ExtendedKind.PICTURE) return 'photos'
if (kindNum === ExtendedKind.VIDEO) return 'videos'
if (kindNum === ExtendedKind.SHORT_VIDEO) return 'short videos'
if (kindNum === ExtendedKind.VOICE) return 'voice posts'
if (kindNum === ExtendedKind.VOICE_COMMENT) return 'voice comments'
return 'media'
}
if (!pubkey) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">No profile selected</div>
</div>
)
}
if (isLoading && timelineEvents.length === 0) {
return (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
if (!filteredEvents.length && !isLoading) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-sm text-muted-foreground">
{searchQuery.trim()
? `No ${getKindLabel(kindFilter)} match your search`
: `No ${getKindLabel(kindFilter)} found`}
</div>
</div>
)
}
return (
<div style={{ marginTop: topSpace || 0 }}>
{isRefreshing && (
<div className="px-4 py-2 text-sm text-green-500 text-center">🔄 Refreshing media...</div>
)}
{(searchQuery.trim() || (kindFilter && kindFilter !== 'all')) && (
<div className="px-4 py-2 text-sm text-muted-foreground">
{filteredEvents.length} of {eventsFilteredByKind.length} {getKindLabel(kindFilter)}
</div>
)}
<div className="space-y-2">
{filteredEvents.map((event) => (
<NoteCard key={event.id} className="w-full" event={event} filterMutedNotes={false} />
))}
</div>
</div>
)
}
)
ProfileMedia.displayName = 'ProfileMedia'
export default ProfileMedia

103
src/components/Profile/index.tsx

@ -30,7 +30,7 @@ import { toNoteList } from '@/lib/link' @@ -30,7 +30,7 @@ import { toNoteList } from '@/lib/link'
import { parseAdvancedSearch } from '@/lib/search-parser'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { FileText, Link, Zap } from 'lucide-react'
import { FileText, Link, Zap, Film } from 'lucide-react'
import { useEffect, useMemo, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
@ -42,8 +42,9 @@ import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags' @@ -42,8 +42,9 @@ import ProfileBookmarksAndHashtags from './ProfileBookmarksAndHashtags'
import SmartFollowings from './SmartFollowings'
import SmartMuteLink from './SmartMuteLink'
import SmartRelays from './SmartRelays'
import ProfileMedia from './ProfileMedia'
type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles'
type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles' | 'media'
export default function Profile({ id }: { id?: string }) {
const { t } = useTranslation()
@ -53,6 +54,8 @@ export default function Profile({ id }: { id?: string }) { @@ -53,6 +54,8 @@ export default function Profile({ id }: { id?: string }) {
const [activeTab, setActiveTab] = useState<ProfileTabValue>('posts')
const [searchQuery, setSearchQuery] = useState('')
const [articleKindFilter, setArticleKindFilter] = useState<string>('all')
const [postKindFilter, setPostKindFilter] = useState<string>('all')
const [mediaKindFilter, setMediaKindFilter] = useState<string>('all')
// Handle search in articles tab - parse advanced search parameters
const handleArticleSearch = (query: string) => {
@ -140,7 +143,10 @@ export default function Profile({ id }: { id?: string }) { @@ -140,7 +143,10 @@ export default function Profile({ id }: { id?: string }) {
const profileFeedRef = useRef<{ refresh: () => void }>(null)
const profileBookmarksRef = useRef<{ refresh: () => void }>(null)
const profileArticlesRef = useRef<{ refresh: () => void; getEvents: () => Event[] }>(null)
const profileMediaRef = useRef<{ refresh: () => void; getEvents: () => Event[] }>(null)
const [articleEvents, setArticleEvents] = useState<Event[]>([])
const [postEvents, setPostEvents] = useState<Event[]>([])
const [mediaEvents, setMediaEvents] = useState<Event[]>([])
const isFollowingYou = useMemo(() => {
// This will be handled by the FollowedBy component
@ -158,6 +164,8 @@ export default function Profile({ id }: { id?: string }) { @@ -158,6 +164,8 @@ export default function Profile({ id }: { id?: string }) {
profileFeedRef.current?.refresh()
} else if (activeTab === 'articles') {
profileArticlesRef.current?.refresh()
} else if (activeTab === 'media') {
profileMediaRef.current?.refresh()
} else {
profileBookmarksRef.current?.refresh()
}
@ -173,6 +181,10 @@ export default function Profile({ id }: { id?: string }) { @@ -173,6 +181,10 @@ export default function Profile({ id }: { id?: string }) {
value: 'articles',
label: 'Articles'
},
{
value: 'media',
label: 'Media'
},
{
value: 'pins',
label: 'Pins'
@ -317,23 +329,51 @@ export default function Profile({ id }: { id?: string }) { @@ -317,23 +329,51 @@ export default function Profile({ id }: { id?: string }) {
<div className="flex items-center gap-2 pr-2 px-1">
<ProfileSearchBar
onSearch={activeTab === 'articles' ? handleArticleSearch : setSearchQuery}
placeholder={`Search ${activeTab}...`}
placeholder={`Search ${
activeTab === 'posts' ? 'posts' : activeTab === 'media' ? 'media' : activeTab
}...`}
className="w-64"
/>
{activeTab === 'posts' && (() => {
const allCount = postEvents.length
const noteCount = postEvents.filter((event) => event.kind === kinds.ShortTextNote).length
const repostCount = postEvents.filter((event) => event.kind === kinds.Repost).length
const commentCount = postEvents.filter((event) => event.kind === ExtendedKind.COMMENT).length
const discussionCount = postEvents.filter((event) => event.kind === ExtendedKind.DISCUSSION).length
const pollCount = postEvents.filter((event) => event.kind === ExtendedKind.POLL).length
const superzapCount = postEvents.filter((event) => event.kind === ExtendedKind.ZAP_RECEIPT).length
return (
<Select value={postKindFilter} onValueChange={setPostKindFilter}>
<SelectTrigger className="w-48">
<FileText className="h-4 w-4 mr-2 shrink-0" />
<SelectValue placeholder="Filter posts" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Posts ({allCount})</SelectItem>
<SelectItem value={String(kinds.ShortTextNote)}>Notes ({noteCount})</SelectItem>
<SelectItem value={String(kinds.Repost)}>Reposts ({repostCount})</SelectItem>
<SelectItem value={String(ExtendedKind.COMMENT)}>Comments ({commentCount})</SelectItem>
<SelectItem value={String(ExtendedKind.DISCUSSION)}>Discussions ({discussionCount})</SelectItem>
<SelectItem value={String(ExtendedKind.POLL)}>Polls ({pollCount})</SelectItem>
<SelectItem value={String(ExtendedKind.ZAP_RECEIPT)}>Superzaps ({superzapCount})</SelectItem>
</SelectContent>
</Select>
)
})()}
{activeTab === 'articles' && (() => {
// Calculate counts for each kind
const allCount = articleEvents.length
const longFormCount = articleEvents.filter(e => e.kind === kinds.LongFormArticle).length
const wikiMarkdownCount = articleEvents.filter(e => e.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN).length
const wikiAsciiDocCount = articleEvents.filter(e => e.kind === ExtendedKind.WIKI_ARTICLE).length
const publicationCount = articleEvents.filter(e => e.kind === ExtendedKind.PUBLICATION).length
const highlightsCount = articleEvents.filter(e => e.kind === kinds.Highlights).length
const longFormCount = articleEvents.filter((e) => e.kind === kinds.LongFormArticle).length
const wikiMarkdownCount = articleEvents.filter((e) => e.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN).length
const wikiAsciiDocCount = articleEvents.filter((e) => e.kind === ExtendedKind.WIKI_ARTICLE).length
const publicationCount = articleEvents.filter((e) => e.kind === ExtendedKind.PUBLICATION).length
const highlightsCount = articleEvents.filter((e) => e.kind === kinds.Highlights).length
return (
<Select value={articleKindFilter} onValueChange={setArticleKindFilter}>
<SelectTrigger className="w-48">
<FileText className="h-4 w-4 mr-2 shrink-0" />
<SelectValue placeholder="Filter by type" />
<SelectValue placeholder="Filter articles" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types ({allCount})</SelectItem>
@ -346,11 +386,32 @@ export default function Profile({ id }: { id?: string }) { @@ -346,11 +386,32 @@ export default function Profile({ id }: { id?: string }) {
</Select>
)
})()}
<RetroRefreshButton
onClick={handleRefresh}
size="sm"
className="flex-shrink-0"
/>
{activeTab === 'media' && (() => {
const allCount = mediaEvents.length
const pictureCount = mediaEvents.filter((event) => event.kind === ExtendedKind.PICTURE).length
const videoCount = mediaEvents.filter((event) => event.kind === ExtendedKind.VIDEO).length
const shortVideoCount = mediaEvents.filter((event) => event.kind === ExtendedKind.SHORT_VIDEO).length
const voiceCount = mediaEvents.filter((event) => event.kind === ExtendedKind.VOICE).length
const voiceCommentCount = mediaEvents.filter((event) => event.kind === ExtendedKind.VOICE_COMMENT).length
return (
<Select value={mediaKindFilter} onValueChange={setMediaKindFilter}>
<SelectTrigger className="w-52">
<Film className="h-4 w-4 mr-2 shrink-0" />
<SelectValue placeholder="Filter media" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Media ({allCount})</SelectItem>
<SelectItem value={String(ExtendedKind.PICTURE)}>Photos ({pictureCount})</SelectItem>
<SelectItem value={String(ExtendedKind.VIDEO)}>Videos ({videoCount})</SelectItem>
<SelectItem value={String(ExtendedKind.SHORT_VIDEO)}>Short Videos ({shortVideoCount})</SelectItem>
<SelectItem value={String(ExtendedKind.VOICE)}>Voice Posts ({voiceCount})</SelectItem>
<SelectItem value={String(ExtendedKind.VOICE_COMMENT)}>Voice Comments ({voiceCommentCount})</SelectItem>
</SelectContent>
</Select>
)
})()}
<RetroRefreshButton onClick={handleRefresh} size="sm" className="flex-shrink-0" />
</div>
</div>
{activeTab === 'posts' && (
@ -359,6 +420,8 @@ export default function Profile({ id }: { id?: string }) { @@ -359,6 +420,8 @@ export default function Profile({ id }: { id?: string }) {
pubkey={pubkey}
topSpace={0}
searchQuery={searchQuery}
kindFilter={postKindFilter}
onEventsChange={setPostEvents}
/>
)}
{activeTab === 'articles' && (
@ -371,6 +434,16 @@ export default function Profile({ id }: { id?: string }) { @@ -371,6 +434,16 @@ export default function Profile({ id }: { id?: string }) {
onEventsChange={setArticleEvents}
/>
)}
{activeTab === 'media' && (
<ProfileMedia
ref={profileMediaRef}
pubkey={pubkey}
topSpace={0}
searchQuery={searchQuery}
kindFilter={mediaKindFilter}
onEventsChange={setMediaEvents}
/>
)}
{(activeTab === 'pins' || activeTab === 'bookmarks' || activeTab === 'interests') && (
<ProfileBookmarksAndHashtags
ref={profileBookmarksRef}

3
src/components/RelayInfo/ReviewEditor.tsx

@ -7,6 +7,7 @@ import { NostrEvent } from 'nostr-tools' @@ -7,6 +7,7 @@ import { NostrEvent } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import logger from '@/lib/logger'
export default function ReviewEditor({
relayUrl,
@ -37,7 +38,7 @@ export default function ReviewEditor({ @@ -37,7 +38,7 @@ export default function ReviewEditor({
} else if (error instanceof Error) {
toast.error(`${t('Failed to review')}: ${error.message}`)
}
console.error(error)
logger.error('Failed to submit relay review', { error, relayUrl })
return
} finally {
setSubmitting(false)

5
src/components/SaveRelayDropdownMenu/index.tsx

@ -24,6 +24,7 @@ import { Ban, Check, FolderPlus, Loader2, Plus, Star } from 'lucide-react' @@ -24,6 +24,7 @@ import { Ban, Check, FolderPlus, Loader2, Plus, Star } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import DrawerMenuItem from '../DrawerMenuItem'
import logger from '@/lib/logger'
export default function SaveRelayDropdownMenu({
urls,
@ -130,7 +131,7 @@ function RelayItem({ urls }: { urls: string[] }) { @@ -130,7 +131,7 @@ function RelayItem({ urls }: { urls: string[] }) {
await addFavoriteRelays(urls)
}
} catch (error) {
console.error('Failed to toggle favorite relay:', error)
logger.error('Failed to toggle favorite relay', { error, url })
} finally {
setIsLoading(false)
}
@ -255,7 +256,7 @@ function BlockRelayItem({ urls }: { urls: string[] }) { @@ -255,7 +256,7 @@ function BlockRelayItem({ urls }: { urls: string[] }) {
await addBlockedRelays(urls)
}
} catch (error) {
console.error('Failed to toggle blocked relay:', error)
logger.error('Failed to toggle blocked relay', { error, url })
} finally {
setIsLoading(false)
}

131
src/components/TrendingNotes/index.tsx

@ -27,7 +27,7 @@ let cachedCustomEvents: { @@ -27,7 +27,7 @@ let cachedCustomEvents: {
// Flag to prevent concurrent initialization
let isInitializing = false
type TrendingTab = 'relays' | 'hashtags'
type TrendingTab = 'nostr' | 'relays' | 'hashtags'
type SortOrder = 'newest' | 'oldest' | 'most-popular' | 'least-popular'
type HashtagFilter = 'popular'
@ -38,10 +38,10 @@ export default function TrendingNotes() { @@ -38,10 +38,10 @@ export default function TrendingNotes() {
const { pubkey, relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const { zapReplyThreshold } = useZap()
const [trendingNotes] = useState<NostrEvent[]>([])
const [nostrEvents, setNostrEvents] = useState<NostrEvent[]>([])
const [nostrLoading, setNostrLoading] = useState(false)
const [showCount, setShowCount] = useState(10)
const [loading] = useState(true)
const [activeTab, setActiveTab] = useState<TrendingTab>('relays')
const [activeTab, setActiveTab] = useState<TrendingTab>('nostr')
const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular')
const [hashtagFilter] = useState<HashtagFilter>('popular')
const [selectedHashtag, setSelectedHashtag] = useState<string | null>(null)
@ -50,6 +50,25 @@ export default function TrendingNotes() { @@ -50,6 +50,25 @@ export default function TrendingNotes() {
const [cacheLoading, setCacheLoading] = useState(false)
const bottomRef = useRef<HTMLDivElement>(null)
// Load Nostr.band trending feed when tab is active
useEffect(() => {
const loadTrending = async () => {
try {
setNostrLoading(true)
const events = await client.fetchTrendingNotes()
setNostrEvents(events)
} catch (error) {
logger.warn('Failed to load nostr.band trending notes', error as Error)
} finally {
setNostrLoading(false)
}
}
if (activeTab === 'nostr' && nostrEvents.length === 0 && !nostrLoading) {
loadTrending()
}
}, [activeTab, nostrEvents.length, nostrLoading])
// Debug: Track cacheEvents changes
useEffect(() => {
logger.debug('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events')
@ -65,10 +84,10 @@ export default function TrendingNotes() { @@ -65,10 +84,10 @@ export default function TrendingNotes() {
// Calculate popular hashtags from cache events (all events from relays)
const calculatePopularHashtags = useMemo(() => {
logger.debug('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length)
logger.debug('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length, 'nostrEvents.length:', nostrEvents.length)
// Use cache events if available, otherwise fallback to trending notes
const eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : trendingNotes
const eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : nostrEvents
if (eventsToAnalyze.length === 0) {
return []
@ -112,7 +131,7 @@ export default function TrendingNotes() { @@ -112,7 +131,7 @@ export default function TrendingNotes() {
logger.debug('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags)
return result
}, [cacheEvents, trendingNotes, activeTab, hashtagFilter, pubkey]) // Use cacheEvents and trendingNotes as dependencies
}, [cacheEvents, nostrEvents, activeTab, hashtagFilter, pubkey])
// Get relays based on user login status
const getRelays = useMemo(() => {
@ -148,13 +167,13 @@ export default function TrendingNotes() { @@ -148,13 +167,13 @@ export default function TrendingNotes() {
setPopularHashtags(calculatePopularHashtags)
}, [calculatePopularHashtags])
// Fallback: populate cacheEvents from trendingNotes if cache is empty
// Fallback: populate cacheEvents from nostrEvents if cache is empty
useEffect(() => {
if (activeTab === 'hashtags' && cacheEvents.length === 0 && trendingNotes.length > 0) {
logger.debug('[TrendingNotes] Fallback: populating cacheEvents from trendingNotes')
setCacheEvents(trendingNotes)
if (activeTab === 'hashtags' && cacheEvents.length === 0 && nostrEvents.length > 0) {
logger.debug('[TrendingNotes] Fallback: populating cacheEvents from nostrEvents')
setCacheEvents(nostrEvents)
}
}, [activeTab, cacheEvents.length, trendingNotes])
}, [activeTab, cacheEvents.length, nostrEvents])
// Initialize cache only once on mount
@ -367,23 +386,9 @@ export default function TrendingNotes() { @@ -367,23 +386,9 @@ export default function TrendingNotes() {
}, []) // Only run once on mount to prevent infinite loop
const filteredEvents = useMemo(() => {
const relaysFilteredEvents = useMemo(() => {
const idSet = new Set<string>()
// Use appropriate data source based on tab and filter
let sourceEvents: NostrEvent[] = []
if (activeTab === 'relays') {
// "on your relays" tab: use cache events from user's relays
sourceEvents = cacheEvents
logger.debug('[TrendingNotes] Relays tab - cacheEvents.length:', cacheEvents.length, 'cacheLoading:', cacheLoading)
} else if (activeTab === 'hashtags') {
// Hashtags tab: use cache events for hashtag analysis
sourceEvents = cacheEvents.length > 0 ? cacheEvents : trendingNotes
logger.debug('[TrendingNotes] Hashtags tab - using ALL events from cache')
logger.debug('[TrendingNotes] Hashtags tab - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length)
}
const sourceEvents = cacheEvents.length > 0 ? cacheEvents : nostrEvents
const filtered = sourceEvents.filter((evt) => {
if (isEventDeleted(evt)) return false
@ -407,11 +412,6 @@ export default function TrendingNotes() { @@ -407,11 +412,6 @@ export default function TrendingNotes() {
if (!allHashtags.includes(selectedHashtag.toLowerCase())) return false
}
}
} else if (activeTab === 'relays') {
// For "on your relays" tab, we'll show all events (they're already from user's relays)
// This is the default behavior, so no additional filtering needed
}
const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
if (idSet.has(id)) {
return false
@ -466,7 +466,26 @@ export default function TrendingNotes() { @@ -466,7 +466,26 @@ export default function TrendingNotes() {
})
return filtered.slice(0, showCount)
}, [trendingNotes, hideUntrustedNotes, showCount, isEventDeleted, isUserTrusted, activeTab, hashtagFilter, selectedHashtag, sortOrder, zapReplyThreshold, cacheEvents])
}, [
cacheEvents,
nostrEvents,
hideUntrustedNotes,
showCount,
isEventDeleted,
isUserTrusted,
activeTab,
hashtagFilter,
selectedHashtag,
sortOrder,
zapReplyThreshold
])
const filteredEvents = useMemo(() => {
if (activeTab === 'nostr') {
return nostrEvents.slice(0, showCount)
}
return relaysFilteredEvents
}, [activeTab, nostrEvents, showCount, relaysFilteredEvents])
@ -491,7 +510,12 @@ export default function TrendingNotes() { @@ -491,7 +510,12 @@ export default function TrendingNotes() {
useEffect(() => {
if (showCount >= trendingNotes.length) return
const totalLength =
activeTab === 'nostr'
? nostrEvents.length
: cacheEvents.length
if (showCount >= totalLength) return
const options = {
root: null,
@ -516,7 +540,7 @@ export default function TrendingNotes() { @@ -516,7 +540,7 @@ export default function TrendingNotes() {
observerInstance.unobserve(currentBottomRef)
}
}
}, [loading, trendingNotes, showCount])
}, [activeTab, cacheEvents.length, nostrEvents.length, showCount, cacheLoading, nostrLoading])
return (
<div className="min-h-screen">
@ -527,6 +551,16 @@ export default function TrendingNotes() { @@ -527,6 +551,16 @@ export default function TrendingNotes() {
<div className="flex items-center gap-2 px-4 pb-2">
<span className="text-sm font-medium text-muted-foreground">Trending:</span>
<div className="flex gap-1">
<button
onClick={() => setActiveTab('nostr')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'nostr'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
on Nostr
</button>
<button
onClick={() => setActiveTab('relays')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
@ -631,6 +665,12 @@ export default function TrendingNotes() { @@ -631,6 +665,12 @@ export default function TrendingNotes() {
)}
</div>
{/* Show loading message for nostr tab */}
{activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && (
<div className="text-center text-sm text-muted-foreground mt-8">
Loading trending notes from nostr.band...
</div>
)}
{/* Show loading message for relays tab when cache is loading */}
{activeTab === 'relays' && cacheLoading && cacheEvents.length === 0 && (
<div className="text-center text-sm text-muted-foreground mt-8">
@ -642,17 +682,14 @@ export default function TrendingNotes() { @@ -642,17 +682,14 @@ export default function TrendingNotes() {
<NoteCard key={event.id} className="w-full" event={event} />
))}
{(() => {
// Determine the current data source length based on active tab
const currentDataLength = activeTab === 'relays' || activeTab === 'hashtags' ? cacheEvents.length :
trendingNotes.length
// Show loading if:
// 1. General loading state is true
// 2. For relays/hashtags tabs, if cache is loading
// 3. If we haven't reached the end of available data
const shouldShowLoading = loading ||
(activeTab === 'relays' && cacheLoading) ||
(activeTab === 'hashtags' && cacheLoading) ||
const currentDataLength =
activeTab === 'nostr'
? nostrEvents.length
: cacheEvents.length
const shouldShowLoading =
(activeTab === 'nostr' && nostrLoading) ||
((activeTab === 'relays' || activeTab === 'hashtags') && cacheLoading) ||
showCount < currentDataLength
if (shouldShowLoading) {

29
src/components/UniversalContent/EnhancedContent.tsx

@ -34,6 +34,33 @@ import ImageGallery from '../ImageGallery' @@ -34,6 +34,33 @@ import ImageGallery from '../ImageGallery'
import MediaPlayer from '../MediaPlayer'
import YoutubeEmbeddedPlayer from '../YoutubeEmbeddedPlayer'
import ParsedContent from './ParsedContent'
import { toNote } from '@/lib/link'
const REDIRECT_REGEX = /Read (naddr1[a-z0-9]+) instead\./i
function renderRedirectText(text: string, key: number) {
const match = text.match(REDIRECT_REGEX)
if (!match) {
return text
}
const [fullMatch, naddr] = match
const [prefix, suffix] = text.split(fullMatch)
const href = toNote(naddr)
return (
<span key={`redirect-${key}`}>
{prefix}
Read{' '}
<a
className="text-primary hover:underline"
href={href}
onClick={(e) => e.stopPropagation()}
>
{naddr}
</a>{' '}
instead.{suffix}
</span>
)
}
export default function EnhancedContent({
event,
@ -195,7 +222,7 @@ export default function EnhancedContent({ @@ -195,7 +222,7 @@ export default function EnhancedContent({
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
return renderRedirectText(node.data, index)
}
// Skip image nodes - they're rendered in the carousel at the top
if (node.type === 'image' || node.type === 'images') {

5
src/components/UniversalContent/HighlightSourcePreview.tsx

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { useMemo } from 'react'
import { nip19 } from 'nostr-tools'
import logger from '@/lib/logger'
import WebPreview from '../WebPreview'
import { EmbeddedNote } from '../Embedded/EmbeddedNote'
import { ExternalLink } from 'lucide-react'
@ -39,7 +40,7 @@ export default function HighlightSourcePreview({ source, className }: HighlightS @@ -39,7 +40,7 @@ export default function HighlightSourcePreview({ source, className }: HighlightS
)
}
} catch (error) {
console.warn('Failed to decode nostr event:', error)
logger.warn('Failed to decode nostr event', error as Error)
}
// If decoding failed, show as Alexandria link
@ -70,7 +71,7 @@ export default function HighlightSourcePreview({ source, className }: HighlightS @@ -70,7 +71,7 @@ export default function HighlightSourcePreview({ source, className }: HighlightS
)
}
} catch (error) {
console.warn('Failed to decode nostr addressable event:', error)
logger.warn('Failed to decode nostr addressable event', error as Error)
}
// If decoding failed, show as Alexandria link

3
src/components/VersionUpdateBanner/index.tsx

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { RefreshCw, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function VersionUpdateBanner() {
const { t } = useTranslation()
@ -64,7 +65,7 @@ export default function VersionUpdateBanner() { @@ -64,7 +65,7 @@ export default function VersionUpdateBanner() {
}
}
} catch (error) {
console.error('Error checking for updates:', error)
logger.error('Error checking for updates', { error })
}
}

3
src/components/VideoPlayer/index.tsx

@ -4,6 +4,7 @@ import mediaManager from '@/services/media-manager.service' @@ -4,6 +4,7 @@ import mediaManager from '@/services/media-manager.service'
import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink'
import { MediaErrorBoundary } from '../MediaErrorBoundary'
import logger from '@/lib/logger'
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
const { autoplay } = useContentPolicy()
@ -51,7 +52,7 @@ export default function VideoPlayer({ src, className }: { src: string; className @@ -51,7 +52,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
onError={(error) => {
// Don't log expected media errors
if (error.name !== 'AbortError' && !error.message.includes('play() request was interrupted')) {
console.warn('Video player error:', error)
logger.warn('Video player error', error)
}
setError(true)
}}

34
src/components/WebPreview/index.tsx

@ -11,6 +11,7 @@ import { nip19, kinds } from 'nostr-tools' @@ -11,6 +11,7 @@ import { nip19, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import Image from '../Image'
import Username from '../Username'
import { cleanUrl } from '@/lib/url'
// Helper function to get event type name
function getEventTypeName(kind: number): string {
@ -79,26 +80,29 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -79,26 +80,29 @@ export default function WebPreview({ url, className }: { url: string; className?
const { autoLoadMedia } = useContentPolicy()
const { isSmallScreen } = useScreenSize()
const { title, description, image } = useFetchWebMetadata(url)
const cleanedUrl = useMemo(() => cleanUrl(url), [url])
const { title, description, image } = useFetchWebMetadata(cleanedUrl)
const hostname = useMemo(() => {
try {
return new URL(url).hostname
return new URL(cleanedUrl).hostname
} catch {
return ''
}
}, [url])
}, [cleanedUrl])
const isInternalJumbleLink = useMemo(() => hostname === 'jumble.imwald.eu', [hostname])
// Extract nostr identifier from URL
const nostrIdentifier = useMemo(() => {
const naddrMatch = url.match(/(naddr1[a-z0-9]+)/i)
const neventMatch = url.match(/(nevent1[a-z0-9]+)/i)
const noteMatch = url.match(/(note1[a-z0-9]{58})/i)
const npubMatch = url.match(/(npub1[a-z0-9]{58})/i)
const nprofileMatch = url.match(/(nprofile1[a-z0-9]+)/i)
const naddrMatch = cleanedUrl.match(/(naddr1[a-z0-9]+)/i)
const neventMatch = cleanedUrl.match(/(nevent1[a-z0-9]+)/i)
const noteMatch = cleanedUrl.match(/(note1[a-z0-9]{58})/i)
const npubMatch = cleanedUrl.match(/(npub1[a-z0-9]{58})/i)
const nprofileMatch = cleanedUrl.match(/(nprofile1[a-z0-9]+)/i)
return naddrMatch?.[1] || neventMatch?.[1] || noteMatch?.[1] || npubMatch?.[1] || nprofileMatch?.[1] || null
}, [url])
}, [cleanedUrl])
// Determine nostr type
const nostrType = useMemo(() => {
@ -132,7 +136,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -132,7 +136,7 @@ export default function WebPreview({ url, className }: { url: string; className?
}
// Check if we have any opengraph data (title, description, or image)
const hasOpengraphData = title || description || image
const hasOpengraphData = !isInternalJumbleLink && (title || description || image)
// If no opengraph metadata available, show enhanced fallback link card
if (!hasOpengraphData) {
@ -149,7 +153,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -149,7 +153,7 @@ export default function WebPreview({ url, className }: { url: string; className?
className={cn('p-3 clickable flex w-full border rounded-lg overflow-hidden gap-3', className)}
onClick={(e) => {
e.stopPropagation()
window.open(url, '_blank')
window.open(cleanedUrl, '_blank')
}}
>
{eventImage && fetchedEvent && (
@ -202,7 +206,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -202,7 +206,7 @@ export default function WebPreview({ url, className }: { url: string; className?
className={cn('p-3 clickable flex w-full border rounded-lg overflow-hidden gap-3', className)}
onClick={(e) => {
e.stopPropagation()
window.open(url, '_blank')
window.open(cleanedUrl, '_blank')
}}
>
<div className="flex-1 min-w-0">
@ -229,7 +233,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -229,7 +233,7 @@ export default function WebPreview({ url, className }: { url: string; className?
className={cn('p-2 clickable flex w-full border rounded-lg overflow-hidden', className)}
onClick={(e) => {
e.stopPropagation()
window.open(url, '_blank')
window.open(cleanedUrl, '_blank')
}}
>
<div className="flex-1 w-0 flex items-center gap-2">
@ -249,7 +253,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -249,7 +253,7 @@ export default function WebPreview({ url, className }: { url: string; className?
className="rounded-lg border mt-2 overflow-hidden"
onClick={(e) => {
e.stopPropagation()
window.open(url, '_blank')
window.open(cleanedUrl, '_blank')
}}
>
<Image image={{ url: image }} className="w-full max-w-[400px] h-44 rounded-none" hideIfError />
@ -271,7 +275,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -271,7 +275,7 @@ export default function WebPreview({ url, className }: { url: string; className?
className={cn('p-2 clickable flex w-full border rounded-lg overflow-hidden gap-2', className)}
onClick={(e) => {
e.stopPropagation()
window.open(url, '_blank')
window.open(cleanedUrl, '_blank')
}}
>
{image && (

3
src/components/YoutubeEmbeddedPlayer/index.tsx

@ -5,6 +5,7 @@ import { YouTubePlayer } from '@/types/youtube' @@ -5,6 +5,7 @@ import { YouTubePlayer } from '@/types/youtube'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ExternalLink from '../ExternalLink'
import logger from '@/lib/logger'
export default function YoutubeEmbeddedPlayer({
url,
@ -70,7 +71,7 @@ export default function YoutubeEmbeddedPlayer({ @@ -70,7 +71,7 @@ export default function YoutubeEmbeddedPlayer({
}
})
} catch (error) {
console.error('Failed to initialize YouTube player:', error)
logger.error('Failed to initialize YouTube player', { error })
setError(true)
return
}

3
src/hooks/useFetchRelayInfo.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import relayInfoService from '@/services/relay-info.service'
import { TRelayInfo } from '@/types'
import { useEffect, useState } from 'react'
import logger from '@/lib/logger'
export function useFetchRelayInfo(url?: string) {
const [isFetching, setIsFetching] = useState(true)
@ -17,7 +18,7 @@ export function useFetchRelayInfo(url?: string) { @@ -17,7 +18,7 @@ export function useFetchRelayInfo(url?: string) {
const relayInfo = await relayInfoService.getRelayInfo(url)
setRelayInfo(relayInfo)
} catch (err) {
console.error(err)
logger.error('Failed to fetch relay info', { error: err, url })
} finally {
clearTimeout(timer)
setIsFetching(false)

3
src/hooks/useFetchRelayInfos.tsx

@ -2,6 +2,7 @@ import { checkAlgoRelay } from '@/lib/relay' @@ -2,6 +2,7 @@ import { checkAlgoRelay } from '@/lib/relay'
import relayInfoService from '@/services/relay-info.service'
import { TRelayInfo } from '@/types'
import { useEffect, useState } from 'react'
import logger from '@/lib/logger'
export function useFetchRelayInfos(urls: string[]) {
const [isFetching, setIsFetching] = useState(true)
@ -33,7 +34,7 @@ export function useFetchRelayInfos(urls: string[]) { @@ -33,7 +34,7 @@ export function useFetchRelayInfos(urls: string[]) {
.map((relayInfo) => relayInfo.url)
)
} catch (err) {
console.error(err)
logger.error('Failed to fetch relay infos', { error: err, urls })
} finally {
clearTimeout(timer)
setIsFetching(false)

3
src/hooks/useFetchRelayList.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import client from '@/services/client.service'
import { TRelayList } from '@/types'
import { useEffect, useState } from 'react'
import logger from '@/lib/logger'
export function useFetchRelayList(pubkey?: string | null) {
const [relayList, setRelayList] = useState<TRelayList>({
@ -21,7 +22,7 @@ export function useFetchRelayList(pubkey?: string | null) { @@ -21,7 +22,7 @@ export function useFetchRelayList(pubkey?: string | null) {
const relayList = await client.fetchRelayList(pubkey)
setRelayList(relayList)
} catch (err) {
console.error(err)
logger.error('Failed to fetch relay list', { error: err, pubkey })
} finally {
setIsFetching(false)
}

202
src/hooks/useProfileTimeline.tsx

@ -0,0 +1,202 @@ @@ -0,0 +1,202 @@
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { Event } from 'nostr-tools'
import client from '@/services/client.service'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
type ProfileTimelineCacheEntry = {
events: Event[]
lastUpdated: number
}
const timelineCache = new Map<string, ProfileTimelineCacheEntry>()
const relayGroupCache = new Map<string, string[][]>()
type UseProfileTimelineOptions = {
pubkey: string
cacheKey: string
kinds: number[]
limit?: number
filterPredicate?: (event: Event) => boolean
}
type UseProfileTimelineResult = {
events: Event[]
isLoading: boolean
refresh: () => void
}
async function getRelayGroups(pubkey: string): Promise<string[][]> {
const cached = relayGroupCache.get(pubkey)
if (cached) {
return cached
}
const [relayList, favoriteRelays] = await Promise.all([
client.fetchRelayList(pubkey).catch(() => ({ read: [], write: [] })),
client.fetchFavoriteRelays(pubkey).catch(() => [])
])
const groups: string[][] = []
const normalizeList = (urls?: string[]) =>
Array.from(
new Set(
(urls || [])
.map((url) => normalizeUrl(url))
.filter((value): value is string => !!value)
)
)
const readRelays = normalizeList(relayList.read)
if (readRelays.length) {
groups.push(readRelays)
}
const writeRelays = normalizeList(relayList.write)
if (writeRelays.length) {
groups.push(writeRelays)
}
const favoriteRelayList = normalizeList(favoriteRelays)
if (favoriteRelayList.length) {
groups.push(favoriteRelayList)
}
const fastReadRelays = normalizeList(FAST_READ_RELAY_URLS)
if (fastReadRelays.length) {
groups.push(fastReadRelays)
}
if (!groups.length) {
relayGroupCache.set(pubkey, [fastReadRelays])
return [fastReadRelays]
}
relayGroupCache.set(pubkey, groups)
return groups
}
function postProcessEvents(
rawEvents: Event[],
filterPredicate: ((event: Event) => boolean) | undefined,
limit: number
) {
const dedupMap = new Map<string, Event>()
rawEvents.forEach((evt) => {
if (!dedupMap.has(evt.id)) {
dedupMap.set(evt.id, evt)
}
})
let events = Array.from(dedupMap.values())
if (filterPredicate) {
events = events.filter(filterPredicate)
}
events.sort((a, b) => b.created_at - a.created_at)
return events.slice(0, limit)
}
export function useProfileTimeline({
pubkey,
cacheKey,
kinds,
limit = 200,
filterPredicate
}: UseProfileTimelineOptions): UseProfileTimelineResult {
const cachedEntry = useMemo(() => timelineCache.get(cacheKey), [cacheKey])
const [events, setEvents] = useState<Event[]>(cachedEntry?.events ?? [])
const [isLoading, setIsLoading] = useState(!cachedEntry)
const [refreshToken, setRefreshToken] = useState(0)
const subscriptionRef = useRef<() => void>(() => {})
useEffect(() => {
let cancelled = false
const refreshIndex = refreshToken
const subscribe = async () => {
setIsLoading(!timelineCache.has(cacheKey))
try {
const relayGroups = await getRelayGroups(pubkey)
if (cancelled) {
return
}
const subRequests = relayGroups
.map((urls) => ({
urls,
filter: {
authors: [pubkey],
kinds,
limit
} as any
}))
.filter((request) => request.urls.length)
if (!subRequests.length) {
updateCache([])
setIsLoading(false)
return
}
const { closer } = await client.subscribeTimeline(
subRequests,
{
onEvents: (fetchedEvents) => {
if (cancelled) return
const processed = postProcessEvents(fetchedEvents as Event[], filterPredicate, limit)
timelineCache.set(cacheKey, {
events: processed,
lastUpdated: Date.now()
})
setEvents(processed)
setIsLoading(false)
},
onNew: (evt) => {
if (cancelled) return
setEvents((prevEvents) => {
const combined = [evt as Event, ...prevEvents]
const processed = postProcessEvents(combined, filterPredicate, limit)
timelineCache.set(cacheKey, {
events: processed,
lastUpdated: Date.now()
})
return processed
})
}
},
{ needSort: true }
)
subscriptionRef.current = () => closer()
} catch (error) {
if (!cancelled) {
setIsLoading(false)
}
}
}
subscribe()
return () => {
cancelled = true
subscriptionRef.current()
subscriptionRef.current = () => {}
}
}, [pubkey, cacheKey, JSON.stringify(kinds), limit, filterPredicate, refreshToken])
const refresh = useCallback(() => {
subscriptionRef.current()
subscriptionRef.current = () => {}
timelineCache.delete(cacheKey)
setIsLoading(true)
setRefreshToken((token) => token + 1)
}, [])
return {
events,
isLoading,
refresh
}
}

6
src/lib/debug-utils.ts

@ -22,17 +22,17 @@ interface DebugUtils { @@ -22,17 +22,17 @@ interface DebugUtils {
const debugUtils: DebugUtils = {
enable: () => {
logger.setDebugMode(true)
console.log('🔧 Jumble debug logging enabled')
logger.info('🔧 Jumble debug logging enabled')
},
disable: () => {
logger.setDebugMode(false)
console.log('🔧 Jumble debug logging disabled')
logger.info('🔧 Jumble debug logging disabled')
},
status: () => {
const enabled = logger.isDebugEnabled()
console.log(`🔧 Jumble debug status: ${enabled ? 'ENABLED' : 'DISABLED'}`)
logger.info(`🔧 Jumble debug status: ${enabled ? 'ENABLED' : 'DISABLED'}`)
return { enabled, level: enabled ? 'debug' : 'info' }
},

15
src/lib/draft-event.ts

@ -4,6 +4,7 @@ import customEmojiService from '@/services/custom-emoji.service' @@ -4,6 +4,7 @@ import customEmojiService from '@/services/custom-emoji.service'
import mediaUpload from '@/services/media-upload.service'
import { prefixNostrAddresses } from '@/lib/nostr-address'
import { normalizeHashtag } from '@/lib/discussion-topics'
import logger from '@/lib/logger'
import {
TDraftEvent,
TEmoji,
@ -731,7 +732,7 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) { @@ -731,7 +732,7 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) {
)
}
} catch (e) {
console.error(e)
logger.error('Failed to decode quoted nostr reference', { error: e, reference: m })
}
}
@ -798,7 +799,7 @@ async function extractCommentMentions(content: string, parentEvent: Event) { @@ -798,7 +799,7 @@ async function extractCommentMentions(content: string, parentEvent: Event) {
)
}
} catch (e) {
console.error(e)
logger.error('Failed to decode quoted nostr reference', { error: e, reference: m })
}
}
@ -1033,7 +1034,7 @@ export async function createHighlightDraftEvent( @@ -1033,7 +1034,7 @@ export async function createHighlightDraftEvent(
}
}
} catch (err) {
console.error('Failed to decode naddr:', err)
logger.error('Failed to decode naddr', { error: err, reference: tag })
}
} else if (sourceValue.startsWith('nevent')) {
// Handle nevent
@ -1055,7 +1056,7 @@ export async function createHighlightDraftEvent( @@ -1055,7 +1056,7 @@ export async function createHighlightDraftEvent(
}
}
} catch (err) {
console.error('Failed to decode nevent:', err)
logger.error('Failed to decode nevent', { error: err, reference: tag })
}
} else if (sourceValue.startsWith('note')) {
// Handle note1... (bech32 encoded event ID)
@ -1072,7 +1073,7 @@ export async function createHighlightDraftEvent( @@ -1072,7 +1073,7 @@ export async function createHighlightDraftEvent(
}
}
} catch (err) {
console.error('Failed to decode note:', err)
logger.error('Failed to decode note', { error: err, reference: tag })
}
} else {
// Regular hex event ID
@ -1089,8 +1090,8 @@ export async function createHighlightDraftEvent( @@ -1089,8 +1090,8 @@ export async function createHighlightDraftEvent(
}
// Add context tag if provided (the full text/quote that the highlight is from)
if (context && context.trim()) {
tags.push(['context', context.trim()])
if (context && context.length) {
tags.push(['context', context])
}
// Add description tag if provided (user's explanation/comment)

3
src/lib/event-metadata.ts

@ -8,6 +8,7 @@ import { formatPubkey, pubkeyToNpub } from './pubkey' @@ -8,6 +8,7 @@ import { formatPubkey, pubkeyToNpub } from './pubkey'
import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
import { isTorBrowser } from './utils'
import logger from '@/lib/logger'
export function getRelayListFromEvent(event?: Event | null, blockedRelays?: string[]) {
if (!event) {
@ -77,7 +78,7 @@ export function getProfileFromEvent(event: Event) { @@ -77,7 +78,7 @@ export function getProfileFromEvent(event: Event) {
created_at: event.created_at
}
} catch (err) {
console.error(event.content, err)
logger.error('Failed to parse event metadata', { error: err, content: event.content })
return {
pubkey: event.pubkey,
npub: pubkeyToNpub(event.pubkey) ?? '',

5
src/lib/nip05.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { LRUCache } from 'lru-cache'
import { isValidPubkey } from './pubkey'
import logger from '@/lib/logger'
type TVerifyNip05Result = {
isVerified: boolean
@ -68,7 +69,7 @@ export async function fetchPubkeysFromDomain(domain: string): Promise<string[]> @@ -68,7 +69,7 @@ export async function fetchPubkeysFromDomain(domain: string): Promise<string[]>
return true
}) as string[]
} catch (error) {
console.error('Error fetching pubkeys from domain:', error)
logger.error('Error fetching pubkeys from domain', { error, nip05Domain })
return []
}
}
@ -85,7 +86,7 @@ export async function getRelaysFromNip07Extension(): Promise<string[]> { @@ -85,7 +86,7 @@ export async function getRelaysFromNip07Extension(): Promise<string[]> {
return Object.keys(relaysObj || {})
}
} catch (error) {
console.log('NIP-07 extension does not support getRelays():', error)
logger.warn('NIP-07 extension does not support getRelays()', error as Error)
}
return []
}

3
src/lib/nostr-parser.tsx

@ -10,6 +10,7 @@ import { cleanUrl, isImage, isMedia } from '@/lib/url' @@ -10,6 +10,7 @@ import { cleanUrl, isImage, isMedia } from '@/lib/url'
import { getImetaInfosFromEvent } from '@/lib/event'
import { TImetaInfo } from '@/types'
import { Event } from 'nostr-tools'
import logger from '@/lib/logger'
export interface ParsedNostrContent {
elements: Array<{
@ -373,7 +374,7 @@ function getNostrType(bech32Id: string): 'npub' | 'nprofile' | 'nevent' | 'naddr @@ -373,7 +374,7 @@ function getNostrType(bech32Id: string): 'npub' | 'nprofile' | 'nevent' | 'naddr
return type as 'npub' | 'nprofile' | 'nevent' | 'naddr' | 'note'
}
} catch (error) {
console.error('Invalid bech32 ID:', bech32Id, error)
logger.error('Invalid bech32 ID', { bech32Id, error })
}
return null
}

3
src/lib/pubkey.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { LRUCache } from 'lru-cache'
import { nip19 } from 'nostr-tools'
import logger from '@/lib/logger'
export function formatPubkey(pubkey: string) {
const npub = pubkeyToNpub(pubkey)
@ -48,7 +49,7 @@ export function userIdToPubkey(userId: string) { @@ -48,7 +49,7 @@ export function userIdToPubkey(userId: string) {
return data.pubkey
}
} catch (error) {
console.error('Error decoding userId:', userId, 'error:', error)
logger.error('Error decoding userId', { userId, error })
}
}
return userId

16
src/lib/url.ts

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
import logger from '@/lib/logger'
export function isWebsocketUrl(url: string): boolean {
return /^wss?:\/\/.+$/.test(url)
}
@ -23,7 +25,7 @@ export function normalizeUrl(url: string): string { @@ -23,7 +25,7 @@ export function normalizeUrl(url: string): string {
// Block URLs with query params or hash fragments (these are likely not relays)
if (hasQueryParams || hasHashFragment) {
console.warn('Skipping URL with query/hash (not a relay):', url)
logger.warn('Skipping URL with query/hash (not a relay)', { url })
return ''
}
@ -37,7 +39,7 @@ export function normalizeUrl(url: string): string { @@ -37,7 +39,7 @@ export function normalizeUrl(url: string): string {
// After protocol normalization, validate it's actually a websocket URL
if (!isWebsocketUrl(p.toString())) {
console.warn('Skipping non-websocket URL:', url)
logger.warn('Skipping non-websocket URL', { url })
return ''
}
@ -56,13 +58,13 @@ export function normalizeUrl(url: string): string { @@ -56,13 +58,13 @@ export function normalizeUrl(url: string): string {
// Final validation: ensure we have a proper websocket URL
const finalUrl = p.toString()
if (!isWebsocketUrl(finalUrl)) {
console.warn('Normalization resulted in invalid websocket URL:', finalUrl)
logger.warn('Normalization resulted in invalid websocket URL', { url: finalUrl })
return ''
}
return finalUrl
} catch {
console.error('Invalid URL:', url)
} catch (error) {
logger.error('Invalid URL', { error, url })
return ''
}
}
@ -87,8 +89,8 @@ export function normalizeHttpUrl(url: string): string { @@ -87,8 +89,8 @@ export function normalizeHttpUrl(url: string): string {
p.searchParams.sort()
p.hash = ''
return p.toString()
} catch {
console.error('Invalid URL:', url)
} catch (error) {
logger.error('Invalid URL', { error, url })
return ''
}
}

2
src/pages/primary/NoteListPage/index.tsx

@ -144,7 +144,7 @@ function NoteListPageTitlebar({ @@ -144,7 +144,7 @@ function NoteListPageTitlebar({
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
console.log('Im Wald clicked, clearing overlay')
logger.debug('Im Wald button clicked, clearing overlay')
setPrimaryNoteView(null)
}}
>

3
src/pages/secondary/HomePage/index.tsx

@ -9,6 +9,7 @@ import { TRelayInfo } from '@/types' @@ -9,6 +9,7 @@ import { TRelayInfo } from '@/types'
import { ArrowRight, Server } from 'lucide-react'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
@ -23,7 +24,7 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => { @@ -23,7 +24,7 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
const relays = await relayInfoService.getRelayInfos(RECOMMENDED_RELAYS)
setRecommendedRelayInfos(relays.filter(Boolean) as TRelayInfo[])
} catch (error) {
console.error('Failed to fetch recommended relays:', error)
logger.error('Failed to fetch recommended relays', { error })
}
}
init()

5
src/pages/secondary/NotePage/NotFound.tsx

@ -7,6 +7,7 @@ import { AlertCircle, Search } from 'lucide-react' @@ -7,6 +7,7 @@ import { AlertCircle, Search } from 'lucide-react'
import { nip19 } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function NotFound({
bech32Id,
@ -55,7 +56,7 @@ export default function NotFound({ @@ -55,7 +56,7 @@ export default function NotFound({
externalRelays = externalRelays.map(url => normalizeUrl(url) || url)
externalRelays = Array.from(new Set(externalRelays))
} catch (err) {
console.error('Failed to parse external relays:', err)
logger.error('Failed to parse external relays', { error: err, bech32Id })
}
} else {
extractedHexEventId = bech32Id
@ -100,7 +101,7 @@ export default function NotFound({ @@ -100,7 +101,7 @@ export default function NotFound({
onEventFound(event)
}
} catch (error) {
console.error('External relay fetch failed:', error)
logger.error('External relay fetch failed', { error, bech32Id, hexEventId })
} finally {
setIsSearchingExternal(false)
setTriedExternal(true)

7
src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx

@ -13,6 +13,7 @@ import { AlertCircle, ArrowUpToLine, Loader, X } from 'lucide-react' @@ -13,6 +13,7 @@ import { AlertCircle, ArrowUpToLine, Loader, X } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger'
export default function BlossomServerListSetting() {
const { t } = useTranslation()
@ -48,7 +49,7 @@ export default function BlossomServerListSetting() { @@ -48,7 +49,7 @@ export default function BlossomServerListSetting() {
setBlossomServerListEvent(newEvent)
setUrl('')
} catch (error) {
console.error('Failed to add Blossom URL:', error)
logger.error('Failed to add Blossom URL', { error, url })
} finally {
setAdding(false)
}
@ -72,7 +73,7 @@ export default function BlossomServerListSetting() { @@ -72,7 +73,7 @@ export default function BlossomServerListSetting() {
await client.updateBlossomServerListEventCache(newEvent)
setBlossomServerListEvent(newEvent)
} catch (error) {
console.error('Failed to remove Blossom URL:', error)
logger.error('Failed to remove Blossom URL', { error, url: serverUrls[idx] })
} finally {
setRemovingIndex(-1)
}
@ -88,7 +89,7 @@ export default function BlossomServerListSetting() { @@ -88,7 +89,7 @@ export default function BlossomServerListSetting() {
await client.updateBlossomServerListEventCache(newEvent)
setBlossomServerListEvent(newEvent)
} catch (error) {
console.error('Failed to move Blossom URL to top:', error)
logger.error('Failed to move Blossom URL to top', { error, url: serverUrls[idx] })
} finally {
setMovingIndex(-1)
}

3
src/providers/MuteListProvider.tsx

@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { z } from 'zod'
import { useNostr } from './NostrProvider'
import logger from '@/lib/logger'
type TMuteListContext = {
mutePubkeySet: Set<string>
@ -68,7 +69,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) { @@ -68,7 +69,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
return privateTags
} catch (error) {
console.error('Failed to decrypt mute list content', error)
logger.error('Failed to decrypt mute list content', { error, eventId: muteListEvent.id })
return []
}
}

4
src/providers/NotificationProvider.tsx

@ -231,7 +231,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -231,7 +231,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
if (isMountedRef.current) {
setTimeout(() => {
if (isMountedRef.current) {
console.log('[NotificationProvider] Reconnecting after close...')
logger.info('[NotificationProvider] Reconnecting after close...')
subscribe()
}
}, 15_000) // Increased from 5s to 15s
@ -243,7 +243,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -243,7 +243,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
subCloserRef.current = subCloser
return subCloser
} catch (error) {
console.error('Subscription error:', error)
logger.error('Subscription error', { error })
// Retry on error if still mounted
if (isMountedRef.current) {

21
src/services/client.service.ts

@ -1023,6 +1023,27 @@ class ClientService extends EventTarget { @@ -1023,6 +1023,27 @@ class ClientService extends EventTarget {
}
}
async fetchFavoriteRelays(pubkey: string): Promise<string[]> {
try {
const favoriteRelaysEvent = await this.fetchReplaceableEvent(pubkey, ExtendedKind.FAVORITE_RELAYS)
if (!favoriteRelaysEvent) return []
const relays: string[] = []
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) {
const normalized = normalizeUrl(tagValue)
if (normalized) {
relays.push(normalized)
}
}
})
return Array.from(new Set(relays))
} catch {
return []
}
}
/**
* Build initial relay list for fetching events
* Priority: FAST_READ_RELAY_URLS, user's favorite relays (10012), user's relay list read relays (10002) including cache relays (10432)

11
src/services/content-parser.service.ts

@ -8,6 +8,7 @@ import { Event, kinds, nip19 } from 'nostr-tools' @@ -8,6 +8,7 @@ import { Event, kinds, nip19 } from 'nostr-tools'
import { getImetaInfosFromEvent } from '@/lib/event'
import { URL_REGEX, ExtendedKind } from '@/constants'
import { TImetaInfo } from '@/types'
import logger from '@/lib/logger'
export interface ParsedContent {
html: string
@ -45,7 +46,7 @@ class ContentParserService { @@ -45,7 +46,7 @@ class ContentParserService {
this.isAsciidoctorLoaded = true
return this.asciidoctor
} catch (error) {
console.warn('Failed to load AsciiDoctor:', error)
logger.warn('Failed to load AsciiDoctor', error as Error)
return null
}
}
@ -91,7 +92,7 @@ class ContentParserService { @@ -91,7 +92,7 @@ class ContentParserService {
const asciidocContent = this.convertToAsciidoc(content, markupType)
html = await this.parseAsciidoc(asciidocContent, { enableMath, enableSyntaxHighlighting })
} catch (error) {
console.error('Content parsing error:', error)
logger.error('Content parsing error', { error })
// Fallback to plain text
html = this.parsePlainText(content)
}
@ -178,7 +179,7 @@ class ContentParserService { @@ -178,7 +179,7 @@ class ContentParserService {
// Debug: log the AsciiDoc HTML output for troubleshooting
if (process.env.NODE_ENV === 'development') {
console.log('AsciiDoc HTML output:', htmlString.substring(0, 1000) + '...')
logger.debug('AsciiDoc HTML output preview', { preview: htmlString.substring(0, 1000) + '...' })
}
// Process wikilinks in the HTML output
@ -196,7 +197,7 @@ class ContentParserService { @@ -196,7 +197,7 @@ class ContentParserService {
// Hide any raw AsciiDoc ToC text that might appear in the content
return this.hideRawTocText(styledHtml)
} catch (error) {
console.error('AsciiDoc parsing error:', error)
logger.error('AsciiDoc parsing error', { error })
return this.parsePlainText(content)
}
}
@ -244,7 +245,7 @@ class ContentParserService { @@ -244,7 +245,7 @@ class ContentParserService {
// Debug: log the converted AsciiDoc for troubleshooting
if (process.env.NODE_ENV === 'development') {
console.log('Converted AsciiDoc:', result)
logger.debug('Converted AsciiDoc', result)
}
return result

24
src/services/indexed-db.service.ts

@ -3,6 +3,7 @@ import { tagNameEquals } from '@/lib/tag' @@ -3,6 +3,7 @@ import { tagNameEquals } from '@/lib/tag'
import { TRelayInfo } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { isReplaceableEvent } from '@/lib/event'
import logger from '@/lib/logger'
type TValue<T = any> = {
key: string
@ -193,7 +194,7 @@ class IndexedDbService { @@ -193,7 +194,7 @@ class IndexedDbService {
}
// Check if the store exists before trying to access it
if (!this.db.objectStoreNames.contains(storeName)) {
console.warn(`Store ${storeName} not found in database. Cannot save event.`)
logger.warn(`Store ${storeName} not found in database. Cannot save event.`)
// Return the event anyway (don't reject) - caching is optional
return resolve(cleanEvent)
}
@ -243,7 +244,7 @@ class IndexedDbService { @@ -243,7 +244,7 @@ class IndexedDbService {
}
// Check if the store exists before trying to access it
if (!this.db.objectStoreNames.contains(storeName)) {
console.warn(`Store ${storeName} not found in database. Returning null.`)
logger.warn(`Store ${storeName} not found in database. Returning null.`)
return resolve(null)
}
const transaction = this.db.transaction(storeName, 'readonly')
@ -569,7 +570,7 @@ class IndexedDbService { @@ -569,7 +570,7 @@ class IndexedDbService {
return reject('database not initialized')
}
if (!this.db.objectStoreNames.contains(storeName)) {
console.warn(`Store ${storeName} not found in database. Cannot save event.`)
logger.warn(`Store ${storeName} not found in database. Cannot save event.`)
return resolve(cleanEvent)
}
const transaction = this.db.transaction(storeName, 'readwrite')
@ -630,7 +631,7 @@ class IndexedDbService { @@ -630,7 +631,7 @@ class IndexedDbService {
return reject('database not initialized')
}
if (!this.db.objectStoreNames.contains(storeName)) {
console.warn(`Store ${storeName} not found in database. Cannot save event.`)
logger.warn(`Store ${storeName} not found in database. Cannot save event.`)
return resolve(event)
}
const transaction = this.db.transaction(storeName, 'readwrite')
@ -1002,7 +1003,7 @@ class IndexedDbService { @@ -1002,7 +1003,7 @@ class IndexedDbService {
}
} catch (error) {
// If we can't generate a replaceable key, skip this item
console.warn('Failed to get replaceable key for item:', item.key, error)
logger.warn('Failed to get replaceable key for item', { key: item.key, error })
invalidItemsCount++
continue
}
@ -1015,7 +1016,11 @@ class IndexedDbService { @@ -1015,7 +1016,11 @@ class IndexedDbService {
if (keysToDelete.length === 0) {
// No duplicates found, but verify counts match
if (totalProcessed + invalidItemsCount !== allItems.length) {
console.warn(`Count mismatch: total items=${allItems.length}, processed=${totalProcessed}, invalid=${invalidItemsCount}`)
logger.warn('Count mismatch while cleaning up replaceable events', {
totalItems: allItems.length,
processed: totalProcessed,
invalid: invalidItemsCount
})
}
return Promise.resolve({ deleted: 0, kept: actualKept })
}
@ -1037,7 +1042,12 @@ class IndexedDbService { @@ -1037,7 +1042,12 @@ class IndexedDbService {
const actualKept = eventMap.size
const totalProcessed = actualKept + deletedCount
if (totalProcessed + invalidItemsCount !== allItems.length) {
console.warn(`Count mismatch after deletion: total items=${allItems.length}, kept=${actualKept}, deleted=${deletedCount}, invalid=${invalidItemsCount}`)
logger.warn('Count mismatch after deletion', {
totalItems: allItems.length,
kept: actualKept,
deleted: deletedCount,
invalid: invalidItemsCount
})
}
resolve({ deleted: deletedCount, kept: actualKept })
}

3
src/services/lightning.service.ts

@ -11,6 +11,7 @@ import { SubCloser } from 'nostr-tools/abstract-pool' @@ -11,6 +11,7 @@ import { SubCloser } from 'nostr-tools/abstract-pool'
import { makeZapRequest } from 'nostr-tools/nip57'
import { utf8Decoder } from 'nostr-tools/utils'
import client from './client.service'
import logger from '@/lib/logger'
export type TRecentSupporter = { pubkey: string; amount: number; comment?: string }
@ -232,7 +233,7 @@ class LightningService { @@ -232,7 +233,7 @@ class LightningService {
}
}
} catch (err) {
console.error(err)
logger.error('Failed to resolve LNURL from profile', { error: err, profile })
}
return null

3
src/services/media-manager.service.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { YouTubePlayer } from '@/types/youtube'
import logger from '@/lib/logger'
type Media = HTMLMediaElement | YouTubePlayer
@ -59,7 +60,7 @@ class MediaManagerService { @@ -59,7 +60,7 @@ class MediaManagerService {
return
}
// Log other unexpected errors
console.error('Error playing media:', error)
logger.error('Error playing media', { error })
this.currentMedia = null
})
}

5
src/services/relay-info.service.ts

@ -3,6 +3,7 @@ import indexDb from '@/services/indexed-db.service' @@ -3,6 +3,7 @@ import indexDb from '@/services/indexed-db.service'
import { TAwesomeRelayCollection, TRelayInfo } from '@/types'
import DataLoader from 'dataloader'
import FlexSearch from 'flexsearch'
import logger from '@/lib/logger'
class RelayInfoService {
static instance: RelayInfoService
@ -102,7 +103,7 @@ class RelayInfoService { @@ -102,7 +103,7 @@ class RelayInfoService {
const data = (await res.json()) as { collections: TAwesomeRelayCollection[] }
return data.collections
} catch (error) {
console.error('Error fetching awesome relay collections:', error)
logger.error('Error fetching awesome relay collections', { error })
return []
}
})()
@ -132,7 +133,7 @@ class RelayInfoService { @@ -132,7 +133,7 @@ class RelayInfoService {
private async fetchRelayNip11(url: string) {
try {
console.log('Fetching NIP-11 for', url)
logger.debug('Fetching NIP-11 metadata', { url })
const res = await fetch(url.replace('ws://', 'http://').replace('wss://', 'https://'), {
headers: { Accept: 'application/nostr+json' }
})

17
src/services/relay-selection.service.ts

@ -4,6 +4,7 @@ import { FAST_WRITE_RELAY_URLS } from '@/constants' @@ -4,6 +4,7 @@ import { FAST_WRITE_RELAY_URLS } from '@/constants'
import client from '@/services/client.service'
import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url'
import { TRelaySet } from '@/types'
import logger from '@/lib/logger'
export interface RelaySelectionContext {
// User's own relays
@ -87,7 +88,7 @@ class RelaySelectionService { @@ -87,7 +88,7 @@ class RelaySelectionService {
selectableRelays.add(normalized)
} else {
// If normalization fails or returns empty (invalid URL), skip it
console.warn('Skipping invalid relay URL:', url)
logger.warn('Skipping invalid relay URL', { url })
}
}
@ -174,7 +175,7 @@ class RelaySelectionService { @@ -174,7 +175,7 @@ class RelaySelectionService {
// Filter out local relays from other users
return this.filterLocalRelaysFromOthers(userRelays)
} catch (error) {
console.warn(`Failed to fetch relay list for ${pubkey}:`, error)
logger.warn('Failed to fetch relay list', { pubkey, error })
return []
}
})
@ -184,7 +185,7 @@ class RelaySelectionService { @@ -184,7 +185,7 @@ class RelaySelectionService {
}
}
} catch (error) {
console.error('Failed to get contextual relays:', error)
logger.error('Failed to get contextual relays', { error, relaySets })
}
return Array.from(contextualRelays)
@ -258,7 +259,7 @@ class RelaySelectionService { @@ -258,7 +259,7 @@ class RelaySelectionService {
// Filter out local relays from other users
return this.filterLocalRelaysFromOthers(userRelays)
} catch (error) {
console.warn(`Failed to fetch relay list for ${pubkey}:`, error)
logger.warn('Failed to fetch relay list', { pubkey, error })
return []
}
})
@ -325,7 +326,7 @@ class RelaySelectionService { @@ -325,7 +326,7 @@ class RelaySelectionService {
// Filter out local relays from other users
return this.filterLocalRelaysFromOthers(userRelays)
} catch (error) {
console.warn(`Failed to fetch relay list for ${pubkey}:`, error)
logger.warn('Failed to fetch relay list', { pubkey, error })
return []
}
})
@ -355,11 +356,11 @@ class RelaySelectionService { @@ -355,11 +356,11 @@ class RelaySelectionService {
})
}
} catch (error) {
console.warn(`Failed to fetch relay list for ${parentEvent.pubkey}:`, error)
logger.warn('Failed to fetch relay list for parent event', { parentPubkey: parentEvent.pubkey, error })
}
}
} catch (error) {
console.error('Failed to get public message relays:', error)
logger.error('Failed to get public message relays', { error, parentEvent: context.parentEvent?.id })
}
return Array.from(relays)
@ -439,7 +440,7 @@ class RelaySelectionService { @@ -439,7 +440,7 @@ class RelaySelectionService {
}
}
} catch (error) {
console.error('Failed to decode nostr address:', error)
logger.error('Failed to decode nostr address', { error, tag })
}
}
}

Loading…
Cancel
Save