/** * A React Native component that renders Nostr event content with rich formatting support. * Handles rendering of mentions, hashtags, URLs, emojis, and images within event content. * * Features: * - Renders nostr: mentions with optional custom mention component * - Formats hashtags with optional press handling * - Renders URLs and images with press handling * - Supports custom emoji rendering from event tags * - Special handling for reaction events (❤️, 👎) * * @package @nostr-dev-kit/ndk-mobile */ import { type NDKEvent, NDKKind, NDKUser } from "@nostr-dev-kit/ndk-hooks"; import { Image } from "expo-image"; // biome-ignore lint/style/useImportType: import React from "react"; import { useEffect, useState } from "react"; import { Pressable, StyleSheet, Text, type TextProps } from "react-native"; import { useNDK, useProfileValue } from "@nostr-dev-kit/ndk-hooks"; const styles = StyleSheet.create({ mention: { fontWeight: "bold", color: "#0066CC", }, hashtag: { fontWeight: "bold", color: "#0066CC", }, url: { fontWeight: "bold", color: "#0066CC", textDecorationLine: "underline", }, image: { width: "100%", height: "100%", resizeMode: "cover", borderRadius: 12, }, }); /** * Props for the EventContent component */ export interface EventContentProps extends TextProps { /** The NDKEvent to render content from */ event?: NDKEvent; /** Optional content override. If not provided, uses event.content */ content?: string; /** Callback when a user mention is pressed */ onUserPress?: (pubkey: string) => void; /** Callback when a hashtag is pressed */ onHashtagPress?: (hashtag: string) => void; /** Callback when a URL is pressed */ onUrlPress?: (url: string) => void; /** Optional custom component to render user mentions */ MentionComponent?: React.ComponentType<{ pubkey: string }>; } /** * Renders an emoji from an event's emoji tags */ function RenderEmoji({ shortcode, event, fontSize }: { shortcode: string; event?: NDKEvent; fontSize?: number }) { if (!event) return :{shortcode}:; const emojiTag = event.tags.find((tag) => tag[0] === "emoji" && tag[1] === shortcode); if (!emojiTag || !emojiTag[2]) return :{shortcode}:; const emojiSize = fontSize || 14; return ( ); } /** * Renders a hashtag with optional press handling */ function RenderHashtag({ hashtag, onHashtagPress, fontSize, style, }: { hashtag: string; onHashtagPress?: (hashtag: string) => void; fontSize?: number; style?: any; }) { const combinedStyle = [styles.hashtag, { fontSize }, style]; if (onHashtagPress) { return ( onHashtagPress(`#${hashtag}`)} style={combinedStyle}> #{hashtag} ); } return #{hashtag}; } /** * Renders a Nostr mention (npub or nprofile) with optional custom component */ interface RenderMentionProps { user: NDKUser; onUserPress?: (pubkey: string) => void; MentionComponent?: React.ComponentType<{ pubkey: string }>; fontSize?: number; style?: any; } function RenderMention({ user, onUserPress, MentionComponent, fontSize, style }: RenderMentionProps) { const userProfile = useProfileValue(user.pubkey); const combinedStyle = [styles.mention, { fontSize }, style]; return ( onUserPress?.(user.pubkey)}> @ {MentionComponent ? ( ) : ( (userProfile?.name ?? user.pubkey.substring(0, 8)) )} ); } function RenderEvent({ entity, onUserPress }: { entity: string; onUserPress?: (pubkey: string) => void }) { const { ndk } = useNDK(); const [event, setEvent] = useState(null); useEffect(() => { if (!entity) return; ndk.fetchEvent(entity).then((event) => setEvent(event)); }, [entity]); if (!event) return {entity}; return ; } /** * Renders a part of the content with appropriate formatting based on content type * Handles: * - Emoji shortcodes (:shortcode:) * - Image URLs * - Regular URLs * - Nostr mentions (nostr:npub1...) * - Hashtags (#hashtag) * - Plain text */ function RenderPart({ part, onUserPress, onHashtagPress, onUrlPress, MentionComponent, event, style, ...props }: { part: string; onUserPress?: (pubkey: string) => void; onHashtagPress?: (hashtag: string) => void; onUrlPress?: (url: string) => void; MentionComponent?: React.ComponentType<{ pubkey: string }>; event?: NDKEvent; style?: any; } & TextProps) { const { ndk } = useNDK(); const fontSize = style?.fontSize; // Check for emoji shortcode const emojiMatch = part.match(/^:([a-zA-Z0-9_+-]+):$/); if (emojiMatch) { return ; } if (part.startsWith("https://") && part.match(/\.(jpg|jpeg|png|gif)/)) { return ( onUrlPress?.(part)}> ); } if (part.startsWith("https://") || part.startsWith("http://")) { return ( onUrlPress?.(part)}> {part} ); } const mentionMatch = part.match(/nostr:([a-zA-Z0-9]+)/)?.[1]; if (mentionMatch) { const entity = ndk.getEntity(mentionMatch); if (entity instanceof NDKUser) { if (MentionComponent) { return ; } return ; } if (entity) { return ; } } const hashtagMatch = part.match(/^#([\p{L}\p{N}_\-]+)/u); if (hashtagMatch) { return ( ); } return ( {part} ); } /** * Main component for rendering Nostr event content with rich formatting * * @example * ```tsx * // Basic usage * * * // With custom handlers * console.log('User pressed:', pubkey)} * onHashtagPress={(hashtag) => console.log('Hashtag pressed:', hashtag)} * onUrlPress={(url) => console.log('URL pressed:', url)} * /> * * // With custom mention component * } * /> * ``` */ const EventContent: React.FC = ({ event, numberOfLines, content, style, onUserPress, onHashtagPress, onUrlPress, MentionComponent, ...props }) => { if (!event && !content) return null; // Handle reaction events if (event?.kind === NDKKind.Reaction) { return {event.content || "❤️"}; } const contentToRender = content || event?.content || ""; const parts = contentToRender.split( /(\s+|(?=https?:\/\/)|(?<=\s)#[\p{L}\p{N}_\-]+|(?<=\s)nostr:[a-zA-Z0-9]+|:[a-zA-Z0-9_+-]+:)/u, ); return ( {parts.map((part, index) => ( ))} ); }; export default EventContent;