Browse Source

bug-fix

imwald
Silberengel 4 weeks ago
parent
commit
4aac303469
  1. 2
      src/components/CalendarEventContent/index.tsx
  2. 4
      src/components/CalendarEventNip52StructuredMeta.tsx
  3. 6
      src/components/CitationCard/index.tsx
  4. 8
      src/components/Content/index.tsx
  5. 6
      src/components/ContentPreview/Content.tsx
  6. 2
      src/components/Embedded/EmbeddedHashtag.tsx
  7. 3
      src/components/Embedded/EmbeddedNormalUrl.tsx
  8. 3
      src/components/ExternalLink/index.tsx
  9. 2
      src/components/FavoriteRelaysActiveStrip/RelayPulseActiveNpubsSheet.tsx
  10. 2
      src/components/Image/index.tsx
  11. 2
      src/components/KeyboardShortcutsHelp/index.tsx
  12. 4
      src/components/Nip05/index.tsx
  13. 4
      src/components/Nip05List/index.tsx
  14. 19
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  15. 2
      src/components/Note/GitRepublicEventCard.tsx
  16. 2
      src/components/Note/Highlight/index.tsx
  17. 60
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  18. 2
      src/components/Note/index.tsx
  19. 91
      src/components/NoteStats/ZapButton.tsx
  20. 14
      src/components/PaymentMethodsSection/index.tsx
  21. 12
      src/components/PaytoLink/index.tsx
  22. 42
      src/components/Profile/index.tsx
  23. 16
      src/components/ProfileAbout/index.tsx
  24. 2
      src/components/RelayInfo/index.tsx
  25. 2
      src/components/RelayStatusDisplay/index.tsx
  26. 2
      src/components/RssFeedItem/index.tsx
  27. 2
      src/components/StandardRssFeedUrlRow/index.tsx
  28. 2
      src/components/UniversalContent/Wikilink.tsx
  29. 2
      src/components/VideoPlayer/index.tsx
  30. 10
      src/components/WebPreview/index.tsx
  31. 14
      src/hooks/useRecipientAlternativePayments.ts
  32. 14
      src/index.css
  33. 10
      src/lib/lightning.ts
  34. 14
      src/lib/link-styles.ts
  35. 89
      src/lib/merge-payment-methods.test.ts
  36. 41
      src/lib/merge-payment-methods.ts
  37. 7
      src/lib/payto-registry.ts
  38. 1
      src/lib/payto.ts
  39. 6
      src/pages/primary/CalendarPrimaryPage.tsx
  40. 4
      src/pages/secondary/PersonalListsSettingsPage/index.tsx
  41. 2
      src/pages/secondary/PostSettingsPage/BlossomServerListSetting.tsx
  42. 6
      src/services/content-parser.service.ts
  43. 5
      tailwind.config.js

2
src/components/CalendarEventContent/index.tsx

@ -283,7 +283,7 @@ export default function CalendarEventContent({ @@ -283,7 +283,7 @@ export default function CalendarEventContent({
href={url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs font-medium text-primary underline-offset-2 hover:underline"
className="inline-flex items-center gap-1 text-xs font-medium text-primary underline-offset-2 hover:text-foreground hover:underline transition-colors"
>
<ExternalLink className="size-3 shrink-0 opacity-80" aria-hidden />
{t('Open link')}

4
src/components/CalendarEventNip52StructuredMeta.tsx

@ -51,7 +51,7 @@ export function CalendarEventNip52StructuredMeta({ @@ -51,7 +51,7 @@ export function CalendarEventNip52StructuredMeta({
if (!hasLocations && !summaryTrim && !hasGeo) return null
const linkClass =
'inline-flex shrink-0 items-center gap-1 text-xs font-medium text-primary underline-offset-2 hover:underline'
'inline-flex shrink-0 items-center gap-1 text-xs font-medium text-primary underline-offset-2 hover:text-foreground hover:underline transition-colors'
return (
<div className="min-w-0 space-y-2.5 border-t border-border/50 pt-2">
@ -183,7 +183,7 @@ export function CalendarEventNip52StructuredMeta({ @@ -183,7 +183,7 @@ export function CalendarEventNip52StructuredMeta({
href={r.value}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-w-0 max-w-full items-start gap-1 break-all text-xs font-medium text-primary underline-offset-2 hover:underline"
className="inline-flex min-w-0 max-w-full items-start gap-1 break-all text-xs font-medium text-primary underline-offset-2 hover:text-foreground hover:underline transition-colors"
>
<Link2 className="mt-0.5 size-3 shrink-0 opacity-80" aria-hidden />
<span>{r.value}</span>

6
src/components/CitationCard/index.tsx

@ -390,7 +390,7 @@ export default function CitationCard({ event, className, displayType = 'end', ci @@ -390,7 +390,7 @@ export default function CitationCard({ event, className, displayType = 'end', ci
<div className="text-muted-foreground">{formatDate(citationData.publishedOn)}</div>
)}
{citationData.url && (
<div className="flex items-center gap-1 text-primary hover:underline">
<div className="flex items-center gap-1 text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors">
<ExternalLink className="w-3 h-3" />
<a href={citationData.url} target="_blank" rel="noreferrer noopener" className="break-all">
{citationData.url}
@ -526,7 +526,7 @@ export default function CitationCard({ event, className, displayType = 'end', ci @@ -526,7 +526,7 @@ export default function CitationCard({ event, className, displayType = 'end', ci
<div className="text-xs text-muted-foreground">{t('Version')}: {citationData.version}</div>
)}
{citationData.url && (
<div className="flex items-center gap-1 text-primary hover:underline">
<div className="flex items-center gap-1 text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors">
<ExternalLink className="w-3 h-3" />
<a href={citationData.url} target="_blank" rel="noreferrer noopener" className="break-all">
{citationData.url}
@ -572,7 +572,7 @@ export default function CitationCard({ event, className, displayType = 'end', ci @@ -572,7 +572,7 @@ export default function CitationCard({ event, className, displayType = 'end', ci
<span className={className}>
<a
href={`/notes/${event.id}`}
className="text-primary hover:underline"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors"
onClick={(e) => {
e.preventDefault()
// Scroll to full citation in references section

8
src/components/Content/index.tsx

@ -65,7 +65,7 @@ function renderRedirectText(text: string, key: number) { @@ -65,7 +65,7 @@ function renderRedirectText(text: string, key: number) {
{prefix}
Read{' '}
<a
className="text-primary hover:underline"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors"
href={href}
onClick={(e) => e.stopPropagation()}
>
@ -618,11 +618,7 @@ export default function Content({ @@ -618,11 +618,7 @@ export default function Content({
}
if (node.type === 'payto') {
return (
<PaytoLink
key={index}
paytoUri={node.data}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
/>
<PaytoLink key={index} paytoUri={node.data} />
)
}
if (node.type === 'websocket-url') {

6
src/components/ContentPreview/Content.tsx

@ -42,11 +42,7 @@ export default function Content({ @@ -42,11 +42,7 @@ export default function Content({
}
if (node.type === 'payto') {
return (
<PaytoLink
key={index}
paytoUri={node.data}
className="text-green-600 dark:text-green-400 hover:underline break-words"
/>
<PaytoLink key={index} paytoUri={node.data} />
)
}
if (node.type === 'emoji') {

2
src/components/Embedded/EmbeddedHashtag.tsx

@ -13,7 +13,7 @@ export function EmbeddedHashtag({ hashtag }: { hashtag: string }) { @@ -13,7 +13,7 @@ export function EmbeddedHashtag({ hashtag }: { hashtag: string }) {
return (
<button
className="text-primary hover:underline cursor-pointer"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors cursor-pointer"
onClick={handleClick}
>
{hashtag}

3
src/components/Embedded/EmbeddedNormalUrl.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { URI_LINK_CLASS } from '@/lib/link-styles'
import { cleanUrl } from '@/lib/url'
import React from 'react'
@ -8,7 +9,7 @@ export function EmbeddedNormalUrl({ url, children }: { url: string; children?: R @@ -8,7 +9,7 @@ export function EmbeddedNormalUrl({ url, children }: { url: string; children?: R
// Render all URLs as green text links (like hashtags) - WebPreview cards shown at bottom
return (
<a
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline"
className={URI_LINK_CLASS}
href={cleanedUrl}
target="_blank"
onClick={(e) => e.stopPropagation()}

3
src/components/ExternalLink/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { URI_LINK_CLASS } from '@/lib/link-styles'
import { cn } from '@/lib/utils'
import { cleanUrl } from '@/lib/url'
@ -5,7 +6,7 @@ export default function ExternalLink({ url, className }: { url: string; classNam @@ -5,7 +6,7 @@ export default function ExternalLink({ url, className }: { url: string; classNam
const cleanedUrl = cleanUrl(url)
return (
<a
className={cn('text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline', className)}
className={cn(URI_LINK_CLASS, className)}
href={cleanedUrl}
target="_blank"
onClick={(e) => e.stopPropagation()}

2
src/components/FavoriteRelaysActiveStrip/RelayPulseActiveNpubsSheet.tsx

@ -52,7 +52,7 @@ function CompactProfileCard({ event }: { event: Event }) { @@ -52,7 +52,7 @@ function CompactProfileCard({ event }: { event: Event }) {
<li key={id} className="truncate font-mono">
<SecondaryPageLink
to={profileUrl}
className="text-primary hover:underline"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors"
onClick={closeDrawer}
>
{id}

2
src/components/Image/index.tsx

@ -361,7 +361,7 @@ export default function Image({ @@ -361,7 +361,7 @@ export default function Image({
href={openLinkHref}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary underline-offset-4 hover:underline break-all max-w-full"
className="text-sm text-primary underline-offset-4 hover:text-foreground hover:underline transition-colors break-all max-w-full"
onClick={(e) => e.stopPropagation()}
>
{t('Open image link')}

2
src/components/KeyboardShortcutsHelp/index.tsx

@ -176,7 +176,7 @@ function ReadmeOverviewPanel({ className }: { className?: string }) { @@ -176,7 +176,7 @@ function ReadmeOverviewPanel({ className }: { className?: string }) {
<div
className={cn(
'min-w-0 pt-1 text-sm prose prose-sm dark:prose-invert max-w-none',
'[&_a]:text-green-600 [&_a]:dark:text-green-400 hover:[&_a]:underline',
'[&_a]:text-link-uri [&_a]:no-underline hover:[&_a]:text-primary hover:[&_a]:underline',
'[&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded',
'[&_pre]:bg-muted [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-x-auto',
'[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-md',

4
src/components/Nip05/index.tsx

@ -29,7 +29,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str @@ -29,7 +29,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str
onClick={(e) => e.stopPropagation()}
>
{nip05Name !== '_' ? (
<span className="text-sm text-muted-foreground truncate">@{nip05Name}</span>
<span className="text-sm text-muted-foreground truncate shrink-0">{nip05Name}</span>
) : null}
{nip05IsVerified ? (
<Favicon
@ -42,7 +42,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str @@ -42,7 +42,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str
)}
<SecondaryPageLink
to={toNoteList({ domain: nip05Domain })}
className={`hover:underline truncate text-sm ${nip05IsVerified ? 'text-primary' : 'text-muted-foreground'}`}
className={`truncate text-sm hover:text-foreground hover:underline underline-offset-2 transition-colors ${nip05IsVerified ? 'text-primary' : 'text-muted-foreground'}`}
>
{nip05Domain}
</SecondaryPageLink>

4
src/components/Nip05List/index.tsx

@ -102,7 +102,7 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[]; @@ -102,7 +102,7 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[];
onClick={(e) => e.stopPropagation()}
>
{nip05Name !== '_' ? (
<span className="text-sm text-muted-foreground truncate">@{nip05Name}</span>
<span className="text-sm text-muted-foreground truncate shrink-0">{nip05Name}@</span>
) : null}
{isVerified ? (
<Favicon
@ -115,7 +115,7 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[]; @@ -115,7 +115,7 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[];
)}
<SecondaryPageLink
to={toNoteList({ domain: nip05Domain })}
className={`hover:underline truncate text-sm ${isVerified ? 'text-primary' : 'text-muted-foreground'}`}
className={`truncate text-sm hover:text-foreground hover:underline underline-offset-2 transition-colors ${isVerified ? 'text-primary' : 'text-muted-foreground'}`}
>
{nip05Domain}
</SecondaryPageLink>

19
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -27,6 +27,7 @@ import { EmbeddedNote, EmbeddedMention } from '@/components/Embedded' @@ -27,6 +27,7 @@ import { EmbeddedNote, EmbeddedMention } from '@/components/Embedded'
import EmbeddedCitation from '@/components/EmbeddedCitation'
import { parsePaytoUri } from '@/lib/payto'
import PaytoLink from '@/components/PaytoLink'
import { URI_LINK_CLASS, URI_LINK_INLINE_HTML_CLASS } from '@/lib/link-styles'
import { DeletedEventProvider } from '@/providers/DeletedEventProvider'
import { ReplyProvider } from '@/providers/ReplyProvider'
import Wikilink from '@/components/UniversalContent/Wikilink'
@ -923,7 +924,7 @@ export default function AsciidocArticle({ @@ -923,7 +924,7 @@ export default function AsciidocArticle({
// Check if the href is a relay URL
else if (isWebsocketUrl(href)) {
const relayPath = `/relays/${encodeURIComponent(href)}`
replacement = `<a href="${relayPath}" class="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer" data-relay-url="${href}" data-original-text="${linkText.replace(/"/g, '&quot;')}">${linkText}</a>`
replacement = `<a href="${relayPath}" class="${URI_LINK_INLINE_HTML_CLASS} cursor-pointer" data-relay-url="${href}" data-original-text="${linkText.replace(/"/g, '&quot;')}">${linkText}</a>`
}
htmlString = htmlString.substring(0, linkMatches[i].index) + replacement + htmlString.substring(linkMatches[i].index + match.length)
@ -946,7 +947,7 @@ export default function AsciidocArticle({ @@ -946,7 +947,7 @@ export default function AsciidocArticle({
// Only replace if not already in a tag (basic check)
if (!match.includes('<') && !match.includes('>') && isWebsocketUrl(match)) {
const relayPath = `/relays/${encodeURIComponent(match)}`
return `<a href="${relayPath}" class="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer" data-relay-url="${match}" data-original-text="${match.replace(/"/g, '&quot;')}">${match}</a>`
return `<a href="${relayPath}" class="${URI_LINK_INLINE_HTML_CLASS} cursor-pointer" data-relay-url="${match}" data-original-text="${match.replace(/"/g, '&quot;')}">${match}</a>`
}
return match
})
@ -961,7 +962,7 @@ export default function AsciidocArticle({ @@ -961,7 +962,7 @@ export default function AsciidocArticle({
if (isImage(rawUrl) || isVideo(rawUrl) || isAudio(rawUrl)) return rawUrl
const cleanedUrl = cleanUrl(rawUrl)
if (!cleanedUrl) return rawUrl
return `<a href="${cleanedUrl}" class="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words" target="_blank" rel="noopener noreferrer">${rawUrl}</a>`
return `<a href="${cleanedUrl}" class="${URI_LINK_INLINE_HTML_CLASS}" target="_blank" rel="noopener noreferrer">${rawUrl}</a>`
})
return `>${replacedText}<`
})
@ -1102,7 +1103,7 @@ export default function AsciidocArticle({ @@ -1102,7 +1103,7 @@ export default function AsciidocArticle({
parent.replaceChild(container, element)
try {
const root = createRoot(container)
root.render(<PaytoLink paytoUri={decoded} className="text-primary hover:underline break-words" />)
root.render(<PaytoLink paytoUri={decoded} />)
reactRootsRef.current.set(container, root)
} catch (error) {
logger.error('Failed to render payto link', { paytoUri: decoded, error })
@ -1198,7 +1199,7 @@ export default function AsciidocArticle({ @@ -1198,7 +1199,7 @@ export default function AsciidocArticle({
const link = document.createElement('a')
link.href = `#${getCitationAnchorId(citation.index)}`
link.id = getCitationRefId(citation.index)
link.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline'
link.className = `${URI_LINK_CLASS} no-underline`
link.textContent = `[${citationNumber}]`
link.addEventListener('click', (e) => {
e.preventDefault()
@ -1218,7 +1219,7 @@ export default function AsciidocArticle({ @@ -1218,7 +1219,7 @@ export default function AsciidocArticle({
const link = document.createElement('a')
link.href = `#${referencesSectionId}`
link.id = getCitationRefId(citation.index)
link.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline'
link.className = `${URI_LINK_CLASS} no-underline`
link.textContent = `[${citationNumber}]`
link.addEventListener('click', (e) => {
e.preventDefault()
@ -1332,7 +1333,7 @@ export default function AsciidocArticle({ @@ -1332,7 +1333,7 @@ export default function AsciidocArticle({
const backLink = document.createElement('a')
backLink.href = `#${getCitationRefId(citation.index)}`
backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center'
backLink.className = `text-xs ml-2 inline-flex items-center ${URI_LINK_CLASS}`
backLink.setAttribute('aria-label', 'Return to citation')
// Use hyperlink icon instead of emoji
backLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>'
@ -1430,7 +1431,7 @@ export default function AsciidocArticle({ @@ -1430,7 +1431,7 @@ export default function AsciidocArticle({
const backLink = document.createElement('a')
backLink.href = `#${getCitationRefId(citation.index)}`
backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center'
backLink.className = `text-xs ml-2 inline-flex items-center ${URI_LINK_CLASS}`
backLink.setAttribute('aria-label', 'Return to citation')
backLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>'
backLink.addEventListener('click', (e) => {
@ -1697,7 +1698,7 @@ export default function AsciidocArticle({ @@ -1697,7 +1698,7 @@ export default function AsciidocArticle({
// Create hashtag link
const link = document.createElement('a')
link.href = `/notes?t=${match[1].toLowerCase()}`
link.className = 'inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer'
link.className = `${URI_LINK_INLINE_HTML_CLASS} cursor-pointer`
link.textContent = `#${match[1]}`
link.addEventListener('click', (e) => {
e.stopPropagation()

2
src/components/Note/GitRepublicEventCard.tsx

@ -145,7 +145,7 @@ export default function GitRepublicEventCard({ @@ -145,7 +145,7 @@ export default function GitRepublicEventCard({
href={webUrl}
target="_blank"
rel="noreferrer noopener"
className="inline-flex max-w-full items-center gap-1 text-xs font-medium text-primary hover:underline"
className="inline-flex max-w-full items-center gap-1 text-xs font-medium text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="size-3 shrink-0" aria-hidden />

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

@ -90,7 +90,7 @@ function HighlightAuthorCard({ @@ -90,7 +90,7 @@ function HighlightAuthorCard({
A{' '}
<button
onClick={handleNoteClick}
className="text-primary hover:text-primary/80 hover:underline font-medium cursor-pointer"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors font-medium cursor-pointer"
>
note
</button>

60
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -25,6 +25,7 @@ import { @@ -25,6 +25,7 @@ import {
} from '@/lib/url'
import { getHttpUrlFromITags, getImetaInfosFromEvent } from '@/lib/event'
import { canonicalizeRssArticleUrl } from '@/lib/rss-article'
import { URI_LINK_CLASS } from '@/lib/link-styles'
import { cn } from '@/lib/utils'
import { Event, kinds } from 'nostr-tools'
import Emoji, { EMOJI_IMG_INLINE_CLASS } from '@/components/Emoji'
@ -2188,7 +2189,7 @@ function parseMarkdownContentLegacy( @@ -2188,7 +2189,7 @@ function parseMarkdownContentLegacy(
<a
key={`link-${patternIdx}`}
href={url}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
target="_blank"
rel="noopener noreferrer"
>
@ -2202,7 +2203,7 @@ function parseMarkdownContentLegacy( @@ -2202,7 +2203,7 @@ function parseMarkdownContentLegacy(
<a
key={`link-${patternIdx}`}
href={url}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
target="_blank"
rel="noopener noreferrer"
>
@ -2236,7 +2237,7 @@ function parseMarkdownContentLegacy( @@ -2236,7 +2237,7 @@ function parseMarkdownContentLegacy(
<a
key={`link-${patternIdx}`}
href={url}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
target="_blank"
rel="noopener noreferrer"
>
@ -2248,7 +2249,7 @@ function parseMarkdownContentLegacy( @@ -2248,7 +2249,7 @@ function parseMarkdownContentLegacy(
<a
key={`link-${patternIdx}`}
href={url}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
target="_blank"
rel="noopener noreferrer"
>
@ -2283,7 +2284,7 @@ function parseMarkdownContentLegacy( @@ -2283,7 +2284,7 @@ function parseMarkdownContentLegacy(
<a
key={`relay-${patternIdx}`}
href={relayPath}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer"
className={cn('inline cursor-pointer', URI_LINK_CLASS)}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
@ -2300,7 +2301,7 @@ function parseMarkdownContentLegacy( @@ -2300,7 +2301,7 @@ function parseMarkdownContentLegacy(
<a
key={`link-${patternIdx}`}
href={url}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
target="_blank"
rel="noopener noreferrer"
>
@ -2347,7 +2348,7 @@ function parseMarkdownContentLegacy( @@ -2347,7 +2348,7 @@ function parseMarkdownContentLegacy(
<a
key={`relay-${patternIdx}`}
href={relayPath}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer"
className={cn('inline cursor-pointer', URI_LINK_CLASS)}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
@ -2547,7 +2548,7 @@ function parseMarkdownContentLegacy( @@ -2547,7 +2548,7 @@ function parseMarkdownContentLegacy(
<a
href={`#footnote-${footnoteId}`}
id={`footnote-ref-${footnoteId}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline"
className={cn(URI_LINK_CLASS, 'no-underline')}
onClick={(e) => {
e.preventDefault()
const footnoteElement = document.getElementById(`footnote-${footnoteId}`)
@ -2585,7 +2586,7 @@ function parseMarkdownContentLegacy( @@ -2585,7 +2586,7 @@ function parseMarkdownContentLegacy(
<a
href={`#citation-${citationIndex}`}
id={`citation-ref-${citationIndex}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline"
className={cn(URI_LINK_CLASS, 'no-underline')}
onClick={(e) => {
e.preventDefault()
const citationElement = document.getElementById(`citation-${citationIndex}`)
@ -2615,7 +2616,7 @@ function parseMarkdownContentLegacy( @@ -2615,7 +2616,7 @@ function parseMarkdownContentLegacy(
<a
href="#references-section"
id={`citation-ref-${citationIndex}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline"
className={cn(URI_LINK_CLASS, 'no-underline')}
onClick={(e) => {
e.preventDefault()
const refSection = document.getElementById('references-section')
@ -2673,7 +2674,7 @@ function parseMarkdownContentLegacy( @@ -2673,7 +2674,7 @@ function parseMarkdownContentLegacy(
<a
key={`hashtag-${patternIdx}`}
href={`/notes?t=${tagLower}`}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer whitespace-nowrap"
className={cn('inline cursor-pointer whitespace-nowrap', URI_LINK_CLASS)}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
@ -3041,7 +3042,7 @@ function parseMarkdownContentLegacy( @@ -3041,7 +3042,7 @@ function parseMarkdownContentLegacy(
{' '}
<a
href={`#footnote-ref-${id}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs"
className={cn('text-xs', URI_LINK_CLASS)}
onClick={(e) => {
e.preventDefault()
const refElement = document.getElementById(`footnote-ref-${id}`)
@ -3082,7 +3083,7 @@ function parseMarkdownContentLegacy( @@ -3082,7 +3083,7 @@ function parseMarkdownContentLegacy(
</span>
<a
href={`#citation-ref-${citation.id.replace('citation-', '')}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center absolute right-0 top-0"
className={cn('text-xs ml-2 inline-flex items-center absolute right-0 top-0', URI_LINK_CLASS)}
aria-label="Return to citation"
onClick={(e) => {
e.preventDefault()
@ -3126,7 +3127,7 @@ function parseMarkdownContentLegacy( @@ -3126,7 +3127,7 @@ function parseMarkdownContentLegacy(
</span>
<a
href={`#citation-ref-${citation.id.replace('citation-', '')}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center absolute right-0 top-0"
className={cn('text-xs ml-2 inline-flex items-center absolute right-0 top-0', URI_LINK_CLASS)}
aria-label="Return to citation"
onClick={(e) => {
e.preventDefault()
@ -3415,8 +3416,9 @@ function parseMarkdownContentMarked( @@ -3415,8 +3416,9 @@ function parseMarkdownContentMarked(
const cleaned = cleanUrl(href)
const linkTip = markdownTokenTitle(token)
const linkVisual = cn(
'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words',
linkTip && 'cursor-help underline decoration-dotted decoration-current/70 underline-offset-2'
URI_LINK_CLASS,
linkTip &&
'cursor-help no-underline hover:underline decoration-dotted decoration-current/70 underline-offset-2'
)
if (href.startsWith('payto://')) {
const children = stripNestedAnchorsFromNodes(
@ -3711,7 +3713,7 @@ function parseMarkdownContentMarked( @@ -3711,7 +3713,7 @@ function parseMarkdownContentMarked(
<a
key={`${key}-relay`}
href={`/relays/${encodeURIComponent(paragraphText)}`}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
onClick={(e) => {
e.preventDefault()
navigateToRelay(paragraphText)
@ -3742,7 +3744,7 @@ function parseMarkdownContentMarked( @@ -3742,7 +3744,7 @@ function parseMarkdownContentMarked(
<a
key={`${key}-line-relay-${lineIdx}`}
href={`/relays/${encodeURIComponent(line)}`}
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
onClick={(e) => {
e.preventDefault()
navigateToRelay(line)
@ -3817,7 +3819,7 @@ function parseMarkdownContentMarked( @@ -3817,7 +3819,7 @@ function parseMarkdownContentMarked(
href={cleaned}
target="_blank"
rel="noopener noreferrer"
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
>
{cleaned}
</a>
@ -3990,7 +3992,7 @@ function parseMarkdownContentMarked( @@ -3990,7 +3992,7 @@ function parseMarkdownContentMarked(
href={cleaned}
target="_blank"
rel="noopener noreferrer"
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={cn('inline', URI_LINK_CLASS)}
>
{cleaned}
</a>
@ -4672,7 +4674,7 @@ function parseMarkdownContentMarked( @@ -4672,7 +4674,7 @@ function parseMarkdownContentMarked(
<span>{parseInlineMarkdown(text, `footnote-${id}`, footnotes, emojiInfos, navigateToHashtag, emojiLightbox)}</span>{' '}
<a
href={`#footnote-ref-${id}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs"
className={cn('text-xs', URI_LINK_CLASS)}
onClick={(e) => {
e.preventDefault()
const refElement = document.getElementById(`footnote-ref-${id}`)
@ -4789,7 +4791,7 @@ function parseInlineMarkdown( @@ -4789,7 +4791,7 @@ function parseInlineMarkdown(
<PaytoLink
key={`${tokenKey}-payto-link`}
paytoUri={href}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
>
{children}
</PaytoLink>
@ -4799,7 +4801,7 @@ function parseInlineMarkdown( @@ -4799,7 +4801,7 @@ function parseInlineMarkdown(
<a
key={`${tokenKey}-link`}
href={href}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
target="_blank"
rel="noopener noreferrer"
>
@ -5076,7 +5078,7 @@ function parseInlineMarkdownLegacy( @@ -5076,7 +5078,7 @@ function parseInlineMarkdownLegacy(
const { text, url } = pattern.data
if (url.startsWith('payto://')) {
parts.push(
<PaytoLink key={`${keyPrefix}-payto-link-${i}`} paytoUri={url} className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words">
<PaytoLink key={`${keyPrefix}-payto-link-${i}`} paytoUri={url} className={URI_LINK_CLASS}>
{parseInlineMarkdownLegacy(text, `${keyPrefix}-link-${i}`, _footnotes, emojiInfos, undefined, emojiLightbox)}
</PaytoLink>
)
@ -5093,7 +5095,7 @@ function parseInlineMarkdownLegacy( @@ -5093,7 +5095,7 @@ function parseInlineMarkdownLegacy(
<a
key={`${keyPrefix}-link-${i}`}
href={url}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
target="_blank"
rel="noopener noreferrer"
>
@ -5109,7 +5111,7 @@ function parseInlineMarkdownLegacy( @@ -5109,7 +5111,7 @@ function parseInlineMarkdownLegacy(
<a
key={`${keyPrefix}-hashtag-${i}`}
href={`/notes?t=${tagLower}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
onClick={(e) => {
if (!navigateToHashtag) return
e.stopPropagation()
@ -5127,7 +5129,7 @@ function parseInlineMarkdownLegacy( @@ -5127,7 +5129,7 @@ function parseInlineMarkdownLegacy(
<a
href={`#footnote-${footnoteId}`}
id={`footnote-ref-${footnoteId}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs"
className={cn('text-xs', URI_LINK_CLASS)}
onClick={(e) => {
e.preventDefault()
const footnoteElement = document.getElementById(`footnote-${footnoteId}`)
@ -5150,7 +5152,7 @@ function parseInlineMarkdownLegacy( @@ -5150,7 +5152,7 @@ function parseInlineMarkdownLegacy(
<a
key={`${keyPrefix}-relay-${i}`}
href={relayPath}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
>
{url}
</a>
@ -5175,7 +5177,7 @@ function parseInlineMarkdownLegacy( @@ -5175,7 +5177,7 @@ function parseInlineMarkdownLegacy(
<PaytoLink
key={`${keyPrefix}-payto-${i}`}
paytoUri={payto.raw}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
/>
)
} else if (pattern.type === 'emoji') {

2
src/components/Note/index.tsx

@ -442,7 +442,7 @@ export default function Note({ @@ -442,7 +442,7 @@ export default function Note({
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary underline-offset-4 hover:underline break-all"
className="text-sm text-primary underline-offset-4 hover:text-foreground hover:underline transition-colors break-all"
>
{href}
</a>

91
src/components/NoteStats/ZapButton.tsx

@ -1,18 +1,22 @@ @@ -1,18 +1,22 @@
import { Skeleton } from '@/components/ui/skeleton'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { getLightningAddressFromProfile } from '@/lib/lightning'
import {
buildOrderedZapLightningAddresses,
recipientHasAnyPaymentOptions
} from '@/lib/merge-payment-methods'
import { getPaymentInfoFromEvent, getProfileFromEvent } from '@/lib/event-metadata'
import { cn } from '@/lib/utils'
import { useNoteFeedProfileContext } from '@/providers/NoteFeedProfileContext'
import { useNostr } from '@/providers/NostrProvider'
import { useZap } from '@/providers/ZapProvider'
import { replaceableEventService } from '@/services/client.service'
import { getProfileFromEvent } from '@/lib/event-metadata'
import { kinds } from 'nostr-tools'
import client, { replaceableEventService } from '@/services/client.service'
import type { TProfile } from '@/types'
import { kinds, type Event } from 'nostr-tools'
import lightning from '@/services/lightning.service'
import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service'
import { Zap } from 'lucide-react'
import { Event } from 'nostr-tools'
import { MouseEvent, TouchEvent, useEffect, useMemo, useRef, useState } from 'react'
import { MouseEvent, TouchEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import ZapDialog from '../ZapDialog'
@ -40,25 +44,79 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -40,25 +44,79 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
}
}, [noteStats, pubkey])
const showZapAmount = !hideCount && (statsLoaded || (zapAmount ?? 0) > 0)
const authorPubkey = event.pubkey.toLowerCase()
const isSelf = !!pubkey && pubkey.toLowerCase() === authorPubkey
const feedProfiles = useNoteFeedProfileContext()
const feedProfile = feedProfiles?.profiles.get(authorPubkey)
const feedProfileRef = useRef(feedProfile)
feedProfileRef.current = feedProfile
const [disable, setDisable] = useState(true)
const [canLightningZap, setCanLightningZap] = useState(false)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const isLongPressRef = useRef(false)
const applyTipAvailability = useCallback(
(
profile: TProfile | null,
profileEvent: Event | null | undefined,
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null
) => {
const canTip = recipientHasAnyPaymentOptions(paymentInfo, profile, profileEvent ?? null)
setDisable(!canTip)
setCanLightningZap(
buildOrderedZapLightningAddresses({ profileEvent, paymentInfo }).length > 0
)
},
[]
)
/** Re-enable when the feed batch loads a real profile (not a placeholder row). */
useEffect(() => {
if (isSelf) return
if (!feedProfile || feedProfile.batchPlaceholder) return
applyTipAvailability(feedProfile, null, null)
}, [isSelf, feedProfile, feedProfiles?.version, applyTipAvailability])
useEffect(() => {
if (isSelf) {
setDisable(true)
setCanLightningZap(false)
return
}
setDisable(true)
setCanLightningZap(false)
let cancelled = false
replaceableEventService.fetchReplaceableEvent(event.pubkey, kinds.Metadata).then((profileEvent) => {
void Promise.allSettled([
replaceableEventService.fetchReplaceableEvent(authorPubkey, kinds.Metadata),
client.fetchPaymentInfoEvent(authorPubkey),
replaceableEventService.getProfileFromIndexedDB(authorPubkey)
]).then(([profileRes, paymentRes, idbRes]) => {
if (cancelled) return
const profile = profileEvent ? getProfileFromEvent(profileEvent) : undefined
if (!profile) return
if (pubkey === profile.pubkey) return
const lightningAddress = getLightningAddressFromProfile(profile)
if (lightningAddress) setDisable(false)
const profileEvent =
profileRes.status === 'fulfilled' ? profileRes.value : undefined
const paymentEvent =
paymentRes.status === 'fulfilled' ? paymentRes.value : undefined
const idbProfile = idbRes.status === 'fulfilled' ? idbRes.value : undefined
const cachedFeed = feedProfileRef.current
const profile =
(profileEvent ? getProfileFromEvent(profileEvent) : null) ??
(cachedFeed && !cachedFeed.batchPlaceholder ? cachedFeed : null) ??
idbProfile ??
null
const paymentInfo = paymentEvent ? getPaymentInfoFromEvent(paymentEvent) : null
applyTipAvailability(profile, profileEvent ?? null, paymentInfo)
})
return () => {
cancelled = true
}
}, [event.pubkey, pubkey])
}, [authorPubkey, isSelf, applyTipAvailability])
const handleZap = async () => {
try {
@ -143,7 +201,14 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -143,7 +201,14 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
setZapping(true)
})
} else if (!isLongPressRef.current) {
if (canLightningZap) {
checkLogin(() => handleZap())
} else {
checkLogin(() => {
setOpenZapDialog(true)
setZapping(true)
})
}
}
isLongPressRef.current = false
}

14
src/components/PaymentMethodsSection/index.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import PaytoLink from '@/components/PaytoLink'
import type { PaymentMethodGroup } from '@/lib/merge-payment-methods'
import { isLightningPaytoType } from '@/lib/payto'
import { PRIMARY_LINK_HOVER_CLASS } from '@/lib/link-styles'
import { isZappableLightningPaytoType } from '@/lib/payto'
import { cn } from '@/lib/utils'
import { Copy } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@ -33,10 +34,7 @@ export default function PaymentMethodsSection({ @@ -33,10 +34,7 @@ export default function PaymentMethodsSection({
{title ?? t('Payment Methods')}
</div>
{headerHelpText ? (
<p
className="mb-3 rounded-md border border-amber-500/45 bg-amber-500/15 px-3 py-2.5 text-sm font-semibold leading-snug text-foreground"
role="note"
>
<p className="mb-3 text-xs leading-snug text-muted-foreground" role="note">
{headerHelpText}
</p>
) : null}
@ -62,13 +60,13 @@ export default function PaymentMethodsSection({ @@ -62,13 +60,13 @@ export default function PaymentMethodsSection({
type={method.type}
authority={method.authority}
paytoUri={method.payto}
pubkey={isLightningPaytoType(method.type) ? recipientPubkey : undefined}
pubkey={isZappableLightningPaytoType(method.type) ? recipientPubkey : undefined}
onOpenZap={
isLightningPaytoType(method.type) && onOpenZap
isZappableLightningPaytoType(method.type) && onOpenZap
? (_pk, authority) => onOpenZap(authority)
: undefined
}
className="hover:underline break-all min-w-0 text-primary flex-1"
className={cn(PRIMARY_LINK_HOVER_CLASS, 'break-all min-w-0 flex-1')}
>
{method.authority}
</PaytoLink>

12
src/components/PaytoLink/index.tsx

@ -10,10 +10,12 @@ import { @@ -10,10 +10,12 @@ import {
getPaytoLogoPath,
getPaytoProfileUrl,
isKnownPaytoType,
isLightningPaytoType
isLightningPaytoType,
isZappableLightningPaytoType
} from '@/lib/payto'
import PaytoDialog from '@/components/PaytoDialog'
import { HelpCircle } from 'lucide-react'
import { PRIMARY_LINK_HOVER_CLASS, URI_LINK_CLASS } from '@/lib/link-styles'
import { cn } from '@/lib/utils'
export default function PaytoLink({
@ -58,7 +60,7 @@ export default function PaytoLink({ @@ -58,7 +60,7 @@ export default function PaytoLink({
const info = getPaytoTypeInfo(type)
const known = isKnownPaytoType(type)
const isLightning = isLightningPaytoType(type)
const canZap = isLightning && !!pubkey && !!onOpenZap
const canZap = isZappableLightningPaytoType(type) && !!pubkey && !!onOpenZap
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
@ -112,7 +114,8 @@ export default function PaytoLink({ @@ -112,7 +114,8 @@ export default function PaytoLink({
target="_blank"
rel="noopener noreferrer"
className={cn(
'text-primary hover:underline cursor-pointer text-left break-words inline-flex items-center gap-1.5',
URI_LINK_CLASS,
'cursor-pointer text-left inline-flex items-center gap-1.5',
className
)}
title={
@ -133,7 +136,8 @@ export default function PaytoLink({ @@ -133,7 +136,8 @@ export default function PaytoLink({
type="button"
onClick={handleClick}
className={cn(
'text-primary hover:underline cursor-pointer text-left break-words inline-flex items-center gap-1.5',
URI_LINK_CLASS,
'cursor-pointer text-left inline-flex items-center gap-1.5',
className
)}
title={

42
src/components/Profile/index.tsx

@ -86,12 +86,13 @@ import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' @@ -86,12 +86,13 @@ import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import { nip66Service } from '@/services/nip66.service'
import PaymentMethodsSection from '@/components/PaymentMethodsSection'
import {
getAlternativePaymentMethods,
groupPaymentMethodsByDisplayType,
mergePaymentMethods,
recipientHasAnyPaymentOptions,
sortMergedPaymentMethods
} from '@/lib/merge-payment-methods'
import { isLightningPaytoType } from '@/lib/payto'
import { PRIMARY_LINK_HOVER_CLASS } from '@/lib/link-styles'
import { cn } from '@/lib/utils'
export default function Profile({
id,
@ -139,8 +140,8 @@ export default function Profile({ @@ -139,8 +140,8 @@ export default function Profile({
const { relaySets, favoriteRelays } = useFavoriteRelays()
const mergedPaymentMethods = useMemo(
() => sortMergedPaymentMethods(mergePaymentMethods(paymentInfo, profile ?? null)),
[paymentInfo, profile]
() => sortMergedPaymentMethods(mergePaymentMethods(paymentInfo, profile ?? null, profileEvent)),
[paymentInfo, profile, profileEvent]
)
const paymentMethodsByType = useMemo(
@ -148,11 +149,10 @@ export default function Profile({ @@ -148,11 +149,10 @@ export default function Profile({
[mergedPaymentMethods]
)
const hasTipDialog = useMemo(() => {
const merged = sortMergedPaymentMethods(mergePaymentMethods(paymentInfo, profile ?? null))
if (merged.some((m) => isLightningPaytoType(m.type))) return true
return getAlternativePaymentMethods(merged).length > 0
}, [paymentInfo, profile])
const hasTipDialog = useMemo(
() => recipientHasAnyPaymentOptions(paymentInfo, profile ?? null, profileEvent),
[paymentInfo, profile, profileEvent]
)
const syncAuthorReplaceablesFromCache = useCallback(async (pubkey: string) => {
try {
@ -563,13 +563,16 @@ export default function Profile({ @@ -563,13 +563,16 @@ export default function Profile({
/>
{/* Display websites - show first one prominently, others below */}
{website && (
<div className="flex gap-1 items-center text-primary mt-2 truncate select-text">
<Link size={14} className="shrink-0" />
<div className="group flex gap-1 items-center mt-2 truncate select-text">
<Link
size={14}
className={cn('shrink-0 text-primary transition-colors', 'group-hover:text-foreground')}
/>
<a
href={website}
target="_blank"
rel="noopener noreferrer"
className="hover:underline truncate flex-1 max-w-fit w-0"
className={cn(PRIMARY_LINK_HOVER_CLASS, 'truncate flex-1 max-w-fit w-0')}
>
{website}
</a>
@ -578,13 +581,22 @@ export default function Profile({ @@ -578,13 +581,22 @@ export default function Profile({
{websiteList && websiteList.length > 1 && (
<div className="flex flex-col gap-1 mt-1">
{websiteList.slice(1).map((url: string, idx: number) => (
<div key={idx} className="flex gap-1 items-center text-primary truncate select-text">
<Link size={12} className="shrink-0" />
<div
key={idx}
className="group flex gap-1 items-center truncate select-text"
>
<Link
size={12}
className={cn(
'shrink-0 text-primary transition-colors',
'group-hover:text-foreground'
)}
/>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="hover:underline truncate text-sm"
className={cn(PRIMARY_LINK_HOVER_CLASS, 'truncate text-sm')}
>
{url}
</a>

16
src/components/ProfileAbout/index.tsx

@ -8,6 +8,8 @@ import { @@ -8,6 +8,8 @@ import {
} from '@/lib/content-parser'
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content'
import PaytoLink from '@/components/PaytoLink'
import { URI_LINK_CLASS } from '@/lib/link-styles'
import { cn } from '@/lib/utils'
import { marked } from 'marked'
import {
EmbeddedHashtag,
@ -53,11 +55,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla @@ -53,11 +55,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
}
if (node.type === 'payto') {
return (
<PaytoLink
key={`${keyPrefix}-payto-${index}`}
paytoUri={node.data}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
/>
<PaytoLink key={`${keyPrefix}-payto-${index}`} paytoUri={node.data} />
)
}
if (node.type === 'hashtag') {
@ -120,11 +118,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla @@ -120,11 +118,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
const label = String(token.text ?? href)
if (href.startsWith('payto://')) {
out.push(
<PaytoLink
key={`${key}-payto-link`}
paytoUri={href}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
>
<PaytoLink key={`${key}-payto-link`} paytoUri={href}>
{label}
</PaytoLink>
)
@ -135,7 +129,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla @@ -135,7 +129,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
className={URI_LINK_CLASS}
>
{label}
</a>

2
src/components/RelayInfo/index.tsx

@ -96,7 +96,7 @@ export default function RelayInfo({ url, className }: { url: string; className?: @@ -96,7 +96,7 @@ export default function RelayInfo({ url, className }: { url: string; className?:
<a
href={normalizeHttpUrl(relayInfo.url)}
target="_blank"
className="hover:underline text-primary select-text truncate block"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors select-text truncate block"
>
{normalizeHttpUrl(relayInfo.url)}
</a>

2
src/components/RelayStatusDisplay/index.tsx

@ -71,7 +71,7 @@ function renderTextWithLinks(text: string): React.ReactNode { @@ -71,7 +71,7 @@ function renderTextWithLinks(text: string): React.ReactNode {
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:underline break-all"
className="text-blue-600 dark:text-blue-400 hover:text-foreground hover:underline underline-offset-2 transition-colors break-all"
onClick={(e) => e.stopPropagation()}
>
{url}

2
src/components/RssFeedItem/index.tsx

@ -914,7 +914,7 @@ export default function RssFeedItem({ @@ -914,7 +914,7 @@ export default function RssFeedItem({
href={item.link}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1 min-w-0 truncate"
className="text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors inline-flex items-center gap-1 min-w-0 truncate"
onClick={(e) => e.stopPropagation()}
>
<span className="truncate">{t('Read full article')}</span>

2
src/components/StandardRssFeedUrlRow/index.tsx

@ -68,7 +68,7 @@ export default function StandardRssFeedUrlRow({ feedUrl, className, actions }: P @@ -68,7 +68,7 @@ export default function StandardRssFeedUrlRow({ feedUrl, className, actions }: P
href={feedUrl}
target="_blank"
rel="noopener noreferrer"
className="block break-all text-xs text-primary hover:underline"
className="block break-all text-xs text-primary hover:text-foreground hover:underline underline-offset-2 transition-colors"
onClick={(e) => e.stopPropagation()}
>
{feedUrl}

2
src/components/UniversalContent/Wikilink.tsx

@ -22,7 +22,7 @@ export default function Wikilink({ dTag, displayText, className }: WikilinkProps @@ -22,7 +22,7 @@ export default function Wikilink({ dTag, displayText, className }: WikilinkProps
<CollapsibleTrigger asChild>
<Button
variant="link"
className="p-0 h-auto text-blue-600 hover:text-blue-800 hover:underline inline-flex items-center gap-1"
className="p-0 h-auto text-blue-600 hover:text-foreground hover:underline underline-offset-2 transition-colors inline-flex items-center gap-1"
>
<span>{displayText}</span>
{isOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}

2
src/components/VideoPlayer/index.tsx

@ -198,7 +198,7 @@ export default function VideoPlayer({ @@ -198,7 +198,7 @@ export default function VideoPlayer({
href={fallbackPageUrl.trim()}
target="_blank"
rel="noopener noreferrer"
className="inline-flex text-sm font-medium text-green-600 underline-offset-2 hover:underline dark:text-green-400 dark:hover:text-green-300"
className="inline-flex text-sm font-medium text-link-uri no-underline underline-offset-2 hover:text-primary hover:underline transition-colors"
onClick={(e) => e.stopPropagation()}
>
{t('Open in browser')}

10
src/components/WebPreview/index.tsx

@ -616,7 +616,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -616,7 +616,7 @@ export default function WebPreview({ url, className }: { url: string; className?
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground truncate block hover:underline break-all"
className="text-xs text-muted-foreground truncate block hover:text-foreground hover:underline underline-offset-2 transition-colors break-all"
>
{truncatedUrl}
</a>
@ -685,7 +685,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -685,7 +685,7 @@ export default function WebPreview({ url, className }: { url: string; className?
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground truncate block hover:underline break-all"
className="text-xs text-muted-foreground truncate block hover:text-foreground hover:underline underline-offset-2 transition-colors break-all"
>
{truncatedUrl}
</a>
@ -738,7 +738,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -738,7 +738,7 @@ export default function WebPreview({ url, className }: { url: string; className?
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground break-all line-clamp-2 block hover:underline"
className="text-xs text-muted-foreground break-all line-clamp-2 block hover:text-foreground hover:underline underline-offset-2 transition-colors"
>
{cleanedUrl}
</a>
@ -780,7 +780,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -780,7 +780,7 @@ export default function WebPreview({ url, className }: { url: string; className?
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground truncate block hover:underline break-all"
className="text-xs text-muted-foreground truncate block hover:text-foreground hover:underline underline-offset-2 transition-colors break-all"
>
{url}
</a>
@ -832,7 +832,7 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -832,7 +832,7 @@ export default function WebPreview({ url, className }: { url: string; className?
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs text-muted-foreground truncate block hover:underline break-all"
className="text-xs text-muted-foreground truncate block hover:text-foreground hover:underline underline-offset-2 transition-colors break-all"
>
{url}
</a>

14
src/hooks/useRecipientAlternativePayments.ts

@ -2,6 +2,7 @@ import { @@ -2,6 +2,7 @@ import {
getAlternativePaymentMethods,
groupPaymentMethodsByDisplayType,
mergePaymentMethods,
recipientHasAnyPaymentOptions,
sortMergedPaymentMethods,
type PaymentMethodGroup
} from '@/lib/merge-payment-methods'
@ -17,6 +18,8 @@ export type RecipientZapPaymentData = { @@ -17,6 +18,8 @@ export type RecipientZapPaymentData = {
profile: TProfile | null
profileEvent: Event | null
alternativeGroups: PaymentMethodGroup[]
/** Any payto / Lightning target on kind 0 or 10133 — used to enable zap UI. */
canReceiveTip: boolean
}
/** Kind 10133 + profile payto targets except the Lightning address used for zapping. */
@ -59,14 +62,19 @@ export function useRecipientZapPaymentData( @@ -59,14 +62,19 @@ export function useRecipientZapPaymentData(
}
}, [recipientPubkey, enabled])
const canReceiveTip = useMemo(
() => recipientHasAnyPaymentOptions(paymentInfo, profile, profileEvent),
[paymentInfo, profile, profileEvent]
)
const alternativeGroups = useMemo(() => {
if (!recipientPubkey) return []
const merged = sortMergedPaymentMethods(mergePaymentMethods(paymentInfo, profile))
const merged = sortMergedPaymentMethods(mergePaymentMethods(paymentInfo, profile, profileEvent))
const alts = getAlternativePaymentMethods(merged)
return groupPaymentMethodsByDisplayType(alts)
}, [recipientPubkey, paymentInfo, profile])
}, [recipientPubkey, paymentInfo, profile, profileEvent])
return { paymentInfo, profile, profileEvent, alternativeGroups }
return { paymentInfo, profile, profileEvent, alternativeGroups, canReceiveTip }
}
/** @deprecated Use {@link useRecipientZapPaymentData} */

14
src/index.css

@ -183,6 +183,8 @@ @@ -183,6 +183,8 @@
--sidebar-border: 130 18% 82%;
--brand-wordmark: 155 32% 22%;
--prose-link: 152 38% 28%;
/* Lighter forest green for inline URIs (http, payto, …); hover uses --primary. */
--uri-link: 154 32% 42%;
--highlight: 152 45% 36%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
@ -221,6 +223,7 @@ @@ -221,6 +223,7 @@
--sidebar-border: 150 14% 20%;
--brand-wordmark: 145 28% 91%;
--prose-link: 145 35% 72%;
--uri-link: 145 28% 68%;
--highlight: 150 40% 48%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
@ -230,12 +233,15 @@ @@ -230,12 +233,15 @@
}
:where(.prose) a {
color: hsl(var(--prose-link));
color: hsl(var(--uri-link));
text-decoration: none;
text-underline-offset: 2px;
transition: color 0.15s ease;
}
:where(.prose) a:hover {
color: hsl(var(--primary));
text-decoration: underline;
}
.dark input[type='datetime-local']::-webkit-calendar-picker-indicator {
@ -475,15 +481,15 @@ @@ -475,15 +481,15 @@
}
.asciidoc-content .footnote a {
@apply text-primary hover:underline;
@apply text-link-uri no-underline hover:text-primary hover:underline underline-offset-2 transition-colors;
}
.asciidoc-content .footnote-backref {
@apply ml-1 text-primary hover:underline;
@apply ml-1 text-link-uri no-underline hover:text-primary hover:underline underline-offset-2 transition-colors;
}
.asciidoc-content .footnoteref {
@apply text-primary hover:underline;
@apply text-link-uri no-underline hover:text-primary hover:underline underline-offset-2 transition-colors;
vertical-align: super;
font-size: 0.75em;
text-decoration: none;

10
src/lib/lightning.ts

@ -14,6 +14,12 @@ export function formatAmount(amount: number) { @@ -14,6 +14,12 @@ export function formatAmount(amount: number) {
}
export function getLightningAddressFromProfile(profile: TProfile) {
if (profile.lightningAddress?.trim()) return profile.lightningAddress.trim()
if (profile.lightningAddressList?.length) {
const first = profile.lightningAddressList.find((a) => a?.trim())
if (first) return first.trim()
}
// Some clients have incorrectly filled in the positions for lud06 and lud16
const { lud16: a, lud06: b } = profile
let lud16: string | undefined
@ -22,11 +28,13 @@ export function getLightningAddressFromProfile(profile: TProfile) { @@ -22,11 +28,13 @@ export function getLightningAddressFromProfile(profile: TProfile) {
lud16 = a
} else if (b && isEmail(b)) {
lud16 = b
} else if (a?.trim() && a.includes('.')) {
lud16 = a.trim()
} else if (b && b.startsWith('lnurl')) {
lud06 = b
} else if (a && a.startsWith('lnurl')) {
lud06 = a
}
return lud16 || lud06 || profile.lightningAddress || undefined
return lud16 || lud06 || undefined
}

14
src/lib/link-styles.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
/** Hover: forest primary + underline (rest state stays lighter green, no underline). */
const URI_LINK_HOVER =
'hover:text-primary hover:underline underline-offset-2 transition-colors'
/** http(s), payto://, and inline URIs in notes, bios, and previews. */
export const URI_LINK_CLASS =
`text-link-uri no-underline ${URI_LINK_HOVER} break-words`
/** Primary-tinted URIs (profile websites, payment rows using theme primary). */
export const PRIMARY_LINK_HOVER_CLASS =
`text-link-uri no-underline ${URI_LINK_HOVER} break-words`
/** For HTML template literals (asciidoc pipeline, etc.). */
export const URI_LINK_INLINE_HTML_CLASS = `inline ${URI_LINK_CLASS}`

89
src/lib/merge-payment-methods.test.ts

@ -1,9 +1,14 @@ @@ -1,9 +1,14 @@
import { describe, expect, it } from 'vitest'
import { isLightningPaytoType } from '@/lib/payto-registry'
import { isLightningPaytoType, isZappableLightningPaytoType } from '@/lib/payto-registry'
import { getProfileFromEvent } from '@/lib/event-metadata'
import { kinds, type Event } from 'nostr-tools'
import {
buildOrderedZapLightningAddresses,
getAlternativePaymentMethods,
prepareZapDialogAlternativePayments,
mergePaymentMethods,
normalizeLightningAuthority
normalizeLightningAuthority,
recipientHasAnyPaymentOptions
} from './merge-payment-methods'
describe('normalizeLightningAuthority', () => {
@ -49,6 +54,86 @@ describe('isLightningPaytoType', () => { @@ -49,6 +54,86 @@ describe('isLightningPaytoType', () => {
})
})
describe('isZappableLightningPaytoType', () => {
it('is LUD-16 lightning only', () => {
expect(isZappableLightningPaytoType('lightning')).toBe(true)
expect(isZappableLightningPaytoType('bip353')).toBe(false)
})
})
describe('getAlternativePaymentMethods', () => {
it('includes BIP-353 in zap dialog other payments', () => {
const merged = mergePaymentMethods(
{
methods: [
{
type: 'lightning',
authority: 'zap@example.com',
payto: 'payto://lightning/zap@example.com',
displayType: 'Lightning Network'
},
{
type: 'bip353',
authority: 'dns@example.com',
payto: 'payto://bip353/dns@example.com',
displayType: 'DNS Payment Instructions (BIP-353)'
}
]
},
null
)
const alts = getAlternativePaymentMethods(merged)
expect(alts.some((m) => m.type === 'bip353')).toBe(true)
expect(alts.some((m) => m.type === 'lightning')).toBe(false)
})
})
describe('buildOrderedZapLightningAddresses', () => {
it('excludes BIP-353 from zap selector (payment options only)', () => {
const addrs = buildOrderedZapLightningAddresses({
profileEvent: null,
paymentInfo: {
methods: [
{
type: 'bip353',
authority: 'user@example.com',
payto: 'payto://bip353/user@example.com',
displayType: 'DNS Payment Instructions (BIP-353)'
},
{
type: 'lightning',
authority: 'zap@example.com',
payto: 'payto://lightning/zap@example.com',
displayType: 'Lightning Network'
}
]
}
})
expect(addrs).toEqual(['zap@example.com'])
})
it('includes lud16 from kind 0 JSON when not in tags', () => {
const profileEvent = {
kind: kinds.Metadata,
pubkey: 'aa'.repeat(32),
created_at: 1,
tags: [] as string[][],
content: JSON.stringify({ lud16: 'user@example.com' }),
id: 'bb'.repeat(64),
sig: 'cc'.repeat(128)
} as Event
const addrs = buildOrderedZapLightningAddresses({
profileEvent,
paymentInfo: null
})
expect(addrs).toEqual(['user@example.com'])
expect(recipientHasAnyPaymentOptions(null, getProfileFromEvent(profileEvent), profileEvent)).toBe(
true
)
})
})
describe('prepareZapDialogAlternativePayments', () => {
const groups = [
{

41
src/lib/merge-payment-methods.ts

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
import { getPaymentInfoFromEvent } from '@/lib/event-metadata'
import { getPaymentInfoFromEvent, getProfileFromEvent } from '@/lib/event-metadata'
import {
buildPaytoUri,
getCanonicalPaytoType,
getPaytoEditorTypeLabel,
getPaytoTypeInfo,
isLightningPaytoType
isLightningPaytoType,
isZappableLightningPaytoType
} from '@/lib/payto'
import { normalizePaypalAuthority } from '@/lib/payto-paypal-url'
import type { TProfile } from '@/types'
@ -132,7 +133,8 @@ export function paytoPaymentSortRank(type: string): number { @@ -132,7 +133,8 @@ export function paytoPaymentSortRank(type: string): number {
/** Merge payment methods from kind 10133 and profile (kind 0: JSON + tags), normalized and deduplicated. */
export function mergePaymentMethods(
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null,
profile: TProfile | null
profile: TProfile | null,
profileEvent?: Event | null
): MergedPaymentMethod[] {
const seen = new Map<string, MergedPaymentMethod>()
const out: MergedPaymentMethod[] = []
@ -232,9 +234,27 @@ export function mergePaymentMethods( @@ -232,9 +234,27 @@ export function mergePaymentMethods(
add(type, authority, paymentInfo.payto, type === 'lightning' ? 'Lightning Network' : paymentInfo.type || 'Payment')
}
if (profileEvent?.kind === kinds.Metadata) {
for (const tag of profileEvent.tags) {
if (tag[0] === 'payto' && tag[1] && tag[2]) {
const type = String(tag[1]).toLowerCase()
add(type, String(tag[2]), buildPaytoUri(getCanonicalPaytoType(type), String(tag[2])))
}
}
}
return out
}
/** True when the recipient has any payto / Lightning target (kind 0 or 10133). */
export function recipientHasAnyPaymentOptions(
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null,
profile: TProfile | null,
profileEvent?: Event | null
): boolean {
return mergePaymentMethods(paymentInfo, profile, profileEvent).length > 0
}
export function sortMergedPaymentMethods(methods: MergedPaymentMethod[]): MergedPaymentMethod[] {
return [...methods].sort((a, b) => paytoPaymentSortRank(a.type) - paytoPaymentSortRank(b.type))
}
@ -277,20 +297,25 @@ export function buildOrderedZapLightningAddresses(opts: { @@ -277,20 +297,25 @@ export function buildOrderedZapLightningAddresses(opts: {
}
const ev = opts.profileEvent
const profile = ev?.kind === kinds.Metadata ? getProfileFromEvent(ev) : null
if (ev?.kind === kinds.Metadata) {
for (const tag of ev.tags) {
if (tag[0] === 'lud16' && tag[1]) add(tag[1])
if (tag[0] === 'lud06' && tag[1]) add(tag[1])
}
for (const tag of ev.tags) {
if (tag[0] === 'w' && tag[1] && tag[2] && String(tag[3]).toLowerCase() === 'lightning') {
if (tag[0] !== 'w' || !tag[1] || !tag[2]) continue
if (tag[3] && String(tag[3]).toLowerCase() === 'lightning') {
add(tag[2])
} else if (!tag[3] && String(tag[1]).toLowerCase() === 'lightning') {
add(tag[2])
}
}
}
const paymentMethods = mergePaymentMethods(opts.paymentInfo, null)
const paymentMethods = mergePaymentMethods(opts.paymentInfo, profile, ev)
for (const m of paymentMethods) {
if (isLightningPaytoType(m.type)) add(m.authority)
if (isZappableLightningPaytoType(m.type)) add(m.authority)
}
return prioritizeZapLightningAddress(out, opts.preferredAddress ?? undefined)
@ -308,7 +333,7 @@ export function prioritizeZapLightningAddress(candidates: string[], preferred?: @@ -308,7 +333,7 @@ export function prioritizeZapLightningAddress(candidates: string[], preferred?:
return [candidates[idx], ...rest]
}
/** Non-Lightning payto targets for zap dialog “other payment methods” (Lightning has its own selector). */
/** Non-zap payto targets for zap dialog “other payment methods” (LUD-16 uses the Lightning selector). */
export function getAlternativePaymentMethods(methods: MergedPaymentMethod[]): MergedPaymentMethod[] {
return methods.filter((m) => !isLightningPaytoType(m.type))
return methods.filter((m) => !isZappableLightningPaytoType(m.type))
}

7
src/lib/payto-registry.ts

@ -120,8 +120,13 @@ export function getPaytoIconChar(type: string): string | null { @@ -120,8 +120,13 @@ export function getPaytoIconChar(type: string): string | null {
return getPaytoTypeRecord(type)?.symbol ?? null
}
/** LUD-16 lightning and BIP-353 DNS payment instructions (not on-chain Bitcoin). */
/** LUD-16 / LNURL lightning and BIP-353 DNS instructions — payment UI, not on-chain Bitcoin. */
export function isLightningPaytoType(type: string): boolean {
const canonical = getCanonicalPaytoType(type)
return canonical === 'lightning' || canonical === 'bip353'
}
/** Lightning targets that support zaps (LUD-16 / LNURL only; BIP-353 is pay/copy, not zappable). */
export function isZappableLightningPaytoType(type: string): boolean {
return getCanonicalPaytoType(type) === 'lightning'
}

1
src/lib/payto.ts

@ -16,6 +16,7 @@ export { @@ -16,6 +16,7 @@ export {
getPaytoTypeInfo,
isKnownPaytoType,
isLightningPaytoType,
isZappableLightningPaytoType,
isPaytoEditorCustomType,
paytoEditorSelectTypes,
PAYTO_EDITOR_OTHER_OPTION,

6
src/pages/primary/CalendarPrimaryPage.tsx

@ -510,7 +510,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -510,7 +510,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
{excess > 0 ? (
<button
type="button"
className="mt-0.5 w-full shrink-0 rounded-md py-1 text-center text-[11px] font-semibold text-primary underline-offset-2 hover:bg-muted/50 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="mt-0.5 w-full shrink-0 rounded-md py-1 text-center text-[11px] font-semibold text-primary underline-offset-2 hover:bg-muted/50 hover:text-foreground hover:underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label={t('calendarPageMoreEventsAria', { count: excess })}
onClick={() => openDayEventsPanel(day)}
>
@ -566,7 +566,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -566,7 +566,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
onClick={() => navigateToNote(toNote(ev), ev)}
className={cn(
'flex w-full min-w-0 items-center gap-0.5 rounded px-0.5 py-px text-left text-[9px] font-medium leading-tight text-primary underline-offset-2',
'hover:bg-muted/80 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:text-[10px]'
'hover:bg-muted/80 hover:text-foreground hover:underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:text-[10px]'
)}
title={title}
>
@ -585,7 +585,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -585,7 +585,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
{excess > 0 ? (
<button
type="button"
className="mt-0.5 w-full shrink-0 rounded py-0.5 text-center text-[9px] font-semibold text-primary underline-offset-2 hover:bg-muted/50 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:text-[10px]"
className="mt-0.5 w-full shrink-0 rounded py-0.5 text-center text-[9px] font-semibold text-primary underline-offset-2 hover:bg-muted/50 hover:text-foreground hover:underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:text-[10px]"
aria-label={t('calendarPageMoreEventsAria', { count: excess })}
onClick={() => openDayEventsPanel(day)}
>

4
src/pages/secondary/PersonalListsSettingsPage/index.tsx

@ -184,7 +184,7 @@ const PersonalListsSettingsPage = forwardRef( @@ -184,7 +184,7 @@ const PersonalListsSettingsPage = forwardRef(
{t('Personal lists bookmarks spell hint')}{' '}
<button
type="button"
className="text-primary underline-offset-4 hover:underline"
className="text-primary underline-offset-4 hover:text-foreground hover:underline transition-colors"
onClick={() => navigatePrimary('spells', { spell: 'bookmarks' })}
>
{t('Bookmarks spell')}
@ -197,7 +197,7 @@ const PersonalListsSettingsPage = forwardRef( @@ -197,7 +197,7 @@ const PersonalListsSettingsPage = forwardRef(
{t('Personal lists interests spell hint')}{' '}
<button
type="button"
className="text-primary underline-offset-4 hover:underline"
className="text-primary underline-offset-4 hover:text-foreground hover:underline transition-colors"
onClick={() => navigatePrimary('spells', { spell: 'interests' })}
>
{t('Interests spell')}

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

@ -134,7 +134,7 @@ export default function BlossomServerListSetting() { @@ -134,7 +134,7 @@ export default function BlossomServerListSetting() {
href={url}
target="_blank"
rel="noopener noreferrer"
className="truncate hover:underline"
className="truncate hover:text-foreground hover:underline underline-offset-2 transition-colors"
>
{url}
</a>

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

@ -563,12 +563,12 @@ class ContentParserService { @@ -563,12 +563,12 @@ class ContentParserService {
// Convert hashtag links to HTML with green styling
processed = processed.replace(/hashtag:([^[]+)\[([^\]]+)\]/g, (_match, normalizedHashtag, displayText) => {
return `<a href="/notes?t=${normalizedHashtag}" class="hashtag-link text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline">${displayText}</a>`
return `<a href="/notes?t=${normalizedHashtag}" class="hashtag-link text-link-uri no-underline hover:text-primary hover:underline underline-offset-2 transition-colors">${displayText}</a>`
})
// Convert wikilink:dtag[display] format to HTML with data attributes
processed = processed.replace(/wikilink:([^[]+)\[([^\]]+)\]/g, (_match, dTag, displayText) => {
return `<span class="wikilink cursor-pointer text-blue-600 hover:text-blue-800 hover:underline border-b border-dotted border-blue-300" data-dtag="${dTag}" data-display="${displayText}">${displayText}</span>`
return `<span class="wikilink cursor-pointer text-blue-600 hover:text-foreground hover:underline underline-offset-2 transition-colors border-b border-dotted border-blue-300" data-dtag="${dTag}" data-display="${displayText}">${displayText}</span>`
})
// Convert nostr: links to proper embedded components
@ -583,7 +583,7 @@ class ContentParserService { @@ -583,7 +583,7 @@ class ContentParserService {
return `<span class="user-handle" data-pubkey="${bech32Id}">@${displayText}</span>`
} else {
// Fallback to regular link
return `<a href="nostr:${bech32Id}" class="nostr-link text-blue-600 hover:text-blue-800 hover:underline" data-nostr-type="${nostrType}" data-bech32="${bech32Id}">${displayText}</a>`
return `<a href="nostr:${bech32Id}" class="nostr-link text-blue-600 hover:text-foreground hover:underline underline-offset-2 transition-colors" data-nostr-type="${nostrType}" data-bech32="${bech32Id}">${displayText}</a>`
}
})

5
tailwind.config.js

@ -64,7 +64,10 @@ export default { @@ -64,7 +64,10 @@ export default {
4: 'hsl(var(--chart-4))',
5: 'hsl(var(--chart-5))'
},
highlight: 'hsl(var(--highlight))'
highlight: 'hsl(var(--highlight))',
link: {
uri: 'hsl(var(--uri-link))'
}
}
}
},

Loading…
Cancel
Save