mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 17:07:27 +02:00
feat: Unify emoji rendering with CustomEmoji and UnicodeEmoji components
- Add xs size variant (size-3.5) to CustomEmoji for small contexts like badges - Add showTooltip prop to CustomEmoji to optionally disable tooltip - Create UnicodeEmoji component with matching size variants (xs, sm, md, lg) - Update RichText/Emoji.tsx to use CustomEmoji for inline custom emoji - Update MessageReactions to use CustomEmoji and UnicodeEmoji - Update ReactionRenderer to use CustomEmoji and UnicodeEmoji - Update ReactionCompactPreview to use CustomEmoji and UnicodeEmoji - Update EmojiSuggestionList to use CustomEmoji and UnicodeEmoji - Update EmojiPickerDialog to use UnicodeEmoji This provides consistent emoji rendering across all components with: - Unified size variants: xs (14px), sm (16px), md (24px), lg (48px) - Proper error handling for custom emoji with fallback display - Optional tooltips showing shortcodes - Accessible aria-labels for screen readers
This commit is contained in:
@@ -7,6 +7,7 @@ import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import { useEmojiSearch } from "@/hooks/useEmojiSearch";
|
||||
import { useEmojiFrequency } from "@/hooks/useEmojiFrequency";
|
||||
import { CustomEmoji } from "../nostr/CustomEmoji";
|
||||
import { UnicodeEmoji } from "../nostr/UnicodeEmoji";
|
||||
|
||||
interface EmojiPickerDialogProps {
|
||||
open: boolean;
|
||||
@@ -173,7 +174,12 @@ export function EmojiPickerDialog({
|
||||
title={`:${result.shortcode}:`}
|
||||
>
|
||||
{result.source === "unicode" ? (
|
||||
<span className="text-xl leading-none">{result.url}</span>
|
||||
<UnicodeEmoji
|
||||
emoji={result.url}
|
||||
shortcode={result.shortcode}
|
||||
size="md"
|
||||
showTooltip={false}
|
||||
/>
|
||||
) : (
|
||||
<CustomEmoji
|
||||
size="md"
|
||||
|
||||
@@ -5,6 +5,8 @@ import eventStore from "@/services/event-store";
|
||||
import pool from "@/services/relay-pool";
|
||||
import accountManager from "@/services/accounts";
|
||||
import { EMOJI_SHORTCODE_REGEX } from "@/lib/emoji-helpers";
|
||||
import { CustomEmoji } from "../nostr/CustomEmoji";
|
||||
import { UnicodeEmoji } from "../nostr/UnicodeEmoji";
|
||||
|
||||
interface MessageReactionsProps {
|
||||
messageId: string;
|
||||
@@ -177,15 +179,20 @@ function ReactionBadge({ reaction }: { reaction: ReactionSummary }) {
|
||||
title={tooltip}
|
||||
>
|
||||
{reaction.customEmoji ? (
|
||||
<img
|
||||
src={reaction.customEmoji.url}
|
||||
alt={`:${reaction.customEmoji.shortcode}:`}
|
||||
className="size-3.5 flex-shrink-0 object-contain"
|
||||
<CustomEmoji
|
||||
shortcode={reaction.customEmoji.shortcode}
|
||||
url={reaction.customEmoji.url}
|
||||
size="xs"
|
||||
showTooltip={false}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs leading-none flex-shrink-0">
|
||||
{reaction.emoji}
|
||||
</span>
|
||||
<UnicodeEmoji
|
||||
emoji={reaction.emoji}
|
||||
size="xs"
|
||||
showTooltip={false}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
} from "react";
|
||||
import type { EmojiSearchResult } from "@/services/emoji-search";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CustomEmoji } from "@/components/nostr/CustomEmoji";
|
||||
import { UnicodeEmoji } from "@/components/nostr/UnicodeEmoji";
|
||||
|
||||
export interface EmojiSuggestionListProps {
|
||||
items: EmojiSearchResult[];
|
||||
@@ -121,19 +123,20 @@ export const EmojiSuggestionList = forwardRef<
|
||||
title={`:${item.shortcode}:`}
|
||||
>
|
||||
{item.source === "unicode" ? (
|
||||
// Unicode emoji - render as text
|
||||
<span className="text-lg leading-none">{item.url}</span>
|
||||
// Unicode emoji - render with UnicodeEmoji component
|
||||
<UnicodeEmoji
|
||||
emoji={item.url}
|
||||
shortcode={item.shortcode}
|
||||
size="md"
|
||||
showTooltip={false}
|
||||
/>
|
||||
) : (
|
||||
// Custom emoji - render as image
|
||||
<img
|
||||
src={item.url}
|
||||
alt={`:${item.shortcode}:`}
|
||||
className="size-6 object-contain"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
// Replace with fallback on error
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
// Custom emoji - render using CustomEmoji component
|
||||
<CustomEmoji
|
||||
shortcode={item.shortcode}
|
||||
url={item.url}
|
||||
size="md"
|
||||
showTooltip={false}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -8,12 +8,15 @@ export interface CustomEmojiProps {
|
||||
/** The image URL */
|
||||
url: string;
|
||||
/** Size variant */
|
||||
size?: "sm" | "md" | "lg";
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
/** Whether to show tooltip on hover (default: true) */
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xs: "size-3.5",
|
||||
sm: "size-4",
|
||||
md: "size-6",
|
||||
lg: "size-12",
|
||||
@@ -28,6 +31,7 @@ export function CustomEmoji({
|
||||
url,
|
||||
size = "md",
|
||||
className,
|
||||
showTooltip = true,
|
||||
}: CustomEmojiProps) {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
@@ -46,22 +50,28 @@ export function CustomEmoji({
|
||||
);
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={url}
|
||||
alt={`:${shortcode}:`}
|
||||
title={`:${shortcode}:`}
|
||||
className={cn(
|
||||
"inline-block object-contain",
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
loading="lazy"
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!showTooltip) {
|
||||
return img;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<img
|
||||
src={url}
|
||||
alt={`:${shortcode}:`}
|
||||
title={`:${shortcode}:`}
|
||||
className={cn(
|
||||
"inline-block object-contain",
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
loading="lazy"
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger asChild>{img}</TooltipTrigger>
|
||||
<TooltipContent>:{shortcode}:</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CustomEmoji } from "../CustomEmoji";
|
||||
|
||||
interface EmojiNodeProps {
|
||||
node: {
|
||||
url: string;
|
||||
@@ -7,11 +9,11 @@ interface EmojiNodeProps {
|
||||
|
||||
export function Emoji({ node }: EmojiNodeProps) {
|
||||
return (
|
||||
<img
|
||||
src={node.url}
|
||||
alt={`:${node.code}:`}
|
||||
title={`:${node.code}:`}
|
||||
className="inline-block size-5"
|
||||
<CustomEmoji
|
||||
shortcode={node.code}
|
||||
url={node.url}
|
||||
size="sm"
|
||||
showTooltip={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
63
src/components/nostr/UnicodeEmoji.tsx
Normal file
63
src/components/nostr/UnicodeEmoji.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
export interface UnicodeEmojiProps {
|
||||
/** The emoji character */
|
||||
emoji: string;
|
||||
/** The shortcode for tooltip (without colons) */
|
||||
shortcode?: string;
|
||||
/** Size variant - matches CustomEmoji sizes */
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
/** Whether to show tooltip on hover (default: true, requires shortcode) */
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text size classes that visually match CustomEmoji image sizes
|
||||
* - xs: size-3.5 (14px) → text-sm (14px)
|
||||
* - sm: size-4 (16px) → text-base (16px)
|
||||
* - md: size-6 (24px) → text-2xl (24px)
|
||||
* - lg: size-12 (48px) → text-5xl (48px)
|
||||
*/
|
||||
const sizeClasses = {
|
||||
xs: "text-sm",
|
||||
sm: "text-base",
|
||||
md: "text-2xl",
|
||||
lg: "text-5xl",
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a unicode emoji with consistent sizing
|
||||
* Size variants match CustomEmoji for visual consistency
|
||||
*/
|
||||
export function UnicodeEmoji({
|
||||
emoji,
|
||||
shortcode,
|
||||
size = "md",
|
||||
className,
|
||||
showTooltip = true,
|
||||
}: UnicodeEmojiProps) {
|
||||
const emojiSpan = (
|
||||
<span
|
||||
className={cn("inline-block leading-none", sizeClasses[size], className)}
|
||||
role="img"
|
||||
aria-label={shortcode ? `:${shortcode}:` : emoji}
|
||||
>
|
||||
{emoji}
|
||||
</span>
|
||||
);
|
||||
|
||||
// Only show tooltip if shortcode is provided and showTooltip is true
|
||||
if (!showTooltip || !shortcode) {
|
||||
return emojiSpan;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{emojiSpan}</TooltipTrigger>
|
||||
<TooltipContent>:{shortcode}:</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import { UserName } from "../UserName";
|
||||
import { RichText } from "../RichText";
|
||||
import { EMOJI_SHORTCODE_REGEX } from "@/lib/emoji-helpers";
|
||||
import { CustomEmoji } from "../CustomEmoji";
|
||||
import { UnicodeEmoji } from "../UnicodeEmoji";
|
||||
|
||||
/**
|
||||
* Compact preview for Kind 7 (Reaction)
|
||||
@@ -100,18 +102,18 @@ export function ReactionCompactPreview({ event }: { event: NostrEvent }) {
|
||||
case "😊":
|
||||
return <Smile className="size-4 fill-yellow-500 text-yellow-500" />;
|
||||
default:
|
||||
return <span className="text-base">{content}</span>;
|
||||
return <UnicodeEmoji emoji={content} size="xs" showTooltip={false} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-sm truncate">
|
||||
{parsedReaction.type === "custom" ? (
|
||||
<img
|
||||
src={parsedReaction.url}
|
||||
alt={`:${parsedReaction.shortcode}:`}
|
||||
title={`:${parsedReaction.shortcode}:`}
|
||||
className="size-3.5 inline-block shrink-0"
|
||||
<CustomEmoji
|
||||
shortcode={parsedReaction.shortcode}
|
||||
url={parsedReaction.url}
|
||||
size="xs"
|
||||
className="shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<span className="shrink-0">
|
||||
|
||||
@@ -7,6 +7,8 @@ import { KindRenderer } from "./index";
|
||||
import { EventCardSkeleton } from "@/components/ui/skeleton";
|
||||
import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
|
||||
import { EMOJI_SHORTCODE_REGEX } from "@/lib/emoji-helpers";
|
||||
import { CustomEmoji } from "../CustomEmoji";
|
||||
import { UnicodeEmoji } from "../UnicodeEmoji";
|
||||
|
||||
/**
|
||||
* Renderer for Kind 7 - Reactions
|
||||
@@ -100,7 +102,7 @@ export function Kind7Renderer({ event }: BaseEventProps) {
|
||||
case "😊":
|
||||
return <Smile className="size-4 fill-yellow-500 text-yellow-500" />;
|
||||
default:
|
||||
return <span className="text-xl">{content}</span>;
|
||||
return <UnicodeEmoji emoji={content} size="md" showTooltip={false} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,11 +112,10 @@ export function Kind7Renderer({ event }: BaseEventProps) {
|
||||
{/* Reaction indicator */}
|
||||
<div className="flex items-center gap-2">
|
||||
{parsedReaction.type === "custom" ? (
|
||||
<img
|
||||
src={parsedReaction.url}
|
||||
alt={`:${parsedReaction.shortcode}:`}
|
||||
title={`:${parsedReaction.shortcode}:`}
|
||||
className="size-6 inline-block"
|
||||
<CustomEmoji
|
||||
shortcode={parsedReaction.shortcode}
|
||||
url={parsedReaction.url}
|
||||
size="md"
|
||||
/>
|
||||
) : (
|
||||
getReactionIcon(parsedReaction.emoji)
|
||||
|
||||
Reference in New Issue
Block a user