15 changed files with 423 additions and 346 deletions
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
@ -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> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -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 |
||||||
|
} |
||||||
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
@ -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> |
|
||||||
) |
|
||||||
} |
|
||||||
Loading…
Reference in new issue