From be03b9c9cc972075a1efe986489f7faf78724938 Mon Sep 17 00:00:00 2001 From: Gigi Date: Sat, 22 Nov 2025 01:59:02 +0100 Subject: [PATCH] feat: add three-dot menu to profile view - Add menu button with options: Copy Link, Share, Open with njump, Open with Native App - Position menu next to AuthorCard in profile header - Add click-outside detection to close menu - Style menu consistently with other menus in the app --- src/components/Profile.tsx | 115 +++++++++++++++++++++++++++++- src/styles/components/me.css | 3 +- src/styles/components/profile.css | 89 +++++++++++++++++++++++ 3 files changed, 203 insertions(+), 4 deletions(-) diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 17ed5e33..ec3afe73 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react' +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faHighlighter, faPenToSquare } from '@fortawesome/free-solid-svg-icons' +import { faHighlighter, faPenToSquare, faEllipsisH, faCopy, faShare, faExternalLinkAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons' import { IEventStore } from 'applesauce-core' import { RelayPool } from 'applesauce-relay' import { nip19 } from 'nostr-tools' @@ -20,6 +20,7 @@ import { Hooks } from 'applesauce-react' import { readingProgressController } from '../services/readingProgressController' import { writingsController } from '../services/writingsController' import { highlightsController } from '../services/highlightsController' +import { getProfileUrl, getNostrUrl } from '../config/nostrGateways' interface ProfileProps { relayPool: RelayPool @@ -38,6 +39,8 @@ const Profile: React.FC = ({ const activeAccount = Hooks.useActiveAccount() const [activeTab, setActiveTab] = useState<'highlights' | 'writings'>(propActiveTab || 'highlights') const [refreshTrigger, setRefreshTrigger] = useState(0) + const [showProfileMenu, setShowProfileMenu] = useState(false) + const profileMenuRef = useRef(null) // Reading progress state (naddr -> progress 0-1) const [readingProgressMap, setReadingProgressMap] = useState>(new Map()) @@ -168,6 +171,68 @@ const Profile: React.FC = ({ const npub = nip19.npubEncode(pubkey) const showSkeletons = cachedHighlights.length === 0 && sortedWritings.length === 0 + // Close menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) { + setShowProfileMenu(false) + } + } + + if (showProfileMenu) { + document.addEventListener('mousedown', handleClickOutside) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [showProfileMenu]) + + // Profile menu handlers + const handleMenuToggle = () => { + setShowProfileMenu(!showProfileMenu) + } + + const handleCopyProfileLink = async () => { + try { + const borisUrl = `${window.location.origin}/p/${npub}` + await navigator.clipboard.writeText(borisUrl) + setShowProfileMenu(false) + } catch (e) { + console.warn('Copy failed', e) + } + } + + const handleShareProfile = async () => { + try { + const borisUrl = `${window.location.origin}/p/${npub}` + if ((navigator as { share?: (d: { title?: string; url?: string }) => Promise }).share) { + await (navigator as { share: (d: { title?: string; url?: string }) => Promise }).share({ + title: 'Profile', + url: borisUrl + }) + } else { + await navigator.clipboard.writeText(borisUrl) + } + } catch (e) { + console.warn('Share failed', e) + } finally { + setShowProfileMenu(false) + } + } + + const handleOpenPortal = () => { + const portalUrl = getProfileUrl(npub) + window.open(portalUrl, '_blank', 'noopener,noreferrer') + setShowProfileMenu(false) + } + + const handleOpenNative = () => { + const nativeUrl = `nostr:${npub}` + window.location.href = nativeUrl + setShowProfileMenu(false) + } + const renderTabContent = () => { switch (activeTab) { case 'highlights': @@ -236,7 +301,51 @@ const Profile: React.FC = ({ pullPosition={pullPosition} />
- +
+ +
+ + {showProfileMenu && ( +
+ + + + +
+ )} +
+