mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 04:59:22 +00:00
Kind 1 text rendering
This commit is contained in:
parent
7ee419bc6d
commit
aa2468b656
20
CONTEXT.md
20
CONTEXT.md
@ -338,6 +338,26 @@ const encrypted = await user.signer.nip44.encrypt(user.pubkey, "hello world");
|
||||
const decrypted = await user.signer.nip44.decrypt(user.pubkey, encrypted) // "hello world"
|
||||
```
|
||||
|
||||
### Rendering Kind 1 Text
|
||||
|
||||
If you need to render kind 1 text, use the `NoteContent` component:
|
||||
|
||||
```tsx
|
||||
import { NoteContent } from "@/components/NoteContent";
|
||||
|
||||
export function Post(/* ...props */) {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<CardContent className="pb-2">
|
||||
<div className="whitespace-pre-wrap break-words">
|
||||
<NoteContent event={post} className="text-sm" />
|
||||
</div>
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Development Practices
|
||||
|
||||
- Uses React Query for data fetching and caching
|
||||
|
7
package-lock.json
generated
7
package-lock.json
generated
@ -47,6 +47,7 @@
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"nostr-tools": "^2.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -4881,9 +4882,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
|
||||
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.0.tgz",
|
||||
"integrity": "sha512-A1arGsvpULqVK0NmZQqK1imwaCiPm8gcG/lo+cTax2NbNqBEYsuplbqAFdVqcGHEopmkByYbTwF76x25+oEbew==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
|
@ -52,6 +52,7 @@
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"nostr-tools": "^2.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
170
src/components/NoteContent.tsx
Normal file
170
src/components/NoteContent.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { type NostrEvent } from '@nostrify/nostrify';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface NoteContentProps {
|
||||
event: NostrEvent;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function NoteContent({
|
||||
event,
|
||||
className,
|
||||
}: NoteContentProps) {
|
||||
const [content, setContent] = useState<React.ReactNode[]>([]);
|
||||
|
||||
// Process the content to render mentions, links, etc.
|
||||
useEffect(() => {
|
||||
if (!event || event.kind !== 1) return;
|
||||
|
||||
const processContent = async () => {
|
||||
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();
|
||||
}, [event]);
|
||||
|
||||
return (
|
||||
<div className={cn("whitespace-pre-wrap break-words", className)}>
|
||||
{content.length > 0 ? content : event.content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/p/${pubkey}`}
|
||||
className="font-medium text-blue-500 hover:underline"
|
||||
>
|
||||
@{displayName}
|
||||
</Link>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user