mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 04:59:22 +00:00
Improve NoteContent and NoteContent tests
This commit is contained in:
parent
acbe053c97
commit
865ca7039f
@ -75,4 +75,30 @@ describe('NoteContent', () => {
|
|||||||
expect(screen.getByText('This is just plain text without any links.')).toBeInTheDocument();
|
expect(screen.getByText('This is just plain text without any links.')).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders hashtags as links', () => {
|
||||||
|
const event: NostrEvent = {
|
||||||
|
id: 'test-id',
|
||||||
|
pubkey: 'test-pubkey',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: 'This is a post about #nostr and #bitcoin development.',
|
||||||
|
sig: 'test-sig',
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestApp>
|
||||||
|
<NoteContent event={event} />
|
||||||
|
</TestApp>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nostrHashtag = screen.getByRole('link', { name: '#nostr' });
|
||||||
|
const bitcoinHashtag = screen.getByRole('link', { name: '#bitcoin' });
|
||||||
|
|
||||||
|
expect(nostrHashtag).toBeInTheDocument();
|
||||||
|
expect(bitcoinHashtag).toBeInTheDocument();
|
||||||
|
expect(nostrHashtag).toHaveAttribute('href', '/t/nostr');
|
||||||
|
expect(bitcoinHashtag).toHaveAttribute('href', '/t/bitcoin');
|
||||||
|
});
|
||||||
});
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { type NostrEvent } from '@nostrify/nostrify';
|
import { type NostrEvent } from '@nostrify/nostrify';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
@ -14,138 +14,98 @@ interface NoteContentProps {
|
|||||||
export function NoteContent({
|
export function NoteContent({
|
||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
}: NoteContentProps) {
|
}: NoteContentProps) {
|
||||||
const [content, setContent] = useState<React.ReactNode[]>([]);
|
|
||||||
|
|
||||||
// Process the content to render mentions, links, etc.
|
// Process the content to render mentions, links, etc.
|
||||||
useEffect(() => {
|
const content = useMemo(() => {
|
||||||
const processContent = async () => {
|
const text = event.content;
|
||||||
const text = event.content;
|
|
||||||
|
|
||||||
// Regular expressions for different patterns
|
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
||||||
const nostrRegex = /nostr:(npub1|note1|nprofile1|nevent1)([a-z0-9]+)/g;
|
|
||||||
const hashtagRegex = /#(\w+)/g;
|
|
||||||
|
|
||||||
// Split the content by these patterns
|
|
||||||
let lastIndex = 0;
|
|
||||||
const parts: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
// Process URLs
|
|
||||||
const processUrls = () => {
|
|
||||||
text.replace(urlRegex, (match, url, index) => {
|
|
||||||
if (index > lastIndex) {
|
|
||||||
parts.push(text.substring(lastIndex, index));
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push(
|
|
||||||
<a
|
|
||||||
key={`url-${index}`}
|
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-500 hover:underline"
|
|
||||||
>
|
|
||||||
{url}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
lastIndex = index + match.length;
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process Nostr references
|
|
||||||
const processNostrRefs = () => {
|
|
||||||
text.replace(nostrRegex, (match, prefix, datastring, index) => {
|
|
||||||
if (index > lastIndex) {
|
|
||||||
parts.push(text.substring(lastIndex, index));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const nostrId = `${prefix}${datastring}`;
|
|
||||||
const decoded = nip19.decode(nostrId);
|
|
||||||
|
|
||||||
if (decoded.type === 'npub') {
|
|
||||||
const pubkey = decoded.data as string;
|
|
||||||
parts.push(
|
|
||||||
<NostrMention key={`mention-${index}`} pubkey={pubkey} />
|
|
||||||
);
|
|
||||||
} else if (decoded.type === 'note') {
|
|
||||||
parts.push(
|
|
||||||
<Link
|
|
||||||
key={`note-${index}`}
|
|
||||||
to={`/e/${nostrId}`}
|
|
||||||
className="text-blue-500 hover:underline"
|
|
||||||
>
|
|
||||||
note
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// For other types, just show as a link
|
|
||||||
parts.push(
|
|
||||||
<Link
|
|
||||||
key={`nostr-${index}`}
|
|
||||||
to={`/${nostrId}`}
|
|
||||||
className="text-blue-500 hover:underline"
|
|
||||||
>
|
|
||||||
{match}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// If decoding fails, just render as text
|
|
||||||
parts.push(match);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = index + match.length;
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process hashtags
|
|
||||||
const processHashtags = () => {
|
|
||||||
text.replace(hashtagRegex, (match, tag, index) => {
|
|
||||||
if (index > lastIndex) {
|
|
||||||
parts.push(text.substring(lastIndex, index));
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push(
|
|
||||||
<Link
|
|
||||||
key={`hashtag-${index}`}
|
|
||||||
to={`/t/${tag}`}
|
|
||||||
className="text-blue-500 hover:underline"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
lastIndex = index + match.length;
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run all processors
|
|
||||||
processUrls();
|
|
||||||
processNostrRefs();
|
|
||||||
processHashtags();
|
|
||||||
|
|
||||||
// Add any remaining text
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
parts.push(text.substring(lastIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no special content was found, just use the plain text
|
|
||||||
if (parts.length === 0) {
|
|
||||||
parts.push(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent(parts);
|
|
||||||
};
|
|
||||||
|
|
||||||
processContent();
|
// Regex to find URLs, Nostr references, and hashtags
|
||||||
|
const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([a-z0-9]+)|(#\w+)/g;
|
||||||
|
|
||||||
|
const parts: React.ReactNode[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
let keyCounter = 0;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const [fullMatch, url, nostrPrefix, nostrData, hashtag] = match;
|
||||||
|
const index = match.index;
|
||||||
|
|
||||||
|
// Add text before this match
|
||||||
|
if (index > lastIndex) {
|
||||||
|
parts.push(text.substring(lastIndex, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
// Handle URLs
|
||||||
|
parts.push(
|
||||||
|
<a
|
||||||
|
key={`url-${keyCounter++}`}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else if (nostrPrefix && nostrData) {
|
||||||
|
// Handle Nostr references
|
||||||
|
try {
|
||||||
|
const nostrId = `${nostrPrefix}${nostrData}`;
|
||||||
|
const decoded = nip19.decode(nostrId);
|
||||||
|
|
||||||
|
if (decoded.type === 'npub') {
|
||||||
|
const pubkey = decoded.data;
|
||||||
|
parts.push(
|
||||||
|
<NostrMention key={`mention-${keyCounter++}`} pubkey={pubkey} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// For other types, just show as a link
|
||||||
|
parts.push(
|
||||||
|
<Link
|
||||||
|
key={`nostr-${keyCounter++}`}
|
||||||
|
to={`/${nostrId}`}
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{fullMatch}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If decoding fails, just render as text
|
||||||
|
parts.push(fullMatch);
|
||||||
|
}
|
||||||
|
} else if (hashtag) {
|
||||||
|
// Handle hashtags
|
||||||
|
const tag = hashtag.slice(1); // Remove the #
|
||||||
|
parts.push(
|
||||||
|
<Link
|
||||||
|
key={`hashtag-${keyCounter++}`}
|
||||||
|
to={`/t/${tag}`}
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{hashtag}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = index + fullMatch.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(text.substring(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no special content was found, just use the plain text
|
||||||
|
if (parts.length === 0) {
|
||||||
|
parts.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
}, [event]);
|
}, [event]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("whitespace-pre-wrap break-words", className)}>
|
<div className={cn("whitespace-pre-wrap break-words", className)}>
|
||||||
{content.length > 0 ? content : event.content}
|
{content.length > 0 ? content : event.content}
|
||||||
@ -156,11 +116,12 @@ export function NoteContent({
|
|||||||
// Helper component to display user mentions
|
// Helper component to display user mentions
|
||||||
function NostrMention({ pubkey }: { pubkey: string }) {
|
function NostrMention({ pubkey }: { pubkey: string }) {
|
||||||
const author = useAuthor(pubkey);
|
const author = useAuthor(pubkey);
|
||||||
const displayName = author.data?.metadata?.name || pubkey.slice(0, 8);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
const displayName = author.data?.metadata?.name ?? npub.slice(0, 8);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`/p/${pubkey}`}
|
to={`/${npub}`}
|
||||||
className="font-medium text-blue-500 hover:underline"
|
className="font-medium text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
@{displayName}
|
@{displayName}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user