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.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 { Link } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
@ -15,135 +15,95 @@ export function NoteContent({
|
||||
event,
|
||||
className,
|
||||
}: NoteContentProps) {
|
||||
const [content, setContent] = useState<React.ReactNode[]>([]);
|
||||
|
||||
// Process the content to render mentions, links, etc.
|
||||
useEffect(() => {
|
||||
const processContent = async () => {
|
||||
const text = event.content;
|
||||
const content = useMemo(() => {
|
||||
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;
|
||||
// Regex to find URLs, Nostr references, and hashtags
|
||||
const regex = /(https?:\/\/[^\s]+)|nostr:(npub1|note1|nprofile1|nevent1)([a-z0-9]+)|(#\w+)/g;
|
||||
|
||||
// Split the content by these patterns
|
||||
let lastIndex = 0;
|
||||
const parts: React.ReactNode[] = [];
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
let keyCounter = 0;
|
||||
|
||||
// Process URLs
|
||||
const processUrls = () => {
|
||||
text.replace(urlRegex, (match, url, index) => {
|
||||
if (index > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, index));
|
||||
}
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const [fullMatch, url, nostrPrefix, nostrData, hashtag] = match;
|
||||
const index = match.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));
|
||||
// Add text before this match
|
||||
if (index > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, index));
|
||||
}
|
||||
|
||||
// If no special content was found, just use the plain text
|
||||
if (parts.length === 0) {
|
||||
parts.push(text);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
setContent(parts);
|
||||
};
|
||||
lastIndex = index + fullMatch.length;
|
||||
}
|
||||
|
||||
processContent();
|
||||
// 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]);
|
||||
|
||||
return (
|
||||
@ -156,11 +116,12 @@ export function NoteContent({
|
||||
// Helper component to display user mentions
|
||||
function NostrMention({ pubkey }: { pubkey: string }) {
|
||||
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 (
|
||||
<Link
|
||||
to={`/p/${pubkey}`}
|
||||
to={`/${npub}`}
|
||||
className="font-medium text-blue-500 hover:underline"
|
||||
>
|
||||
@{displayName}
|
||||
|
Loading…
x
Reference in New Issue
Block a user