fix: make mentions list scrollable
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Check } from 'lucide-react'
|
||||
import { Event, nip19 } from 'nostr-tools'
|
||||
import { HTMLAttributes, useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SimpleUserAvatar } from '../UserAvatar'
|
||||
import { SimpleUsername } from '../Username'
|
||||
@@ -22,6 +30,8 @@ export default function Mentions({
|
||||
parentEvent?: Event
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
const { pubkey } = useNostr()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const [potentialMentions, setPotentialMentions] = useState<string[]>([])
|
||||
@@ -56,9 +66,67 @@ export default function Mentions({
|
||||
setMentions(newMentions)
|
||||
}, [potentialMentions, removedPubkeys])
|
||||
|
||||
const items = useMemo(() => {
|
||||
return potentialMentions.map((_, index) => {
|
||||
const pubkey = potentialMentions[potentialMentions.length - 1 - index]
|
||||
const isParentPubkey = pubkey === parentEventPubkey
|
||||
return (
|
||||
<MenuItem
|
||||
key={`${pubkey}-${index}`}
|
||||
checked={isParentPubkey ? true : mentions.includes(pubkey)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (isParentPubkey) {
|
||||
return
|
||||
}
|
||||
if (checked) {
|
||||
setRemovedPubkeys((pubkeys) => pubkeys.filter((p) => p !== pubkey))
|
||||
} else {
|
||||
setRemovedPubkeys((pubkeys) => [...pubkeys, pubkey])
|
||||
}
|
||||
}}
|
||||
disabled={isParentPubkey}
|
||||
>
|
||||
<SimpleUserAvatar userId={pubkey} size="small" />
|
||||
<SimpleUsername
|
||||
userId={pubkey}
|
||||
className="font-semibold text-sm truncate"
|
||||
skeletonClassName="h-3"
|
||||
/>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}, [potentialMentions, parentEventPubkey, mentions])
|
||||
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="px-3"
|
||||
variant="ghost"
|
||||
disabled={potentialMentions.length === 0}
|
||||
onClick={() => setIsDrawerOpen(true)}
|
||||
>
|
||||
{t('Mentions')}{' '}
|
||||
{potentialMentions.length > 0 && `(${mentions.length}/${potentialMentions.length})`}
|
||||
</Button>
|
||||
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
|
||||
<DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
|
||||
<DrawerContent className="max-h-[80vh]" hideOverlay>
|
||||
<div
|
||||
className="overflow-y-auto overscroll-contain py-2"
|
||||
style={{ touchAction: 'pan-y' }}
|
||||
>
|
||||
{items}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="px-3"
|
||||
variant="ghost"
|
||||
@@ -68,67 +136,57 @@ export default function Mentions({
|
||||
{t('Mentions')}{' '}
|
||||
{potentialMentions.length > 0 && `(${mentions.length}/${potentialMentions.length})`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52 p-0 py-1">
|
||||
<div className="space-y-1">
|
||||
{potentialMentions.map((_, index) => {
|
||||
const pubkey = potentialMentions[potentialMentions.length - 1 - index]
|
||||
const isParentPubkey = pubkey === parentEventPubkey
|
||||
return (
|
||||
<PopoverCheckboxItem
|
||||
key={`${pubkey}-${index}`}
|
||||
checked={isParentPubkey ? true : mentions.includes(pubkey)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (isParentPubkey) {
|
||||
return
|
||||
}
|
||||
if (checked) {
|
||||
setRemovedPubkeys((pubkeys) => pubkeys.filter((p) => p !== pubkey))
|
||||
} else {
|
||||
setRemovedPubkeys((pubkeys) => [...pubkeys, pubkey])
|
||||
}
|
||||
}}
|
||||
disabled={isParentPubkey}
|
||||
>
|
||||
<SimpleUserAvatar userId={pubkey} size="small" />
|
||||
<SimpleUsername
|
||||
userId={pubkey}
|
||||
className="font-semibold text-sm truncate"
|
||||
skeletonClassName="h-3"
|
||||
/>
|
||||
</PopoverCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="max-w-96 max-h-[50vh]" showScrollButtons>
|
||||
{items}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverCheckboxItem({
|
||||
function MenuItem({
|
||||
children,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLButtonElement> & {
|
||||
disabled?: boolean
|
||||
onCheckedChange
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
checked: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
disabled?: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="px-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full rounded-md justify-start px-2"
|
||||
onClick={() => onCheckedChange?.(!checked)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
onCheckedChange(!checked)
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-3 clickable',
|
||||
disabled ? 'opacity-50 pointer-events-none' : ''
|
||||
)}
|
||||
>
|
||||
{checked ? <Check className="shrink-0" /> : <div className="w-4 shrink-0" />}
|
||||
<div className="flex items-center justify-center size-4 shrink-0">
|
||||
{checked && <Check className="size-4" />}
|
||||
</div>
|
||||
{children}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
onCheckedChange={onCheckedChange}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const DropdownMenu = ({
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
||||
const isControlled = controlledOpen !== undefined
|
||||
const open = isControlled ? controlledOpen : uncontrolledOpen
|
||||
const backdropRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(newOpen: boolean) => {
|
||||
@@ -24,11 +25,29 @@ const DropdownMenu = ({
|
||||
[isControlled, controlledOnOpenChange]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
const preventScroll = (e: Event) => e.preventDefault()
|
||||
|
||||
document.addEventListener('wheel', preventScroll, { passive: false })
|
||||
document.addEventListener('touchmove', preventScroll, { passive: false })
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('wheel', preventScroll)
|
||||
document.removeEventListener('touchmove', preventScroll)
|
||||
}
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<>
|
||||
{open &&
|
||||
createPortal(
|
||||
<div className="fixed inset-0 z-50" onClick={() => handleOpenChange(false)} />,
|
||||
<div
|
||||
ref={backdropRef}
|
||||
className="fixed inset-0 z-50"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
<DropdownMenuPrimitive.Root
|
||||
@@ -224,6 +243,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={scrollAreaRef}
|
||||
className={cn('p-1 overflow-y-auto', className)}
|
||||
onScroll={checkScrollability}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user