|
|
|
|
@ -7,13 +7,17 @@ import noteStatsService from '@/services/note-stats.service'
@@ -7,13 +7,17 @@ import noteStatsService from '@/services/note-stats.service'
|
|
|
|
|
import { TEmoji } from '@/types' |
|
|
|
|
import { Loader } from 'lucide-react' |
|
|
|
|
import { Event } from 'nostr-tools' |
|
|
|
|
import { useMemo, useState } from 'react' |
|
|
|
|
import { useMemo, useRef, useState } from 'react' |
|
|
|
|
import Emoji from '../Emoji' |
|
|
|
|
|
|
|
|
|
export default function Likes({ event }: { event: Event }) { |
|
|
|
|
const { pubkey, checkLogin, publish } = useNostr() |
|
|
|
|
const noteStats = useNoteStatsById(event.id) |
|
|
|
|
const [liking, setLiking] = useState<string | null>(null) |
|
|
|
|
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null) |
|
|
|
|
const [isLongPressing, setIsLongPressing] = useState<string | null>(null) |
|
|
|
|
const [isCompleted, setIsCompleted] = useState<string | null>(null) |
|
|
|
|
|
|
|
|
|
const likes = useMemo(() => { |
|
|
|
|
const _likes = noteStats?.likes |
|
|
|
|
if (!_likes) return [] |
|
|
|
|
@ -51,6 +55,59 @@ export default function Likes({ event }: { event: Event }) {
@@ -51,6 +55,59 @@ export default function Likes({ event }: { event: Event }) {
|
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleMouseDown = (key: string) => { |
|
|
|
|
if (pubkey && likes.find((l) => l.key === key)?.pubkeys.has(pubkey)) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setIsLongPressing(key) |
|
|
|
|
longPressTimerRef.current = setTimeout(() => { |
|
|
|
|
setIsCompleted(key) |
|
|
|
|
setIsLongPressing(null) |
|
|
|
|
}, 1000) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleMouseUp = () => { |
|
|
|
|
if (longPressTimerRef.current) { |
|
|
|
|
clearTimeout(longPressTimerRef.current) |
|
|
|
|
longPressTimerRef.current = null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (isCompleted) { |
|
|
|
|
const completedKey = isCompleted |
|
|
|
|
const completedEmoji = likes.find((l) => l.key === completedKey)?.emoji |
|
|
|
|
if (completedEmoji) { |
|
|
|
|
like(completedKey, completedEmoji) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setIsLongPressing(null) |
|
|
|
|
setIsCompleted(null) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleMouseLeave = () => { |
|
|
|
|
if (longPressTimerRef.current) { |
|
|
|
|
clearTimeout(longPressTimerRef.current) |
|
|
|
|
longPressTimerRef.current = null |
|
|
|
|
} |
|
|
|
|
setIsLongPressing(null) |
|
|
|
|
setIsCompleted(null) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const handleTouchMove = (e: React.TouchEvent) => { |
|
|
|
|
const touch = e.touches[0] |
|
|
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() |
|
|
|
|
const isInside = |
|
|
|
|
touch.clientX >= rect.left && |
|
|
|
|
touch.clientX <= rect.right && |
|
|
|
|
touch.clientY >= rect.top && |
|
|
|
|
touch.clientY <= rect.bottom |
|
|
|
|
|
|
|
|
|
if (!isInside) { |
|
|
|
|
handleMouseLeave() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<ScrollArea className="pb-2 mb-1"> |
|
|
|
|
<div className="flex gap-1"> |
|
|
|
|
@ -58,25 +115,46 @@ export default function Likes({ event }: { event: Event }) {
@@ -58,25 +115,46 @@ export default function Likes({ event }: { event: Event }) {
|
|
|
|
|
<div |
|
|
|
|
key={key} |
|
|
|
|
className={cn( |
|
|
|
|
'flex h-7 w-fit gap-2 px-2 rounded-full items-center border shrink-0', |
|
|
|
|
'flex h-7 w-fit gap-2 px-2 rounded-full items-center border shrink-0 select-none relative overflow-hidden transition-all duration-200', |
|
|
|
|
pubkey && pubkeys.has(pubkey) |
|
|
|
|
? 'border-primary bg-primary/20 text-foreground cursor-not-allowed' |
|
|
|
|
: 'transition-colors bg-muted/80 text-muted-foreground cursor-pointer hover:bg-primary/40 hover:border-primary hover:text-foreground' |
|
|
|
|
: 'bg-muted/80 text-muted-foreground cursor-pointer hover:bg-primary/40 hover:border-primary hover:text-foreground', |
|
|
|
|
(isLongPressing === key || isCompleted === key) && 'border-primary bg-primary/20' |
|
|
|
|
)} |
|
|
|
|
onClick={(e) => { |
|
|
|
|
e.stopPropagation() |
|
|
|
|
if (pubkey && pubkeys.has(pubkey)) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
like(key, emoji) |
|
|
|
|
}} |
|
|
|
|
onMouseDown={() => handleMouseDown(key)} |
|
|
|
|
onMouseUp={handleMouseUp} |
|
|
|
|
onMouseLeave={handleMouseLeave} |
|
|
|
|
onTouchStart={() => handleMouseDown(key)} |
|
|
|
|
onTouchMove={handleTouchMove} |
|
|
|
|
onTouchEnd={handleMouseUp} |
|
|
|
|
onTouchCancel={handleMouseLeave} |
|
|
|
|
> |
|
|
|
|
{liking === key ? ( |
|
|
|
|
<Loader className="animate-spin size-4" /> |
|
|
|
|
) : ( |
|
|
|
|
<Emoji emoji={emoji} classNames={{ img: 'size-4' }} /> |
|
|
|
|
{(isLongPressing === key || isCompleted === key) && ( |
|
|
|
|
<div className="absolute inset-0 rounded-full overflow-hidden"> |
|
|
|
|
<div |
|
|
|
|
className="h-full bg-gradient-to-r from-primary/40 via-primary/60 to-primary/80" |
|
|
|
|
style={{ |
|
|
|
|
width: isCompleted === key ? '100%' : '0%', |
|
|
|
|
animation: |
|
|
|
|
isLongPressing === key ? 'progressFill 1000ms ease-out forwards' : 'none' |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
<div className="text-sm">{pubkeys.size}</div> |
|
|
|
|
<div className="relative z-10 flex items-center gap-2"> |
|
|
|
|
{liking === key ? ( |
|
|
|
|
<Loader className="animate-spin size-4" /> |
|
|
|
|
) : ( |
|
|
|
|
<div |
|
|
|
|
style={{ |
|
|
|
|
animation: isCompleted === key ? 'shake 0.5s ease-in-out infinite' : undefined |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<Emoji emoji={emoji} classNames={{ img: 'size-4' }} /> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
<div className="text-sm">{pubkeys.size}</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
))} |
|
|
|
|
</div> |
|
|
|
|
|