Replace zap button with emoji reaction button on user profiles
Add ProfileReactionButton component that opens an emoji picker to send kind 7 reactions to a user's profile with a, e, and p tags. Update notifications to display 'reacted to your profile' for profile reactions and skip rendering the referenced card for kind 0 events.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { SmilePlus } from 'lucide-react';
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { QuickReactMenu } from '@/components/QuickReactMenu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import { useEmojiUsage } from '@/hooks/useEmojiUsage';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
interface ProfileReactionButtonProps {
|
||||
/** The kind 0 metadata event for the profile being reacted to. */
|
||||
profileEvent: NostrEvent;
|
||||
/** Optional extra class names for the trigger button. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji reaction button for user profiles.
|
||||
* Opens an emoji picker and publishes a kind 7 reaction targeting
|
||||
* the user's kind 0 profile event with `a`, `e`, and `p` tags.
|
||||
*/
|
||||
export function ProfileReactionButton({ profileEvent, className }: ProfileReactionButtonProps) {
|
||||
const { user } = useCurrentUser();
|
||||
const { mutate: publishEvent } = useNostrPublish();
|
||||
const { trackEmojiUsage } = useEmojiUsage();
|
||||
const { toast } = useToast();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const pickerExpandedRef = useRef(false);
|
||||
const justClosedRef = useRef(false);
|
||||
|
||||
const handleReact = useCallback((emoji: string, emojiTag?: string[]) => {
|
||||
if (!user) return;
|
||||
|
||||
trackEmojiUsage(emoji);
|
||||
|
||||
const tags: string[][] = [
|
||||
['e', profileEvent.id],
|
||||
['p', profileEvent.pubkey],
|
||||
['a', `0:${profileEvent.pubkey}:`],
|
||||
['k', '0'],
|
||||
];
|
||||
if (emojiTag) tags.push(emojiTag);
|
||||
|
||||
publishEvent(
|
||||
{
|
||||
kind: 7,
|
||||
content: emoji,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({ title: 'Reaction sent!' });
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [user, profileEvent, publishEvent, trackEmojiUsage, toast]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={menuOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (open && justClosedRef.current) return;
|
||||
if (!open) pickerExpandedRef.current = false;
|
||||
setMenuOpen(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={className ?? 'rounded-full size-10'}
|
||||
title="React to this profile"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (justClosedRef.current) return;
|
||||
setMenuOpen((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
<SmilePlus className="size-5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0 border-0 bg-transparent shadow-none"
|
||||
side="top"
|
||||
align="start"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<QuickReactMenu
|
||||
eventId={profileEvent.id}
|
||||
eventPubkey={profileEvent.pubkey}
|
||||
eventKind={0}
|
||||
onReact={handleReact}
|
||||
onExpandChange={(expanded) => {
|
||||
pickerExpandedRef.current = expanded;
|
||||
}}
|
||||
onClose={() => {
|
||||
pickerExpandedRef.current = false;
|
||||
justClosedRef.current = true;
|
||||
setMenuOpen(false);
|
||||
setTimeout(() => {
|
||||
justClosedRef.current = false;
|
||||
}, 300);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,7 @@ type NotificationTab = 'all' | 'mentions';
|
||||
* Falls back to "post" for unknown kinds.
|
||||
*/
|
||||
const NOTIFICATION_KIND_NOUNS: Record<number, string> = {
|
||||
0: 'profile',
|
||||
1: 'post',
|
||||
4: 'encrypted message',
|
||||
6: 'repost',
|
||||
@@ -459,10 +460,12 @@ function ActorLink({ pubkey, name }: { pubkey: string; name: string }) {
|
||||
// Like Notification (single actor)
|
||||
// ──────────────────────────────────────
|
||||
function LikeNotification({ item, isNew }: { item: NotificationItem; isNew: boolean }) {
|
||||
const noun = getNotificationKindNoun(item.referencedEvent?.kind);
|
||||
const isProfileReaction = item.referencedEvent?.kind === 0
|
||||
|| item.event.tags.some(([name, value]) => name === 'a' && value?.startsWith('0:'));
|
||||
const noun = isProfileReaction ? 'profile' : getNotificationKindNoun(item.referencedEvent?.kind);
|
||||
return (
|
||||
<NotificationWrapper isNew={isNew}>
|
||||
<div className="px-4 pt-3">
|
||||
<div className={cn('px-4 pt-3', isProfileReaction && 'pb-3')}>
|
||||
<NotificationHeader
|
||||
actorPubkey={item.event.pubkey}
|
||||
icon={
|
||||
@@ -473,7 +476,7 @@ function LikeNotification({ item, isNew }: { item: NotificationItem; isNew: bool
|
||||
action={`reacted to your ${noun}`}
|
||||
/>
|
||||
</div>
|
||||
<ReferencedNoteCard item={item} />
|
||||
{!isProfileReaction && <ReferencedNoteCard item={item} />}
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
@@ -558,7 +561,9 @@ function ZapNotification({ item, isNew }: { item: NotificationItem; isNew: boole
|
||||
function LikeNotificationGroup({ group }: { group: GroupedNotificationItem }) {
|
||||
// Use the first actor's reaction emoji as the icon
|
||||
const firstEvent = group.actors[0].event;
|
||||
const noun = getNotificationKindNoun(group.referencedEvent?.kind);
|
||||
const isProfileReaction = group.referencedEvent?.kind === 0
|
||||
|| firstEvent.tags.some(([name, value]) => name === 'a' && value?.startsWith('0:'));
|
||||
const noun = isProfileReaction ? 'profile' : getNotificationKindNoun(group.referencedEvent?.kind);
|
||||
return (
|
||||
<NotificationWrapper isNew={group.isNew}>
|
||||
<GroupHeader
|
||||
@@ -570,7 +575,7 @@ function LikeNotificationGroup({ group }: { group: GroupedNotificationItem }) {
|
||||
}
|
||||
action={`reacted to your ${noun}`}
|
||||
/>
|
||||
<ReferencedNoteCard item={group.actors[0]} />
|
||||
{!isProfileReaction && <ReferencedNoteCard item={group.actors[0]} />}
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ProfileRightSidebar } from '@/components/ProfileRightSidebar';
|
||||
import { NoteCard } from '@/components/NoteCard';
|
||||
import { ComposeBox } from '@/components/ComposeBox';
|
||||
import { ReplyComposeModal } from '@/components/ReplyComposeModal';
|
||||
import { ZapDialog } from '@/components/ZapDialog';
|
||||
import { ProfileReactionButton } from '@/components/ProfileReactionButton';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { Nip05Badge, VerifiedNip05Text } from '@/components/Nip05Badge';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
@@ -44,7 +44,6 @@ import { ThreadedReplyList } from '@/components/ThreadedReplyList';
|
||||
import { useNip05Resolve } from '@/hooks/useNip05Resolve';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
|
||||
import { canZap } from '@/lib/canZap';
|
||||
import { shareOrCopy } from '@/lib/share';
|
||||
import { openUrl } from '@/lib/downloadFile';
|
||||
import { EmojifiedText } from '@/components/CustomEmoji';
|
||||
@@ -2077,13 +2076,9 @@ type EditableTab = { label: string; isCore: boolean; tab?: ProfileTab };
|
||||
<Share2 className="size-5" />
|
||||
</Button>
|
||||
)}
|
||||
{/* Zap button */}
|
||||
{!isOwnProfile && authorEvent && canZap(metadata) && (
|
||||
<ZapDialog target={authorEvent}>
|
||||
<Button variant="outline" size="icon" className="rounded-full size-10" title="Zap this user">
|
||||
<Zap className="size-5" />
|
||||
</Button>
|
||||
</ZapDialog>
|
||||
{/* Profile reaction button */}
|
||||
{!isOwnProfile && authorEvent && (
|
||||
<ProfileReactionButton profileEvent={authorEvent} />
|
||||
)}
|
||||
{isOwnProfile ? (
|
||||
<Link to="/settings/profile">
|
||||
|
||||
Reference in New Issue
Block a user