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

23
src/components/HelpAndAccountMenu.tsx

@ -29,8 +29,6 @@ import { useFetchProfile } from '@/hooks/useFetchProfile' @@ -29,8 +29,6 @@ import { useFetchProfile } from '@/hooks/useFetchProfile'
import { useNostr } from '@/providers/NostrProvider'
import { AccountQuickSwitchMenuItems } from '@/components/AccountQuickSwitchMenuItems'
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 { useCallback, useMemo, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
@ -84,7 +82,6 @@ function AccountDropdownItems({ @@ -84,7 +82,6 @@ function AccountDropdownItems({
<Database className="size-4" />
{t('Browse Cache')}
</DropdownMenuItem>
<ActiveRelaysDropdownSection />
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onSwitchAccount}>
<ArrowDownUp className="size-4" />
@ -239,9 +236,7 @@ function TitlebarAccountMenu({ @@ -239,9 +236,7 @@ function TitlebarAccountMenu({
function LoggedOutTitlebarMenu({ onLogin }: { onLogin: () => void }) {
const { t } = useTranslation()
const { rows } = useRelayConnectionRows()
if (rows.length === 0) {
return (
<Button variant="ghost" size="titlebar-icon" onClick={onLogin} title={t('Login')}>
<UserRound />
@ -249,24 +244,6 @@ function LoggedOutTitlebarMenu({ onLogin }: { onLogin: () => void }) { @@ -249,24 +244,6 @@ function LoggedOutTitlebarMenu({ onLogin }: { onLogin: () => void }) {
)
}
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. */
export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccountMenuVariant }) {
const { pubkey, checkLogin, isNip07LoginInFlight } = useNostr()

39
src/components/PostEditor/PostContent.tsx

@ -46,7 +46,7 @@ import { @@ -46,7 +46,7 @@ import {
} from '@/constants'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useReply } from '@/providers/ReplyProvider'
import { useReplyIngress } from '@/hooks/useReplyIngress'
import { canonicalizeRssArticleUrl, getArticleUrlFromCommentITags } from '@/lib/rss-article'
import { cleanUrl, isBlossomBudBlobUrl, rewritePlainTextHttpUrls } from '@/lib/url'
import logger from '@/lib/logger'
@ -199,7 +199,7 @@ export default function PostContent({ @@ -199,7 +199,7 @@ export default function PostContent({
}) {
const { t, i18n } = useTranslation()
const { pubkey, publish, checkLogin, canSignEvents } = useNostr()
const { addReplies } = useReply()
const { addReplies } = useReplyIngress()
const mergePublishedReplyIntoThread = useCallback(
(reply: Event, relayStatuses?: TRelayPublishStatus[]) => {
@ -742,6 +742,7 @@ export default function PostContent({ @@ -742,6 +742,7 @@ export default function PostContent({
}, [getDeterminedKind, defaultContent, parentEvent, isNsfw, isPoll, pollCreateData, addClientTag])
const prevComposerShellOpenRef = useRef(open)
const prevComposerPubkeyRef = useRef(pubkey)
useEffect(() => {
const wasOpen = prevComposerShellOpenRef.current
prevComposerShellOpenRef.current = open
@ -750,6 +751,18 @@ export default function PostContent({ @@ -750,6 +751,18 @@ export default function PostContent({
}
}, [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 => {
if (!parentEvent || parentEvent.kind !== ExtendedKind.RSS_THREAD_ROOT) return undefined
const raw =
@ -1245,6 +1258,10 @@ export default function PostContent({ @@ -1245,6 +1258,10 @@ export default function PostContent({
const post = async (e?: React.MouseEvent) => {
e?.stopPropagation()
checkLogin(async () => {
if (!canSignEvents) {
toast.error(t('readOnlySession.cannotPublish'))
return
}
if (!canPost) {
logger.warn('Attempted to post while canPost is false')
return
@ -1387,6 +1404,16 @@ export default function PostContent({ @@ -1387,6 +1404,16 @@ export default function PostContent({
close()
} catch (error) {
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
}
// 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({ @@ -3524,7 +3551,13 @@ export default function PostContent({
</Button>
</div>
{open ? (
<StoredAccountSwitchSelect withTopBorder alignEnd className="w-full" showLabelAlways />
<StoredAccountSwitchSelect
withTopBorder
alignEnd
className="w-full"
showLabelAlways
inComposer
/>
) : null}
{/* Media Kind Selection Dialog */}

26
src/components/PostEditor/index.tsx

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

9
src/components/RefreshButton/index.tsx

@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button' @@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useLongPressAction } from '@/hooks/use-long-press-action'
import { hardReloadPreservingFeedSnapshots } from '@/services/session-feed-snapshot.service'
import { RefreshCcw } from 'lucide-react'
import { RefreshCw } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -46,12 +46,13 @@ export function RefreshButton({ @@ -46,12 +46,13 @@ export function RefreshButton({
onClick()
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 ? (
<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>
)

8
src/components/Sidebar/PostButton.tsx

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

19
src/components/StoredAccountSwitchSelect.tsx

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

8
src/i18n/locales/de.ts

@ -466,7 +466,9 @@ export default { @@ -466,7 +466,9 @@ export default {
readOnlySession: {
label: 'Read-only',
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',
'Logged in Accounts': 'Angemeldete Konten',
@ -1000,6 +1002,8 @@ export default { @@ -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.',
'accountSwitch.extensionRetry': 'Erweiterung erneut',
'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':
'Erweiterungsschlüssel passt noch nicht. Schlüssel in der Erweiterung wechseln und erneut versuchen.',
'accountSwitch.extensionUnavailable':
@ -2086,6 +2090,8 @@ export default { @@ -2086,6 +2090,8 @@ export default {
'nip07.useExtensionIdentity': 'Erweiterungs-Identität verwenden',
'nip07.switchedToExtensionIdentity': 'Auf die aktuelle Identität Ihrer Erweiterung umgestellt.',
'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',
'Long-form Article': 'Long-form Article',
'Mailbox relays saved': 'Mailbox relays saved',

8
src/i18n/locales/en.ts

@ -467,7 +467,9 @@ export default { @@ -467,7 +467,9 @@ export default {
readOnlySession: {
label: 'Read-only',
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',
'Logged in Accounts': 'Logged in Accounts',
@ -1019,6 +1021,8 @@ export default { @@ -1019,6 +1021,8 @@ export default {
'accountSwitch.extensionConnected': 'Extension connected for this account.',
'accountSwitch.extensionRetryFailed':
'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':
'Could not reach the browser extension. Unlock nos2x/Alby, allow this site, then click the account again.',
'Show untrusted {type}': 'Show untrusted {{type}}',
@ -2085,6 +2089,8 @@ export default { @@ -2085,6 +2089,8 @@ export default {
'nip07.useExtensionIdentity': 'Use extension identity',
'nip07.switchedToExtensionIdentity': "Switched to your extension's current 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',
'Long-form Article': 'Long-form Article',
'Mailbox relays saved': 'Mailbox relays saved',

52
src/providers/NostrProvider/index.tsx

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

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

@ -2,6 +2,20 @@ class PostEditorService extends EventTarget { @@ -2,6 +2,20 @@ class PostEditorService extends EventTarget {
static instance: PostEditorService
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() {
super()

Loading…
Cancel
Save