Browse Source

add sending public messages to the menus

imwald
Silberengel 2 months ago
parent
commit
6d101b11c7
  1. 26
      src/components/GifPicker/index.tsx
  2. 10
      src/components/Note/index.tsx
  3. 12
      src/components/NoteOptions/index.tsx
  4. 17
      src/components/NoteOptions/useMenuActions.tsx
  5. 23
      src/components/PostEditor/PostContent.tsx
  6. 32
      src/components/PostEditor/index.tsx
  7. 14
      src/components/Profile/index.tsx
  8. 17
      src/components/ProfileOptions/index.tsx
  9. 1
      src/i18n/locales/de.ts
  10. 1
      src/i18n/locales/en.ts
  11. 2
      src/services/indexed-db.service.ts

26
src/components/GifPicker/index.tsx

@ -280,19 +280,6 @@ export default function GifPicker({
</ScrollArea> </ScrollArea>
</div> </div>
<div className="flex flex-col gap-2 border-t pt-2 shrink-0"> <div className="flex flex-col gap-2 border-t pt-2 shrink-0">
{isLoggedIn && (
<div className="grid gap-1">
<Label className="text-xs text-muted-foreground">
{t('Description (optional, for search)')}
</Label>
<Input
placeholder={t('e.g. happy birthday, thumbs up')}
value={publishDescription}
onChange={(e) => setPublishDescription(e.target.value)}
className="min-w-0"
/>
</div>
)}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Button <Button
type="button" type="button"
@ -330,6 +317,19 @@ export default function GifPicker({
</div> </div>
</div> </div>
</div> </div>
{isLoggedIn && (
<div className="grid gap-1">
<Label className="text-xs text-muted-foreground">
{t('Description (optional, for search)')}
</Label>
<Input
placeholder={t('e.g. happy birthday, thumbs up')}
value={publishDescription}
onChange={(e) => setPublishDescription(e.target.value)}
className="min-w-0"
/>
</div>
)}
{isLoggedIn && ( {isLoggedIn && (
<> <>
<input <input

10
src/components/Note/index.tsx

@ -73,10 +73,17 @@ export default function Note({
const [highlightData, setHighlightData] = useState<HighlightData | undefined>(undefined) const [highlightData, setHighlightData] = useState<HighlightData | undefined>(undefined)
const [highlightDefaultContent, setHighlightDefaultContent] = useState<string>('') const [highlightDefaultContent, setHighlightDefaultContent] = useState<string>('')
const [postEditorOpen, setPostEditorOpen] = useState(false) const [postEditorOpen, setPostEditorOpen] = useState(false)
const [publicMessageTo, setPublicMessageTo] = useState<string | null>(null)
const openHighlight = useCallback((data: HighlightData, eventContent?: string) => { const openHighlight = useCallback((data: HighlightData, eventContent?: string) => {
setHighlightData(data) setHighlightData(data)
setHighlightDefaultContent(eventContent ?? '') setHighlightDefaultContent(eventContent ?? '')
setPublicMessageTo(null)
setPostEditorOpen(true)
}, [])
const openPublicMessage = useCallback((pubkey: string) => {
setPublicMessageTo(pubkey)
setPostEditorOpen(true) setPostEditorOpen(true)
}, []) }, [])
@ -276,7 +283,10 @@ export default function Note({
setPostEditorOpen(false) setPostEditorOpen(false)
setHighlightData(undefined) setHighlightData(undefined)
setHighlightDefaultContent('') setHighlightDefaultContent('')
setPublicMessageTo(null)
}} }}
onOpenPublicMessage={openPublicMessage}
initialPublicMessageTo={publicMessageTo}
/> />
)} )}
</div> </div>

12
src/components/NoteOptions/index.tsx

@ -16,7 +16,9 @@ export default function NoteOptions({
initialHighlightData, initialHighlightData,
highlightDefaultContent, highlightDefaultContent,
isPostEditorOpen, isPostEditorOpen,
onPostEditorClose onPostEditorClose,
onOpenPublicMessage,
initialPublicMessageTo
}: { }: {
event: Event event: Event
className?: string className?: string
@ -24,6 +26,10 @@ export default function NoteOptions({
highlightDefaultContent?: string highlightDefaultContent?: string
isPostEditorOpen?: boolean isPostEditorOpen?: boolean
onPostEditorClose?: () => void 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 { isSmallScreen } = useScreenSize()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false) const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
@ -54,7 +60,8 @@ export default function NoteOptions({
showSubMenuActions, showSubMenuActions,
setIsRawEventDialogOpen, setIsRawEventDialogOpen,
setIsReportDialogOpen, setIsReportDialogOpen,
isSmallScreen isSmallScreen,
onOpenPublicMessage
}) })
const trigger = useMemo( const trigger = useMemo(
@ -105,6 +112,7 @@ export default function NoteOptions({
}} }}
defaultContent={highlightDefaultContent ?? ''} defaultContent={highlightDefaultContent ?? ''}
initialHighlightData={initialHighlightData} initialHighlightData={initialHighlightData}
initialPublicMessageTo={initialPublicMessageTo ?? undefined}
/> />
)} )}
</div> </div>

17
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 { 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 { nip66Service } from '@/services/nip66.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 { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { useMemo, useState, useEffect, useContext } from 'react' import { useMemo, useState, useEffect, useContext } from 'react'
@ -46,6 +46,8 @@ interface UseMenuActionsProps {
setIsRawEventDialogOpen: (open: boolean) => void setIsRawEventDialogOpen: (open: boolean) => void
setIsReportDialogOpen: (open: boolean) => void setIsReportDialogOpen: (open: boolean) => void
isSmallScreen: boolean 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({ export function useMenuActions({
@ -55,6 +57,7 @@ export function useMenuActions({
setIsRawEventDialogOpen, setIsRawEventDialogOpen,
setIsReportDialogOpen, setIsReportDialogOpen,
isSmallScreen, isSmallScreen,
onOpenPublicMessage,
}: UseMenuActionsProps) { }: UseMenuActionsProps) {
const { t } = useTranslation() const { t } = useTranslation()
// Use useContext directly to avoid error if provider is not available // Use useContext directly to avoid error if provider is not available
@ -588,6 +591,18 @@ export function useMenuActions({
closeDrawer() closeDrawer()
} }
}, },
...(pubkey && event.pubkey !== pubkey && onOpenPublicMessage
? [
{
icon: MessageCircle,
label: t('Send public message'),
onClick: () => {
closeDrawer()
onOpenPublicMessage(event.pubkey)
}
} as MenuAction
]
: []),
{ {
icon: Link, icon: Link,
label: t('Share with Jumble'), label: t('Share with Jumble'),

23
src/components/PostEditor/PostContent.tsx

@ -67,13 +67,16 @@ export default function PostContent({
parentEvent, parentEvent,
close, close,
openFrom, openFrom,
initialHighlightData initialHighlightData,
initialPublicMessageTo
}: { }: {
defaultContent?: string defaultContent?: string
parentEvent?: Event parentEvent?: Event
close: () => void close: () => void
openFrom?: string[] openFrom?: string[]
initialHighlightData?: HighlightData initialHighlightData?: HighlightData
/** When set, opens in public message mode with this pubkey in the mention list. */
initialPublicMessageTo?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin } = useNostr()
@ -90,8 +93,10 @@ export default function PostContent({
const [mentions, setMentions] = useState<string[]>([]) const [mentions, setMentions] = useState<string[]>([])
const [isNsfw, setIsNsfw] = useState(false) const [isNsfw, setIsNsfw] = useState(false)
const [isPoll, setIsPoll] = useState(false) const [isPoll, setIsPoll] = useState(false)
const [isPublicMessage, setIsPublicMessage] = useState(false) const [isPublicMessage, setIsPublicMessage] = useState(!!initialPublicMessageTo)
const [extractedMentions, setExtractedMentions] = useState<string[]>([]) const [extractedMentions, setExtractedMentions] = useState<string[]>(
initialPublicMessageTo ? [initialPublicMessageTo] : []
)
const [isProtectedEvent, setIsProtectedEvent] = useState(false) const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([]) const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
const [isHighlight, setIsHighlight] = useState(!!initialHighlightData) const [isHighlight, setIsHighlight] = useState(!!initialHighlightData)
@ -283,7 +288,7 @@ export default function PostContent({
useEffect(() => { useEffect(() => {
if (!text) { if (!text) {
setExtractedMentions([]) if (!initialPublicMessageTo) setExtractedMentions([])
return return
} }
@ -295,7 +300,7 @@ export default function PostContent({
return () => { return () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
} }
}, [text, extractMentionsFromContent]) }, [text, extractMentionsFromContent, initialPublicMessageTo])
// Check for private relays availability // Check for private relays availability
useEffect(() => { useEffect(() => {
@ -1533,7 +1538,7 @@ export default function PostContent({
} }
return ( return (
<div className="space-y-2"> <div className="space-y-2 min-w-0">
{/* Dynamic Title based on mode */} {/* Dynamic Title based on mode */}
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">
{(() => { {(() => {
@ -2237,8 +2242,8 @@ export default function PostContent({
mentions={extractedMentions} mentions={extractedMentions}
/> />
)} )}
<div className="flex items-center justify-between"> <div className="flex flex-wrap items-center justify-between gap-2 min-w-0">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center min-w-0 shrink-0">
{/* Audio button for replies and new PMs - placed before image button */} {/* Audio button for replies and new PMs - placed before image button */}
{(parentEvent || isPublicMessage) && ( {(parentEvent || isPublicMessage) && (
<Uploader <Uploader
@ -2305,7 +2310,7 @@ export default function PostContent({
<Settings /> <Settings />
</Button> </Button>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center shrink-0">
<Mentions <Mentions
content={text} content={text}
parentEvent={parentEvent} parentEvent={parentEvent}

32
src/components/PostEditor/index.tsx

@ -14,6 +14,7 @@ import {
SheetTitle SheetTitle
} from '@/components/ui/sheet' } from '@/components/ui/sheet'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { pubkeyToNpub } from '@/lib/pubkey'
import postEditor from '@/services/post-editor.service' import postEditor from '@/services/post-editor.service'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { Dispatch, useMemo } from 'react' import { Dispatch, useMemo } from 'react'
@ -25,7 +26,8 @@ export default function PostEditor({
open, open,
setOpen, setOpen,
openFrom, openFrom,
initialHighlightData initialHighlightData,
initialPublicMessageTo
}: { }: {
defaultContent?: string defaultContent?: string
parentEvent?: Event parentEvent?: Event
@ -33,13 +35,18 @@ export default function PostEditor({
setOpen: Dispatch<boolean> setOpen: Dispatch<boolean>
openFrom?: string[] openFrom?: string[]
initialHighlightData?: import('./HighlightEditor').HighlightData initialHighlightData?: import('./HighlightEditor').HighlightData
/** When set, opens in public message mode with this pubkey in the mention list. */
initialPublicMessageTo?: string
}) { }) {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
// If initialHighlightData is provided and we're creating a highlight from an event, const effectiveDefaultContent = useMemo(() => {
// we need to pass the event content as defaultContent for the main editor if (initialPublicMessageTo) {
// Note: This is handled separately - we'll pass the event content when opening from menu const npub = pubkeyToNpub(initialPublicMessageTo)
const effectiveDefaultContent = defaultContent return npub ? `nostr:${npub} ` : defaultContent
}
return defaultContent
}, [initialPublicMessageTo, defaultContent])
const content = useMemo(() => { const content = useMemo(() => {
return ( return (
@ -49,15 +56,16 @@ export default function PostEditor({
close={() => setOpen(false)} close={() => setOpen(false)}
openFrom={openFrom} openFrom={openFrom}
initialHighlightData={initialHighlightData} initialHighlightData={initialHighlightData}
initialPublicMessageTo={initialPublicMessageTo}
/> />
) )
}, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData]) }, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData, initialPublicMessageTo])
if (isSmallScreen) { if (isSmallScreen) {
return ( return (
<Sheet open={open} onOpenChange={setOpen}> <Sheet open={open} onOpenChange={setOpen}>
<SheetContent <SheetContent
className="h-full w-full p-0 border-none" className="h-full w-full max-w-full p-0 border-none overflow-hidden"
side="bottom" side="bottom"
hideClose hideClose
onEscapeKeyDown={(e) => { onEscapeKeyDown={(e) => {
@ -67,8 +75,8 @@ export default function PostEditor({
} }
}} }}
> >
<ScrollArea className="px-4 h-full max-h-screen" scrollBarClassName="opacity-100"> <ScrollArea className="px-4 h-full max-h-screen min-w-0 overflow-x-auto" scrollBarClassName="opacity-100">
<div className="space-y-4 px-2 py-6"> <div className="space-y-4 px-2 pr-4 py-6 min-w-0">
<SheetHeader className="sr-only"> <SheetHeader className="sr-only">
<SheetTitle>Post Editor</SheetTitle> <SheetTitle>Post Editor</SheetTitle>
<SheetDescription>Create a new post or reply</SheetDescription> <SheetDescription>Create a new post or reply</SheetDescription>
@ -84,7 +92,7 @@ export default function PostEditor({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent <DialogContent
className="p-0 max-w-2xl" className="p-0 max-w-2xl w-[calc(100vw-2rem)] sm:w-full overflow-hidden"
withoutClose withoutClose
onEscapeKeyDown={(e) => { onEscapeKeyDown={(e) => {
if (postEditor.isSuggestionPopupOpen) { if (postEditor.isSuggestionPopupOpen) {
@ -93,8 +101,8 @@ export default function PostEditor({
} }
}} }}
> >
<ScrollArea className="px-4 h-full max-h-screen" scrollBarClassName="opacity-100"> <ScrollArea className="px-4 h-full max-h-screen min-w-0" scrollBarClassName="opacity-100">
<div className="space-y-4 px-2 py-6"> <div className="space-y-4 px-2 pr-4 py-6 min-w-0">
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>Post Editor</DialogTitle> <DialogTitle>Post Editor</DialogTitle>
<DialogDescription>Create a new post or reply</DialogDescription> <DialogDescription>Create a new post or reply</DialogDescription>

14
src/components/Profile/index.tsx

@ -51,6 +51,7 @@ import ProfileNotes from './ProfileNotes'
import { toFollowPacks } from '@/lib/link' import { toFollowPacks } from '@/lib/link'
import ZapDialog from '@/components/ZapDialog' import ZapDialog from '@/components/ZapDialog'
import PaytoLink from '@/components/PaytoLink' import PaytoLink from '@/components/PaytoLink'
import PostEditor from '@/components/PostEditor'
import type { TProfile } from '@/types' import type { TProfile } from '@/types'
type ProfileTabValue = 'posts' | 'pins' | 'bookmarks' | 'interests' | 'articles' | 'media' | 'you' | 'notes' 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 { pubkey: accountPubkey } = useNostr()
const [paymentInfo, setPaymentInfo] = useState<ReturnType<typeof getPaymentInfoFromEvent> | null>(null) const [paymentInfo, setPaymentInfo] = useState<ReturnType<typeof getPaymentInfoFromEvent> | null>(null)
const [openZapDialog, setOpenZapDialog] = useState(false) const [openZapDialog, setOpenZapDialog] = useState(false)
const [openPublicMessageTo, setOpenPublicMessageTo] = useState<string | null>(null)
const mergedPaymentMethods = useMemo(() => { const mergedPaymentMethods = useMemo(() => {
const list = mergePaymentMethods(paymentInfo, profile ?? null) const list = mergePaymentMethods(paymentInfo, profile ?? null)
@ -426,7 +428,10 @@ export default function Profile({ id }: { id?: string }) {
</div> </div>
<div className="px-4"> <div className="px-4">
<div className="flex justify-end h-8 gap-2 items-center"> <div className="flex justify-end h-8 gap-2 items-center">
<ProfileOptions pubkey={pubkey} /> <ProfileOptions
pubkey={pubkey}
onSendPublicMessage={!isSelf ? () => setOpenPublicMessageTo(pubkey) : undefined}
/>
{isSelf ? ( {isSelf ? (
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
@ -760,6 +765,13 @@ export default function Profile({ id }: { id?: string }) {
/> />
)} )}
</div> </div>
{openPublicMessageTo && (
<PostEditor
open={!!openPublicMessageTo}
setOpen={(open) => !open && setOpenPublicMessageTo(null)}
initialPublicMessageTo={openPublicMessageTo}
/>
)}
</> </>
) )
} }

17
src/components/ProfileOptions/index.tsx

@ -8,11 +8,18 @@ import {
import { pubkeyToNpub } from '@/lib/pubkey' import { pubkeyToNpub } from '@/lib/pubkey'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' 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 { useMemo } from 'react'
import { useTranslation } from 'react-i18next' 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 { t } = useTranslation()
const { pubkey: accountPubkey } = useNostr() const { pubkey: accountPubkey } = useNostr()
const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList()
@ -28,6 +35,12 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
{onSendPublicMessage && (
<DropdownMenuItem onClick={onSendPublicMessage}>
<MessageCircle />
{t('Send public message')}
</DropdownMenuItem>
)}
<DropdownMenuItem <DropdownMenuItem
onClick={() => navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))} onClick={() => navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))}
> >

1
src/i18n/locales/de.ts

@ -45,6 +45,7 @@ export default {
Quote: 'Zitat', Quote: 'Zitat',
'Copy event ID': 'Ereignis-ID kopieren', 'Copy event ID': 'Ereignis-ID kopieren',
'Copy user ID': 'Benutzer-ID kopieren', 'Copy user ID': 'Benutzer-ID kopieren',
'Send public message': 'Öffentliche Nachricht senden',
'View raw event': 'Rohdaten anzeigen', 'View raw event': 'Rohdaten anzeigen',
Like: 'Gefällt mir', Like: 'Gefällt mir',
'switch to light theme': 'Wechsel zum hellen Design', 'switch to light theme': 'Wechsel zum hellen Design',

1
src/i18n/locales/en.ts

@ -45,6 +45,7 @@ export default {
Quote: 'Quote', Quote: 'Quote',
'Copy event ID': 'Copy event ID', 'Copy event ID': 'Copy event ID',
'Copy user ID': 'Copy user ID', 'Copy user ID': 'Copy user ID',
'Send public message': 'Send public message',
'View raw event': 'View raw event', 'View raw event': 'View raw event',
'View full profile': 'View full profile', 'View full profile': 'View full profile',
Like: 'Like', Like: 'Like',

2
src/services/indexed-db.service.ts

@ -48,7 +48,7 @@ export const StoreNames = {
} }
/** Schema version we expect. When adding stores or migrations, bump this. */ /** 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). */ /** Max age for profile and payment info cache before we refetch (5 min). */
const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000 const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000

Loading…
Cancel
Save