feat: add emoji autocompletion to zap comments and refine UI

Zap Comment Enhancements:
- Replace plain Input with MentionEditor for emoji autocompletion
- Add NIP-30 emoji tag support to zap requests (kind 9734)
- Emoji tags are properly serialized and included in zap events
- Support :emoji: syntax with custom emoji from emoji search

Event Preview Refinements:
- Remove Card wrapper from zapped event preview
- Remove padding and borders for cleaner display
- Event renders directly without container styling

Implementation Details:
- Add EmojiTag interface to create-zap-request.ts
- Update ZapRequestParams to include emojiTags array
- Extract emoji tags from MentionEditor in handleZap
- Pass emoji tags through zap request creation pipeline
- Add useProfileSearch and useEmojiSearch hooks to ZapWindow
- Use MentionEditor ref to get serialized content with emojis

All tests passing (939 passed)
Build successful
This commit is contained in:
Claude
2026-01-18 20:52:42 +00:00
parent db200a2d93
commit 07c17d5bae
2 changed files with 43 additions and 21 deletions

View File

@@ -11,7 +11,7 @@
* - Shows feed render of zapped event
*/
import { useState, useMemo, useEffect } from "react";
import { useState, useMemo, useEffect, useRef } from "react";
import { toast } from "sonner";
import {
Zap,
@@ -24,7 +24,6 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
@@ -43,6 +42,12 @@ import { KindRenderer } from "./nostr/kinds";
import type { EventPointer, AddressPointer } from "@/lib/open-parser";
import { useGrimoire } from "@/core/state";
import accountManager from "@/services/accounts";
import {
MentionEditor,
type MentionEditorHandle,
} from "./editor/MentionEditor";
import { useEmojiSearch } from "@/hooks/useEmojiSearch";
import { useProfileSearch } from "@/hooks/useProfileSearch";
export interface ZapWindowProps {
/** Recipient pubkey (who receives the zap) */
@@ -99,13 +104,17 @@ export function ZapWindow({
const [selectedAmount, setSelectedAmount] = useState<number | null>(null);
const [customAmount, setCustomAmount] = useState("");
const [comment, setComment] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [isPaid, setIsPaid] = useState(false);
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [invoice, setInvoice] = useState<string>("");
const [showQrDialog, setShowQrDialog] = useState(false);
// Editor ref and search functions
const editorRef = useRef<MentionEditorHandle>(null);
const { searchProfiles } = useProfileSearch();
const { searchEmojis } = useEmojiSearch();
// Load custom amounts and usage stats from localStorage
const [customAmounts, setCustomAmounts] = useState<number[]>(() => {
const stored = localStorage.getItem(STORAGE_KEY_CUSTOM_AMOUNTS);
@@ -235,6 +244,15 @@ export function ZapWindow({
);
}
// Get comment and emoji tags from editor
const serialized = editorRef.current?.getSerializedContent() || {
text: "",
emojiTags: [],
blobAttachments: [],
};
const comment = serialized.text;
const emojiTags = serialized.emojiTags;
// Validate comment length if provided
if (comment && lnurlData.commentAllowed) {
if (comment.length > lnurlData.commentAllowed) {
@@ -255,6 +273,7 @@ export function ZapWindow({
comment,
eventPointer,
lnurl: lud16 || undefined,
emojiTags,
});
const serializedZapRequest = serializeZapRequest(zapRequest);
@@ -331,18 +350,7 @@ export function ZapWindow({
<div className="flex-1 overflow-y-auto">
<div className="max-w-2xl mx-auto p-6 space-y-6">
{/* Show event preview if zapping an event */}
{event && (
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Zapping Event
</CardTitle>
</CardHeader>
<CardContent>
<KindRenderer event={event} />
</CardContent>
</Card>
)}
{event && <KindRenderer event={event} />}
{/* Amount Selection */}
<div className="space-y-4">
@@ -386,15 +394,15 @@ export function ZapWindow({
/>
</div>
{/* Comment */}
{/* Comment with emoji support */}
<div className="space-y-2">
<Label>Comment (optional)</Label>
<Input
id="comment"
<MentionEditor
ref={editorRef}
placeholder="Say something nice..."
value={comment}
onChange={(e) => setComment(e.target.value)}
maxLength={200}
searchProfiles={searchProfiles}
searchEmojis={searchEmojis}
className="min-h-[60px] rounded-md border border-input bg-background px-3 py-2"
/>
</div>
</div>

View File

@@ -9,6 +9,11 @@ import accountManager from "@/services/accounts";
import { relayListCache } from "@/services/relay-list-cache";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
export interface EmojiTag {
shortcode: string;
url: string;
}
export interface ZapRequestParams {
/** Recipient pubkey (who receives the zap) */
recipientPubkey: string;
@@ -22,6 +27,8 @@ export interface ZapRequestParams {
relays?: string[];
/** LNURL for the recipient */
lnurl?: string;
/** NIP-30 custom emoji tags */
emojiTags?: EmojiTag[];
}
/**
@@ -87,6 +94,13 @@ export async function createZapRequest(
}
}
// Add NIP-30 emoji tags
if (params.emojiTags) {
for (const emoji of params.emojiTags) {
tags.push(["emoji", emoji.shortcode, emoji.url]);
}
}
// Create event template
const template = {
kind: 9734,