Browse Source

add nostr.watch and nostr.archive links

imwald
Silberengel 4 weeks ago
parent
commit
af09237eb0
  1. 10
      src/components/Profile/index.tsx
  2. 9
      src/components/ProfileOptions/index.tsx
  3. 24
      src/components/RelayInfo/index.tsx
  4. 3
      src/i18n/locales/de.ts
  5. 3
      src/i18n/locales/en.ts
  6. 21
      src/lib/link-external-sites.test.ts
  7. 20
      src/lib/link.ts

10
src/components/Profile/index.tsx

@ -16,7 +16,7 @@ import { kinds, type NostrEvent } from 'nostr-tools' @@ -16,7 +16,7 @@ import { kinds, type NostrEvent } from 'nostr-tools'
import { createReactionDraftEvent } from '@/lib/draft-event'
import { getPaymentInfoFromEvent } from '@/lib/event-metadata'
import { showSimplePublishSuccess, toastPublishPromise } from '@/lib/publishing-feedback'
import { toProfileEditor } from '@/lib/link'
import { getNostrArchivesProfileUrl, openExternalUrl, toProfileEditor } from '@/lib/link'
import { encodeProfileInteractionsSpellId } from '@/pages/primary/SpellsPage/fauxSpellConfig'
import { generateImageByPubkey } from '@/lib/pubkey'
import { isVideo, normalizeAnyRelayUrl } from '@/lib/url'
@ -35,6 +35,7 @@ import { @@ -35,6 +35,7 @@ import {
import {
Copy,
Ellipsis,
ExternalLink,
Calendar,
MapPin,
Pencil,
@ -502,6 +503,7 @@ export default function Profile({ @@ -502,6 +503,7 @@ export default function Profile({
if (!profile) return null // TypeScript guard - should never reach here but satisfies type checker
const { banner, username, about, avatar, pubkey, website, websiteList, nip05List, isBot } = profile
const nostrArchivesProfileUrl = getNostrArchivesProfileUrl(pubkey)
return (
<>
@ -623,6 +625,12 @@ export default function Profile({ @@ -623,6 +625,12 @@ export default function Profile({
<Network />
{t('Interactions map')}
</DropdownMenuItem>
{nostrArchivesProfileUrl ? (
<DropdownMenuItem onClick={() => openExternalUrl(nostrArchivesProfileUrl)}>
<ExternalLink />
{t('View on Nostr.Archives')}
</DropdownMenuItem>
) : null}
<DropdownMenuItem onClick={() => push(toProfileEditor())}>
<Pencil />
{t('Edit')}

9
src/components/ProfileOptions/index.tsx

@ -8,6 +8,7 @@ import { @@ -8,6 +8,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { usePrimaryPage } from '@/contexts/primary-page-context'
import { buildHiveTalkJoinUrl, roomIdForPubkeys } from '@/lib/hivetalk'
import { getNostrArchivesProfileUrl, openExternalUrl } from '@/lib/link'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
@ -26,6 +27,7 @@ import { @@ -26,6 +27,7 @@ import {
Code,
Copy,
Ellipsis,
ExternalLink,
ThumbsUp,
MessageCircle,
Network,
@ -92,6 +94,7 @@ export default function ProfileOptions({ @@ -92,6 +94,7 @@ export default function ProfileOptions({
const isMuted = useMemo(() => muteSetHas(mutePubkeySet, pubkey), [mutePubkeySet, pubkey])
const displayName = profile?.username ?? (accountPubkey ? formatPubkey(accountPubkey) : 'jumble')
const nostrArchivesProfileUrl = useMemo(() => getNostrArchivesProfileUrl(pubkey), [pubkey])
/** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */
const allAvailableRelayUrls = useMemo(() => {
@ -255,6 +258,12 @@ export default function ProfileOptions({ @@ -255,6 +258,12 @@ export default function ProfileOptions({
<Network />
{t('Interactions map')}
</DropdownMenuItem>
{nostrArchivesProfileUrl && (
<DropdownMenuItem onClick={() => openExternalUrl(nostrArchivesProfileUrl)}>
<ExternalLink />
{t('View on Nostr.Archives')}
</DropdownMenuItem>
)}
{kind0ForRelay && (
<>
<DropdownMenuSeparator />

24
src/components/RelayInfo/index.tsx

@ -1,13 +1,20 @@ @@ -1,13 +1,20 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { useFetchRelayInfo } from '@/hooks'
import { getNostrWatchRelayUrl, openExternalUrl } from '@/lib/link'
import { normalizeHttpUrl } from '@/lib/url'
import client from '@/services/client.service'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { nip66Service } from '@/services/nip66.service'
import { Check, Copy, GitBranch, Link, Mail, SquareCode, Activity } from 'lucide-react'
import { Activity, Check, Copy, Ellipsis, ExternalLink, GitBranch, Link, Mail, SquareCode } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@ -263,8 +270,10 @@ function RelayLivelinessSection({ discovery }: { discovery: TNip66RelayDiscovery @@ -263,8 +270,10 @@ function RelayLivelinessSection({ discovery }: { discovery: TNip66RelayDiscovery
}
function RelayControls({ url }: { url: string }) {
const { t } = useTranslation()
const [copiedUrl, setCopiedUrl] = useState(false)
const [copiedShareableUrl, setCopiedShareableUrl] = useState(false)
const nostrWatchUrl = useMemo(() => getNostrWatchRelayUrl(url), [url])
const handleCopyUrl = () => {
navigator.clipboard.writeText(url)
@ -287,6 +296,19 @@ function RelayControls({ url }: { url: string }) { @@ -287,6 +296,19 @@ function RelayControls({ url }: { url: string }) {
<Button variant="ghost" size="titlebar-icon" onClick={handleCopyUrl}>
{copiedUrl ? <Check /> : <Copy />}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="titlebar-icon" aria-label={t('Relay options')}>
<Ellipsis />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openExternalUrl(nostrWatchUrl)}>
<ExternalLink />
{t('View on Nostr.Watch')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<SaveRelayDropdownMenu urls={[url]} bigButton />
</div>
)

3
src/i18n/locales/de.ts

@ -1967,6 +1967,9 @@ export default { @@ -1967,6 +1967,9 @@ export default {
"View on Alexandria": "View on Alexandria",
"View on DecentNewsroom": "View on DecentNewsroom",
"View on Wikistr": "View on Wikistr",
"View on Nostr.Watch": "Auf Nostr.Watch ansehen",
"View on Nostr.Archives": "Auf Nostr.Archives ansehen",
"Relay options": "Relay-Optionen",
"View recent console logs for debugging": "View recent console logs for debugging",
"Voice Comment": "Voice Comment",
"Voice Note": "Voice Note",

3
src/i18n/locales/en.ts

@ -2038,6 +2038,9 @@ export default { @@ -2038,6 +2038,9 @@ export default {
"View on Alexandria": "View on Alexandria",
"View on DecentNewsroom": "View on DecentNewsroom",
"View on Wikistr": "View on Wikistr",
"View on Nostr.Watch": "View on Nostr.Watch",
"View on Nostr.Archives": "View on Nostr.Archives",
"Relay options": "Relay options",
"View recent console logs for debugging": "View recent console logs for debugging",
"Voice Comment": "Voice Comment",
"Voice Note": "Voice Note",

21
src/lib/link-external-sites.test.ts

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { getNostrArchivesProfileUrl, getNostrWatchRelayUrl } from './link'
describe('getNostrWatchRelayUrl', () => {
it('maps wss relay URL to nostr.watch path', () => {
expect(getNostrWatchRelayUrl('wss://relay.noswhere.com')).toBe(
'https://nostr.watch/relays/wss/relay.noswhere.com'
)
})
})
describe('getNostrArchivesProfileUrl', () => {
it('maps hex pubkey to profile URL', () => {
const hex = '63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed'
expect(getNostrArchivesProfileUrl(hex)).toBe(`https://nostrarchives.com/profiles/${hex}`)
})
it('returns null for invalid pubkey', () => {
expect(getNostrArchivesProfileUrl('not-a-pubkey')).toBeNull()
})
})

20
src/lib/link.ts

@ -1,7 +1,9 @@ @@ -1,7 +1,9 @@
import { Event, kinds, nip19 } from 'nostr-tools'
import { ExtendedKind } from '@/constants'
import { getNoteBech32Id, isReplaceableEvent } from './event'
import { isValidPubkey, normalizeHexPubkey } from './pubkey'
import { TSearchParams } from '@/types'
import { normalizeAnyRelayUrl } from './url'
/** Same kinds as {@link useMenuActions} `isArticleType` for naddr + Alexandria publication URLs. */
const ALEXANDRIA_PUBLICATION_NADDR_KINDS = new Set<number>([
@ -146,3 +148,21 @@ export const toChachiChat = (relay: string, d: string) => { @@ -146,3 +148,21 @@ export const toChachiChat = (relay: string, d: string) => {
return `https://chachi.chat/${relay.replace(/^wss?:\/\//, '').replace(/\/$/, '')}/${d}`
}
export const toAlexandria = (id: string) => `https://next-alexandria.gitcitadel.eu/events?id=${encodeURIComponent(id)}`
/** {@link https://nostr.watch/relays/wss/relay.example.com} path slug from a relay WebSocket/HTTP URL. */
export function getNostrWatchRelayUrl(relayUrl: string): string {
const normalized = (normalizeAnyRelayUrl(relayUrl) || relayUrl).trim().replace(/\/+$/, '')
const slug = normalized.replace(/^([a-z][a-z0-9+.-]*):\/\//i, '$1/')
return `https://nostr.watch/relays/${slug}`
}
/** Profile page on nostrarchives.com (hex pubkey). */
export function getNostrArchivesProfileUrl(pubkey: string): string | null {
const hex = normalizeHexPubkey(pubkey)
if (!isValidPubkey(hex)) return null
return `https://nostrarchives.com/profiles/${hex}`
}
export function openExternalUrl(url: string): void {
window.open(url, '_blank', 'noopener,noreferrer')
}

Loading…
Cancel
Save