|
|
|
@ -1,8 +1,19 @@ |
|
|
|
import { ExtendedKind, READ_ALOUD_KINDS } from '@/constants' |
|
|
|
import { ExtendedKind, READ_ALOUD_KINDS } from '@/constants' |
|
|
|
import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event' |
|
|
|
import { |
|
|
|
|
|
|
|
getNoteBech32Id, |
|
|
|
|
|
|
|
getReplaceableCoordinateFromEvent, |
|
|
|
|
|
|
|
isProtectedEvent, |
|
|
|
|
|
|
|
isReplaceableEvent, |
|
|
|
|
|
|
|
getRootEventHexId |
|
|
|
|
|
|
|
} from '@/lib/event' |
|
|
|
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' |
|
|
|
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' |
|
|
|
import { buildHiveTalkJoinUrl } from '@/lib/hivetalk' |
|
|
|
import { buildHiveTalkJoinUrl } from '@/lib/hivetalk' |
|
|
|
import { toAlexandria, encodeArticleLikePublicationNaddr, openAlexandriaPublicationFromNaddr } from '@/lib/link' |
|
|
|
import { |
|
|
|
|
|
|
|
toAlexandria, |
|
|
|
|
|
|
|
encodeArticleLikePublicationNaddr, |
|
|
|
|
|
|
|
openAlexandriaPublicationFromNaddr, |
|
|
|
|
|
|
|
toRelay |
|
|
|
|
|
|
|
} from '@/lib/link' |
|
|
|
import logger from '@/lib/logger' |
|
|
|
import logger from '@/lib/logger' |
|
|
|
import { pubkeyToNpub } from '@/lib/pubkey' |
|
|
|
import { pubkeyToNpub } from '@/lib/pubkey' |
|
|
|
import { |
|
|
|
import { |
|
|
|
@ -35,6 +46,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
|
|
|
import { useMuteList } from '@/contexts/mute-list-context' |
|
|
|
import { useMuteList } from '@/contexts/mute-list-context' |
|
|
|
import { muteSetHas } from '@/lib/mute-set' |
|
|
|
import { muteSetHas } from '@/lib/mute-set' |
|
|
|
import { useNostr } from '@/providers/NostrProvider' |
|
|
|
import { useNostr } from '@/providers/NostrProvider' |
|
|
|
|
|
|
|
import { useBookmarksOptional } from '@/providers/bookmarks-context' |
|
|
|
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' |
|
|
|
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' |
|
|
|
import client from '@/services/client.service' |
|
|
|
import client from '@/services/client.service' |
|
|
|
import { eventService } from '@/services/client.service' |
|
|
|
import { eventService } from '@/services/client.service' |
|
|
|
@ -42,21 +54,11 @@ import { nip66Service } from '@/services/nip66.service' |
|
|
|
import { |
|
|
|
import { |
|
|
|
Bell, |
|
|
|
Bell, |
|
|
|
BellOff, |
|
|
|
BellOff, |
|
|
|
BookOpen, |
|
|
|
Bookmark, |
|
|
|
Code, |
|
|
|
|
|
|
|
Copy, |
|
|
|
|
|
|
|
FileDown, |
|
|
|
|
|
|
|
GitFork, |
|
|
|
|
|
|
|
Globe, |
|
|
|
|
|
|
|
Link, |
|
|
|
|
|
|
|
MessageCircle, |
|
|
|
|
|
|
|
PencilLine, |
|
|
|
|
|
|
|
Pin, |
|
|
|
Pin, |
|
|
|
SatelliteDish, |
|
|
|
Settings, |
|
|
|
Send, |
|
|
|
Share2, |
|
|
|
Sparkles, |
|
|
|
|
|
|
|
Trash2, |
|
|
|
Trash2, |
|
|
|
TriangleAlert, |
|
|
|
|
|
|
|
Video, |
|
|
|
Video, |
|
|
|
Volume2, |
|
|
|
Volume2, |
|
|
|
Languages |
|
|
|
Languages |
|
|
|
@ -84,6 +86,8 @@ import { useMemo, useState, useEffect, useRef, useContext, useSyncExternalStore |
|
|
|
import { useTranslation } from 'react-i18next' |
|
|
|
import { useTranslation } from 'react-i18next' |
|
|
|
import { toast } from 'sonner' |
|
|
|
import { toast } from 'sonner' |
|
|
|
import RelayIcon from '../RelayIcon' |
|
|
|
import RelayIcon from '../RelayIcon' |
|
|
|
|
|
|
|
import { useSeenOnRelays } from '@/hooks/useSeenOnRelays' |
|
|
|
|
|
|
|
import { useSecondaryPage } from '@/PageManager' |
|
|
|
import { PrimaryPageContext } from '@/contexts/primary-page-context' |
|
|
|
import { PrimaryPageContext } from '@/contexts/primary-page-context' |
|
|
|
import { showPublishingFeedback, toastPublishPromise } from '@/lib/publishing-feedback' |
|
|
|
import { showPublishingFeedback, toastPublishPromise } from '@/lib/publishing-feedback' |
|
|
|
import type { TEditOrCloneMode } from './EditOrCloneEventDialog' |
|
|
|
import type { TEditOrCloneMode } from './EditOrCloneEventDialog' |
|
|
|
@ -95,6 +99,8 @@ export interface SubMenuAction { |
|
|
|
separator?: boolean |
|
|
|
separator?: boolean |
|
|
|
/** Lowercase haystack for submenu filter when the parent sets {@link MenuAction.subMenuSearchable}. */ |
|
|
|
/** Lowercase haystack for submenu filter when the parent sets {@link MenuAction.subMenuSearchable}. */ |
|
|
|
filterHaystack?: string |
|
|
|
filterHaystack?: string |
|
|
|
|
|
|
|
/** Nested submenu (desktop dropdown only). */ |
|
|
|
|
|
|
|
subMenu?: SubMenuAction[] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export interface MenuAction { |
|
|
|
export interface MenuAction { |
|
|
|
@ -127,6 +133,8 @@ interface UseMenuActionsProps { |
|
|
|
pinned?: boolean |
|
|
|
pinned?: boolean |
|
|
|
/** Opens JSON viewer for the kind 9741 attestation of this payment or zap receipt. */ |
|
|
|
/** Opens JSON viewer for the kind 9741 attestation of this payment or zap receipt. */ |
|
|
|
onViewAttestation?: () => void |
|
|
|
onViewAttestation?: () => void |
|
|
|
|
|
|
|
/** When set (home favorites feed), "Seen on" in Advanced matches the feed allowlist. */ |
|
|
|
|
|
|
|
seenOnAllowlist?: readonly string[] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function useMenuActions({ |
|
|
|
export function useMenuActions({ |
|
|
|
@ -140,13 +148,31 @@ export function useMenuActions({ |
|
|
|
onOpenCallInvite, |
|
|
|
onOpenCallInvite, |
|
|
|
onOpenEditOrClone, |
|
|
|
onOpenEditOrClone, |
|
|
|
pinned: _pinnedInFeed = false, |
|
|
|
pinned: _pinnedInFeed = false, |
|
|
|
onViewAttestation |
|
|
|
onViewAttestation, |
|
|
|
|
|
|
|
seenOnAllowlist |
|
|
|
}: UseMenuActionsProps) { |
|
|
|
}: UseMenuActionsProps) { |
|
|
|
const { t } = useTranslation() |
|
|
|
const { t } = useTranslation() |
|
|
|
|
|
|
|
const { push } = useSecondaryPage() |
|
|
|
|
|
|
|
const seenOnRelays = useSeenOnRelays(event.id, seenOnAllowlist) |
|
|
|
// Use useContext directly to avoid error if provider is not available
|
|
|
|
// Use useContext directly to avoid error if provider is not available
|
|
|
|
const primaryPageContext = useContext(PrimaryPageContext) |
|
|
|
const primaryPageContext = useContext(PrimaryPageContext) |
|
|
|
const currentPrimaryPage = primaryPageContext?.current ?? null |
|
|
|
const currentPrimaryPage = primaryPageContext?.current ?? null |
|
|
|
const { pubkey, profile, attemptDelete, publish, account, relayList } = useNostr() |
|
|
|
const { |
|
|
|
|
|
|
|
pubkey, |
|
|
|
|
|
|
|
profile, |
|
|
|
|
|
|
|
attemptDelete, |
|
|
|
|
|
|
|
publish, |
|
|
|
|
|
|
|
account, |
|
|
|
|
|
|
|
relayList, |
|
|
|
|
|
|
|
bookmarkListEvent, |
|
|
|
|
|
|
|
checkLogin |
|
|
|
|
|
|
|
} = useNostr() |
|
|
|
|
|
|
|
const bookmarksContext = useBookmarksOptional() |
|
|
|
|
|
|
|
const { addBookmark, removeBookmark } = bookmarksContext ?? { |
|
|
|
|
|
|
|
addBookmark: async () => {}, |
|
|
|
|
|
|
|
removeBookmark: async () => false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const [bookmarkUpdating, setBookmarkUpdating] = useState(false) |
|
|
|
const canSignEvents = account != null && account.signerType !== 'npub' |
|
|
|
const canSignEvents = account != null && account.signerType !== 'npub' |
|
|
|
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() |
|
|
|
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() |
|
|
|
const { relaySets, favoriteRelays } = useFavoriteRelays() |
|
|
|
const { relaySets, favoriteRelays } = useFavoriteRelays() |
|
|
|
@ -184,6 +210,15 @@ export function useMenuActions({ |
|
|
|
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList() |
|
|
|
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList() |
|
|
|
const isMuted = useMemo(() => muteSetHas(mutePubkeySet, event.pubkey), [mutePubkeySet, event]) |
|
|
|
const isMuted = useMemo(() => muteSetHas(mutePubkeySet, event.pubkey), [mutePubkeySet, event]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isBookmarked = useMemo(() => { |
|
|
|
|
|
|
|
if (!bookmarkListEvent) return false |
|
|
|
|
|
|
|
const isReplaceable = isReplaceableEvent(event.kind) |
|
|
|
|
|
|
|
const eventKey = isReplaceable ? getReplaceableCoordinateFromEvent(event) : event.id |
|
|
|
|
|
|
|
return bookmarkListEvent.tags.some((tag) => |
|
|
|
|
|
|
|
isReplaceable ? tag[0] === 'a' && tag[1] === eventKey : tag[0] === 'e' && tag[1] === eventKey |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
}, [bookmarkListEvent, event]) |
|
|
|
|
|
|
|
|
|
|
|
const noteTranslationFromMenu = useSyncExternalStore( |
|
|
|
const noteTranslationFromMenu = useSyncExternalStore( |
|
|
|
subscribeNoteTranslations, |
|
|
|
subscribeNoteTranslations, |
|
|
|
() => getNoteTranslation(event.id), |
|
|
|
() => getNoteTranslation(event.id), |
|
|
|
@ -932,122 +967,133 @@ export function useMenuActions({ |
|
|
|
] |
|
|
|
] |
|
|
|
: [] |
|
|
|
: [] |
|
|
|
|
|
|
|
|
|
|
|
const actions: MenuAction[] = [ |
|
|
|
const pushSubMenuParent = ( |
|
|
|
{ |
|
|
|
target: MenuAction[], |
|
|
|
icon: Copy, |
|
|
|
icon: MenuAction['icon'], |
|
|
|
label: t('Copy event ID'), |
|
|
|
title: string, |
|
|
|
onClick: () => { |
|
|
|
subMenu: SubMenuAction[], |
|
|
|
navigator.clipboard.writeText(getNoteBech32Id(event)) |
|
|
|
options?: { separator?: boolean; subMenuSearchable?: boolean; className?: string } |
|
|
|
closeDrawer() |
|
|
|
) => { |
|
|
|
} |
|
|
|
if (subMenu.length === 0) return |
|
|
|
}, |
|
|
|
target.push({ |
|
|
|
{ |
|
|
|
icon, |
|
|
|
icon: Copy, |
|
|
|
label: title, |
|
|
|
label: t('Copy user ID'), |
|
|
|
separator: options?.separator, |
|
|
|
onClick: () => { |
|
|
|
className: options?.className, |
|
|
|
navigator.clipboard.writeText(pubkeyToNpub(event.pubkey) ?? '') |
|
|
|
subMenuSearchable: options?.subMenuSearchable, |
|
|
|
closeDrawer() |
|
|
|
onClick: isSmallScreen |
|
|
|
} |
|
|
|
? () => |
|
|
|
}, |
|
|
|
showSubMenuActions(subMenu, title, { |
|
|
|
...(READ_ALOUD_KINDS.includes(event.kind) |
|
|
|
subMenuSearchable: options?.subMenuSearchable |
|
|
|
? [ |
|
|
|
}) |
|
|
|
{ |
|
|
|
: undefined, |
|
|
|
icon: Volume2, |
|
|
|
subMenu: isSmallScreen ? undefined : subMenu |
|
|
|
label: t('Read this note aloud'), |
|
|
|
}) |
|
|
|
onClick: () => { |
|
|
|
} |
|
|
|
closeDrawer() |
|
|
|
|
|
|
|
void speakNoteReadAloud(event).then((result) => { |
|
|
|
const connectionsSubMenu: SubMenuAction[] = [ |
|
|
|
if (result === 'unsupported') { |
|
|
|
|
|
|
|
toast.error(t('Read-aloud is not supported in this browser')) |
|
|
|
|
|
|
|
} else if (result === 'empty') { |
|
|
|
|
|
|
|
toast.error(t('Nothing to read aloud')) |
|
|
|
|
|
|
|
} else if (result === 'error') { |
|
|
|
|
|
|
|
toast.error(t('Read-aloud failed')) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} as MenuAction |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
: []), |
|
|
|
|
|
|
|
...(noteSupportsTranslateMenu |
|
|
|
|
|
|
|
? [ |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
icon: Languages, |
|
|
|
|
|
|
|
label: t('Translate note'), |
|
|
|
|
|
|
|
onClick: isSmallScreen |
|
|
|
|
|
|
|
? () => |
|
|
|
|
|
|
|
showSubMenuActions(translateTargetSubmenu, t('Translate note'), { |
|
|
|
|
|
|
|
subMenuSearchable: true |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
: undefined, |
|
|
|
|
|
|
|
subMenu: isSmallScreen ? undefined : translateTargetSubmenu, |
|
|
|
|
|
|
|
subMenuSearchable: true |
|
|
|
|
|
|
|
} as MenuAction |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
: []), |
|
|
|
|
|
|
|
...(pubkey && event.pubkey !== pubkey && onOpenPublicMessage |
|
|
|
...(pubkey && event.pubkey !== pubkey && onOpenPublicMessage |
|
|
|
? [ |
|
|
|
? [ |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: MessageCircle, |
|
|
|
|
|
|
|
label: t('Send public message'), |
|
|
|
label: t('Send public message'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
onOpenPublicMessage(event.pubkey) |
|
|
|
onOpenPublicMessage(event.pubkey) |
|
|
|
} |
|
|
|
} |
|
|
|
} as MenuAction |
|
|
|
} |
|
|
|
] |
|
|
|
] |
|
|
|
: []), |
|
|
|
: []), |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: Link, |
|
|
|
|
|
|
|
label: t('Share with Imwald'), |
|
|
|
label: t('Share with Imwald'), |
|
|
|
|
|
|
|
separator: pubkey != null && event.pubkey !== pubkey && !!onOpenPublicMessage, |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
const noteId = getNoteBech32Id(event) |
|
|
|
const noteId = getNoteBech32Id(event) |
|
|
|
// Contextual URL when on Spells (e.g. discussions faux-spell); plain /notes/{id} otherwise
|
|
|
|
|
|
|
|
const path = |
|
|
|
const path = |
|
|
|
currentPrimaryPage === 'spells' |
|
|
|
currentPrimaryPage === 'spells' |
|
|
|
? `/spells/notes/${noteId}` |
|
|
|
? `/spells/notes/${noteId}` |
|
|
|
: currentPrimaryPage === 'rss' |
|
|
|
: currentPrimaryPage === 'rss' |
|
|
|
? `/rss/notes/${noteId}` |
|
|
|
? `/rss/notes/${noteId}` |
|
|
|
: `/notes/${noteId}` |
|
|
|
: `/notes/${noteId}` |
|
|
|
const appShareUrl = `https://jumble.imwald.eu${path}` |
|
|
|
navigator.clipboard.writeText(`https://jumble.imwald.eu${path}`) |
|
|
|
navigator.clipboard.writeText(appShareUrl) |
|
|
|
|
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: BookOpen, |
|
|
|
|
|
|
|
label: t('Share with Alexandria'), |
|
|
|
label: t('Share with Alexandria'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
navigator.clipboard.writeText(toAlexandria(getNoteBech32Id(event))) |
|
|
|
navigator.clipboard.writeText(toAlexandria(getNoteBech32Id(event))) |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event.kind === ExtendedKind.PUBLIC_MESSAGE) { |
|
|
|
|
|
|
|
connectionsSubMenu.push({ |
|
|
|
|
|
|
|
label: t('View on Alexandria'), |
|
|
|
|
|
|
|
separator: true, |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
|
|
|
|
closeDrawer() |
|
|
|
|
|
|
|
window.open( |
|
|
|
|
|
|
|
'https://next-alexandria.gitcitadel.eu/profile/notifications', |
|
|
|
|
|
|
|
'_blank', |
|
|
|
|
|
|
|
'noopener,noreferrer' |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isArticleType) { |
|
|
|
|
|
|
|
if (event.kind === kinds.LongFormArticle) { |
|
|
|
|
|
|
|
if (naddr) { |
|
|
|
|
|
|
|
connectionsSubMenu.push({ |
|
|
|
|
|
|
|
label: t('View on Alexandria'), |
|
|
|
|
|
|
|
separator: connectionsSubMenu.length > 0, |
|
|
|
|
|
|
|
onClick: handleViewOnAlexandria |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (dTag && authorNpubForDecentNewsroom) { |
|
|
|
|
|
|
|
connectionsSubMenu.push({ |
|
|
|
|
|
|
|
label: t('View on DecentNewsroom'), |
|
|
|
|
|
|
|
onClick: handleViewOnDecentNewsroom |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if ( |
|
|
|
|
|
|
|
event.kind === ExtendedKind.PUBLICATION_CONTENT || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.PUBLICATION || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.WIKI_ARTICLE || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.NOSTR_SPECIFICATION |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
if (naddr) { |
|
|
|
|
|
|
|
connectionsSubMenu.push({ |
|
|
|
|
|
|
|
label: t('View on Alexandria'), |
|
|
|
|
|
|
|
separator: connectionsSubMenu.length > 0, |
|
|
|
|
|
|
|
onClick: handleViewOnAlexandria |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const callsSubMenu: SubMenuAction[] = [ |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: Video, |
|
|
|
|
|
|
|
label: t('Start call about this'), |
|
|
|
label: t('Start call about this'), |
|
|
|
separator: true, |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
const roomId = `imwald-note-${event.id}` |
|
|
|
const roomId = `imwald-note-${event.id}` |
|
|
|
const url = buildHiveTalkJoinUrl({ room: roomId }) |
|
|
|
window.open(buildHiveTalkJoinUrl({ room: roomId }), '_blank', 'noopener,noreferrer') |
|
|
|
window.open(url, '_blank', 'noopener,noreferrer') |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: Copy, |
|
|
|
|
|
|
|
label: t('Copy call invite link'), |
|
|
|
label: t('Copy call invite link'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
const roomId = `imwald-note-${event.id}` |
|
|
|
const roomId = `imwald-note-${event.id}` |
|
|
|
const url = buildHiveTalkJoinUrl({ room: roomId }) |
|
|
|
navigator.clipboard.writeText(buildHiveTalkJoinUrl({ room: roomId })) |
|
|
|
navigator.clipboard.writeText(url) |
|
|
|
|
|
|
|
toast.success(t('Copied to clipboard')) |
|
|
|
toast.success(t('Copied to clipboard')) |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
...(onOpenCallInvite |
|
|
|
...(onOpenCallInvite |
|
|
|
? [ |
|
|
|
? [ |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: Send, |
|
|
|
|
|
|
|
label: t('Send call invite'), |
|
|
|
label: t('Send call invite'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
@ -1055,161 +1101,207 @@ export function useMenuActions({ |
|
|
|
const url = buildHiveTalkJoinUrl({ room: roomId }) |
|
|
|
const url = buildHiveTalkJoinUrl({ room: roomId }) |
|
|
|
onOpenCallInvite(`${t('Join the video call')}: ${url}`) |
|
|
|
onOpenCallInvite(`${t('Join the video call')}: ${url}`) |
|
|
|
} |
|
|
|
} |
|
|
|
} as MenuAction |
|
|
|
} |
|
|
|
] |
|
|
|
] |
|
|
|
: []) |
|
|
|
: []) |
|
|
|
] |
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
// Add "View on Alexandria" menu item for public messages (PMs)
|
|
|
|
const isProtected = isProtectedEvent(event) |
|
|
|
if (event.kind === ExtendedKind.PUBLIC_MESSAGE) { |
|
|
|
const isDiscussion = event.kind === ExtendedKind.DISCUSSION |
|
|
|
actions.push({ |
|
|
|
const showRepublish = |
|
|
|
icon: Globe, |
|
|
|
broadcastSubMenu.length > 0 && |
|
|
|
label: t('View on Alexandria'), |
|
|
|
(!isProtected || event.pubkey === pubkey) && |
|
|
|
|
|
|
|
!isDiscussion && |
|
|
|
|
|
|
|
!isReplyToDiscussion |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const advancedSubMenu: SubMenuAction[] = [ |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
label: t('Copy event ID'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
|
|
|
|
navigator.clipboard.writeText(getNoteBech32Id(event)) |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
window.open('https://next-alexandria.gitcitadel.eu/profile/notifications', '_blank', 'noopener,noreferrer') |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
separator: true |
|
|
|
{ |
|
|
|
|
|
|
|
label: t('Copy user ID'), |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
|
|
|
|
navigator.clipboard.writeText(pubkeyToNpub(event.pubkey) ?? '') |
|
|
|
|
|
|
|
closeDrawer() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pubkey && event.pubkey !== pubkey) { |
|
|
|
|
|
|
|
advancedSubMenu.push({ |
|
|
|
|
|
|
|
label: t('Report'), |
|
|
|
|
|
|
|
className: 'text-destructive focus:text-destructive', |
|
|
|
|
|
|
|
separator: true, |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
|
|
|
|
closeDrawer() |
|
|
|
|
|
|
|
setIsReportDialogOpen(true) |
|
|
|
|
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (canSignEvents && pubkey && onOpenEditOrClone) { |
|
|
|
if (canSignEvents && pubkey && onOpenEditOrClone) { |
|
|
|
const isOwn = event.pubkey === pubkey |
|
|
|
advancedSubMenu.push({ |
|
|
|
actions.push({ |
|
|
|
label: t('Edit or fork this event'), |
|
|
|
icon: isOwn ? PencilLine : GitFork, |
|
|
|
separator: advancedSubMenu.length > 2, |
|
|
|
label: isOwn ? t('Edit this event') : t('Clone or fork this event'), |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
onOpenEditOrClone(isOwn ? 'edit' : 'clone') |
|
|
|
onOpenEditOrClone(event.pubkey === pubkey ? 'edit' : 'clone') |
|
|
|
}, |
|
|
|
} |
|
|
|
separator: true |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
actions.push({ |
|
|
|
advancedSubMenu.push({ |
|
|
|
icon: Code, |
|
|
|
|
|
|
|
label: t('View raw event'), |
|
|
|
label: t('View raw event'), |
|
|
|
|
|
|
|
separator: true, |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
setIsRawEventDialogOpen(true) |
|
|
|
setIsRawEventDialogOpen(true) |
|
|
|
}, |
|
|
|
} |
|
|
|
separator: !onViewAttestation |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
if (onViewAttestation) { |
|
|
|
if (onViewAttestation) { |
|
|
|
actions.push({ |
|
|
|
advancedSubMenu.push({ |
|
|
|
icon: Sparkles, |
|
|
|
|
|
|
|
label: t('View attestation'), |
|
|
|
label: t('View attestation'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
onViewAttestation() |
|
|
|
onViewAttestation() |
|
|
|
}, |
|
|
|
} |
|
|
|
separator: true |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Add export options for article-type events
|
|
|
|
if (showRepublish) { |
|
|
|
|
|
|
|
advancedSubMenu.push({ |
|
|
|
|
|
|
|
label: t('Republish to ...'), |
|
|
|
|
|
|
|
separator: true, |
|
|
|
|
|
|
|
onClick: isSmallScreen |
|
|
|
|
|
|
|
? () => showSubMenuActions(broadcastSubMenu, t('Republish to ...')) |
|
|
|
|
|
|
|
: () => {}, |
|
|
|
|
|
|
|
subMenu: isSmallScreen ? undefined : broadcastSubMenu |
|
|
|
|
|
|
|
} as SubMenuAction) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (isArticleType) { |
|
|
|
if (isArticleType) { |
|
|
|
const isMarkdownFormat = event.kind === kinds.LongFormArticle || event.kind === ExtendedKind.NOSTR_SPECIFICATION |
|
|
|
const isMarkdownFormat = |
|
|
|
const isAsciidocFormat = event.kind === ExtendedKind.WIKI_ARTICLE || event.kind === ExtendedKind.PUBLICATION || event.kind === ExtendedKind.PUBLICATION_CONTENT |
|
|
|
event.kind === kinds.LongFormArticle || event.kind === ExtendedKind.NOSTR_SPECIFICATION |
|
|
|
|
|
|
|
const isAsciidocFormat = |
|
|
|
|
|
|
|
event.kind === ExtendedKind.WIKI_ARTICLE || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.PUBLICATION || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.PUBLICATION_CONTENT |
|
|
|
|
|
|
|
|
|
|
|
if (isMarkdownFormat) { |
|
|
|
if (isMarkdownFormat) { |
|
|
|
actions.push({ |
|
|
|
advancedSubMenu.push({ |
|
|
|
icon: FileDown, |
|
|
|
|
|
|
|
label: t('Export as Markdown'), |
|
|
|
label: t('Export as Markdown'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
exportAsMarkdown() |
|
|
|
exportAsMarkdown() |
|
|
|
}, |
|
|
|
} |
|
|
|
separator: true |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (isAsciidocFormat) { |
|
|
|
if (isAsciidocFormat) { |
|
|
|
actions.push({ |
|
|
|
advancedSubMenu.push({ |
|
|
|
icon: FileDown, |
|
|
|
|
|
|
|
label: t('Export as AsciiDoc'), |
|
|
|
label: t('Export as AsciiDoc'), |
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
exportAsAsciidoc() |
|
|
|
exportAsAsciidoc() |
|
|
|
}, |
|
|
|
} |
|
|
|
separator: true |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (event.kind === ExtendedKind.PUBLICATION && publicationBroadcastSubMenu.length > 0) { |
|
|
|
// Add view options based on event kind
|
|
|
|
advancedSubMenu.push({ |
|
|
|
if (event.kind === kinds.LongFormArticle) { |
|
|
|
|
|
|
|
// For LongFormArticle (30023): Alexandria and DecentNewsroom
|
|
|
|
|
|
|
|
if (naddr) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
|
|
|
|
icon: BookOpen, |
|
|
|
|
|
|
|
label: t('View on Alexandria'), |
|
|
|
|
|
|
|
onClick: handleViewOnAlexandria |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (dTag && authorNpubForDecentNewsroom) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
|
|
|
|
icon: Globe, |
|
|
|
|
|
|
|
label: t('View on DecentNewsroom'), |
|
|
|
|
|
|
|
onClick: handleViewOnDecentNewsroom |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if ( |
|
|
|
|
|
|
|
event.kind === ExtendedKind.PUBLICATION_CONTENT || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.PUBLICATION || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.WIKI_ARTICLE || |
|
|
|
|
|
|
|
event.kind === ExtendedKind.NOSTR_SPECIFICATION |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
// For 30041, 30040, 30818, 30817: Alexandria
|
|
|
|
|
|
|
|
if (naddr) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
|
|
|
|
icon: BookOpen, |
|
|
|
|
|
|
|
label: t('View on Alexandria'), |
|
|
|
|
|
|
|
onClick: handleViewOnAlexandria |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event.kind === ExtendedKind.PUBLICATION) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
|
|
|
|
icon: SatelliteDish, |
|
|
|
|
|
|
|
label: t('Rebroadcast entire publication'), |
|
|
|
label: t('Rebroadcast entire publication'), |
|
|
|
|
|
|
|
separator: true, |
|
|
|
onClick: isSmallScreen |
|
|
|
onClick: isSmallScreen |
|
|
|
? () => showSubMenuActions(publicationBroadcastSubMenu, t('Rebroadcast entire publication to ...')) |
|
|
|
? () => |
|
|
|
: undefined, |
|
|
|
showSubMenuActions( |
|
|
|
subMenu: isSmallScreen ? undefined : publicationBroadcastSubMenu, |
|
|
|
publicationBroadcastSubMenu, |
|
|
|
separator: true |
|
|
|
t('Rebroadcast entire publication to ...') |
|
|
|
}) |
|
|
|
) |
|
|
|
|
|
|
|
: () => {}, |
|
|
|
|
|
|
|
subMenu: isSmallScreen ? undefined : publicationBroadcastSubMenu |
|
|
|
|
|
|
|
} as SubMenuAction) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const isProtected = isProtectedEvent(event) |
|
|
|
if (seenOnRelays.length > 0) { |
|
|
|
const isDiscussion = event.kind === ExtendedKind.DISCUSSION |
|
|
|
advancedSubMenu.push({ |
|
|
|
if ((!isProtected || event.pubkey === pubkey) && !isDiscussion && !isReplyToDiscussion) { |
|
|
|
label: ( |
|
|
|
actions.push({ |
|
|
|
<div |
|
|
|
icon: SatelliteDish, |
|
|
|
className="flex flex-wrap gap-2 py-0.5" |
|
|
|
label: t('Republish to ...'), |
|
|
|
role="group" |
|
|
|
onClick: isSmallScreen |
|
|
|
aria-label={t('Seen on')} |
|
|
|
? () => showSubMenuActions(broadcastSubMenu, t('Republish to ...')) |
|
|
|
onClick={(e) => e.stopPropagation()} |
|
|
|
: undefined, |
|
|
|
onPointerDown={(e) => e.stopPropagation()} |
|
|
|
subMenu: isSmallScreen ? undefined : broadcastSubMenu, |
|
|
|
> |
|
|
|
|
|
|
|
{seenOnRelays.map((relay) => ( |
|
|
|
|
|
|
|
<button |
|
|
|
|
|
|
|
key={relay} |
|
|
|
|
|
|
|
type="button" |
|
|
|
|
|
|
|
title={simplifyUrl(relay)} |
|
|
|
|
|
|
|
className="rounded-md p-1 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" |
|
|
|
|
|
|
|
onClick={(e) => { |
|
|
|
|
|
|
|
e.stopPropagation() |
|
|
|
|
|
|
|
closeDrawer() |
|
|
|
|
|
|
|
push(toRelay(relay)) |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<RelayIcon url={relay} className="size-8 shrink-0" /> |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
))} |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
onClick: () => {}, |
|
|
|
separator: true |
|
|
|
separator: true |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (pubkey && event.pubkey !== pubkey) { |
|
|
|
const actions: MenuAction[] = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (READ_ALOUD_KINDS.includes(event.kind)) { |
|
|
|
actions.push({ |
|
|
|
actions.push({ |
|
|
|
icon: TriangleAlert, |
|
|
|
icon: Volume2, |
|
|
|
label: t('Report'), |
|
|
|
label: t('Read this note aloud'), |
|
|
|
className: 'text-destructive focus:text-destructive', |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
onClick: () => { |
|
|
|
closeDrawer() |
|
|
|
closeDrawer() |
|
|
|
setIsReportDialogOpen(true) |
|
|
|
void speakNoteReadAloud(event).then((result) => { |
|
|
|
}, |
|
|
|
if (result === 'unsupported') { |
|
|
|
separator: true |
|
|
|
toast.error(t('Read-aloud is not supported in this browser')) |
|
|
|
|
|
|
|
} else if (result === 'empty') { |
|
|
|
|
|
|
|
toast.error(t('Nothing to read aloud')) |
|
|
|
|
|
|
|
} else if (result === 'error') { |
|
|
|
|
|
|
|
toast.error(t('Read-aloud failed')) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (noteSupportsTranslateMenu) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
|
|
|
|
icon: Languages, |
|
|
|
|
|
|
|
label: t('Translate note'), |
|
|
|
|
|
|
|
onClick: isSmallScreen |
|
|
|
|
|
|
|
? () => |
|
|
|
|
|
|
|
showSubMenuActions(translateTargetSubmenu, t('Translate note'), { |
|
|
|
|
|
|
|
subMenuSearchable: true |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
: undefined, |
|
|
|
|
|
|
|
subMenu: isSmallScreen ? undefined : translateTargetSubmenu, |
|
|
|
|
|
|
|
subMenuSearchable: true |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pushSubMenuParent(actions, Share2, t('Connections'), connectionsSubMenu, { |
|
|
|
|
|
|
|
separator: actions.length > 0 |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
pushSubMenuParent(actions, Video, t('Calls'), callsSubMenu) |
|
|
|
|
|
|
|
pushSubMenuParent(actions, Settings, t('Advanced'), advancedSubMenu, { |
|
|
|
|
|
|
|
separator: actions.length > 0 |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
if (pubkey && event.pubkey !== pubkey) { |
|
|
|
if (pubkey && event.pubkey !== pubkey) { |
|
|
|
if (isMuted) { |
|
|
|
if (isMuted) { |
|
|
|
actions.push({ |
|
|
|
actions.push({ |
|
|
|
@ -1232,7 +1324,7 @@ export function useMenuActions({ |
|
|
|
mutePubkeyPrivately(event.pubkey) |
|
|
|
mutePubkeyPrivately(event.pubkey) |
|
|
|
}, |
|
|
|
}, |
|
|
|
className: 'text-destructive focus:text-destructive', |
|
|
|
className: 'text-destructive focus:text-destructive', |
|
|
|
separator: true |
|
|
|
separator: actions.length > 0 |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
icon: BellOff, |
|
|
|
icon: BellOff, |
|
|
|
@ -1247,8 +1339,7 @@ export function useMenuActions({ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Pin / unpin only against the signed-in user's list (not another profile's pinned section).
|
|
|
|
if (pubkey && event.pubkey === pubkey) { |
|
|
|
if (pubkey) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
actions.push({ |
|
|
|
icon: Pin, |
|
|
|
icon: Pin, |
|
|
|
label: isPinnedInMyList ? t('Unpin note') : t('Pin note'), |
|
|
|
label: isPinnedInMyList ? t('Unpin note') : t('Pin note'), |
|
|
|
@ -1257,6 +1348,34 @@ export function useMenuActions({ |
|
|
|
}, |
|
|
|
}, |
|
|
|
separator: true |
|
|
|
separator: true |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
} else if (pubkey && event.pubkey !== pubkey && bookmarksContext) { |
|
|
|
|
|
|
|
actions.push({ |
|
|
|
|
|
|
|
icon: Bookmark, |
|
|
|
|
|
|
|
label: isBookmarked ? t('Remove bookmark') : t('Bookmark'), |
|
|
|
|
|
|
|
onClick: () => { |
|
|
|
|
|
|
|
closeDrawer() |
|
|
|
|
|
|
|
void checkLogin(async () => { |
|
|
|
|
|
|
|
if (bookmarkUpdating) return |
|
|
|
|
|
|
|
setBookmarkUpdating(true) |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
if (isBookmarked) { |
|
|
|
|
|
|
|
await removeBookmark(event) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
await addBookmark(event) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
toast.error( |
|
|
|
|
|
|
|
(isBookmarked ? t('Remove bookmark failed') : t('Bookmark failed')) + |
|
|
|
|
|
|
|
': ' + |
|
|
|
|
|
|
|
(error as Error).message |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} finally { |
|
|
|
|
|
|
|
setBookmarkUpdating(false) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
separator: true |
|
|
|
|
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Delete only when signed in as the author with a signing key (not read-only npub)
|
|
|
|
// Delete only when signed in as the author with a signing key (not read-only npub)
|
|
|
|
@ -1290,6 +1409,13 @@ export function useMenuActions({ |
|
|
|
attemptDelete, |
|
|
|
attemptDelete, |
|
|
|
isPinnedInMyList, |
|
|
|
isPinnedInMyList, |
|
|
|
handlePinNote, |
|
|
|
handlePinNote, |
|
|
|
|
|
|
|
bookmarkListEvent, |
|
|
|
|
|
|
|
bookmarksContext, |
|
|
|
|
|
|
|
isBookmarked, |
|
|
|
|
|
|
|
bookmarkUpdating, |
|
|
|
|
|
|
|
addBookmark, |
|
|
|
|
|
|
|
removeBookmark, |
|
|
|
|
|
|
|
checkLogin, |
|
|
|
isArticleType, |
|
|
|
isArticleType, |
|
|
|
articleMetadata, |
|
|
|
articleMetadata, |
|
|
|
dTag, |
|
|
|
dTag, |
|
|
|
@ -1302,7 +1428,11 @@ export function useMenuActions({ |
|
|
|
profile, |
|
|
|
profile, |
|
|
|
noteTranslationFromMenu, |
|
|
|
noteTranslationFromMenu, |
|
|
|
translateMenuOptions, |
|
|
|
translateMenuOptions, |
|
|
|
onViewAttestation |
|
|
|
onViewAttestation, |
|
|
|
|
|
|
|
seenOnRelays, |
|
|
|
|
|
|
|
push, |
|
|
|
|
|
|
|
currentPrimaryPage, |
|
|
|
|
|
|
|
isReplyToDiscussion |
|
|
|
]) |
|
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
return menuActions |
|
|
|
return menuActions |
|
|
|
|