Browse Source

clean up articles

imwald
Silberengel 5 months ago
parent
commit
23402b9309
  1. 37
      src/components/Embedded/EmbeddedNote.tsx
  2. 38
      src/components/Note/Article/index.tsx
  3. 39
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  4. 23
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  5. 47
      src/pages/secondary/NotePage/NotFound.tsx

37
src/components/Embedded/EmbeddedNote.tsx

@ -1,4 +1,5 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { useFetchEvent } from '@/hooks' import { useFetchEvent } from '@/hooks'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -94,17 +95,20 @@ function EmbeddedNoteNotFound({
const [isSearchingExternal, setIsSearchingExternal] = useState(false) const [isSearchingExternal, setIsSearchingExternal] = useState(false)
const [triedExternal, setTriedExternal] = useState(false) const [triedExternal, setTriedExternal] = useState(false)
const [externalRelays, setExternalRelays] = useState<string[]>([]) const [externalRelays, setExternalRelays] = useState<string[]>([])
const [hexEventId, setHexEventId] = useState<string | null>(null)
// Calculate which external relays would be tried // Calculate which external relays would be tried
useEffect(() => { useEffect(() => {
const getExternalRelays = async () => { const getExternalRelays = async () => {
let relays: string[] = [] let relays: string[] = []
let extractedHexEventId: string | null = null
if (!/^[0-9a-f]{64}$/.test(noteId)) { if (!/^[0-9a-f]{64}$/.test(noteId)) {
try { try {
const { type, data } = nip19.decode(noteId) const { type, data } = nip19.decode(noteId)
if (type === 'nevent') { if (type === 'nevent') {
extractedHexEventId = data.id
if (data.relays) relays.push(...data.relays) if (data.relays) relays.push(...data.relays)
if (data.author) { if (data.author) {
const authorRelayList = await client.fetchRelayList(data.author) const authorRelayList = await client.fetchRelayList(data.author)
@ -114,6 +118,8 @@ function EmbeddedNoteNotFound({
if (data.relays) relays.push(...data.relays) if (data.relays) relays.push(...data.relays)
const authorRelayList = await client.fetchRelayList(data.pubkey) const authorRelayList = await client.fetchRelayList(data.pubkey)
relays.push(...authorRelayList.write.slice(0, 6)) relays.push(...authorRelayList.write.slice(0, 6))
} else if (type === 'note') {
extractedHexEventId = data
} }
// Normalize and deduplicate relays // Normalize and deduplicate relays
relays = relays.map(url => normalizeUrl(url) || url) relays = relays.map(url => normalizeUrl(url) || url)
@ -121,13 +127,30 @@ function EmbeddedNoteNotFound({
} catch (err) { } catch (err) {
console.error('Failed to parse external relays:', err) console.error('Failed to parse external relays:', err)
} }
} else {
extractedHexEventId = noteId
} }
const seenOn = client.getSeenEventRelayUrls(noteId) setHexEventId(extractedHexEventId)
const seenOn = extractedHexEventId ? client.getSeenEventRelayUrls(extractedHexEventId) : []
relays.push(...seenOn) relays.push(...seenOn)
// Normalize and deduplicate final relay list // Normalize all relays first
const normalizedRelays = relays.map(url => normalizeUrl(url) || url) let normalizedRelays = relays.map(url => normalizeUrl(url) || url).filter(Boolean)
normalizedRelays = Array.from(new Set(normalizedRelays))
// If no external relays from hints, try SEARCHABLE_RELAY_URLS as fallback
// Filter out relays that overlap with BIG_RELAY_URLS (already tried first)
if (normalizedRelays.length === 0) {
const searchableRelays = SEARCHABLE_RELAY_URLS
.map(url => normalizeUrl(url) || url)
.filter((url): url is string => Boolean(url))
.filter(relay => !BIG_RELAY_URLS.includes(relay))
normalizedRelays.push(...searchableRelays)
}
// Deduplicate final relay list
setExternalRelays(Array.from(new Set(normalizedRelays))) setExternalRelays(Array.from(new Set(normalizedRelays)))
} }
@ -135,11 +158,11 @@ function EmbeddedNoteNotFound({
}, [noteId]) }, [noteId])
const handleTryExternalRelays = async () => { const handleTryExternalRelays = async () => {
if (isSearchingExternal) return if (!hexEventId || isSearchingExternal) return
setIsSearchingExternal(true) setIsSearchingExternal(true)
try { try {
const event = await client.fetchEventWithExternalRelays(noteId, []) const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays)
if (event && onEventFound) { if (event && onEventFound) {
onEventFound(event) onEventFound(event)
} }
@ -195,11 +218,11 @@ function EmbeddedNoteNotFound({
)} )}
{!triedExternal && !hasExternalRelays && ( {!triedExternal && !hasExternalRelays && (
<p className="text-xs text-center">{t('No external relay hints available')}</p> <div className="text-xs text-center">{t('No external relay hints available')}</div>
)} )}
{triedExternal && ( {triedExternal && (
<p className="text-xs text-center">{t('Note could not be found anywhere')}</p> <div className="text-xs text-center">{t('Note could not be found anywhere')}</div>
)} )}
<ClientSelect className="w-full" originalNoteId={noteId} /> <ClientSelect className="w-full" originalNoteId={noteId} />

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

@ -6,7 +6,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo, useState, useEffect, useRef } from 'react' import { useMemo, useState, useEffect, useRef } from 'react'
import { useEventFieldParser } from '@/hooks/useContentParser' import { useEventFieldParser } from '@/hooks/useContentParser'
import WebPreview from '../../WebPreview'
import HighlightSourcePreview from '../../UniversalContent/HighlightSourcePreview' import HighlightSourcePreview from '../../UniversalContent/HighlightSourcePreview'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
@ -249,7 +248,7 @@ export default function Article({
/> />
{/* Collapsible Article Info - only for article-type events */} {/* Collapsible Article Info - only for article-type events */}
{isArticleType && (parsedContent?.media?.length > 0 || parsedContent?.links?.length > 0 || parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && ( {isArticleType && (parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
<Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4"> <Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4">
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between"> <Button variant="outline" className="w-full justify-between">
@ -258,41 +257,6 @@ export default function Article({
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2"> <CollapsibleContent className="space-y-4 mt-2">
{/* Media thumbnails */}
{parsedContent?.media?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-3">Images in this article:</h4>
<div className="grid grid-cols-8 sm:grid-cols-12 md:grid-cols-16 gap-1">
{parsedContent?.media?.map((media, index) => (
<div key={index} className="aspect-square">
<ImageWithLightbox
image={media}
className="w-full h-full object-cover rounded cursor-pointer hover:opacity-80 transition-opacity"
classNames={{
wrapper: 'w-full h-full'
}}
/>
</div>
))}
</div>
</div>
)}
{/* Links summary with OpenGraph previews */}
{parsedContent?.links?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-3">Links in this article:</h4>
<div className="space-y-3">
{parsedContent?.links?.map((link, index) => (
<WebPreview
key={index}
url={link.url}
className="w-full"
/>
))}
</div>
</div>
)}
{/* Nostr links summary */} {/* Nostr links summary */}
{parsedContent?.nostrLinks?.length > 0 && ( {parsedContent?.nostrLinks?.length > 0 && (

39
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -1,14 +1,11 @@
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox' import ImageWithLightbox from '@/components/ImageWithLightbox'
import ImageCarousel from '@/components/ImageCarousel/ImageCarousel'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNoteList } from '@/lib/link' import { toNoteList } from '@/lib/link'
import { extractAllImagesFromEvent } from '@/lib/image-extraction'
import { ChevronDown, ChevronRight } from 'lucide-react' import { ChevronDown, ChevronRight } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo, useState, useEffect, useRef } from 'react' import { useMemo, useState, useEffect, useRef } from 'react'
import { useEventFieldParser } from '@/hooks/useContentParser' import { useEventFieldParser } from '@/hooks/useContentParser'
import WebPreview from '../../WebPreview'
import HighlightSourcePreview from '../../UniversalContent/HighlightSourcePreview' import HighlightSourcePreview from '../../UniversalContent/HighlightSourcePreview'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
@ -26,10 +23,6 @@ export default function AsciidocArticle({
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const [isInfoOpen, setIsInfoOpen] = useState(false) const [isInfoOpen, setIsInfoOpen] = useState(false)
const [isImagesOpen, setIsImagesOpen] = useState(false)
// Extract all images from the event
const allImages = useMemo(() => extractAllImagesFromEvent(event), [event])
// Determine if this is an article-type event that should show ToC and Article Info // Determine if this is an article-type event that should show ToC and Article Info
const isArticleType = useMemo(() => { const isArticleType = useMemo(() => {
@ -310,23 +303,9 @@ export default function AsciidocArticle({
dangerouslySetInnerHTML={{ __html: parsedContent?.html || '' }} dangerouslySetInnerHTML={{ __html: parsedContent?.html || '' }}
/> />
{/* Image Carousel - Collapsible */}
{!hideImagesAndInfo && allImages.length > 0 && (
<Collapsible open={isImagesOpen} onOpenChange={setIsImagesOpen} className="mt-8">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
<span>Images in this article ({allImages.length})</span>
{isImagesOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4">
<ImageCarousel images={allImages} />
</CollapsibleContent>
</Collapsible>
)}
{/* Collapsible Article Info - only for article-type events */} {/* Collapsible Article Info - only for article-type events */}
{!hideImagesAndInfo && isArticleType && (parsedContent?.links?.length > 0 || parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && ( {!hideImagesAndInfo && isArticleType && (parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
<Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4"> <Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4">
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between"> <Button variant="outline" className="w-full justify-between">
@ -336,22 +315,6 @@ export default function AsciidocArticle({
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2"> <CollapsibleContent className="space-y-4 mt-2">
{/* Links summary with OpenGraph previews */}
{parsedContent?.links?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-3">Links in this article:</h4>
<div className="space-y-3">
{parsedContent?.links?.map((link, index) => (
<WebPreview
key={index}
url={link.url}
className="w-full"
/>
))}
</div>
</div>
)}
{/* Nostr links summary */} {/* Nostr links summary */}
{parsedContent?.nostrLinks?.length > 0 && ( {parsedContent?.nostrLinks?.length > 0 && (
<div className="p-4 bg-muted rounded-lg"> <div className="p-4 bg-muted rounded-lg">

23
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -1,15 +1,14 @@
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager' import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox' import ImageWithLightbox from '@/components/ImageWithLightbox'
import ImageCarousel from '@/components/ImageCarousel/ImageCarousel'
import MediaPlayer from '@/components/MediaPlayer' import MediaPlayer from '@/components/MediaPlayer'
import Wikilink from '@/components/UniversalContent/Wikilink' import Wikilink from '@/components/UniversalContent/Wikilink'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList, toProfile } from '@/lib/link' import { toNote, toNoteList, toProfile } from '@/lib/link'
import { useMediaExtraction } from '@/hooks' import { useMediaExtraction } from '@/hooks'
import { cleanUrl } from '@/lib/url' import { cleanUrl } from '@/lib/url'
import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react' import { ExternalLink } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import React, { useMemo, useEffect, useRef, useState } from 'react' import React, { useMemo, useEffect, useRef } from 'react'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
@ -17,8 +16,6 @@ import 'katex/dist/katex.min.css'
import NostrNode from './NostrNode' import NostrNode from './NostrNode'
import { remarkNostr } from './remarkNostr' import { remarkNostr } from './remarkNostr'
import { Components } from './types' import { Components } from './types'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
export default function MarkdownArticle({ export default function MarkdownArticle({
event, event,
@ -31,7 +28,6 @@ export default function MarkdownArticle({
}) { }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const [isImagesOpen, setIsImagesOpen] = useState(false)
const contentRef = useRef<HTMLDivElement>(null) const contentRef = useRef<HTMLDivElement>(null)
// Use unified media extraction service // Use unified media extraction service
@ -466,21 +462,6 @@ export default function MarkdownArticle({
</div> </div>
)} )}
{/* Image Carousel - Only show for article content (30023, 30041, 30817, 30818) */}
{/* Only show images that aren't already in the content (from tags only) */}
{showImageGallery && carouselImages.length > 0 && (
<Collapsible open={isImagesOpen} onOpenChange={setIsImagesOpen} className="mt-8">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
<span>Images in this article ({carouselImages.length})</span>
{isImagesOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4">
<ImageCarousel images={carouselImages} />
</CollapsibleContent>
</Collapsible>
)}
{metadata.tags.filter(tag => !contentHashtags.has(tag.toLowerCase())).length > 0 && ( {metadata.tags.filter(tag => !contentHashtags.has(tag.toLowerCase())).length > 0 && (
<div className="flex gap-2 flex-wrap pb-2 mt-4"> <div className="flex gap-2 flex-wrap pb-2 mt-4">
{metadata.tags {metadata.tags

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

@ -1,5 +1,6 @@
import ClientSelect from '@/components/ClientSelect' import ClientSelect from '@/components/ClientSelect'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service' import client from '@/services/client.service'
import { AlertCircle, Search } from 'lucide-react' import { AlertCircle, Search } from 'lucide-react'
@ -18,6 +19,7 @@ export default function NotFound({
const [isSearchingExternal, setIsSearchingExternal] = useState(false) const [isSearchingExternal, setIsSearchingExternal] = useState(false)
const [triedExternal, setTriedExternal] = useState(false) const [triedExternal, setTriedExternal] = useState(false)
const [externalRelays, setExternalRelays] = useState<string[]>([]) const [externalRelays, setExternalRelays] = useState<string[]>([])
const [hexEventId, setHexEventId] = useState<string | null>(null)
// Calculate which external relays would be tried (excluding already-tried relays) // Calculate which external relays would be tried (excluding already-tried relays)
useEffect(() => { useEffect(() => {
@ -28,6 +30,7 @@ export default function NotFound({
const alreadyTriedRelays: string[] = await client.getAlreadyTriedRelays() const alreadyTriedRelays: string[] = await client.getAlreadyTriedRelays()
let externalRelays: string[] = [] let externalRelays: string[] = []
let extractedHexEventId: string | null = null
// Parse relay hints and author from bech32 ID // Parse relay hints and author from bech32 ID
if (!/^[0-9a-f]{64}$/.test(bech32Id)) { if (!/^[0-9a-f]{64}$/.test(bech32Id)) {
@ -35,6 +38,7 @@ export default function NotFound({
const { type, data } = nip19.decode(bech32Id) const { type, data } = nip19.decode(bech32Id)
if (type === 'nevent') { if (type === 'nevent') {
extractedHexEventId = data.id
if (data.relays) externalRelays.push(...data.relays) if (data.relays) externalRelays.push(...data.relays)
if (data.author) { if (data.author) {
const authorRelayList = await client.fetchRelayList(data.author) const authorRelayList = await client.fetchRelayList(data.author)
@ -44,6 +48,8 @@ export default function NotFound({
if (data.relays) externalRelays.push(...data.relays) if (data.relays) externalRelays.push(...data.relays)
const authorRelayList = await client.fetchRelayList(data.pubkey) const authorRelayList = await client.fetchRelayList(data.pubkey)
externalRelays.push(...authorRelayList.write.slice(0, 6)) externalRelays.push(...authorRelayList.write.slice(0, 6))
} else if (type === 'note') {
extractedHexEventId = data
} }
// Normalize and deduplicate external relays // Normalize and deduplicate external relays
externalRelays = externalRelays.map(url => normalizeUrl(url) || url) externalRelays = externalRelays.map(url => normalizeUrl(url) || url)
@ -51,28 +57,45 @@ export default function NotFound({
} catch (err) { } catch (err) {
console.error('Failed to parse external relays:', err) console.error('Failed to parse external relays:', err)
} }
} else {
extractedHexEventId = bech32Id
} }
const seenOn = client.getSeenEventRelayUrls(bech32Id) setHexEventId(extractedHexEventId)
const seenOn = extractedHexEventId ? client.getSeenEventRelayUrls(extractedHexEventId) : []
externalRelays.push(...seenOn) externalRelays.push(...seenOn)
// Normalize all relays first
let normalizedRelays = externalRelays.map(url => normalizeUrl(url) || url).filter(Boolean)
normalizedRelays = Array.from(new Set(normalizedRelays))
// If no external relays from hints, try SEARCHABLE_RELAY_URLS as fallback
// Filter out relays that overlap with BIG_RELAY_URLS (already tried first)
if (normalizedRelays.length === 0) {
const searchableRelays = SEARCHABLE_RELAY_URLS
.map(url => normalizeUrl(url) || url)
.filter((url): url is string => Boolean(url))
.filter(relay => !BIG_RELAY_URLS.includes(relay))
normalizedRelays.push(...searchableRelays)
}
// Filter out relays that were already tried in tiers 1-3 // Filter out relays that were already tried in tiers 1-3
const newRelays = externalRelays.filter(relay => !alreadyTriedRelays.includes(relay)) const newRelays = normalizedRelays.filter(relay => !alreadyTriedRelays.includes(relay))
// Normalize and deduplicate final relay list // Deduplicate final relay list
const normalizedRelays = newRelays.map(url => normalizeUrl(url) || url) setExternalRelays(Array.from(new Set(newRelays)))
setExternalRelays(Array.from(new Set(normalizedRelays)))
} }
getExternalRelays() getExternalRelays()
}, [bech32Id]) }, [bech32Id])
const handleTryExternalRelays = async () => { const handleTryExternalRelays = async () => {
if (!bech32Id || isSearchingExternal) return if (!hexEventId || isSearchingExternal) return
setIsSearchingExternal(true) setIsSearchingExternal(true)
try { try {
const event = await client.fetchEventWithExternalRelays(bech32Id, externalRelays) const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays)
if (event && onEventFound) { if (event && onEventFound) {
onEventFound(event) onEventFound(event)
} }
@ -93,9 +116,9 @@ export default function NotFound({
{bech32Id && !triedExternal && hasExternalRelays && ( {bech32Id && !triedExternal && hasExternalRelays && (
<div className="flex flex-col items-center gap-3 max-w-md"> <div className="flex flex-col items-center gap-3 max-w-md">
<p className="text-sm text-center text-muted-foreground"> <div className="text-sm text-center text-muted-foreground">
{t('The note was not found on your relays or default relays.')} {t('The note was not found on your relays or default relays.')}
</p> </div>
<Button <Button
variant="default" variant="default"
@ -132,13 +155,13 @@ export default function NotFound({
)} )}
{bech32Id && !triedExternal && !hasExternalRelays && ( {bech32Id && !triedExternal && !hasExternalRelays && (
<p className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{t('No external relay hints available')} {t('No external relay hints available')}
</p> </div>
)} )}
{triedExternal && ( {triedExternal && (
<p className="text-sm">{t('Note could not be found anywhere')}</p> <div className="text-sm">{t('Note could not be found anywhere')}</div>
)} )}
<ClientSelect originalNoteId={bech32Id} /> <ClientSelect originalNoteId={bech32Id} />

Loading…
Cancel
Save