Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
15a33300ee
  1. 4
      src/components/BottomNavigationBar/WriteButton.tsx
  2. 23
      src/components/HelpAndAccountMenu.tsx
  3. 39
      src/components/PostEditor/PostContent.tsx
  4. 26
      src/components/PostEditor/index.tsx
  5. 9
      src/components/RefreshButton/index.tsx
  6. 8
      src/components/Sidebar/PostButton.tsx
  7. 19
      src/components/StoredAccountSwitchSelect.tsx
  8. 8
      src/i18n/locales/de.ts
  9. 8
      src/i18n/locales/en.ts
  10. 52
      src/providers/NostrProvider/index.tsx
  11. 14
      src/services/post-editor.service.ts

4
src/components/BottomNavigationBar/WriteButton.tsx

@ -18,10 +18,9 @@ export default function WriteButton() {
return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest) return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest)
}, [canSignEvents, checkLogin]) }, [canSignEvents, checkLogin])
if (!canSignEvents) return null
return ( return (
<> <>
{canSignEvents ? (
<BottomNavigationBarItem <BottomNavigationBarItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@ -32,6 +31,7 @@ export default function WriteButton() {
> >
<PencilLine /> <PencilLine />
</BottomNavigationBarItem> </BottomNavigationBarItem>
) : null}
<PostEditor open={open} setOpen={setOpen} /> <PostEditor open={open} setOpen={setOpen} />
</> </>
) )

23
src/components/HelpAndAccountMenu.tsx

@ -29,8 +29,6 @@ import { useFetchProfile } from '@/hooks/useFetchProfile'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { AccountQuickSwitchMenuItems } from '@/components/AccountQuickSwitchMenuItems' import { AccountQuickSwitchMenuItems } from '@/components/AccountQuickSwitchMenuItems'
import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator' import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator'
import { ActiveRelaysDropdownSection } from '@/components/ConnectedRelays/ActiveRelaysDropdownSection'
import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows'
import { ArrowDownUp, Database, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react' import { ArrowDownUp, Database, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react'
import { useCallback, useMemo, useState, type ReactNode } from 'react' import { useCallback, useMemo, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -84,7 +82,6 @@ function AccountDropdownItems({
<Database className="size-4" /> <Database className="size-4" />
{t('Browse Cache')} {t('Browse Cache')}
</DropdownMenuItem> </DropdownMenuItem>
<ActiveRelaysDropdownSection />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={onSwitchAccount}> <DropdownMenuItem onClick={onSwitchAccount}>
<ArrowDownUp className="size-4" /> <ArrowDownUp className="size-4" />
@ -239,32 +236,12 @@ function TitlebarAccountMenu({
function LoggedOutTitlebarMenu({ onLogin }: { onLogin: () => void }) { function LoggedOutTitlebarMenu({ onLogin }: { onLogin: () => void }) {
const { t } = useTranslation() const { t } = useTranslation()
const { rows } = useRelayConnectionRows()
if (rows.length === 0) {
return ( return (
<Button variant="ghost" size="titlebar-icon" onClick={onLogin} title={t('Login')}> <Button variant="ghost" size="titlebar-icon" onClick={onLogin} title={t('Login')}>
<UserRound /> <UserRound />
</Button> </Button>
) )
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="titlebar-icon" title={t('Login')} aria-label={t('Login')}>
<UserRound />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className={titlebarAccountMenuContentClassName}>
<DropdownMenuItem onClick={onLogin}>
<LogIn className="size-4" />
{t('Login')}
</DropdownMenuItem>
<ActiveRelaysDropdownSection />
</DropdownMenuContent>
</DropdownMenu>
)
} }
/** Sidebar: account / login stack. Titlebar (mobile): compact account or login control. */ /** Sidebar: account / login stack. Titlebar (mobile): compact account or login control. */

39
src/components/PostEditor/PostContent.tsx

@ -46,7 +46,7 @@ import {
} from '@/constants' } from '@/constants'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReplyIngress } from '@/hooks/useReplyIngress'
import { canonicalizeRssArticleUrl, getArticleUrlFromCommentITags } from '@/lib/rss-article' import { canonicalizeRssArticleUrl, getArticleUrlFromCommentITags } from '@/lib/rss-article'
import { cleanUrl, isBlossomBudBlobUrl, rewritePlainTextHttpUrls } from '@/lib/url' import { cleanUrl, isBlossomBudBlobUrl, rewritePlainTextHttpUrls } from '@/lib/url'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -199,7 +199,7 @@ export default function PostContent({
}) { }) {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { pubkey, publish, checkLogin, canSignEvents } = useNostr() const { pubkey, publish, checkLogin, canSignEvents } = useNostr()
const { addReplies } = useReply() const { addReplies } = useReplyIngress()
const mergePublishedReplyIntoThread = useCallback( const mergePublishedReplyIntoThread = useCallback(
(reply: Event, relayStatuses?: TRelayPublishStatus[]) => { (reply: Event, relayStatuses?: TRelayPublishStatus[]) => {
@ -742,6 +742,7 @@ export default function PostContent({
}, [getDeterminedKind, defaultContent, parentEvent, isNsfw, isPoll, pollCreateData, addClientTag]) }, [getDeterminedKind, defaultContent, parentEvent, isNsfw, isPoll, pollCreateData, addClientTag])
const prevComposerShellOpenRef = useRef(open) const prevComposerShellOpenRef = useRef(open)
const prevComposerPubkeyRef = useRef(pubkey)
useEffect(() => { useEffect(() => {
const wasOpen = prevComposerShellOpenRef.current const wasOpen = prevComposerShellOpenRef.current
prevComposerShellOpenRef.current = open prevComposerShellOpenRef.current = open
@ -750,6 +751,18 @@ export default function PostContent({
} }
}, [open, getDeterminedKind, defaultContent, parentEvent]) }, [open, getDeterminedKind, defaultContent, parentEvent])
useEffect(() => {
if (!open) {
prevComposerPubkeyRef.current = pubkey
return
}
const prevPk = prevComposerPubkeyRef.current
prevComposerPubkeyRef.current = pubkey
if (prevPk && pubkey && prevPk !== pubkey && !advancedLabOpenRef.current) {
textareaRef.current?.syncFromPostCache()
}
}, [open, pubkey])
const rssReplyExtraPreviewTags = useMemo((): string[][] | undefined => { const rssReplyExtraPreviewTags = useMemo((): string[][] | undefined => {
if (!parentEvent || parentEvent.kind !== ExtendedKind.RSS_THREAD_ROOT) return undefined if (!parentEvent || parentEvent.kind !== ExtendedKind.RSS_THREAD_ROOT) return undefined
const raw = const raw =
@ -1245,6 +1258,10 @@ export default function PostContent({
const post = async (e?: React.MouseEvent) => { const post = async (e?: React.MouseEvent) => {
e?.stopPropagation() e?.stopPropagation()
checkLogin(async () => { checkLogin(async () => {
if (!canSignEvents) {
toast.error(t('readOnlySession.cannotPublish'))
return
}
if (!canPost) { if (!canPost) {
logger.warn('Attempted to post while canPost is false') logger.warn('Attempted to post while canPost is false')
return return
@ -1387,6 +1404,16 @@ export default function PostContent({
close() close()
} catch (error) { } catch (error) {
if (error instanceof LoginRequiredError) { if (error instanceof LoginRequiredError) {
toast.error(t('readOnlySession.cannotPublish'))
return
}
const message = error instanceof Error ? error.message : String(error)
if (
message === t('Cancelled') ||
message.includes('Signer pubkey does not match') ||
message.includes(t('nip07.publishExtensionMismatch'))
) {
toast.error(t('nip07.publishExtensionMismatch'))
return return
} }
// AggregateError = "Failed to publish to any relay" is already logged in NostrProvider with relayStatuses; avoid duplicate noise // AggregateError = "Failed to publish to any relay" is already logged in NostrProvider with relayStatuses; avoid duplicate noise
@ -3524,7 +3551,13 @@ export default function PostContent({
</Button> </Button>
</div> </div>
{open ? ( {open ? (
<StoredAccountSwitchSelect withTopBorder alignEnd className="w-full" showLabelAlways /> <StoredAccountSwitchSelect
withTopBorder
alignEnd
className="w-full"
showLabelAlways
inComposer
/>
) : null} ) : null}
{/* Media Kind Selection Dialog */} {/* Media Kind Selection Dialog */}

26
src/components/PostEditor/index.tsx

@ -17,7 +17,9 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { pubkeyToNpub } from '@/lib/pubkey' 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 postEditorService from '@/services/post-editor.service'
import { Dispatch, useEffect, useMemo } from 'react'
import { useNostr } from '@/providers/NostrProvider'
import type { TDiscussionDynamicTopics } from '@/lib/discussion-thread-composer' import type { TDiscussionDynamicTopics } from '@/lib/discussion-thread-composer'
import PostContent from './PostContent' import PostContent from './PostContent'
@ -46,6 +48,16 @@ export default function PostEditor({
discussionDynamicTopics?: TDiscussionDynamicTopics | null discussionDynamicTopics?: TDiscussionDynamicTopics | null
}) { }) {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { isAccountSessionHydrating, isNip07LoginInFlight } = useNostr()
useEffect(() => {
if (!open) return
postEditorService.setComposerShellOpen(true)
return () => postEditorService.setComposerShellOpen(false)
}, [open])
const blockDismissForAccountSwitch =
isAccountSessionHydrating || isNip07LoginInFlight
const effectiveDefaultContent = useMemo(() => { const effectiveDefaultContent = useMemo(() => {
if (initialPublicMessageTo) { if (initialPublicMessageTo) {
@ -89,6 +101,12 @@ export default function PostEditor({
className="h-full w-full max-w-full p-0 border-none overflow-hidden" className="h-full w-full max-w-full p-0 border-none overflow-hidden"
side="bottom" side="bottom"
hideClose hideClose
onInteractOutside={(e) => {
if (blockDismissForAccountSwitch) e.preventDefault()
}}
onPointerDownOutside={(e) => {
if (blockDismissForAccountSwitch) e.preventDefault()
}}
onEscapeKeyDown={(e) => { onEscapeKeyDown={(e) => {
if (postEditor.isSuggestionPopupOpen) { if (postEditor.isSuggestionPopupOpen) {
e.preventDefault() e.preventDefault()
@ -115,6 +133,12 @@ export default function PostEditor({
<DialogContent <DialogContent
className="p-0 max-w-2xl w-[calc(100vw-2rem)] sm:w-full overflow-hidden" className="p-0 max-w-2xl w-[calc(100vw-2rem)] sm:w-full overflow-hidden"
withoutClose withoutClose
onInteractOutside={(e) => {
if (blockDismissForAccountSwitch) e.preventDefault()
}}
onPointerDownOutside={(e) => {
if (blockDismissForAccountSwitch) e.preventDefault()
}}
onEscapeKeyDown={(e) => { onEscapeKeyDown={(e) => {
if (postEditor.isSuggestionPopupOpen) { if (postEditor.isSuggestionPopupOpen) {
e.preventDefault() e.preventDefault()

9
src/components/RefreshButton/index.tsx

@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useLongPressAction } from '@/hooks/use-long-press-action' import { useLongPressAction } from '@/hooks/use-long-press-action'
import { hardReloadPreservingFeedSnapshots } from '@/services/session-feed-snapshot.service' import { hardReloadPreservingFeedSnapshots } from '@/services/session-feed-snapshot.service'
import { RefreshCcw } from 'lucide-react' import { RefreshCw } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -46,12 +46,13 @@ export function RefreshButton({
onClick() onClick()
setTimeout(() => setRefreshing(false), 500) setTimeout(() => setRefreshing(false), 500)
}} }}
className="h-8 shrink-0 px-1.5 text-muted-foreground focus:text-foreground [&_svg]:size-3" className="shrink-0 text-muted-foreground focus:text-foreground"
aria-label={t('Refresh')}
> >
{refreshing ? ( {refreshing ? (
<Skeleton className="size-3 shrink-0 rounded-sm" aria-hidden /> <Skeleton className="size-5 shrink-0 rounded-sm" aria-hidden />
) : ( ) : (
<RefreshCcw /> <RefreshCw className="size-5" aria-hidden />
)} )}
</Button> </Button>
) )

8
src/components/Sidebar/PostButton.tsx

@ -18,9 +18,9 @@ export default function PostButton() {
return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest) return () => postEditorService.removeEventListener('requestOpenNewPost', onRequest)
}, [canSignEvents, checkLogin]) }, [canSignEvents, checkLogin])
if (!canSignEvents) return null
return ( return (
<>
{canSignEvents ? (
<div className="pt-4"> <div className="pt-4">
<SidebarItem <SidebarItem
title="New post" title="New post"
@ -36,7 +36,9 @@ export default function PostButton() {
> >
<PencilLine strokeWidth={3} /> <PencilLine strokeWidth={3} />
</SidebarItem> </SidebarItem>
<PostEditor open={open} setOpen={setOpen} />
</div> </div>
) : null}
<PostEditor open={open} setOpen={setOpen} />
</>
) )
} }

19
src/components/StoredAccountSwitchSelect.tsx

@ -5,6 +5,7 @@ import { accountPubkeyToHex, formatPubkey, hexPubkeysEqual, normalizeHexPubkey }
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Nip07Signer } from '@/providers/NostrProvider/nip-07.signer' import { Nip07Signer } from '@/providers/NostrProvider/nip-07.signer'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import type { TAccountPointer } from '@/types' import type { TAccountPointer } from '@/types'
import { Loader2, X } from 'lucide-react' import { Loader2, X } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
@ -22,6 +23,8 @@ type Props = {
withTopBorder?: boolean withTopBorder?: boolean
/** Align chips to the end (e.g. beside the publish button). */ /** Align chips to the end (e.g. beside the publish button). */
alignEnd?: boolean alignEnd?: boolean
/** Post composer: keep clicks from bubbling to the dialog/sheet dismiss layer. */
inComposer?: boolean
} }
const EXTENSION_SYNC_HINT_DISMISSED_PREFIX = 'extensionSyncHintDismissed:' const EXTENSION_SYNC_HINT_DISMISSED_PREFIX = 'extensionSyncHintDismissed:'
@ -44,7 +47,8 @@ export default function StoredAccountSwitchSelect({
showLabelAlways = false, showLabelAlways = false,
withBottomBorder = false, withBottomBorder = false,
withTopBorder = false, withTopBorder = false,
alignEnd = false alignEnd = false,
inComposer = false
}: Props) { }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
@ -158,11 +162,17 @@ export default function StoredAccountSwitchSelect({
toast.error(t('notificationsSwitchAccountFailed')) toast.error(t('notificationsSwitchAccountFailed'))
return return
} }
if (inComposer && nextAccount.signerType === 'nip-07') {
const current = storage.getCurrentAccount()
if (current?.signerType === 'npub' && hexPubkeysEqual(current.pubkey, switched)) {
toast.error(t('accountSwitch.composerExtensionMismatch'))
}
}
} finally { } finally {
setSwitchingKey(null) setSwitchingKey(null)
} }
}, },
[account, switchAccount, retryNip07SignerForPreferredAccount, t] [account, switchAccount, retryNip07SignerForPreferredAccount, t, inComposer]
) )
const handleRetryExtension = useCallback(async () => { const handleRetryExtension = useCallback(async () => {
@ -257,7 +267,10 @@ export default function StoredAccountSwitchSelect({
: 'ring-transparent hover:ring-muted-foreground/35', : 'ring-transparent hover:ring-muted-foreground/35',
busy && !isSwitching && 'opacity-50' busy && !isSwitching && 'opacity-50'
)} )}
onClick={() => void handlePick(act)} onClick={(e) => {
if (inComposer) e.stopPropagation()
void handlePick(act)
}}
> >
<SimpleUserAvatar userId={pk} size="small" deferRemoteAvatar={false} /> <SimpleUserAvatar userId={pk} size="small" deferRemoteAvatar={false} />
{isSwitching ? ( {isSwitching ? (

8
src/i18n/locales/de.ts

@ -466,7 +466,9 @@ export default {
readOnlySession: { readOnlySession: {
label: 'Read-only', label: 'Read-only',
labelShort: 'R/O', labelShort: 'R/O',
hint: 'Browsing without a signing key. Sign in with an extension, nsec, or another method to post, react, and edit.' hint: 'Nur-Lesen ohne Signierschlüssel. Mit passender Erweiterung, nsec o.Ä. anmelden, um zu posten, reagieren und bearbeiten.',
cannotPublish:
'Dieses Konto ist nur lesbar. Verbinde die passende Erweiterung oder wähle ein Konto, das signieren kann.'
}, },
'reload notes': 'Notizen neu laden', 'reload notes': 'Notizen neu laden',
'Logged in Accounts': 'Angemeldete Konten', 'Logged in Accounts': 'Angemeldete Konten',
@ -1000,6 +1002,8 @@ export default {
'Nur-Lesen-Ansicht. „Erneut“ verbindet, wenn die Erweiterung zu diesem Schlüssel passt — ein anderer Erweiterungsschlüssel ist in Ordnung, wenn du nur stöbern willst.', 'Nur-Lesen-Ansicht. „Erneut“ verbindet, wenn die Erweiterung zu diesem Schlüssel passt — ein anderer Erweiterungsschlüssel ist in Ordnung, wenn du nur stöbern willst.',
'accountSwitch.extensionRetry': 'Erweiterung erneut', 'accountSwitch.extensionRetry': 'Erweiterung erneut',
'accountSwitch.extensionConnected': 'Erweiterung für dieses Konto verbunden.', 'accountSwitch.extensionConnected': 'Erweiterung für dieses Konto verbunden.',
'accountSwitch.composerExtensionMismatch':
'Anmeldung für dieses Konto fehlgeschlagen — die Erweiterung nutzt einen anderen Schlüssel. Schlüssel in der Erweiterung wechseln oder „Erweiterung erneut“ unten tippen.',
'accountSwitch.extensionRetryFailed': 'accountSwitch.extensionRetryFailed':
'Erweiterungsschlüssel passt noch nicht. Schlüssel in der Erweiterung wechseln und erneut versuchen.', 'Erweiterungsschlüssel passt noch nicht. Schlüssel in der Erweiterung wechseln und erneut versuchen.',
'accountSwitch.extensionUnavailable': 'accountSwitch.extensionUnavailable':
@ -2086,6 +2090,8 @@ export default {
'nip07.useExtensionIdentity': 'Erweiterungs-Identität verwenden', 'nip07.useExtensionIdentity': 'Erweiterungs-Identität verwenden',
'nip07.switchedToExtensionIdentity': 'Auf die aktuelle Identität Ihrer Erweiterung umgestellt.', 'nip07.switchedToExtensionIdentity': 'Auf die aktuelle Identität Ihrer Erweiterung umgestellt.',
'nip07.adoptExtensionFailed': 'Wechsel zur Erweiterungs-Identität fehlgeschlagen', 'nip07.adoptExtensionFailed': 'Wechsel zur Erweiterungs-Identität fehlgeschlagen',
'nip07.publishExtensionMismatch':
'Die Erweiterung hat mit einem anderen Schlüssel signiert als das gewählte Konto. Schlüssel in der Erweiterung wechseln oder „Erweiterung erneut“ im Editor nutzen.',
'Login to configure RSS feeds': 'Login to configure RSS feeds', 'Login to configure RSS feeds': 'Login to configure RSS feeds',
'Long-form Article': 'Long-form Article', 'Long-form Article': 'Long-form Article',
'Mailbox relays saved': 'Mailbox relays saved', 'Mailbox relays saved': 'Mailbox relays saved',

8
src/i18n/locales/en.ts

@ -467,7 +467,9 @@ export default {
readOnlySession: { readOnlySession: {
label: 'Read-only', label: 'Read-only',
labelShort: 'R/O', labelShort: 'R/O',
hint: 'Browsing without a signing key. Sign in with an extension, nsec, or another method to post, react, and edit.' hint: 'Browsing without a signing key. Sign in with an extension, nsec, or another method to post, react, and edit.',
cannotPublish:
'This account is read-only. Connect the matching browser extension key or pick an account that can sign.'
}, },
'reload notes': 'reload notes', 'reload notes': 'reload notes',
'Logged in Accounts': 'Logged in Accounts', 'Logged in Accounts': 'Logged in Accounts',
@ -1019,6 +1021,8 @@ export default {
'accountSwitch.extensionConnected': 'Extension connected for this account.', 'accountSwitch.extensionConnected': 'Extension connected for this account.',
'accountSwitch.extensionRetryFailed': 'accountSwitch.extensionRetryFailed':
'Extension key still does not match. Switch the key in your extension, then try again.', 'Extension key still does not match. Switch the key in your extension, then try again.',
'accountSwitch.composerExtensionMismatch':
'Could not sign in as this account — your extension is using a different key. Switch the key in the extension or tap “Retry extension” below.',
'accountSwitch.extensionUnavailable': 'accountSwitch.extensionUnavailable':
'Could not reach the browser extension. Unlock nos2x/Alby, allow this site, then click the account again.', 'Could not reach the browser extension. Unlock nos2x/Alby, allow this site, then click the account again.',
'Show untrusted {type}': 'Show untrusted {{type}}', 'Show untrusted {type}': 'Show untrusted {{type}}',
@ -2085,6 +2089,8 @@ export default {
'nip07.useExtensionIdentity': 'Use extension identity', 'nip07.useExtensionIdentity': 'Use extension identity',
'nip07.switchedToExtensionIdentity': "Switched to your extension's current identity.", 'nip07.switchedToExtensionIdentity': "Switched to your extension's current identity.",
'nip07.adoptExtensionFailed': 'Could not switch to extension identity', 'nip07.adoptExtensionFailed': 'Could not switch to extension identity',
'nip07.publishExtensionMismatch':
'Your extension signed with a different key than the selected account. Switch the key in the extension or use “Retry extension” in the composer.',
'Login to configure RSS feeds': 'Login to configure RSS feeds', 'Login to configure RSS feeds': 'Login to configure RSS feeds',
'Long-form Article': 'Long-form Article', 'Long-form Article': 'Long-form Article',
'Mailbox relays saved': 'Mailbox relays saved', 'Mailbox relays saved': 'Mailbox relays saved',

52
src/providers/NostrProvider/index.tsx

@ -63,6 +63,7 @@ import { queryService, replaceableEventService } from '@/services/client.service
import customEmojiService from '@/services/custom-emoji.service' import customEmojiService from '@/services/custom-emoji.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import postEditorService from '@/services/post-editor.service'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import { import {
ISigner, ISigner,
@ -938,6 +939,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const prev = prevAccountPubkeyRef.current const prev = prevAccountPubkeyRef.current
const curr = account?.pubkey ?? null const curr = account?.pubkey ?? null
prevAccountPubkeyRef.current = curr prevAccountPubkeyRef.current = curr
if (postEditorService.isComposerShellOpen) {
return
}
if (prev != null && curr != null && prev !== curr) { if (prev != null && curr != null && prev !== curr) {
postEditorCache.clearOnAccountChange() postEditorCache.clearOnAccountChange()
} else if (prev != null && curr === null) { } else if (prev != null && curr === null) {
@ -1893,6 +1897,30 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return event as VerifiedEvent return event as VerifiedEvent
} }
const publishExtensionMismatchError = () =>
new Error(
t('nip07.publishExtensionMismatch', {
defaultValue:
'Your extension signed with a different key than the selected account. Switch the key in the extension or use “Retry extension” in the composer.'
})
)
const assertSignerMatchesAccountForPublish = async () => {
if (!account || !signer || account.signerType === 'npub') return
const accountPk = accountPubkeyToHex(account.pubkey)
if (!accountPk) throw new LoginRequiredError()
if (account.signerType !== 'nip-07') return
let signerPk: string | null = null
try {
signerPk = pubkeyFromNip07Extension(await signer.getPublicKey())
} catch {
return
}
if (signerPk && !hexPubkeysEqual(signerPk, accountPk)) {
throw publishExtensionMismatchError()
}
}
const publish = async ( const publish = async (
draftEvent: TDraftEvent, draftEvent: TDraftEvent,
{ minPow = 0, ...options }: TPublishOptions = {} { minPow = 0, ...options }: TPublishOptions = {}
@ -1901,6 +1929,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
throw new LoginRequiredError() throw new LoginRequiredError()
} }
const accountPk = accountPubkeyToHex(account.pubkey)
await assertSignerMatchesAccountForPublish()
const normalizeOpts = { addClientTag: options.addClientTag } const normalizeOpts = { addClientTag: options.addClientTag }
const draft = normalizeDraftEventTags(draftEvent, normalizeOpts) const draft = normalizeDraftEventTags(draftEvent, normalizeOpts)
let event: Event let event: Event
@ -1932,18 +1963,12 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
event = await signEvent(draft, normalizeOpts) event = await signEvent(draft, normalizeOpts)
} }
if (event.kind !== kinds.Application && event.pubkey !== account.pubkey) { if (
const profileEvent = await replaceableEventService.fetchReplaceableEvent(event.pubkey, kinds.Metadata) event.kind !== kinds.Application &&
const eventAuthor = profileEvent ? getProfileFromEvent(profileEvent) : undefined accountPk &&
const result = confirm( !hexPubkeysEqual(event.pubkey, accountPk)
t( ) {
'You are about to publish an event signed by [{{eventAuthorName}}]. You are currently logged in as [{{currentUsername}}]. Are you sure?', throw publishExtensionMismatchError()
{ eventAuthorName: eventAuthor?.username, currentUsername: profile?.username }
)
)
if (!result) {
throw new Error(t('Cancelled'))
}
} }
client.interruptBackgroundQueries() client.interruptBackgroundQueries()
@ -2142,6 +2167,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const checkLogin = async <T,>(cb?: () => T | Promise<T>): Promise<T | void> => { const checkLogin = async <T,>(cb?: () => T | Promise<T>): Promise<T | void> => {
if (account?.signerType === 'npub') { if (account?.signerType === 'npub') {
if (cb) {
toast.error(t('readOnlySession.cannotPublish'))
}
return return
} }
if (!signer) { if (!signer) {

14
src/services/post-editor.service.ts

@ -2,6 +2,20 @@ class PostEditorService extends EventTarget {
static instance: PostEditorService static instance: PostEditorService
isSuggestionPopupOpen = false isSuggestionPopupOpen = false
/** Ref-count of open PostEditor / Sheet shells (reply, new post, etc.). */
private composerShellOpenCount = 0
get isComposerShellOpen(): boolean {
return this.composerShellOpenCount > 0
}
setComposerShellOpen(open: boolean) {
if (open) {
this.composerShellOpenCount += 1
} else {
this.composerShellOpenCount = Math.max(0, this.composerShellOpenCount - 1)
}
}
constructor() { constructor() {
super() super()

Loading…
Cancel
Save