Browse Source

tidy article cards. move menu items. add decentnewsroom jump menu

added aria descriptions to dialgos
imwald
Silberengel 4 months ago
parent
commit
f20306c963
  1. 7
      src/components/Note/LongFormArticlePreview.tsx
  2. 49
      src/components/Note/PublicationCard.tsx
  3. 72
      src/components/Note/WikiCard.tsx
  4. 195
      src/components/NoteOptions/useMenuActions.tsx
  5. 3
      src/components/ZapDialog/index.tsx

7
src/components/Note/LongFormArticlePreview.tsx

@ -6,7 +6,6 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import Image from '../Image' import Image from '../Image'
import ArticleExportMenu from '../ArticleExportMenu/ArticleExportMenu'
export default function LongFormArticlePreview({ export default function LongFormArticlePreview({
event, event,
@ -66,9 +65,6 @@ export default function LongFormArticlePreview({
{titleComponent} {titleComponent}
{summaryComponent} {summaryComponent}
{tagsComponent} {tagsComponent}
<div className="flex justify-end">
<ArticleExportMenu event={event} title={metadata.title || 'Article'} />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -93,9 +89,6 @@ export default function LongFormArticlePreview({
{titleComponent} {titleComponent}
{summaryComponent} {summaryComponent}
{tagsComponent} {tagsComponent}
<div className="flex justify-end">
<ArticleExportMenu event={event} title={metadata.title || 'Article'} />
</div>
</div> </div>
</div> </div>
</div> </div>

49
src/components/Note/PublicationCard.tsx

@ -4,11 +4,8 @@ import { useSecondaryPage } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { BookOpen } from 'lucide-react'
import Image from '../Image' import Image from '../Image'
import ArticleExportMenu from '../ArticleExportMenu/ArticleExportMenu'
export default function PublicationCard({ export default function PublicationCard({
event, event,
@ -22,39 +19,11 @@ export default function PublicationCard({
const { autoLoadMedia } = useContentPolicy() const { autoLoadMedia } = useContentPolicy()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
// Generate naddr for Alexandria URL
const naddr = useMemo(() => {
try {
const dTag = event.tags.find(tag => tag[0] === 'd')?.[1] || ''
const relays = event.tags
.filter(tag => tag[0] === 'relay')
.map(tag => tag[1])
.filter(Boolean)
return nip19.naddrEncode({
kind: event.kind,
pubkey: event.pubkey,
identifier: dTag,
relays: relays.length > 0 ? relays : undefined
})
} catch (error) {
console.error('Error generating naddr:', error)
return ''
}
}, [event])
const handleCardClick = (e: React.MouseEvent) => { const handleCardClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
push(toNote(event.id)) push(toNote(event.id))
} }
const handleAlexandriaClick = (e: React.MouseEvent) => {
e.stopPropagation()
if (naddr) {
window.open(`https://next-alexandria.gitcitadel.eu/publication/naddr/${naddr}`, '_blank', 'noopener,noreferrer')
}
}
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div> const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
const tagsComponent = metadata.tags.length > 0 && ( const tagsComponent = metadata.tags.length > 0 && (
@ -78,16 +47,6 @@ export default function PublicationCard({
<div className="text-sm text-muted-foreground line-clamp-4">{metadata.summary}</div> <div className="text-sm text-muted-foreground line-clamp-4">{metadata.summary}</div>
) )
const alexandriaButton = naddr && (
<button
onClick={handleAlexandriaClick}
className="flex items-center gap-2 px-3 py-2 text-sm bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-800 dark:text-blue-200 rounded-md transition-colors"
>
<BookOpen className="w-4 h-4" />
View in Alexandria
</button>
)
if (isSmallScreen) { if (isSmallScreen) {
return ( return (
<div className={className}> <div className={className}>
@ -106,10 +65,6 @@ export default function PublicationCard({
{titleComponent} {titleComponent}
{summaryComponent} {summaryComponent}
{tagsComponent} {tagsComponent}
<div className="flex justify-end gap-2 items-center">
<ArticleExportMenu event={event} title={metadata.title || 'Article'} />
{alexandriaButton}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -134,10 +89,6 @@ export default function PublicationCard({
{titleComponent} {titleComponent}
{summaryComponent} {summaryComponent}
{tagsComponent} {tagsComponent}
<div className="flex justify-end gap-2 items-center">
<ArticleExportMenu event={event} title={metadata.title || 'Article'} />
{alexandriaButton}
</div>
</div> </div>
</div> </div>
</div> </div>

72
src/components/Note/WikiCard.tsx

@ -4,11 +4,8 @@ import { useSecondaryPage } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { BookOpen, Globe } from 'lucide-react'
import Image from '../Image' import Image from '../Image'
import ArticleExportMenu from '../ArticleExportMenu/ArticleExportMenu'
export default function WikiCard({ export default function WikiCard({
event, event,
@ -22,50 +19,11 @@ export default function WikiCard({
const { autoLoadMedia } = useContentPolicy() const { autoLoadMedia } = useContentPolicy()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
// Extract d-tag for Wikistr URL
const dTag = useMemo(() => {
return event.tags.find(tag => tag[0] === 'd')?.[1] || ''
}, [event])
// Generate naddr for Alexandria URL
const naddr = useMemo(() => {
try {
const relays = event.tags
.filter(tag => tag[0] === 'relay')
.map(tag => tag[1])
.filter(Boolean)
return nip19.naddrEncode({
kind: event.kind,
pubkey: event.pubkey,
identifier: dTag,
relays: relays.length > 0 ? relays : undefined
})
} catch (error) {
console.error('Error generating naddr:', error)
return ''
}
}, [event, dTag])
const handleCardClick = (e: React.MouseEvent) => { const handleCardClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
push(toNote(event.id)) push(toNote(event.id))
} }
const handleWikistrClick = (e: React.MouseEvent) => {
e.stopPropagation()
if (dTag) {
window.open(`https://wikistr.imwald.eu/${dTag}*${event.pubkey}`, '_blank', 'noopener,noreferrer')
}
}
const handleAlexandriaClick = (e: React.MouseEvent) => {
e.stopPropagation()
if (naddr) {
window.open(`https://next-alexandria.gitcitadel.eu/publication/naddr/${naddr}`, '_blank', 'noopener,noreferrer')
}
}
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div> const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
const tagsComponent = metadata.tags.length > 0 && ( const tagsComponent = metadata.tags.length > 0 && (
@ -88,30 +46,6 @@ export default function WikiCard({
const summaryComponent = metadata.summary && ( const summaryComponent = metadata.summary && (
<div className="text-sm text-muted-foreground line-clamp-4">{metadata.summary}</div> <div className="text-sm text-muted-foreground line-clamp-4">{metadata.summary}</div>
) )
const buttons = (
<div className="flex gap-2 flex-wrap items-center">
<ArticleExportMenu event={event} title={metadata.title || 'Article'} />
{dTag && (
<button
onClick={handleWikistrClick}
className="flex items-center gap-2 px-3 py-2 text-sm bg-green-100 hover:bg-green-200 dark:bg-green-900 dark:hover:bg-green-800 text-green-800 dark:text-green-200 rounded-md transition-colors"
>
<Globe className="w-4 h-4" />
View in Wikistr
</button>
)}
{naddr && (
<button
onClick={handleAlexandriaClick}
className="flex items-center gap-2 px-3 py-2 text-sm bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-800 dark:text-blue-200 rounded-md transition-colors"
>
<BookOpen className="w-4 h-4" />
View in Alexandria
</button>
)}
</div>
)
if (isSmallScreen) { if (isSmallScreen) {
return ( return (
@ -131,9 +65,6 @@ export default function WikiCard({
{titleComponent} {titleComponent}
{summaryComponent} {summaryComponent}
{tagsComponent} {tagsComponent}
<div className="flex justify-end">
{buttons}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -158,9 +89,6 @@ export default function WikiCard({
{titleComponent} {titleComponent}
{summaryComponent} {summaryComponent}
{tagsComponent} {tagsComponent}
<div className="flex justify-end">
{buttons}
</div>
</div> </div>
</div> </div>
</div> </div>

195
src/components/NoteOptions/useMenuActions.tsx

@ -1,5 +1,6 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event' import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNjump } from '@/lib/link' import { toNjump } from '@/lib/link'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { pubkeyToNpub } from '@/lib/pubkey' import { pubkeyToNpub } from '@/lib/pubkey'
@ -10,8 +11,9 @@ import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin } from 'lucide-react' import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin, FileDown, Globe, BookOpen } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools'
import { useMemo, useState, useEffect } from 'react' import { useMemo, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -303,7 +305,120 @@ export function useMenuActions({
return items return items
}, [pubkey, relayUrls, relaySets]) }, [pubkey, relayUrls, relaySets])
// Check if this is an article-type event
const isArticleType = useMemo(() => {
return event.kind === kinds.LongFormArticle ||
event.kind === ExtendedKind.WIKI_ARTICLE ||
event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN ||
event.kind === ExtendedKind.PUBLICATION ||
event.kind === ExtendedKind.PUBLICATION_CONTENT
}, [event.kind])
// Get article metadata for export
const articleMetadata = useMemo(() => {
if (!isArticleType) return null
return getLongFormArticleMetadataFromEvent(event)
}, [isArticleType, event])
// Extract d-tag for Wikistr URL
const dTag = useMemo(() => {
if (!isArticleType) return ''
return event.tags.find(tag => tag[0] === 'd')?.[1] || ''
}, [isArticleType, event])
// Generate naddr for Alexandria URL
const naddr = useMemo(() => {
if (!isArticleType || !dTag) return ''
try {
const relays = event.tags
.filter(tag => tag[0] === 'relay')
.map(tag => tag[1])
.filter(Boolean)
return nip19.naddrEncode({
kind: event.kind,
pubkey: event.pubkey,
identifier: dTag,
relays: relays.length > 0 ? relays : undefined
})
} catch (error) {
console.error('Error generating naddr:', error)
return ''
}
}, [isArticleType, event, dTag])
const menuActions: MenuAction[] = useMemo(() => { const menuActions: MenuAction[] = useMemo(() => {
// Export functions for articles
const exportAsMarkdown = () => {
if (!isArticleType) return
try {
const title = articleMetadata?.title || 'Article'
const content = event.content
const filename = `${title}.md`
const blob = new Blob([content], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
logger.info('[NoteOptions] Exported article as Markdown')
toast.success(t('Article exported as Markdown'))
} catch (error) {
logger.error('[NoteOptions] Error exporting article:', error)
toast.error(t('Failed to export article'))
}
}
const exportAsAsciidoc = () => {
if (!isArticleType) return
try {
const title = articleMetadata?.title || 'Article'
const content = event.content
const filename = `${title}.adoc`
const blob = new Blob([content], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
logger.info('[NoteOptions] Exported article as AsciiDoc')
toast.success(t('Article exported as AsciiDoc'))
} catch (error) {
logger.error('[NoteOptions] Error exporting article:', error)
toast.error(t('Failed to export article'))
}
}
// View on external sites functions
const handleViewOnWikistr = () => {
if (!dTag) return
closeDrawer()
window.open(`https://wikistr.imwald.eu/${dTag}*${event.pubkey}`, '_blank', 'noopener,noreferrer')
}
const handleViewOnAlexandria = () => {
if (!naddr) return
closeDrawer()
window.open(`https://next-alexandria.gitcitadel.eu/publication/naddr/${naddr}`, '_blank', 'noopener,noreferrer')
}
const handleViewOnDecentNewsroom = () => {
if (!dTag) return
closeDrawer()
window.open(`https://decentnewsroom.com/article/d/${dTag}`, '_blank', 'noopener,noreferrer')
}
const actions: MenuAction[] = [ const actions: MenuAction[] = [
{ {
icon: Copy, icon: Copy,
@ -340,6 +455,76 @@ export function useMenuActions({
} }
] ]
// Add export options for article-type events
if (isArticleType) {
const isMarkdownFormat = event.kind === kinds.LongFormArticle || event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN
const isAsciidocFormat = event.kind === ExtendedKind.WIKI_ARTICLE || event.kind === ExtendedKind.PUBLICATION || event.kind === ExtendedKind.PUBLICATION_CONTENT
if (isMarkdownFormat) {
actions.push({
icon: FileDown,
label: t('Export as Markdown'),
onClick: () => {
closeDrawer()
exportAsMarkdown()
},
separator: true
})
}
if (isAsciidocFormat) {
actions.push({
icon: FileDown,
label: t('Export as AsciiDoc'),
onClick: () => {
closeDrawer()
exportAsAsciidoc()
},
separator: true
})
}
// Add view options based on event kind
if (event.kind === kinds.LongFormArticle) {
// For LongFormArticle (30023): Alexandria and DecentNewsroom
if (naddr) {
actions.push({
icon: BookOpen,
label: t('View on Alexandria'),
onClick: handleViewOnAlexandria
})
}
if (dTag) {
actions.push({
icon: Globe,
label: t('View on DecentNewsroom'),
onClick: handleViewOnDecentNewsroom
})
}
} else if (
event.kind === ExtendedKind.PUBLICATION_CONTENT ||
event.kind === ExtendedKind.PUBLICATION ||
event.kind === ExtendedKind.WIKI_ARTICLE ||
event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN
) {
// For 30041, 30040, 30818, 30817: Alexandria and Wikistr
if (naddr) {
actions.push({
icon: BookOpen,
label: t('View on Alexandria'),
onClick: handleViewOnAlexandria
})
}
if (dTag) {
actions.push({
icon: Globe,
label: t('View on Wikistr'),
onClick: handleViewOnWikistr
})
}
}
}
const isProtected = isProtectedEvent(event) const isProtected = isProtectedEvent(event)
const isDiscussion = event.kind === ExtendedKind.DISCUSSION const isDiscussion = event.kind === ExtendedKind.DISCUSSION
if ((!isProtected || event.pubkey === pubkey) && !isDiscussion && !isReplyToDiscussion) { if ((!isProtected || event.pubkey === pubkey) && !isDiscussion && !isReplyToDiscussion) {
@ -446,7 +631,11 @@ export function useMenuActions({
unmutePubkey, unmutePubkey,
attemptDelete, attemptDelete,
isPinned, isPinned,
handlePinNote handlePinNote,
isArticleType,
articleMetadata,
dTag,
naddr
]) ])
return menuActions return menuActions

3
src/components/ZapDialog/index.tsx

@ -93,7 +93,7 @@ export default function ZapDialog({
<UserAvatar size="small" userId={pubkey} /> <UserAvatar size="small" userId={pubkey} />
<Username userId={pubkey} className="truncate flex-1 w-0 text-start h-5" /> <Username userId={pubkey} className="truncate flex-1 w-0 text-start h-5" />
</DrawerTitle> </DrawerTitle>
<DialogDescription></DialogDescription> <DialogDescription className="sr-only">{t('Send a Lightning payment to this user')}</DialogDescription>
</DrawerHeader> </DrawerHeader>
<ZapDialogContent <ZapDialogContent
open={open} open={open}
@ -117,6 +117,7 @@ export default function ZapDialog({
<UserAvatar size="small" userId={pubkey} /> <UserAvatar size="small" userId={pubkey} />
<Username userId={pubkey} className="truncate flex-1 max-w-fit text-start h-5" /> <Username userId={pubkey} className="truncate flex-1 max-w-fit text-start h-5" />
</DialogTitle> </DialogTitle>
<DialogDescription className="sr-only">{t('Send a Lightning payment to this user')}</DialogDescription>
</DialogHeader> </DialogHeader>
<ZapDialogContent <ZapDialogContent
open={open} open={open}

Loading…
Cancel
Save