diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index bff951cc..c0edd5ef 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -1,11 +1,14 @@ -import React, { useMemo, useState, useEffect } from 'react' +import React, { useMemo, useState, useEffect, useRef } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeRaw from 'rehype-raw' import rehypePrism from 'rehype-prism-plus' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import 'prismjs/themes/prism-tomorrow.css' -import { faSpinner, faCheck } from '@fortawesome/free-solid-svg-icons' +import { faSpinner, faCheck, faEllipsisH, faExternalLinkAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons' +import { nip19 } from 'nostr-tools' +import { getNostrUrl } from '../config/nostrGateways' +import { RELAYS } from '../config/relays' import { RelayPool } from 'applesauce-relay' import { IAccount } from 'applesauce-accounts' import { NostrEvent } from 'nostr-tools' @@ -82,6 +85,8 @@ const ContentPanel: React.FC = ({ const [isMarkedAsRead, setIsMarkedAsRead] = useState(false) const [isCheckingReadStatus, setIsCheckingReadStatus] = useState(false) const [showCheckAnimation, setShowCheckAnimation] = useState(false) + const [showArticleMenu, setShowArticleMenu] = useState(false) + const articleMenuRef = useRef(null) const { renderedHtml: renderedMarkdownHtml, previewRef: markdownPreviewRef, processedMarkdown } = useMarkdownToHTML(markdown, relayPool) const { finalHtml, relevantHighlights } = useHighlightedContent({ @@ -104,6 +109,22 @@ const ContentPanel: React.FC = ({ onClearSelection }) + // Close menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (articleMenuRef.current && !articleMenuRef.current.contains(event.target as Node)) { + setShowArticleMenu(false) + } + } + + if (showArticleMenu) { + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + } + }, [showArticleMenu]) + const readingStats = useMemo(() => { const content = markdown || html || '' if (!content) return null @@ -115,6 +136,48 @@ const ContentPanel: React.FC = ({ // Determine if we're on a nostr-native article (/a/) or external URL (/r/) const isNostrArticle = selectedUrl && selectedUrl.startsWith('nostr:') + + // Get article links for menu + const getArticleLinks = () => { + if (!currentArticle) return null + + const dTag = currentArticle.tags.find(t => t[0] === 'd')?.[1] || '' + const relayHints = RELAYS.filter(r => + !r.includes('localhost') && !r.includes('127.0.0.1') + ).slice(0, 3) + + const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: currentArticle.pubkey, + identifier: dTag, + relays: relayHints + }) + + return { + portal: getNostrUrl(naddr), + native: `nostr:${naddr}` + } + } + + const articleLinks = getArticleLinks() + + const handleMenuToggle = () => { + setShowArticleMenu(!showArticleMenu) + } + + const handleOpenPortal = () => { + if (articleLinks) { + window.open(articleLinks.portal, '_blank', 'noopener,noreferrer') + } + setShowArticleMenu(false) + } + + const handleOpenNative = () => { + if (articleLinks) { + window.location.href = articleLinks.native + } + setShowArticleMenu(false) + } // Check if article is already marked as read when URL/article changes useEffect(() => { @@ -277,6 +340,40 @@ const ContentPanel: React.FC = ({ /> )} + {/* Article menu for nostr-native articles */} + {isNostrArticle && currentArticle && articleLinks && ( +
+
+ + + {showArticleMenu && ( +
+ + +
+ )} +
+
+ )} + {/* Mark as Read button */} {activeAccount && (
diff --git a/src/components/HighlightItem.tsx b/src/components/HighlightItem.tsx index bbb6b7e9..b47ba003 100644 --- a/src/components/HighlightItem.tsx +++ b/src/components/HighlightItem.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash, faEllipsisH } from '@fortawesome/free-solid-svg-icons' +import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash, faEllipsisH, faMobileAlt } from '@fortawesome/free-solid-svg-icons' import { Highlight } from '../types/highlights' import { useEventModel } from 'applesauce-react/hooks' import { Models, IEventStore } from 'applesauce-core' @@ -123,7 +123,7 @@ export const HighlightItem: React.FC = ({ } } - const getHighlightLink = () => { + const getHighlightLinks = () => { // Encode the highlight event itself (kind 9802) as a nevent // Get non-local relays for the hint const relayHints = RELAYS.filter(r => @@ -136,10 +136,14 @@ export const HighlightItem: React.FC = ({ author: highlight.pubkey, kind: 9802 }) - return getNostrUrl(nevent) + + return { + portal: getNostrUrl(nevent), + native: `nostr:${nevent}` + } } - const highlightLink = getHighlightLink() + const highlightLinks = getHighlightLinks() // Handle rebroadcast to all relays const handleRebroadcast = async (e: React.MouseEvent) => { @@ -283,9 +287,15 @@ export const HighlightItem: React.FC = ({ setShowMenu(!showMenu) } - const handleOpenExternal = (e: React.MouseEvent) => { + const handleOpenPortal = (e: React.MouseEvent) => { e.stopPropagation() - window.open(highlightLink, '_blank', 'noopener,noreferrer') + window.open(highlightLinks.portal, '_blank', 'noopener,noreferrer') + setShowMenu(false) + } + + const handleOpenNative = (e: React.MouseEvent) => { + e.stopPropagation() + window.location.href = highlightLinks.native setShowMenu(false) } @@ -364,11 +374,18 @@ export const HighlightItem: React.FC = ({
+ {canDelete && (