@nostr-dev-kit/ndk-mobile
Version:
NDK Mobile
311 lines (300 loc) • 7.49 kB
JavaScript
"use strict";
/**
* 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 { NDKKind, NDKUser } from "@nostr-dev-kit/ndk-hooks";
import { Image } from "expo-image";
// biome-ignore lint/style/useImportType: <explanation>
import React from "react";
import { useEffect, useState } from "react";
import { Pressable, StyleSheet, Text } from "react-native";
import { useNDK, useProfileValue } from "@nostr-dev-kit/ndk-hooks";
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
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
*/
/**
* Renders an emoji from an event's emoji tags
*/
function RenderEmoji({
shortcode,
event,
fontSize
}) {
if (!event) return /*#__PURE__*/_jsxs(Text, {
style: {
fontSize
},
children: [":", shortcode, ":"]
});
const emojiTag = event.tags.find(tag => tag[0] === "emoji" && tag[1] === shortcode);
if (!emojiTag || !emojiTag[2]) return /*#__PURE__*/_jsxs(Text, {
style: {
fontSize
},
children: [":", shortcode, ":"]
});
const emojiSize = fontSize || 14;
return /*#__PURE__*/_jsx(Image, {
source: {
uri: emojiTag[2]
},
style: {
width: emojiSize,
height: emojiSize,
resizeMode: "contain"
}
});
}
/**
* Renders a hashtag with optional press handling
*/
function RenderHashtag({
hashtag,
onHashtagPress,
fontSize,
style
}) {
const combinedStyle = [styles.hashtag, {
fontSize
}, style];
if (onHashtagPress) {
return /*#__PURE__*/_jsxs(Text, {
onPress: () => onHashtagPress(`#${hashtag}`),
style: combinedStyle,
children: ["#", hashtag]
});
}
return /*#__PURE__*/_jsxs(Text, {
style: combinedStyle,
children: ["#", hashtag]
});
}
/**
* Renders a Nostr mention (npub or nprofile) with optional custom component
*/
function RenderMention({
user,
onUserPress,
MentionComponent,
fontSize,
style
}) {
const userProfile = useProfileValue(user.pubkey);
const combinedStyle = [styles.mention, {
fontSize
}, style];
return /*#__PURE__*/_jsxs(Text, {
style: combinedStyle,
onPress: () => onUserPress?.(user.pubkey),
children: ["@", MentionComponent ? /*#__PURE__*/_jsx(MentionComponent, {
pubkey: user.pubkey
}) : userProfile?.name ?? user.pubkey.substring(0, 8)]
});
}
function RenderEvent({
entity,
onUserPress
}) {
const {
ndk
} = useNDK();
const [event, setEvent] = useState(null);
useEffect(() => {
if (!entity) return;
ndk.fetchEvent(entity).then(event => setEvent(event));
}, [entity]);
if (!event) return /*#__PURE__*/_jsx(Text, {
children: entity
});
return /*#__PURE__*/_jsx(EventContent, {
event: event,
onUserPress: onUserPress
});
}
/**
* 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
}) {
const {
ndk
} = useNDK();
const fontSize = style?.fontSize;
// Check for emoji shortcode
const emojiMatch = part.match(/^:([a-zA-Z0-9_+-]+):$/);
if (emojiMatch) {
return /*#__PURE__*/_jsx(RenderEmoji, {
shortcode: emojiMatch[1],
event: event,
fontSize: fontSize
});
}
if (part.startsWith("https://") && part.match(/\.(jpg|jpeg|png|gif)/)) {
return /*#__PURE__*/_jsx(Pressable, {
onPress: () => onUrlPress?.(part),
children: /*#__PURE__*/_jsx(Image, {
source: {
uri: part
},
style: [styles.image, style]
})
});
}
if (part.startsWith("https://") || part.startsWith("http://")) {
return /*#__PURE__*/_jsx(Text, {
style: [styles.url, style],
onPress: () => onUrlPress?.(part),
children: 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 /*#__PURE__*/_jsx(MentionComponent, {
pubkey: entity.pubkey
});
}
return /*#__PURE__*/_jsx(RenderMention, {
user: entity,
onUserPress: onUserPress,
fontSize: fontSize,
style: style
});
}
if (entity) {
return /*#__PURE__*/_jsx(RenderEvent, {
entity: mentionMatch,
onUserPress: onUserPress
});
}
}
const hashtagMatch = part.match(/^#([\p{L}\p{N}_\-]+)/u);
if (hashtagMatch) {
return /*#__PURE__*/_jsx(RenderHashtag, {
hashtag: hashtagMatch[1],
onHashtagPress: onHashtagPress,
fontSize: fontSize,
style: style
});
}
return /*#__PURE__*/_jsx(Text, {
style: {
...style,
fontSize
},
...props,
children: part
});
}
/**
* Main component for rendering Nostr event content with rich formatting
*
* @example
* ```tsx
* // Basic usage
* <EventContent event={ndkEvent} />
*
* // With custom handlers
* <EventContent
* event={ndkEvent}
* onUserPress={(pubkey) => console.log('User pressed:', pubkey)}
* onHashtagPress={(hashtag) => console.log('Hashtag pressed:', hashtag)}
* onUrlPress={(url) => console.log('URL pressed:', url)}
* />
*
* // With custom mention component
* <EventContent
* event={ndkEvent}
* MentionComponent={({ pubkey }) => <UserName pubkey={pubkey} />}
* />
* ```
*/
const EventContent = ({
event,
numberOfLines,
content,
style,
onUserPress,
onHashtagPress,
onUrlPress,
MentionComponent,
...props
}) => {
if (!event && !content) return null;
// Handle reaction events
if (event?.kind === NDKKind.Reaction) {
return /*#__PURE__*/_jsx(Text, {
style: style,
children: 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 /*#__PURE__*/_jsx(Text, {
numberOfLines: numberOfLines,
style: style,
...props,
children: parts.map((part, index) => /*#__PURE__*/_jsx(React.Fragment, {
children: /*#__PURE__*/_jsx(RenderPart, {
part: part,
onUserPress: onUserPress,
onHashtagPress: onHashtagPress,
onUrlPress: onUrlPress,
MentionComponent: MentionComponent,
event: event,
style: style
})
}, index))
});
};
export default EventContent;
//# sourceMappingURL=content.js.map