Browse Source

make new notes form more advanced

imwald
Silberengel 1 month ago
parent
commit
18a89da035
  1. 228
      src/components/PostEditor/PostContent.tsx
  2. 15
      src/components/PostEditor/index.tsx
  3. 122
      src/components/StoredAccountSwitchSelect.tsx
  4. 89
      src/pages/primary/SpellsPage/index.tsx

228
src/components/PostEditor/PostContent.tsx

@ -4,7 +4,6 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { import {
DropdownMenu, DropdownMenu,
@ -31,12 +30,10 @@ import {
createCitationInternalDraftEvent, createCitationInternalDraftEvent,
createCitationExternalDraftEvent, createCitationExternalDraftEvent,
createCitationHardcopyDraftEvent, createCitationHardcopyDraftEvent,
createCitationPromptDraftEvent, createCitationPromptDraftEvent
createGitReleaseDraftEvent
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { parseRepoOwnerPubkeyInput } from '@/lib/git-republic-event' import { isTouchDevice } from '@/lib/utils'
import { cn, isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
@ -243,15 +240,6 @@ export default function PostContent({
const [citationGeohash, setCitationGeohash] = useState('') const [citationGeohash, setCitationGeohash] = useState('')
const [citationVersion, setCitationVersion] = useState('') const [citationVersion, setCitationVersion] = useState('')
const [citationSummary, setCitationSummary] = useState('') const [citationSummary, setCitationSummary] = useState('')
const [isGitRelease, setIsGitRelease] = useState(false)
const [releaseRepoOwnerInput, setReleaseRepoOwnerInput] = useState('')
const [releaseRepoId, setReleaseRepoId] = useState('')
const [releaseTagName, setReleaseTagName] = useState('')
const [releaseTagHash, setReleaseTagHash] = useState('')
const [releaseTitle, setReleaseTitle] = useState('')
const [releaseDownloadUrl, setReleaseDownloadUrl] = useState('')
const [releaseDraft, setReleaseDraft] = useState(false)
const [releasePrerelease, setReleasePrerelease] = useState(false)
const [hasPrivateRelaysAvailable, setHasPrivateRelaysAvailable] = useState(false) const [hasPrivateRelaysAvailable, setHasPrivateRelaysAvailable] = useState(false)
const [showMediaKindDialog, setShowMediaKindDialog] = useState(false) const [showMediaKindDialog, setShowMediaKindDialog] = useState(false)
@ -265,16 +253,6 @@ export default function PostContent({
} }
}, [mediaNoteKind, mediaImetaTags]) }, [mediaNoteKind, mediaImetaTags])
const isFirstRender = useRef(true) const isFirstRender = useRef(true)
const releaseFieldsOk = useMemo(() => {
if (!isGitRelease) return true
const owner = parseRepoOwnerPubkeyInput(releaseRepoOwnerInput)
return (
!!owner &&
!!releaseRepoId.trim() &&
!!releaseTagName.trim() &&
/^[0-9a-f]{40}$/i.test(releaseTagHash.trim())
)
}, [isGitRelease, releaseRepoOwnerInput, releaseRepoId, releaseTagName, releaseTagHash])
const canPost = useMemo(() => { const canPost = useMemo(() => {
const isArticle = isLongFormArticle || isWikiArticle || isWikiArticleMarkdown || isPublicationContent const isArticle = isLongFormArticle || isWikiArticle || isWikiArticleMarkdown || isPublicationContent
@ -282,15 +260,14 @@ export default function PostContent({
!!pubkey && !!pubkey &&
!posting && !posting &&
!uploadProgresses.length && !uploadProgresses.length &&
// For media notes, text is optional - just need media; Git releases use the editor as release notes (optional) // For media notes, text is optional - just need media
((mediaNoteKind !== null && mediaUrl) || !!text || isGitRelease) && ((mediaNoteKind !== null && mediaUrl) || !!text) &&
(!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) && (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) &&
(!isPublicMessage || extractedMentions.length > 0 || parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE) && (!isPublicMessage || extractedMentions.length > 0 || parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE) &&
(!isProtectedEvent || additionalRelayUrls.length > 0) && (!isProtectedEvent || additionalRelayUrls.length > 0) &&
(!isHighlight || highlightData.sourceValue.trim() !== '') && (!isHighlight || highlightData.sourceValue.trim() !== '') &&
// For articles, dTag is mandatory // For articles, dTag is mandatory
(!isArticle || !!articleDTag.trim()) && (!isArticle || !!articleDTag.trim()) &&
(!isGitRelease || releaseFieldsOk) &&
// For citations, required fields must be filled // For citations, required fields must be filled
(!isCitationInternal || !!citationInternalCTag.trim()) && (!isCitationInternal || !!citationInternalCTag.trim()) &&
(!isCitationExternal || (!!citationExternalUrl.trim() && !!citationAccessedOn.trim())) && (!isCitationExternal || (!!citationExternalUrl.trim() && !!citationAccessedOn.trim())) &&
@ -320,8 +297,6 @@ export default function PostContent({
isWikiArticleMarkdown, isWikiArticleMarkdown,
isPublicationContent, isPublicationContent,
articleDTag, articleDTag,
isGitRelease,
releaseFieldsOk,
isCitationInternal, isCitationInternal,
citationInternalCTag, citationInternalCTag,
isCitationExternal, isCitationExternal,
@ -413,8 +388,6 @@ export default function PostContent({
return ExtendedKind.WIKI_ARTICLE_MARKDOWN return ExtendedKind.WIKI_ARTICLE_MARKDOWN
} else if (isPublicationContent) { } else if (isPublicationContent) {
return ExtendedKind.PUBLICATION_CONTENT return ExtendedKind.PUBLICATION_CONTENT
} else if (isGitRelease) {
return ExtendedKind.GIT_RELEASE
} else if (isCitationInternal) { } else if (isCitationInternal) {
return ExtendedKind.CITATION_INTERNAL return ExtendedKind.CITATION_INTERNAL
} else if (isCitationExternal) { } else if (isCitationExternal) {
@ -439,7 +412,6 @@ export default function PostContent({
isWikiArticle, isWikiArticle,
isWikiArticleMarkdown, isWikiArticleMarkdown,
isPublicationContent, isPublicationContent,
isGitRelease,
isCitationInternal, isCitationInternal,
isCitationExternal, isCitationExternal,
isCitationHardcopy, isCitationHardcopy,
@ -675,23 +647,6 @@ export default function PostContent({
}) })
} }
if (isGitRelease) {
const ownerHex = parseRepoOwnerPubkeyInput(releaseRepoOwnerInput)
if (!ownerHex) {
throw new Error(t('Invalid repository owner pubkey'))
}
return createGitReleaseDraftEvent(cleanedText, {
repoOwnerPubkey: ownerHex,
repoId: releaseRepoId.trim(),
tagName: releaseTagName.trim(),
tagHash: releaseTagHash.trim().toLowerCase(),
title: releaseTitle.trim() || undefined,
downloadUrl: releaseDownloadUrl.trim() || undefined,
isDraft: releaseDraft,
isPrerelease: releasePrerelease
})
}
// Citations // Citations
if (isCitationInternal) { if (isCitationInternal) {
return createCitationInternalDraftEvent(cleanedText, { return createCitationInternalDraftEvent(cleanedText, {
@ -827,15 +782,6 @@ export default function PostContent({
isWikiArticle, isWikiArticle,
isWikiArticleMarkdown, isWikiArticleMarkdown,
isPublicationContent, isPublicationContent,
isGitRelease,
releaseRepoOwnerInput,
releaseRepoId,
releaseTagName,
releaseTagHash,
releaseTitle,
releaseDownloadUrl,
releaseDraft,
releasePrerelease,
isCitationInternal, isCitationInternal,
isCitationExternal, isCitationExternal,
isCitationHardcopy, isCitationHardcopy,
@ -864,9 +810,6 @@ export default function PostContent({
if (isArticle && !articleDTag.trim()) { if (isArticle && !articleDTag.trim()) {
throw new Error(t('D-Tag is required for articles')) throw new Error(t('D-Tag is required for articles'))
} }
if (isGitRelease && !releaseFieldsOk) {
throw new Error(t('Fill repository release fields'))
}
if (!pubkey) { if (!pubkey) {
return JSON.stringify({ error: 'Not logged in' }, null, 2) return JSON.stringify({ error: 'Not logged in' }, null, 2)
@ -889,8 +832,6 @@ export default function PostContent({
isWikiArticleMarkdown, isWikiArticleMarkdown,
isPublicationContent, isPublicationContent,
articleDTag, articleDTag,
isGitRelease,
releaseFieldsOk,
createDraftEvent, createDraftEvent,
t t
]) ])
@ -1059,7 +1000,6 @@ export default function PostContent({
// When enabling poll mode, clear other modes // When enabling poll mode, clear other modes
setIsPublicMessage(false) setIsPublicMessage(false)
setIsHighlight(false) setIsHighlight(false)
setIsGitRelease(false)
setIsCitationInternal(false) setIsCitationInternal(false)
setIsCitationExternal(false) setIsCitationExternal(false)
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
@ -1075,7 +1015,6 @@ export default function PostContent({
// When enabling public message mode, clear other modes // When enabling public message mode, clear other modes
setIsPoll(false) setIsPoll(false)
setIsHighlight(false) setIsHighlight(false)
setIsGitRelease(false)
setIsCitationInternal(false) setIsCitationInternal(false)
setIsCitationExternal(false) setIsCitationExternal(false)
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
@ -1091,7 +1030,6 @@ export default function PostContent({
// When enabling highlight mode, clear other modes and set client tag to true // When enabling highlight mode, clear other modes and set client tag to true
setIsPoll(false) setIsPoll(false)
setIsPublicMessage(false) setIsPublicMessage(false)
setIsGitRelease(false)
setIsCitationInternal(false) setIsCitationInternal(false)
setIsCitationExternal(false) setIsCitationExternal(false)
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
@ -1483,7 +1421,6 @@ export default function PostContent({
setIsCitationExternal(false) setIsCitationExternal(false)
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
setIsCitationPrompt(false) setIsCitationPrompt(false)
setIsGitRelease(false)
// Clear uploaded file from map and picture accumulation ref // Clear uploaded file from map and picture accumulation ref
uploadedMediaFileMap.current.clear() uploadedMediaFileMap.current.clear()
@ -1503,7 +1440,6 @@ export default function PostContent({
setIsPublicMessage(false) setIsPublicMessage(false)
setIsHighlight(false) setIsHighlight(false)
setMediaNoteKind(null) setMediaNoteKind(null)
setIsGitRelease(false)
setIsCitationInternal(false) setIsCitationInternal(false)
setIsCitationExternal(false) setIsCitationExternal(false)
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
@ -1534,7 +1470,6 @@ export default function PostContent({
const handleCitationToggle = (type: 'internal' | 'external' | 'hardcopy' | 'prompt') => { const handleCitationToggle = (type: 'internal' | 'external' | 'hardcopy' | 'prompt') => {
if (parentEvent) return // Can't create citations as replies if (parentEvent) return // Can't create citations as replies
setIsGitRelease(false)
setIsCitationInternal(type === 'internal') setIsCitationInternal(type === 'internal')
setIsCitationExternal(type === 'external') setIsCitationExternal(type === 'external')
setIsCitationHardcopy(type === 'hardcopy') setIsCitationHardcopy(type === 'hardcopy')
@ -1556,24 +1491,6 @@ export default function PostContent({
} }
} }
const handleGitReleaseFromMenu = () => {
if (parentEvent) return
setIsGitRelease(true)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsPoll(false)
setIsPublicMessage(false)
setIsHighlight(false)
setMediaNoteKind(null)
setIsLongFormArticle(false)
setIsWikiArticle(false)
setIsWikiArticleMarkdown(false)
setIsPublicationContent(false)
}
const handleClear = () => { const handleClear = () => {
// Clear the post editor cache // Clear the post editor cache
postEditorCache.clearPostCache({ kind: getDeterminedKind, defaultContent, parentEvent }) postEditorCache.clearPostCache({ kind: getDeterminedKind, defaultContent, parentEvent })
@ -1599,15 +1516,6 @@ export default function PostContent({
setIsCitationExternal(false) setIsCitationExternal(false)
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
setIsCitationPrompt(false) setIsCitationPrompt(false)
setIsGitRelease(false)
setReleaseRepoOwnerInput('')
setReleaseRepoId('')
setReleaseTagName('')
setReleaseTagHash('')
setReleaseTitle('')
setReleaseDownloadUrl('')
setReleaseDraft(false)
setReleasePrerelease(false)
// Clear citation fields // Clear citation fields
setCitationInternalCTag('') setCitationInternalCTag('')
setCitationInternalRelayHint('') setCitationInternalRelayHint('')
@ -1688,8 +1596,6 @@ export default function PostContent({
return t('New Hardcopy Citation') return t('New Hardcopy Citation')
} else if (determinedKind === ExtendedKind.CITATION_PROMPT) { } else if (determinedKind === ExtendedKind.CITATION_PROMPT) {
return t('New Prompt Citation') return t('New Prompt Citation')
} else if (determinedKind === ExtendedKind.GIT_RELEASE) {
return t('New Repository Release')
} else { } else {
return t('New Note') return t('New Note')
} }
@ -1787,13 +1693,10 @@ export default function PostContent({
{(isCitationInternal || {(isCitationInternal ||
isCitationExternal || isCitationExternal ||
isCitationHardcopy || isCitationHardcopy ||
isCitationPrompt || isCitationPrompt) && (
isGitRelease) && (
<div className="p-4 border rounded-lg bg-muted/30"> <div className="p-4 border rounded-lg bg-muted/30">
<div className="text-sm font-medium mb-3"> <div className="text-sm font-medium mb-3">
{isGitRelease {isCitationInternal
? t('Repository release')
: isCitationInternal
? t('Internal Citation Settings') ? t('Internal Citation Settings')
: isCitationExternal : isCitationExternal
? t('External Citation Settings') ? t('External Citation Settings')
@ -1801,9 +1704,7 @@ export default function PostContent({
? t('Hardcopy Citation Settings') ? t('Hardcopy Citation Settings')
: t('Prompt Citation Settings')} : t('Prompt Citation Settings')}
</div> </div>
{(isCitationInternal || isCitationExternal || isCitationHardcopy || isCitationPrompt) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Prompt Citation specific fields - shown first if prompt */} {/* Prompt Citation specific fields - shown first if prompt */}
{isCitationPrompt && ( {isCitationPrompt && (
<> <>
@ -2137,115 +2038,6 @@ export default function PostContent({
</> </>
)} )}
</div> </div>
)}
{isGitRelease && (
<div
className={cn(
'mt-4 grid grid-cols-1 gap-3 md:grid-cols-2',
(isCitationInternal ||
isCitationExternal ||
isCitationHardcopy ||
isCitationPrompt) &&
'border-t border-border pt-4'
)}
>
<p className="text-xs text-muted-foreground md:col-span-2">
{t('Release notes use the editor below (optional).')}
</p>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-repo-owner" className="text-sm font-medium">
{t('Repository owner (npub or hex)')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-repo-owner"
value={releaseRepoOwnerInput}
onChange={(e) => setReleaseRepoOwnerInput(e.target.value)}
placeholder="npub1…"
className={
releaseRepoOwnerInput.trim() && !parseRepoOwnerPubkeyInput(releaseRepoOwnerInput)
? 'border-destructive'
: ''
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="release-repo-id" className="text-sm font-medium">
{t('Repository id (d-tag)')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-repo-id"
value={releaseRepoId}
onChange={(e) => setReleaseRepoId(e.target.value)}
placeholder={t('e.g. my-repo')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="release-tag-name" className="text-sm font-medium">
{t('Git tag name')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-tag-name"
value={releaseTagName}
onChange={(e) => setReleaseTagName(e.target.value)}
placeholder="v1.0.0"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-tag-hash" className="text-sm font-medium">
{t('Tag target (40-char commit hash)')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-tag-hash"
value={releaseTagHash}
onChange={(e) => setReleaseTagHash(e.target.value.trim())}
placeholder={t('40-character hex SHA-1')}
className={
releaseTagHash.trim() && !/^[0-9a-f]{40}$/i.test(releaseTagHash.trim())
? 'border-destructive'
: ''
}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-title" className="text-sm font-medium">
{t('Release title')}
</Label>
<Input
id="release-title"
value={releaseTitle}
onChange={(e) => setReleaseTitle(e.target.value)}
placeholder={t('Optional display title')}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-download-url" className="text-sm font-medium">
{t('Download URL')}
</Label>
<Input
id="release-download-url"
value={releaseDownloadUrl}
onChange={(e) => setReleaseDownloadUrl(e.target.value)}
placeholder={t('https://…')}
/>
</div>
<div className="flex flex-wrap items-center gap-6 md:col-span-2">
<label className="flex cursor-pointer items-center gap-2 text-sm">
<Checkbox
checked={releaseDraft}
onCheckedChange={(v) => setReleaseDraft(v === true)}
/>
{t('Draft release')}
</label>
<label className="flex cursor-pointer items-center gap-2 text-sm">
<Checkbox
checked={releasePrerelease}
onCheckedChange={(v) => setReleasePrerelease(v === true)}
/>
{t('Pre-release')}
</label>
</div>
</div>
)}
</div> </div>
)} )}
@ -2363,7 +2155,7 @@ export default function PostContent({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
{/* Citations (private relays) + repository release */} {/* Citations (private relays) */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@ -2374,8 +2166,7 @@ export default function PostContent({
isCitationInternal || isCitationInternal ||
isCitationExternal || isCitationExternal ||
isCitationHardcopy || isCitationHardcopy ||
isCitationPrompt || isCitationPrompt
isGitRelease
? 'bg-accent' ? 'bg-accent'
: '' : ''
} }
@ -2404,9 +2195,6 @@ export default function PostContent({
{t('Citations require private relays (NIP-65).')} {t('Citations require private relays (NIP-65).')}
</div> </div>
)} )}
<DropdownMenuItem onClick={handleGitReleaseFromMenu}>
{t('Repository release')}
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</> </>

15
src/components/PostEditor/index.tsx

@ -1,3 +1,4 @@
import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -86,6 +87,13 @@ export default function PostEditor({
<SheetTitle>Post Editor</SheetTitle> <SheetTitle>Post Editor</SheetTitle>
<SheetDescription>Create a new post or reply</SheetDescription> <SheetDescription>Create a new post or reply</SheetDescription>
</SheetHeader> </SheetHeader>
{open ? (
<StoredAccountSwitchSelect
withBottomBorder
className="w-full flex-wrap"
showLabelAlways
/>
) : null}
{content} {content}
</div> </div>
</ScrollArea> </ScrollArea>
@ -112,6 +120,13 @@ export default function PostEditor({
<DialogTitle>Post Editor</DialogTitle> <DialogTitle>Post Editor</DialogTitle>
<DialogDescription>Create a new post or reply</DialogDescription> <DialogDescription>Create a new post or reply</DialogDescription>
</DialogHeader> </DialogHeader>
{open ? (
<StoredAccountSwitchSelect
withBottomBorder
className="w-full flex-wrap"
showLabelAlways
/>
) : null}
{content} {content}
</div> </div>
</ScrollArea> </ScrollArea>

122
src/components/StoredAccountSwitchSelect.tsx

@ -0,0 +1,122 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import UserAvatar from '@/components/UserAvatar'
import Username from '@/components/Username'
import { cn } from '@/lib/utils'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
type Props = {
className?: string
triggerClassName?: string
/** Show the inline label on narrow viewports (e.g. full-screen post composer). */
showLabelAlways?: boolean
/** Separator under the row (e.g. post editor header). */
withBottomBorder?: boolean
}
/**
* Switch {@link useNostr} session among stored accounts (same as notifications spell).
* Renders nothing when there is only one stored account or no session.
*/
export default function StoredAccountSwitchSelect({
className,
triggerClassName,
showLabelAlways = false,
withBottomBorder = false
}: Props) {
const { t } = useTranslation()
const { pubkey, accounts, switchAccount, isAccountSessionHydrating } = useNostr()
const sessionPubkey = useMemo(() => {
const cur = pubkey?.trim()
return cur ? normalizeHexPubkey(cur) : null
}, [pubkey])
const storedAccountPubkeys = useMemo(() => {
const seen = new Set<string>()
const out: string[] = []
for (const a of accounts) {
const raw = a.pubkey?.trim()
if (!raw) continue
const p = normalizeHexPubkey(raw)
if (!seen.has(p)) {
seen.add(p)
out.push(p)
}
}
return out
}, [accounts])
const handlePick = useCallback(
async (v: string) => {
const target = normalizeHexPubkey(v)
if (pubkey && hexPubkeysEqual(target, pubkey)) return
const nextAccount = accounts.find((a) => hexPubkeysEqual(a.pubkey, target))
if (!nextAccount) {
toast.error(t('notificationsSwitchAccountFailed'))
return
}
const switched = await switchAccount(nextAccount)
if (!switched || !hexPubkeysEqual(normalizeHexPubkey(switched), target)) {
toast.error(t('notificationsSwitchAccountFailed'))
}
},
[pubkey, accounts, switchAccount, t]
)
if (storedAccountPubkeys.length <= 1 || !sessionPubkey) return null
return (
<div
className={cn(
'flex min-w-0 items-center gap-2',
withBottomBorder && '-mx-1 mb-1 border-b border-border/60 px-1 pb-3',
className
)}
>
<span
className={cn(
'shrink-0 text-xs text-muted-foreground',
showLabelAlways ? 'inline' : 'hidden sm:inline'
)}
>
{t('notificationsViewAsAccount')}
</span>
<Select
value={sessionPubkey}
disabled={isAccountSessionHydrating}
onValueChange={(v) => void handlePick(v)}
>
<SelectTrigger
className={cn('h-9 min-w-0 flex-1', triggerClassName)}
aria-label={t('notificationsViewAsAccountAria')}
>
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
{storedAccountPubkeys.map((pk) => (
<SelectItem key={pk} value={pk}>
<span className="flex min-w-0 items-center gap-2">
<UserAvatar userId={pk} size="small" className="shrink-0" />
<Username
userId={pk}
className="min-w-0 truncate text-left font-normal"
skeletonClassName="h-4 w-24"
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}

89
src/pages/primary/SpellsPage/index.tsx

@ -1,5 +1,6 @@
import HideUntrustedContentButton from '@/components/HideUntrustedContentButton' import HideUntrustedContentButton from '@/components/HideUntrustedContentButton'
import NoteList, { type TNoteListRef } from '@/components/NoteList' import NoteList, { type TNoteListRef } from '@/components/NoteList'
import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@ -15,13 +16,6 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger DropdownMenuTrigger
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import UserAvatar from '@/components/UserAvatar' import UserAvatar from '@/components/UserAvatar'
@ -57,7 +51,7 @@ import {
FIRST_RELAY_RESULT_GRACE_MS, FIRST_RELAY_RESULT_GRACE_MS,
} from '@/constants' } from '@/constants'
import { filterEventsExcludingTombstones, isUserInEventMentions } from '@/lib/event' import { filterEventsExcludingTombstones, isUserInEventMentions } from '@/lib/event'
import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { formatPubkey, normalizeHexPubkey } from '@/lib/pubkey'
import { import {
augmentSubRequestsWithFavoritesFastReadAndInbox, augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox getRelayUrlsWithFavoritesFastReadAndInbox
@ -105,7 +99,6 @@ import type { Event } from 'nostr-tools'
import { kinds as nostrKinds, verifyEvent } from 'nostr-tools' import { kinds as nostrKinds, verifyEvent } from 'nostr-tools'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import CreateSpellDialog from './CreateSpellDialog' import CreateSpellDialog from './CreateSpellDialog'
import { import {
appendCuratedReadOnlyRelays, appendCuratedReadOnlyRelays,
@ -318,17 +311,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
) { ) {
const { t } = useTranslation() const { t } = useTranslation()
const { navigate: navigatePrimary } = usePrimaryPage() const { navigate: navigatePrimary } = usePrimaryPage()
const { const { pubkey, account, relayList, attemptDelete, bookmarkListEvent, interestListEvent } = useNostr()
pubkey,
account,
accounts,
relayList,
attemptDelete,
bookmarkListEvent,
interestListEvent,
switchAccount,
isAccountSessionHydrating
} = useNostr()
const { addBookmark, removeBookmark } = useBookmarks() const { addBookmark, removeBookmark } = useBookmarks()
const { hideUntrustedNotifications } = useUserTrust() const { hideUntrustedNotifications } = useUserTrust()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -375,38 +358,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
return cur ? normalizeHexPubkey(cur) : null return cur ? normalizeHexPubkey(cur) : null
}, [pubkey]) }, [pubkey])
const storedAccountPubkeys = useMemo(() => {
const seen = new Set<string>()
const out: string[] = []
for (const a of accounts) {
const raw = a.pubkey?.trim()
if (!raw) continue
const p = normalizeHexPubkey(raw)
if (!seen.has(p)) {
seen.add(p)
out.push(p)
}
}
return out
}, [accounts])
const handleNotificationsAccountPick = useCallback(
async (v: string) => {
const target = normalizeHexPubkey(v)
if (pubkey && hexPubkeysEqual(target, pubkey)) return
const nextAccount = accounts.find((a) => hexPubkeysEqual(a.pubkey, target))
if (!nextAccount) {
toast.error(t('notificationsSwitchAccountFailed'))
return
}
const switched = await switchAccount(nextAccount)
if (!switched || !hexPubkeysEqual(normalizeHexPubkey(switched), target)) {
toast.error(t('notificationsSwitchAccountFailed'))
}
},
[pubkey, accounts, switchAccount, t]
)
const logSpellFeedPickerSelection = useCallback((label: string, extra?: Record<string, unknown>) => { const logSpellFeedPickerSelection = useCallback((label: string, extra?: Record<string, unknown>) => {
spellFeedInstrT0Ref.current = performance.now() spellFeedInstrT0Ref.current = performance.now()
spellFeedInstrLabelRef.current = label spellFeedInstrLabelRef.current = label
@ -1726,38 +1677,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
<> <>
{selectedFauxSpell === 'notifications' ? ( {selectedFauxSpell === 'notifications' ? (
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 px-1 pb-2 sm:justify-between"> <div className="flex shrink-0 flex-wrap items-center justify-end gap-2 px-1 pb-2 sm:justify-between">
{storedAccountPubkeys.length > 1 && notificationsFeedPubkey ? ( {notificationsFeedPubkey ? (
<div className="flex min-w-0 flex-1 items-center gap-2 sm:max-w-[min(100%,20rem)]"> <StoredAccountSwitchSelect className="min-w-0 flex-1 sm:max-w-[min(100%,20rem)]" />
<span className="hidden shrink-0 text-xs text-muted-foreground sm:inline">
{t('notificationsViewAsAccount')}
</span>
<Select
value={notificationsFeedPubkey}
disabled={isAccountSessionHydrating}
onValueChange={(v) => void handleNotificationsAccountPick(v)}
>
<SelectTrigger
className="h-9 min-w-0 flex-1"
aria-label={t('notificationsViewAsAccountAria')}
>
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
{storedAccountPubkeys.map((pk) => (
<SelectItem key={pk} value={pk}>
<span className="flex min-w-0 items-center gap-2">
<UserAvatar userId={pk} size="small" className="shrink-0" />
<Username
userId={pk}
className="min-w-0 truncate text-left font-normal"
skeletonClassName="h-4 w-24"
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null} ) : null}
<HideUntrustedContentButton type="notifications" size="titlebar-icon" /> <HideUntrustedContentButton type="notifications" size="titlebar-icon" />
</div> </div>

Loading…
Cancel
Save