diff --git a/src/components/GifPicker/index.tsx b/src/components/GifPicker/index.tsx index 699e8b70..87aaabbf 100644 --- a/src/components/GifPicker/index.tsx +++ b/src/components/GifPicker/index.tsx @@ -280,19 +280,6 @@ export default function GifPicker({
- {isLoggedIn && ( -
- - setPublishDescription(e.target.value)} - className="min-w-0" - /> -
- )}
+ {isLoggedIn && ( +
+ + setPublishDescription(e.target.value)} + className="min-w-0" + /> +
+ )} {isLoggedIn && ( <> (undefined) const [highlightDefaultContent, setHighlightDefaultContent] = useState('') const [postEditorOpen, setPostEditorOpen] = useState(false) + const [publicMessageTo, setPublicMessageTo] = useState(null) const openHighlight = useCallback((data: HighlightData, eventContent?: string) => { setHighlightData(data) setHighlightDefaultContent(eventContent ?? '') + setPublicMessageTo(null) + setPostEditorOpen(true) + }, []) + + const openPublicMessage = useCallback((pubkey: string) => { + setPublicMessageTo(pubkey) setPostEditorOpen(true) }, []) @@ -276,7 +283,10 @@ export default function Note({ setPostEditorOpen(false) setHighlightData(undefined) setHighlightDefaultContent('') + setPublicMessageTo(null) }} + onOpenPublicMessage={openPublicMessage} + initialPublicMessageTo={publicMessageTo} /> )} diff --git a/src/components/NoteOptions/index.tsx b/src/components/NoteOptions/index.tsx index 42a3ef9e..160946a2 100644 --- a/src/components/NoteOptions/index.tsx +++ b/src/components/NoteOptions/index.tsx @@ -16,7 +16,9 @@ export default function NoteOptions({ initialHighlightData, highlightDefaultContent, isPostEditorOpen, - onPostEditorClose + onPostEditorClose, + onOpenPublicMessage, + initialPublicMessageTo }: { event: Event className?: string @@ -24,6 +26,10 @@ export default function NoteOptions({ highlightDefaultContent?: string isPostEditorOpen?: boolean onPostEditorClose?: () => void + /** Opens the post editor in public message mode with the given pubkey in the mention list. */ + onOpenPublicMessage?: (pubkey: string) => void + /** When set, the post editor is opened in public message mode with this pubkey pre-filled. */ + initialPublicMessageTo?: string | null }) { const { isSmallScreen } = useScreenSize() const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false) @@ -54,7 +60,8 @@ export default function NoteOptions({ showSubMenuActions, setIsRawEventDialogOpen, setIsReportDialogOpen, - isSmallScreen + isSmallScreen, + onOpenPublicMessage }) const trigger = useMemo( @@ -105,6 +112,7 @@ export default function NoteOptions({ }} defaultContent={highlightDefaultContent ?? ''} initialHighlightData={initialHighlightData} + initialPublicMessageTo={initialPublicMessageTo ?? undefined} /> )} diff --git a/src/components/NoteOptions/useMenuActions.tsx b/src/components/NoteOptions/useMenuActions.tsx index 27d4282c..c01960ae 100644 --- a/src/components/NoteOptions/useMenuActions.tsx +++ b/src/components/NoteOptions/useMenuActions.tsx @@ -13,7 +13,7 @@ import { useNostr } from '@/providers/NostrProvider' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import client from '@/services/client.service' import { nip66Service } from '@/services/nip66.service' -import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin, FileDown, Globe, BookOpen } from 'lucide-react' +import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin, FileDown, Globe, BookOpen, MessageCircle } from 'lucide-react' import { Event, kinds } from 'nostr-tools' import { nip19 } from 'nostr-tools' import { useMemo, useState, useEffect, useContext } from 'react' @@ -46,6 +46,8 @@ interface UseMenuActionsProps { setIsRawEventDialogOpen: (open: boolean) => void setIsReportDialogOpen: (open: boolean) => void isSmallScreen: boolean + /** When provided, adds "Send public message" to open composer with this pubkey in the mention list. */ + onOpenPublicMessage?: (pubkey: string) => void } export function useMenuActions({ @@ -55,6 +57,7 @@ export function useMenuActions({ setIsRawEventDialogOpen, setIsReportDialogOpen, isSmallScreen, + onOpenPublicMessage, }: UseMenuActionsProps) { const { t } = useTranslation() // Use useContext directly to avoid error if provider is not available @@ -588,6 +591,18 @@ export function useMenuActions({ closeDrawer() } }, + ...(pubkey && event.pubkey !== pubkey && onOpenPublicMessage + ? [ + { + icon: MessageCircle, + label: t('Send public message'), + onClick: () => { + closeDrawer() + onOpenPublicMessage(event.pubkey) + } + } as MenuAction + ] + : []), { icon: Link, label: t('Share with Jumble'), diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 7111a440..bde3c5d1 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -67,13 +67,16 @@ export default function PostContent({ parentEvent, close, openFrom, - initialHighlightData + initialHighlightData, + initialPublicMessageTo }: { defaultContent?: string parentEvent?: Event close: () => void openFrom?: string[] initialHighlightData?: HighlightData + /** When set, opens in public message mode with this pubkey in the mention list. */ + initialPublicMessageTo?: string }) { const { t } = useTranslation() const { pubkey, publish, checkLogin } = useNostr() @@ -90,8 +93,10 @@ export default function PostContent({ const [mentions, setMentions] = useState([]) const [isNsfw, setIsNsfw] = useState(false) const [isPoll, setIsPoll] = useState(false) - const [isPublicMessage, setIsPublicMessage] = useState(false) - const [extractedMentions, setExtractedMentions] = useState([]) + const [isPublicMessage, setIsPublicMessage] = useState(!!initialPublicMessageTo) + const [extractedMentions, setExtractedMentions] = useState( + initialPublicMessageTo ? [initialPublicMessageTo] : [] + ) const [isProtectedEvent, setIsProtectedEvent] = useState(false) const [additionalRelayUrls, setAdditionalRelayUrls] = useState([]) const [isHighlight, setIsHighlight] = useState(!!initialHighlightData) @@ -283,7 +288,7 @@ export default function PostContent({ useEffect(() => { if (!text) { - setExtractedMentions([]) + if (!initialPublicMessageTo) setExtractedMentions([]) return } @@ -295,7 +300,7 @@ export default function PostContent({ return () => { clearTimeout(timeoutId) } - }, [text, extractMentionsFromContent]) + }, [text, extractMentionsFromContent, initialPublicMessageTo]) // Check for private relays availability useEffect(() => { @@ -1533,7 +1538,7 @@ export default function PostContent({ } return ( -
+
{/* Dynamic Title based on mode */}
{(() => { @@ -2237,8 +2242,8 @@ export default function PostContent({ mentions={extractedMentions} /> )} -
-
+
+
{/* Audio button for replies and new PMs - placed before image button */} {(parentEvent || isPublicMessage) && (
-
+
openFrom?: string[] initialHighlightData?: import('./HighlightEditor').HighlightData + /** When set, opens in public message mode with this pubkey in the mention list. */ + initialPublicMessageTo?: string }) { const { isSmallScreen } = useScreenSize() - // If initialHighlightData is provided and we're creating a highlight from an event, - // we need to pass the event content as defaultContent for the main editor - // Note: This is handled separately - we'll pass the event content when opening from menu - const effectiveDefaultContent = defaultContent + const effectiveDefaultContent = useMemo(() => { + if (initialPublicMessageTo) { + const npub = pubkeyToNpub(initialPublicMessageTo) + return npub ? `nostr:${npub} ` : defaultContent + } + return defaultContent + }, [initialPublicMessageTo, defaultContent]) const content = useMemo(() => { return ( @@ -49,15 +56,16 @@ export default function PostEditor({ close={() => setOpen(false)} openFrom={openFrom} initialHighlightData={initialHighlightData} + initialPublicMessageTo={initialPublicMessageTo} /> ) - }, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData]) + }, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData, initialPublicMessageTo]) if (isSmallScreen) { return ( { @@ -67,8 +75,8 @@ export default function PostEditor({ } }} > - -
+ +
Post Editor Create a new post or reply @@ -84,7 +92,7 @@ export default function PostEditor({ return ( { if (postEditor.isSuggestionPopupOpen) { @@ -93,8 +101,8 @@ export default function PostEditor({ } }} > - -
+ +
Post Editor Create a new post or reply diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 518a3faa..a6a5e650 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -51,6 +51,7 @@ import ProfileNotes from './ProfileNotes' import { toFollowPacks } from '@/lib/link' import ZapDialog from '@/components/ZapDialog' import PaytoLink from '@/components/PaytoLink' +import PostEditor from '@/components/PostEditor' import type { TProfile } from '@/types' type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles' | 'media' | 'you' | 'notes' @@ -162,6 +163,7 @@ export default function Profile({ id }: { id?: string }) { const { pubkey: accountPubkey } = useNostr() const [paymentInfo, setPaymentInfo] = useState | null>(null) const [openZapDialog, setOpenZapDialog] = useState(false) + const [openPublicMessageTo, setOpenPublicMessageTo] = useState(null) const mergedPaymentMethods = useMemo(() => { const list = mergePaymentMethods(paymentInfo, profile ?? null) @@ -426,7 +428,10 @@ export default function Profile({ id }: { id?: string }) {
- + setOpenPublicMessageTo(pubkey) : undefined} + /> {isSelf ? (
+ {openPublicMessageTo && ( + !open && setOpenPublicMessageTo(null)} + initialPublicMessageTo={openPublicMessageTo} + /> + )} ) } diff --git a/src/components/ProfileOptions/index.tsx b/src/components/ProfileOptions/index.tsx index 9004d4d2..8510e1fc 100644 --- a/src/components/ProfileOptions/index.tsx +++ b/src/components/ProfileOptions/index.tsx @@ -8,11 +8,18 @@ import { import { pubkeyToNpub } from '@/lib/pubkey' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' -import { Bell, BellOff, Copy, Ellipsis } from 'lucide-react' +import { Bell, BellOff, Copy, Ellipsis, MessageCircle } from 'lucide-react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -export default function ProfileOptions({ pubkey }: { pubkey: string }) { +export default function ProfileOptions({ + pubkey, + onSendPublicMessage +}: { + pubkey: string + /** Opens the post editor in public message mode with this profile's pubkey in the mention list. */ + onSendPublicMessage?: () => void +}) { const { t } = useTranslation() const { pubkey: accountPubkey } = useNostr() const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() @@ -28,6 +35,12 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) { + {onSendPublicMessage && ( + + + {t('Send public message')} + + )} navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))} > diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index aba684cb..9af1d876 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -45,6 +45,7 @@ export default { Quote: 'Zitat', 'Copy event ID': 'Ereignis-ID kopieren', 'Copy user ID': 'Benutzer-ID kopieren', + 'Send public message': 'Öffentliche Nachricht senden', 'View raw event': 'Rohdaten anzeigen', Like: 'Gefällt mir', 'switch to light theme': 'Wechsel zum hellen Design', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index bc2e3590..70e25bb4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -45,6 +45,7 @@ export default { Quote: 'Quote', 'Copy event ID': 'Copy event ID', 'Copy user ID': 'Copy user ID', + 'Send public message': 'Send public message', 'View raw event': 'View raw event', 'View full profile': 'View full profile', Like: 'Like', diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index e9e0e233..01129beb 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -48,7 +48,7 @@ export const StoreNames = { } /** Schema version we expect. When adding stores or migrations, bump this. */ -const DB_VERSION = 22 +const DB_VERSION = 23 /** Max age for profile and payment info cache before we refetch (5 min). */ const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000