fix: make mentions list scrollable

This commit is contained in:
codytseng
2025-10-25 17:51:14 +08:00
parent 4fedc8bece
commit e089aa9663
2 changed files with 134 additions and 56 deletions

View File

@@ -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>
)
}

View File

@@ -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>