mirror of
https://github.com/dergigi/boris.git
synced 2026-01-03 15:04:24 +01:00
feat: add three-dot menu for external URLs in /r/ path
- Add menu button with options to open original URL, copy URL, and share - Reuse existing menu styling for consistency - Menu positioned at end of article content before mark-as-read button
This commit is contained in:
@@ -98,8 +98,10 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
const [showCheckAnimation, setShowCheckAnimation] = useState(false)
|
||||
const [showArticleMenu, setShowArticleMenu] = useState(false)
|
||||
const [showVideoMenu, setShowVideoMenu] = useState(false)
|
||||
const [showExternalMenu, setShowExternalMenu] = useState(false)
|
||||
const articleMenuRef = useRef<HTMLDivElement>(null)
|
||||
const videoMenuRef = useRef<HTMLDivElement>(null)
|
||||
const externalMenuRef = useRef<HTMLDivElement>(null)
|
||||
const [ytMeta, setYtMeta] = useState<{ title?: string; description?: string; transcript?: string } | null>(null)
|
||||
const { renderedHtml: renderedMarkdownHtml, previewRef: markdownPreviewRef, processedMarkdown } = useMarkdownToHTML(markdown, relayPool)
|
||||
|
||||
@@ -145,15 +147,18 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
if (videoMenuRef.current && !videoMenuRef.current.contains(target)) {
|
||||
setShowVideoMenu(false)
|
||||
}
|
||||
if (externalMenuRef.current && !externalMenuRef.current.contains(target)) {
|
||||
setShowExternalMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showArticleMenu || showVideoMenu) {
|
||||
if (showArticleMenu || showVideoMenu || showExternalMenu) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}
|
||||
}, [showArticleMenu, showVideoMenu])
|
||||
}, [showArticleMenu, showVideoMenu, showExternalMenu])
|
||||
|
||||
const readingStats = useMemo(() => {
|
||||
const content = markdown || html || ''
|
||||
@@ -280,6 +285,38 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
setShowVideoMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
// External article actions
|
||||
const toggleExternalMenu = () => setShowExternalMenu(v => !v)
|
||||
|
||||
const handleOpenExternalUrl = () => {
|
||||
if (selectedUrl) window.open(selectedUrl, '_blank', 'noopener,noreferrer')
|
||||
setShowExternalMenu(false)
|
||||
}
|
||||
|
||||
const handleCopyExternalUrl = async () => {
|
||||
try {
|
||||
if (selectedUrl) await navigator.clipboard.writeText(selectedUrl)
|
||||
} catch (e) {
|
||||
console.warn('Clipboard copy failed', e)
|
||||
} finally {
|
||||
setShowExternalMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareExternalUrl = async () => {
|
||||
try {
|
||||
if (selectedUrl && (navigator as { share?: (d: { title?: string; url?: string }) => Promise<void> }).share) {
|
||||
await (navigator as { share: (d: { title?: string; url?: string }) => Promise<void> }).share({ title: title || 'Article', url: selectedUrl })
|
||||
} else if (selectedUrl) {
|
||||
await navigator.clipboard.writeText(selectedUrl)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Share failed', e)
|
||||
} finally {
|
||||
setShowExternalMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if article is already marked as read when URL/article changes
|
||||
useEffect(() => {
|
||||
@@ -533,6 +570,47 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Article menu for external URLs */}
|
||||
{!isNostrArticle && !isExternalVideo && selectedUrl && (
|
||||
<div className="article-menu-container">
|
||||
<div className="article-menu-wrapper" ref={externalMenuRef}>
|
||||
<button
|
||||
className="article-menu-btn"
|
||||
onClick={toggleExternalMenu}
|
||||
title="More options"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisH} />
|
||||
</button>
|
||||
|
||||
{showExternalMenu && (
|
||||
<div className="article-menu">
|
||||
<button
|
||||
className="article-menu-item"
|
||||
onClick={handleOpenExternalUrl}
|
||||
>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
<span>Open Original URL</span>
|
||||
</button>
|
||||
<button
|
||||
className="article-menu-item"
|
||||
onClick={handleCopyExternalUrl}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
<span>Copy URL</span>
|
||||
</button>
|
||||
<button
|
||||
className="article-menu-item"
|
||||
onClick={handleShareExternalUrl}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShare} />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Article menu for nostr-native articles */}
|
||||
{isNostrArticle && currentArticle && articleLinks && (
|
||||
<div className="article-menu-container">
|
||||
|
||||
Reference in New Issue
Block a user