Browse Source

hide more advanced options on editors

imwald
Silberengel 2 weeks ago
parent
commit
ae0882240a
  1. 40
      src/components/ConnectedRelays/ActiveRelaysDropdownSection.tsx
  2. 106
      src/components/ConnectedRelays/ActiveRelaysIconGrid.tsx
  3. 117
      src/components/ConnectedRelays/ConnectedRelaysSidebarStrip.tsx
  4. 13
      src/components/ConnectedRelays/active-relays-display.ts
  5. 7
      src/components/HelpAndAccountMenu.tsx
  6. 15
      src/components/PostEditor/Mentions.tsx
  7. 23
      src/components/PostEditor/PollEditor.tsx
  8. 114
      src/components/PostEditor/PostContent.tsx
  9. 189
      src/components/PostEditor/PostEditorAdvancedPanel.tsx
  10. 30
      src/components/PostEditor/PostEditorFormatToolbar.tsx
  11. 84
      src/components/PostEditor/PostOptions.tsx
  12. 2
      src/components/PostEditor/PostTextarea/index.tsx
  13. 2
      src/components/Sidebar/index.tsx
  14. 9
      src/i18n/locales/de.ts
  15. 4
      src/i18n/locales/en.ts

40
src/components/ConnectedRelays/ActiveRelaysDropdownSection.tsx

@ -1,34 +1,14 @@ @@ -1,34 +1,14 @@
import { useSmartRelayNavigation } from '@/PageManager'
import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator
} from '@/components/ui/dropdown-menu'
import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows'
import { toRelay } from '@/lib/link'
import { simplifyUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
import { ActiveRelaysIconGrid } from './ActiveRelaysIconGrid'
function rowMuted(connected: boolean) {
return !connected
}
function rowTitle(url: string, connected: boolean, t: (k: string) => string) {
const base = simplifyUrl(url)
if (!connected) return `${base}${t('Not connected')}`
return base
}
function rowClass(connected: boolean) {
return cn(rowMuted(connected) && 'opacity-45 text-muted-foreground')
}
/** Relay list block for account (or similar) dropdown menus. */
/** Compact active-relay icons in the account (user badge) dropdown. */
export function ActiveRelaysDropdownSection() {
const { t } = useTranslation()
const { navigateToRelay } = useSmartRelayNavigation()
const { rows, connectedCount } = useRelayConnectionRows()
if (rows.length === 0) return null
@ -42,17 +22,13 @@ export function ActiveRelaysDropdownSection() { @@ -42,17 +22,13 @@ export function ActiveRelaysDropdownSection() {
<span>{t('Active relays')}</span>
<span className="tabular-nums text-muted-foreground">{countSummary}</span>
</DropdownMenuLabel>
{rows.map(({ url, connected }) => (
<DropdownMenuItem
key={url}
title={rowTitle(url, connected, t)}
onClick={() => navigateToRelay(toRelay(url))}
className={cn('min-w-52 gap-2', rowClass(connected))}
<div
className="px-2 pb-2"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<RelayIcon url={url} />
{simplifyUrl(url)}
</DropdownMenuItem>
))}
<ActiveRelaysIconGrid />
</div>
</>
)
}

106
src/components/ConnectedRelays/ActiveRelaysIconGrid.tsx

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
import { useSmartRelayNavigation } from '@/PageManager'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows'
import { toRelay } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
import {
ACTIVE_RELAYS_MAX_ICONS,
activeRelayRowMuted,
activeRelayRowTitle
} from './active-relays-display'
/**
* Compact relay status: icon buttons only (no hostname labels).
*/
export function ActiveRelaysIconGrid({ className }: { className?: string }) {
const { t } = useTranslation()
const { navigateToRelay } = useSmartRelayNavigation()
const { rows } = useRelayConnectionRows()
const shown = rows.slice(0, ACTIVE_RELAYS_MAX_ICONS)
const overflowRows = rows.slice(ACTIVE_RELAYS_MAX_ICONS)
const overflow = overflowRows.length
if (rows.length === 0) {
return (
<p className={cn('text-xs text-muted-foreground', className)} title={t('Active relays')}>
</p>
)
}
return (
<div className={cn('flex flex-wrap gap-1', className)} title={t('Active relays')}>
{shown.map(({ url, connected }) => (
<Button
key={url}
type="button"
variant="ghost"
size="sm"
className={cn(
'h-7 w-7 min-h-7 min-w-7 shrink-0 rounded-full p-0 hover:bg-muted/80',
activeRelayRowMuted(connected) && 'opacity-40 grayscale'
)}
title={activeRelayRowTitle(url, connected, t)}
aria-label={activeRelayRowTitle(url, connected, t)}
onClick={() => navigateToRelay(toRelay(url))}
>
<RelayIcon url={url} className="h-6 w-6" iconSize={12} />
</Button>
))}
{overflow > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 min-h-7 min-w-7 shrink-0 rounded-full bg-muted px-1.5 py-0 text-[0.65rem] font-medium tabular-nums text-muted-foreground hover:bg-muted/80 hover:text-foreground"
title={t('More relays', { count: overflow })}
aria-label={t('More relays', { count: overflow })}
>
+{overflow}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="right"
className="w-auto max-w-[min(18rem,calc(100vw-1.5rem))] p-2"
>
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground py-1">
{t('More relays', { count: overflow })}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="flex flex-wrap gap-1 max-w-[16rem]">
{overflowRows.map(({ url, connected }) => (
<Button
key={url}
type="button"
variant="ghost"
size="sm"
className={cn(
'h-7 w-7 min-h-7 min-w-7 shrink-0 rounded-full p-0 hover:bg-muted/80',
activeRelayRowMuted(connected) && 'opacity-40 grayscale'
)}
title={activeRelayRowTitle(url, connected, t)}
aria-label={activeRelayRowTitle(url, connected, t)}
onClick={() => navigateToRelay(toRelay(url))}
>
<RelayIcon url={url} className="h-6 w-6" iconSize={12} />
</Button>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
)
}

117
src/components/ConnectedRelays/ConnectedRelaysSidebarStrip.tsx

@ -1,117 +0,0 @@ @@ -1,117 +0,0 @@
import { useSmartRelayNavigation } from '@/PageManager'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows'
import { toRelay } from '@/lib/link'
import { simplifyUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
const MAX_ICONS = 14
function rowMuted(connected: boolean) {
return !connected
}
function rowMenuClass(connected: boolean) {
return cn(rowMuted(connected) && 'opacity-50 text-muted-foreground')
}
function rowTitle(url: string, connected: boolean, t: (k: string) => string) {
const base = simplifyUrl(url)
if (!connected) return `${base}${t('Not connected')}`
return base
}
/**
* Desktop sidebar: relay avatars for relays with an open WebSocket in the pool.
*/
export function ConnectedRelaysSidebarStrip({ className }: { className?: string }) {
const { t } = useTranslation()
const { navigateToRelay } = useSmartRelayNavigation()
const { rows } = useRelayConnectionRows()
const shown = rows.slice(0, MAX_ICONS)
const overflowRows = rows.slice(MAX_ICONS)
const overflow = overflowRows.length
if (rows.length === 0) {
return (
<div className={cn('px-1 py-1.5 xl:px-0', className)} title={t('Active relays')}>
<p className="text-center text-[0.6rem] font-medium text-muted-foreground xl:text-left">{t('Active relays')}</p>
<p className="mt-0.5 text-center text-[0.55rem] text-muted-foreground/80 xl:text-left"></p>
</div>
)
}
return (
<div className={cn('px-1 py-2 xl:px-0', className)} title={t('Active relays')}>
<p className="mb-1.5 text-center text-[0.65rem] font-medium leading-snug text-foreground xl:text-left">
{t('Active relays')}
</p>
<div className="flex flex-wrap justify-center gap-1 xl:justify-start">
{shown.map(({ url, connected }) => (
<Button
key={url}
type="button"
variant="ghost"
size="sm"
className={cn(
'h-5 w-5 min-h-5 min-w-5 shrink-0 rounded-full p-0 hover:bg-muted/80',
rowMuted(connected) && 'opacity-40 grayscale'
)}
title={rowTitle(url, connected, t)}
aria-label={rowTitle(url, connected, t)}
onClick={() => navigateToRelay(toRelay(url))}
>
<RelayIcon url={url} className="h-5 w-5" iconSize={11} />
</Button>
))}
{overflow > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-5 min-h-5 min-w-5 shrink-0 rounded-full bg-muted px-1 py-0 text-[0.6rem] font-medium tabular-nums text-muted-foreground hover:bg-muted/80 hover:text-foreground"
title={t('More relays', { count: overflow })}
aria-label={t('More relays', { count: overflow })}
>
+{overflow}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="right"
className="w-[min(18rem,calc(100vw-1.5rem))]"
>
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
{t('More relays', { count: overflow })}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{overflowRows.map(({ url, connected }) => (
<DropdownMenuItem
key={url}
className={cn('min-w-0 gap-2', rowMenuClass(connected))}
title={rowTitle(url, connected, t)}
onClick={() => navigateToRelay(toRelay(url))}
>
<RelayIcon url={url} className="h-5 w-5 shrink-0" iconSize={11} />
<span className="truncate">{simplifyUrl(url)}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
</div>
)
}

13
src/components/ConnectedRelays/active-relays-display.ts

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
import { simplifyUrl } from '@/lib/url'
export const ACTIVE_RELAYS_MAX_ICONS = 14
export function activeRelayRowMuted(connected: boolean) {
return !connected
}
export function activeRelayRowTitle(url: string, connected: boolean, t: (k: string) => string) {
const base = simplifyUrl(url)
if (!connected) return `${base}${t('Not connected')}`
return base
}

7
src/components/HelpAndAccountMenu.tsx

@ -34,13 +34,11 @@ export type HelpAndAccountMenuVariant = 'sidebar' | 'titlebar' @@ -34,13 +34,11 @@ export type HelpAndAccountMenuVariant = 'sidebar' | 'titlebar'
function AccountDropdownItems({
onSwitchAccount,
onLogoutClick,
onBrowseCache,
showActiveRelays = false
onBrowseCache
}: {
onSwitchAccount: () => void
onLogoutClick: () => void
onBrowseCache: () => void
showActiveRelays?: boolean
}) {
const { t } = useTranslation()
const { navigate } = usePrimaryPage()
@ -59,7 +57,7 @@ function AccountDropdownItems({ @@ -59,7 +57,7 @@ function AccountDropdownItems({
<Database className="size-4" />
{t('Browse Cache')}
</DropdownMenuItem>
{showActiveRelays ? <ActiveRelaysDropdownSection /> : null}
<ActiveRelaysDropdownSection />
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onSwitchAccount}>
<ArrowDownUp className="size-4" />
@ -191,7 +189,6 @@ function TitlebarAccountMenu({ @@ -191,7 +189,6 @@ function TitlebarAccountMenu({
onSwitchAccount={onSwitchAccount}
onLogoutClick={onLogoutClick}
onBrowseCache={onBrowseCache}
showActiveRelays
/>
</DropdownMenuContent>
</DropdownMenu>

15
src/components/PostEditor/Mentions.tsx

@ -17,12 +17,15 @@ export default function Mentions({ @@ -17,12 +17,15 @@ export default function Mentions({
content,
mentions,
setMentions,
parentEvent
parentEvent,
/** When true, trigger shows only the count (section label supplies the title). */
compactTrigger = false
}: {
content: string
mentions: string[]
setMentions: (mentions: string[]) => void
parentEvent?: Event
compactTrigger?: boolean
}) {
const { t } = useTranslation()
const { pubkey } = useNostr()
@ -75,8 +78,18 @@ export default function Mentions({ @@ -75,8 +78,18 @@ export default function Mentions({
disabled={potentialMentions.length === 0}
onClick={(e) => e.stopPropagation()}
>
{compactTrigger ? (
potentialMentions.length > 0 ? (
`(${mentions.length}/${potentialMentions.length})`
) : (
'—'
)
) : (
<>
{t('Mentions')}{' '}
{potentialMentions.length > 0 && `(${mentions.length}/${potentialMentions.length})`}
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[min(13rem,calc(100vw-1.5rem))] max-w-none p-0 py-1">

23
src/components/PostEditor/PollEditor.tsx

@ -7,18 +7,14 @@ import dayjs from 'dayjs' @@ -7,18 +7,14 @@ import dayjs from 'dayjs'
import { Eraser, X } from 'lucide-react'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PostRelaySelector from './PostRelaySelector'
export default function PollEditor({
pollCreateData,
setPollCreateData,
setIsPoll: _setIsPoll,
content = ''
setIsPoll: _setIsPoll
}: {
pollCreateData: TPollCreateData
setPollCreateData: Dispatch<SetStateAction<TPollCreateData>>
setIsPoll: Dispatch<SetStateAction<boolean>>
content?: string
}) {
const { t } = useTranslation()
const [isMultipleChoice, setIsMultipleChoice] = useState(pollCreateData.isMultipleChoice)
@ -26,15 +22,14 @@ export default function PollEditor({ @@ -26,15 +22,14 @@ export default function PollEditor({
const [endsAt, setEndsAt] = useState(
pollCreateData.endsAt ? dayjs(pollCreateData.endsAt * 1000).format('YYYY-MM-DDTHH:mm') : ''
)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>(pollCreateData.relays)
useEffect(() => {
setPollCreateData({
setPollCreateData((prev) => ({
...prev,
isMultipleChoice,
options,
endsAt: endsAt ? dayjs(endsAt).startOf('minute').unix() : undefined,
relays: additionalRelayUrls
})
}, [isMultipleChoice, options, endsAt, additionalRelayUrls, setPollCreateData])
endsAt: endsAt ? dayjs(endsAt).startOf('minute').unix() : undefined
}))
}, [isMultipleChoice, options, endsAt, setPollCreateData])
const handleAddOption = () => {
setOptions([...options, ''])
@ -110,12 +105,6 @@ export default function PollEditor({ @@ -110,12 +105,6 @@ export default function PollEditor({
</div>
</div>
<div className="space-y-2">
<PostRelaySelector
setAdditionalRelayUrls={setAdditionalRelayUrls}
content={content}
/>
</div>
</div>
)
}

114
src/components/PostEditor/PostContent.tsx

@ -42,7 +42,6 @@ import { @@ -42,7 +42,6 @@ import {
ExtendedKind,
isNip71ShortVideoKind,
isNip71StyleVideoKind,
MAX_PUBLISH_RELAYS
} from '@/constants'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
@ -106,10 +105,9 @@ import { useTranslation } from 'react-i18next' @@ -106,10 +105,9 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import Mentions, { extractMentions } from './Mentions'
import { extractMentions } from './Mentions'
import PollEditor from './PollEditor'
import PostOptions from './PostOptions'
import PostRelaySelector from './PostRelaySelector'
import PostEditorAdvancedPanel from './PostEditorAdvancedPanel'
import PostTextarea, { TPostTextareaHandle } from './PostTextarea'
import {
newNostrSpecAffectedKindRow,
@ -479,6 +477,13 @@ export default function PostContent({ @@ -479,6 +477,13 @@ export default function PostContent({
if (isPoll) setRelayCapBlockInfo(null)
}, [isPoll])
useEffect(() => {
if (!isPoll) return
setPollCreateData((prev) =>
prev.relays === additionalRelayUrls ? prev : { ...prev, relays: additionalRelayUrls }
)
}, [isPoll, additionalRelayUrls])
// Clear highlight data when initialHighlightData changes or is removed
useEffect(() => {
if (initialHighlightData) {
@ -3086,7 +3091,9 @@ export default function PostContent({ @@ -3086,7 +3091,9 @@ export default function PostContent({
title={t('Advanced event lab')}
>
<Code2 className="h-3.5 w-3.5 shrink-0" />
<span className="hidden sm:inline max-w-[9rem] truncate">{t('Advanced event lab')}</span>
<span className="max-w-[9rem] truncate text-xs sm:text-sm">
{t('Advanced event lab')}
</span>
</Button>
{!parentEvent ? (
<>
@ -3281,7 +3288,6 @@ export default function PostContent({ @@ -3281,7 +3288,6 @@ export default function PostContent({
pollCreateData={pollCreateData}
setPollCreateData={setPollCreateData}
setIsPoll={setIsPoll}
content={text}
/>
)}
{isHighlight && (
@ -3294,24 +3300,19 @@ export default function PostContent({ @@ -3294,24 +3300,19 @@ export default function PostContent({
{isPublicMessage && (
<div className="rounded-lg border bg-muted/40 p-3">
<div className="mb-2 text-sm font-medium">{t('Recipients')}</div>
<div className="space-y-2">
<Mentions
content={text}
parentEvent={undefined}
mentions={extractedMentions}
setMentions={setExtractedMentions}
/>
{extractedMentions.length > 0 ? (
<div className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground">
{t('Recipients detected from your message:')} {extractedMentions.length}
</div>
{!showMoreOptions ? (
<span className="block text-xs mt-1">{t('Open Advanced to adjust mention recipients')}</span>
) : null}
</p>
) : (
<div className="text-sm text-muted-foreground">
{t('Add recipients using nostr: mentions (e.g., nostr:npub1...) or the recipient selector above')}
</div>
<p className="text-sm text-muted-foreground">
{t('Add recipients using nostr: mentions (e.g., nostr:npub1...) or open Advanced')}
</p>
)}
</div>
</div>
)}
{uploadProgresses.length > 0 &&
uploadProgresses.map(({ file, progress, cancel, phase }, index) => (
@ -3354,43 +3355,6 @@ export default function PostContent({ @@ -3354,43 +3355,6 @@ export default function PostContent({
</button>
</div>
))}
{!isPoll && (
<div
className={cn(
'shrink-0',
isDiscussionThread && threadErrors.relay && 'rounded-md ring-1 ring-destructive'
)}
>
<PostRelaySelector
setAdditionalRelayUrls={setAdditionalRelayUrls}
onRelayPublishCapChange={handleRelayPublishCapChange}
parentEvent={parentEvent}
openFrom={openFrom}
content={text}
isPublicMessage={isPublicMessage}
mentions={extractedMentions}
/>
{relayCapBlockInfo && (
<p className="mt-2 text-sm text-amber-600 dark:text-amber-500" role="alert">
{relayCapBlockInfo.outboxSlotsInPublish > 0
? t('Publish relay cap hint with outbox first', {
max: MAX_PUBLISH_RELAYS,
reservedSlots: relayCapBlockInfo.outboxSlotsInPublish,
selected: relayCapBlockInfo.selectedTotal,
selectedContacted: relayCapBlockInfo.selectedContacted
})
: t('Publish relay cap hint', {
max: MAX_PUBLISH_RELAYS,
selected: relayCapBlockInfo.selectedTotal,
selectedContacted: relayCapBlockInfo.selectedContacted
})}
</p>
)}
{isDiscussionThread && threadErrors.relay && (
<p className="mt-1 text-sm text-destructive">{threadErrors.relay}</p>
)}
</div>
)}
{/* Hidden uploader for the "Media Note" dropdown item */}
{!parentEvent && (
<Uploader
@ -3406,8 +3370,8 @@ export default function PostContent({ @@ -3406,8 +3370,8 @@ export default function PostContent({
<button ref={mediaUploaderBtnRef} type="button" aria-hidden="true" tabIndex={-1} />
</Uploader>
)}
<div className="flex flex-wrap items-center justify-between gap-2 min-w-0">
<div className="flex gap-2 items-center min-w-0 shrink-0">
<div className="flex min-w-0 w-full items-center gap-1.5">
<div className="min-w-0 flex-1 overflow-x-auto overscroll-x-contain">
<PostEditorFormatToolbar
insertText={(txt) => textareaRef.current?.insertText(txt)}
insertEmoji={(em) => textareaRef.current?.insertEmoji(em)}
@ -3422,13 +3386,7 @@ export default function PostContent({ @@ -3422,13 +3386,7 @@ export default function PostContent({
onToggleMoreOptions={() => setShowMoreOptions((pre) => !pre)}
/>
</div>
<div className="flex gap-2 items-center shrink-0">
<Mentions
content={text}
parentEvent={parentEvent}
mentions={mentions}
setMentions={setMentions}
/>
<div className="flex shrink-0 items-center gap-1.5">
<div className="flex gap-2 items-center max-sm:hidden">
<Button
type="button"
@ -3480,15 +3438,37 @@ export default function PostContent({ @@ -3480,15 +3438,37 @@ export default function PostContent({
</div>
</div>
</div>
<PostOptions
posting={posting}
<PostEditorAdvancedPanel
show={showMoreOptions}
posting={posting}
addClientTag={addClientTag}
setAddClientTag={setAddClientTag}
isNsfw={isNsfw}
setIsNsfw={setIsNsfw}
minPow={minPow}
setMinPow={setMinPow}
showMentionsPicker={!isHighlight}
mentionsContent={text}
mentionsParentEvent={isPublicMessage ? undefined : parentEvent}
mentions={isPublicMessage ? extractedMentions : mentions}
setMentions={isPublicMessage ? setExtractedMentions : setMentions}
showRelayPicker={
!isPublicationContent &&
!isCitationInternal &&
!isCitationExternal &&
!isCitationHardcopy &&
!isCitationPrompt
}
setAdditionalRelayUrls={setAdditionalRelayUrls}
onRelayPublishCapChange={handleRelayPublishCapChange}
relayParentEvent={parentEvent}
relayOpenFrom={openFrom}
relayContent={text}
relayIsPublicMessage={isPublicMessage}
relayMentions={extractedMentions}
relayCapBlockInfo={relayCapBlockInfo}
discussionThreadRelayError={threadErrors.relay}
isDiscussionThread={isDiscussionThread}
/>
<div className="flex gap-2 items-center justify-around sm:hidden">
<Button

189
src/components/PostEditor/PostEditorAdvancedPanel.tsx

@ -0,0 +1,189 @@ @@ -0,0 +1,189 @@
import { MAX_PUBLISH_RELAYS } from '@/constants'
import { Label } from '@/components/ui/label'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import type { TPrePublishRelayCapPreview } from '@/lib/pre-publish-relay-cap'
import { cn } from '@/lib/utils'
import storage from '@/services/local-storage.service'
import type { Event } from 'nostr-tools'
import { Dispatch, SetStateAction, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Mentions from './Mentions'
import PostRelaySelector from './PostRelaySelector'
export type PostEditorAdvancedPanelProps = {
show: boolean
posting: boolean
addClientTag: boolean
setAddClientTag: Dispatch<SetStateAction<boolean>>
isNsfw: boolean
setIsNsfw: Dispatch<SetStateAction<boolean>>
minPow: number
setMinPow: Dispatch<SetStateAction<number>>
/** Relay picker + cap hints (hidden for modes that do not pick relays). */
showRelayPicker?: boolean
setAdditionalRelayUrls?: Dispatch<SetStateAction<string[]>>
onRelayPublishCapChange?: (preview: TPrePublishRelayCapPreview) => void
relayParentEvent?: Event
relayOpenFrom?: string[]
relayContent?: string
relayIsPublicMessage?: boolean
relayMentions?: string[]
relayCapBlockInfo?: {
outboxSlotsInPublish: number
selectedTotal: number
selectedContacted: number
} | null
discussionThreadRelayError?: string | null
isDiscussionThread?: boolean
/** Reply / PM mention recipient picker. */
showMentionsPicker?: boolean
mentionsContent?: string
mentionsParentEvent?: Event
mentions?: string[]
setMentions?: Dispatch<SetStateAction<string[]>>
}
export default function PostEditorAdvancedPanel({
show,
posting,
addClientTag,
setAddClientTag,
isNsfw,
setIsNsfw,
minPow,
setMinPow,
showRelayPicker = false,
setAdditionalRelayUrls,
onRelayPublishCapChange,
relayParentEvent,
relayOpenFrom,
relayContent = '',
relayIsPublicMessage = false,
relayMentions = [],
relayCapBlockInfo = null,
discussionThreadRelayError = null,
isDiscussionThread = false,
showMentionsPicker = false,
mentionsContent = '',
mentionsParentEvent,
mentions = [],
setMentions
}: PostEditorAdvancedPanelProps) {
const { t } = useTranslation()
useEffect(() => {
setAddClientTag(storage.getAddClientTag())
}, [setAddClientTag])
if (!show) return null
const onAddClientTagChange = (checked: boolean) => {
storage.setAddClientTag(checked)
setAddClientTag(checked)
}
return (
<div className="space-y-4 rounded-lg border border-border bg-muted/25 p-3">
<div>
<p className="text-sm font-medium">{t('Advanced')}</p>
<p className="text-xs text-muted-foreground mt-0.5">{t('Post editor advanced hint')}</p>
</div>
{showMentionsPicker && setMentions ? (
<div className="space-y-2">
<Label className="text-sm font-normal">{t('Mentions')}</Label>
<Mentions
content={mentionsContent}
parentEvent={mentionsParentEvent}
mentions={mentions}
setMentions={setMentions}
compactTrigger
/>
</div>
) : null}
{showRelayPicker && setAdditionalRelayUrls ? (
<div
className={cn(
'space-y-2',
isDiscussionThread && discussionThreadRelayError && 'rounded-md ring-1 ring-destructive p-2'
)}
>
<Label className="text-sm font-normal">{t('Post to')}</Label>
<PostRelaySelector
setAdditionalRelayUrls={setAdditionalRelayUrls}
onRelayPublishCapChange={onRelayPublishCapChange}
parentEvent={relayParentEvent}
openFrom={relayOpenFrom}
content={relayContent}
isPublicMessage={relayIsPublicMessage}
mentions={relayMentions}
/>
{relayCapBlockInfo ? (
<p className="text-sm text-amber-600 dark:text-amber-500" role="alert">
{relayCapBlockInfo.outboxSlotsInPublish > 0
? t('Publish relay cap hint with outbox first', {
max: MAX_PUBLISH_RELAYS,
reservedSlots: relayCapBlockInfo.outboxSlotsInPublish,
selected: relayCapBlockInfo.selectedTotal,
selectedContacted: relayCapBlockInfo.selectedContacted
})
: t('Publish relay cap hint', {
max: MAX_PUBLISH_RELAYS,
selected: relayCapBlockInfo.selectedTotal,
selectedContacted: relayCapBlockInfo.selectedContacted
})}
</p>
) : null}
{isDiscussionThread && discussionThreadRelayError ? (
<p className="text-sm text-destructive">{discussionThreadRelayError}</p>
) : null}
</div>
) : null}
<div className="space-y-4 pt-1 border-t border-border">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="add-client-tag" className="text-sm font-normal">
{t('Add client tag')}
</Label>
<Switch
id="add-client-tag"
checked={addClientTag}
onCheckedChange={onAddClientTagChange}
disabled={posting}
/>
</div>
<p className="text-muted-foreground text-xs">{t('Show others this was sent via Imwald')}</p>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="add-nsfw-tag" className="text-sm font-normal">
{t('NSFW')}
</Label>
<Switch
id="add-nsfw-tag"
checked={isNsfw}
onCheckedChange={setIsNsfw}
disabled={posting}
/>
</div>
<div className="grid gap-2">
<Label className="text-sm font-normal">
{t('Proof of Work (difficulty {{minPow}})', { minPow })}
</Label>
<Slider
defaultValue={[0]}
value={[minPow]}
onValueChange={([pow]) => setMinPow(pow)}
max={28}
step={1}
disabled={posting}
/>
</div>
</div>
</div>
)
}

30
src/components/PostEditor/PostEditorFormatToolbar.tsx

@ -3,7 +3,7 @@ import GifPicker from '@/components/GifPicker' @@ -3,7 +3,7 @@ import GifPicker from '@/components/GifPicker'
import MemePicker from '@/components/MemePicker'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { isTouchDevice } from '@/lib/utils'
import { cn, isTouchDevice } from '@/lib/utils'
import type { TEmoji } from '@/types'
import { Film, ImageUp, Laugh, Mic, Settings, Smile } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@ -46,8 +46,10 @@ export function PostEditorFormatToolbar({ @@ -46,8 +46,10 @@ export function PostEditorFormatToolbar({
}: PostEditorFormatToolbarProps) {
const { t } = useTranslation()
const iconBtnClass = 'h-8 w-8 shrink-0 p-0'
return (
<div className="flex flex-wrap items-center gap-2 min-w-0 shrink-0">
<div className="flex flex-nowrap items-center gap-0.5 min-w-0">
{showAudioUpload && (
<Uploader
onUploadSuccess={upload.onUploadSuccess}
@ -63,7 +65,7 @@ export function PostEditorFormatToolbar({ @@ -63,7 +65,7 @@ export function PostEditorFormatToolbar({
variant="ghost"
size="icon"
title={audioUploadTitle}
className={audioButtonHighlighted ? 'bg-accent' : ''}
className={cn(iconBtnClass, audioButtonHighlighted && 'bg-accent')}
>
<Mic className="h-4 w-4" />
</Button>
@ -78,11 +80,11 @@ export function PostEditorFormatToolbar({ @@ -78,11 +80,11 @@ export function PostEditorFormatToolbar({
onUploadCompressProgress={upload.onUploadCompressProgress}
accept="image/*"
>
<Button type="button" variant="ghost" size="icon" title={t('Upload Image')}>
<Button type="button" variant="ghost" size="icon" className={iconBtnClass} title={t('Upload Image')}>
<ImageUp />
</Button>
</Uploader>
<Separator orientation="vertical" className="h-6 shrink-0" />
<Separator orientation="vertical" className="mx-0.5 h-5 shrink-0 max-sm:hidden" />
{!isTouchDevice() && (
<EmojiPickerDialog
onEmojiClick={(emoji) => {
@ -90,29 +92,33 @@ export function PostEditorFormatToolbar({ @@ -90,29 +92,33 @@ export function PostEditorFormatToolbar({
insertEmoji(emoji)
}}
>
<Button type="button" variant="ghost" size="icon" title={t('Insert emoji')}>
<Button type="button" variant="ghost" size="icon" className={iconBtnClass} title={t('Insert emoji')}>
<Smile />
</Button>
</EmojiPickerDialog>
)}
<GifPicker onSelect={(gifUrl) => insertText(gifUrl)}>
<Button type="button" variant="ghost" size="icon" title={t('Insert GIF')}>
<Button type="button" variant="ghost" size="icon" className={iconBtnClass} title={t('Insert GIF')}>
<Film className="h-4 w-4" />
</Button>
</GifPicker>
<MemePicker onSelect={(memeUrl) => insertText(memeUrl)}>
<Button type="button" variant="ghost" size="icon" title={t('Insert meme')}>
<Button type="button" variant="ghost" size="icon" className={iconBtnClass} title={t('Insert meme')}>
<Laugh className="h-4 w-4" />
</Button>
</MemePicker>
<Separator orientation="vertical" className="h-6 shrink-0" />
<MentionAndEventToolbarButtons insertAtCursor={insertText} variant="ghost" />
<Separator orientation="vertical" className="mx-0.5 h-5 shrink-0 max-sm:hidden" />
<MentionAndEventToolbarButtons
insertAtCursor={insertText}
variant="ghost"
buttonClassName={iconBtnClass}
/>
<Button
type="button"
variant="ghost"
size="icon"
title={t('More options')}
className={showMoreOptions ? 'bg-accent' : ''}
title={t('Advanced')}
className={cn(iconBtnClass, showMoreOptions && 'bg-accent')}
onClick={onToggleMoreOptions}
>
<Settings />

84
src/components/PostEditor/PostOptions.tsx

@ -1,84 +0,0 @@ @@ -1,84 +0,0 @@
import { Label } from '@/components/ui/label'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import storage from '@/services/local-storage.service'
import { Dispatch, SetStateAction, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
export default function PostOptions({
posting,
show,
addClientTag,
setAddClientTag,
isNsfw,
setIsNsfw,
minPow,
setMinPow
}: {
posting: boolean
show: boolean
addClientTag: boolean
setAddClientTag: Dispatch<SetStateAction<boolean>>
isNsfw: boolean
setIsNsfw: Dispatch<SetStateAction<boolean>>
minPow: number
setMinPow: Dispatch<SetStateAction<number>>
}) {
const { t } = useTranslation()
useEffect(() => {
setAddClientTag(storage.getAddClientTag())
}, [])
if (!show) return null
const onAddClientTagChange = (checked: boolean) => {
storage.setAddClientTag(checked)
setAddClientTag(checked)
}
const onNsfwChange = (checked: boolean) => {
setIsNsfw(checked)
}
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="add-client-tag">{t('Add client tag')}</Label>
<Switch
id="add-client-tag"
checked={addClientTag}
onCheckedChange={onAddClientTagChange}
disabled={posting}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Show others this was sent via Imwald')}
</div>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="add-nsfw-tag">{t('NSFW')}</Label>
<Switch
id="add-nsfw-tag"
checked={isNsfw}
onCheckedChange={onNsfwChange}
disabled={posting}
/>
</div>
<div className="grid gap-4 pb-4">
<Label>{t('Proof of Work (difficulty {{minPow}})', { minPow })}</Label>
<Slider
defaultValue={[0]}
value={[minPow]}
onValueChange={([pow]) => setMinPow(pow)}
max={28}
step={1}
disabled={posting}
/>
</div>
</div>
)
}

2
src/components/PostEditor/PostTextarea/index.tsx

@ -282,7 +282,7 @@ const PostTextarea = forwardRef< @@ -282,7 +282,7 @@ const PostTextarea = forwardRef<
</TabsTrigger>
</TabsList>
{headerActions && (
<div className="flex gap-1 items-center flex-wrap">
<div className="flex min-w-0 flex-nowrap items-center justify-end gap-1 overflow-x-auto overscroll-x-contain">
{headerActions}
</div>
)}

2
src/components/Sidebar/index.tsx

@ -11,7 +11,6 @@ import SearchButton from './SearchButton' @@ -11,7 +11,6 @@ import SearchButton from './SearchButton'
import FavoritesButton from './FavoritesButton'
import DiscussionsButton from './DiscussionsButton'
import SpellsButton from './SpellsButton'
import { ConnectedRelaysSidebarStrip } from '@/components/ConnectedRelays/ConnectedRelaysSidebarStrip'
import PaneModeToggle from './PaneModeToggle'
import DownloadDesktopSidebarButton from './DownloadDesktopSidebarButton'
import LiveActivitiesStrip from '@/components/LiveActivitiesStrip'
@ -47,7 +46,6 @@ export default function PrimaryPageSidebar() { @@ -47,7 +46,6 @@ export default function PrimaryPageSidebar() {
<DiscussionsButton />
<SpellsButton />
<RssButton />
<ConnectedRelaysSidebarStrip />
<PostButton />
<div className="max-xl:hidden w-full min-w-0 space-y-2 px-1">
<LiveActivitiesStrip placement="sidebar" />

9
src/i18n/locales/de.ts

@ -69,7 +69,7 @@ export default { @@ -69,7 +69,7 @@ export default {
'load more older replies': 'ältere Antworten laden',
'Write something...': 'Schreibe etwas...',
Cancel: 'Abbrechen',
Mentions: '@',
Mentions: 'Erwähnungen',
'Search for event or address…': 'Nach Event oder Adresse suchen…',
'Search notes…': 'Notizen suchen…',
'No notes found': 'Keine Notizen gefunden',
@ -765,6 +765,13 @@ export default { @@ -765,6 +765,13 @@ export default {
'Wenn an, listet der Hinweis jedes Relay (angenommen, fehlgeschlagen, Fehlertext). Wenn aus, nur eine kurze Zusammenfassung.',
'Publishing feedback errors note':
'Fehlgeschlagene Veröffentlichungen und andere Fehler zeigen immer einen Hinweis — mit Kurzfassung oder Pro-Relay-Aufschlüsselung wie oben.',
Advanced: 'Erweitert',
'Post editor advanced hint':
'Relay-Ziele, Erwähnungen, Client-Tag, NSFW und Proof-of-Work.',
'Open Advanced to adjust mention recipients':
'Öffne Erweitert, um Empfänger anzupassen.',
'Add recipients using nostr: mentions (e.g., nostr:npub1...) or open Advanced':
'Erwähne nostr:npub… oder nostr:nevent… im Text oder öffne Erweitert für die Empfängerauswahl.',
'Publish successful': 'Veröffentlichung erfolgreich',
'Media upload service': 'Medien-Upload-Service',
BlossomUploadYourListOption: 'Blossom (eigene Liste)',

4
src/i18n/locales/en.ts

@ -293,6 +293,10 @@ export default { @@ -293,6 +293,10 @@ export default {
Connections: 'Connections',
Calls: 'Calls',
Advanced: 'Advanced',
'Post editor advanced hint': 'Relay targets, mention recipients, client tag, NSFW, and proof of work.',
'Open Advanced to adjust mention recipients': 'Open Advanced to adjust who receives this message.',
'Add recipients using nostr: mentions (e.g., nostr:npub1...) or open Advanced':
'Add nostr:npub… or nostr:nevent… mentions in the text, or open Advanced to pick recipients.',
'Share with Imwald': 'Share with Imwald',
'Share with Alexandria': 'Share with Alexandria',
'Start video call': 'Start video call',

Loading…
Cancel
Save