feat: quick reaction
This commit is contained in:
parent
77d56265ad
commit
33fa1ec441
23 changed files with 305 additions and 47 deletions
|
|
@ -1,25 +1,22 @@
|
|||
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
||||
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
||||
import { BIG_RELAY_URLS, LONG_PRESS_THRESHOLD } from '@/constants'
|
||||
import { useStuff } from '@/hooks/useStuff'
|
||||
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
||||
import {
|
||||
createExternalContentReactionDraftEvent,
|
||||
createReactionDraftEvent
|
||||
} from '@/lib/draft-event'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import client from '@/services/client.service'
|
||||
import stuffStatsService from '@/services/stuff-stats.service'
|
||||
import { TEmoji } from '@/types'
|
||||
import { Loader, SmilePlus } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Emoji from '../Emoji'
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
|
|
@ -31,10 +28,13 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
|||
const { isSmallScreen } = useScreenSize()
|
||||
const { pubkey, publish, checkLogin } = useNostr()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { quickReaction, quickReactionEmoji } = useUserPreferences()
|
||||
const { event, externalContent, stuffKey } = useStuff(stuff)
|
||||
const [liking, setLiking] = useState(false)
|
||||
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const isLongPressRef = useRef(false)
|
||||
const noteStats = useStuffStatsById(stuffKey)
|
||||
const { myLastEmoji, likeCount } = useMemo(() => {
|
||||
const stats = noteStats || {}
|
||||
|
|
@ -45,6 +45,10 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
|||
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length }
|
||||
}, [noteStats, pubkey, hideUntrustedInteractions])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setIsPickerOpen(false), 100)
|
||||
}, [isEmojiReactionsOpen])
|
||||
|
||||
const like = async (emoji: string | TEmoji) => {
|
||||
checkLogin(async () => {
|
||||
if (liking || !pubkey) return
|
||||
|
|
@ -72,16 +76,50 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
|||
})
|
||||
}
|
||||
|
||||
const handleLongPressStart = () => {
|
||||
if (!quickReaction) return
|
||||
isLongPressRef.current = false
|
||||
longPressTimerRef.current = setTimeout(() => {
|
||||
isLongPressRef.current = true
|
||||
setIsEmojiReactionsOpen(true)
|
||||
}, LONG_PRESS_THRESHOLD)
|
||||
}
|
||||
|
||||
const handleLongPressEnd = () => {
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current)
|
||||
longPressTimerRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
if (quickReaction) {
|
||||
// If it was a long press, don't trigger the click action
|
||||
if (isLongPressRef.current) {
|
||||
isLongPressRef.current = false
|
||||
return
|
||||
}
|
||||
// Quick reaction mode: click to react with default emoji
|
||||
// Prevent dropdown from opening
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
like(quickReactionEmoji)
|
||||
} else {
|
||||
setIsEmojiReactionsOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const trigger = (
|
||||
<button
|
||||
className="flex items-center enabled:hover:text-primary gap-1 px-3 h-full text-muted-foreground"
|
||||
title={t('Like')}
|
||||
disabled={liking}
|
||||
onClick={() => {
|
||||
if (isSmallScreen) {
|
||||
setIsEmojiReactionsOpen(true)
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleLongPressStart}
|
||||
onMouseUp={handleLongPressEnd}
|
||||
onMouseLeave={handleLongPressEnd}
|
||||
onTouchStart={handleLongPressStart}
|
||||
onTouchEnd={handleLongPressEnd}
|
||||
>
|
||||
{liking ? (
|
||||
<Loader className="animate-spin" />
|
||||
|
|
@ -121,17 +159,9 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={isEmojiReactionsOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsEmojiReactionsOpen(open)
|
||||
if (open) {
|
||||
setIsPickerOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="p-0 w-fit">
|
||||
<Popover open={isEmojiReactionsOpen} onOpenChange={(open) => setIsEmojiReactionsOpen(open)}>
|
||||
<PopoverAnchor asChild>{trigger}</PopoverAnchor>
|
||||
<PopoverContent side="top" className="p-0 w-fit border-0 shadow-lg">
|
||||
{isPickerOpen ? (
|
||||
<EmojiPicker
|
||||
onEmojiClick={(emoji, e) => {
|
||||
|
|
@ -153,7 +183,7 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,47 @@ import * as React from 'react'
|
|||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const Popover = ({
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>) => {
|
||||
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) => {
|
||||
if (!isControlled) {
|
||||
setUncontrolledOpen(newOpen)
|
||||
}
|
||||
controlledOnOpenChange?.(newOpen)
|
||||
},
|
||||
[isControlled, controlledOnOpenChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{open &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={backdropRef}
|
||||
className="fixed inset-0 z-40 pointer-events-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleOpenChange(false)
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
<PopoverPrimitive.Root {...props} open={open} onOpenChange={handleOpenChange} modal={false} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
Popover.displayName = 'Popover'
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue