refactor text parser
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ImagePreview, LinkPreview, MentionNote, VideoPreview } from '@shared/notes';
|
||||
import {
|
||||
Boost,
|
||||
Hashtag,
|
||||
ImagePreview,
|
||||
Invoice,
|
||||
LinkPreview,
|
||||
MentionNote,
|
||||
MentionUser,
|
||||
VideoPreview,
|
||||
} from '@shared/notes';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
|
||||
@@ -17,9 +27,30 @@ export function TextNote(props: { content?: string; truncate?: boolean }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-600 prose-a:hover:underline">
|
||||
<Markdown
|
||||
options={{
|
||||
overrides: {
|
||||
Hashtag: {
|
||||
component: Hashtag,
|
||||
},
|
||||
Boost: {
|
||||
component: Boost,
|
||||
},
|
||||
MentionUser: {
|
||||
component: MentionUser,
|
||||
},
|
||||
Invoice: {
|
||||
component: Invoice,
|
||||
},
|
||||
},
|
||||
slugify: (str) => str,
|
||||
forceBlock: true,
|
||||
enforceAtxHeadings: true,
|
||||
}}
|
||||
className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert prose-headings:mb-1 prose-headings:mt-3 prose-p:mb-0 prose-p:mt-0 prose-p:last:mb-1 prose-a:font-normal prose-a:text-blue-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-blue-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:bg-white/10 prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2 hover:prose-a:text-blue-600 prose-a:hover:underline"
|
||||
>
|
||||
{richContent.parsed}
|
||||
</div>
|
||||
</Markdown>
|
||||
{richContent.images.length ? <ImagePreview urls={richContent.images} /> : null}
|
||||
{richContent.videos.length ? <VideoPreview urls={richContent.videos} /> : null}
|
||||
{richContent.links.length ? <LinkPreview urls={richContent.links} /> : null}
|
||||
|
||||
@@ -75,13 +75,14 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
|
||||
<div
|
||||
role="button"
|
||||
onClick={(e) => openThread(e, id)}
|
||||
className="mt-3 cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
||||
<div className="mt-1 text-left">{renderKind(data)}</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -124,7 +124,13 @@ export function NewsfeedWidget() {
|
||||
sub(
|
||||
filter,
|
||||
async (event) => {
|
||||
queryClient.setQueryData(['newsfeed'], (old: NDKEvent[]) => [event, ...old]);
|
||||
queryClient.setQueryData(
|
||||
['newsfeed'],
|
||||
(prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({
|
||||
...prev,
|
||||
pages: [[event], ...prev.pages],
|
||||
})
|
||||
);
|
||||
},
|
||||
false,
|
||||
'newsfeed'
|
||||
|
||||
@@ -77,10 +77,13 @@ export function NotificationWidget() {
|
||||
sub(
|
||||
filter,
|
||||
async (event) => {
|
||||
queryClient.setQueryData(['notification'], (old: NDKEvent[]) => [
|
||||
event,
|
||||
...old,
|
||||
]);
|
||||
queryClient.setQueryData(
|
||||
['notification'],
|
||||
(prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({
|
||||
...prev,
|
||||
pages: [[event], ...prev.pages],
|
||||
})
|
||||
);
|
||||
|
||||
const user = ndk.getUser({ hexpubkey: event.pubkey });
|
||||
await user.fetchProfile();
|
||||
|
||||
132
src/utils/parser.ts
Normal file
132
src/utils/parser.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import {
|
||||
AddressPointer,
|
||||
EventPointer,
|
||||
ProfilePointer,
|
||||
} from 'nostr-tools/lib/types/nip19';
|
||||
|
||||
import { RichContent } from '@utils/types';
|
||||
|
||||
function isURL(string: string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
if (url.protocol.length > 0) {
|
||||
if (url.protocol === 'https:' || url.protocol === 'http:') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function parser(content: string) {
|
||||
const richContent: RichContent = {
|
||||
parsed: null,
|
||||
images: [],
|
||||
videos: [],
|
||||
links: [],
|
||||
notes: [],
|
||||
};
|
||||
|
||||
const parsed = content
|
||||
.trim()
|
||||
.split(/(\s+)/)
|
||||
.map((word) => {
|
||||
// url
|
||||
if (isURL(word)) {
|
||||
const url = new URL(word);
|
||||
url.search = '';
|
||||
|
||||
if (url.pathname.match(/\.(jpg|jpeg|gif|png|webp|avif)$/)) {
|
||||
// image url
|
||||
richContent.images.push(word);
|
||||
// remove url from original content
|
||||
return word.replace(word, '');
|
||||
}
|
||||
|
||||
if (url.pathname.match(/\.(mp4|mov|webm|wmv|flv|mts|avi|ogv|mkv|mp3|m3u8)$/)) {
|
||||
// video url
|
||||
richContent.videos.push(word);
|
||||
// remove url from original content
|
||||
return word.replace(word, '');
|
||||
}
|
||||
|
||||
// normal url
|
||||
if (richContent.links.length < 1) {
|
||||
richContent.links.push(url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// hashtag
|
||||
if (word.startsWith('#') && word.length > 1) {
|
||||
return word.replace(word, `<Hashtag tag='${word}' />`);
|
||||
}
|
||||
|
||||
// boost
|
||||
if (word.startsWith('$prism') && word.length > 1) {
|
||||
return word.replace(word, `<Boost boost='${word}' />`);
|
||||
}
|
||||
|
||||
// nostr account references (depreciated)
|
||||
if (word.startsWith('@npub1')) {
|
||||
const npub = word.replace('@', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
return word.replace(
|
||||
word,
|
||||
`<MentionUser pubkey='${nip19.decode(npub).data as string}' />`
|
||||
);
|
||||
}
|
||||
|
||||
// nostr account references
|
||||
if (word.startsWith('nostr:npub1') || word.startsWith('npub1')) {
|
||||
const npub = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
return word.replace(
|
||||
word,
|
||||
`<MentionUser pubkey='${nip19.decode(npub).data as string}' />`
|
||||
);
|
||||
}
|
||||
|
||||
// nostr profile references
|
||||
if (word.startsWith('nostr:nprofile1') || word.startsWith('nprofile1')) {
|
||||
const nprofile = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
const decoded = nip19.decode(nprofile).data as ProfilePointer;
|
||||
return word.replace(word, `<MentionUser pubkey='${decoded.pubkey}' />`);
|
||||
}
|
||||
|
||||
// nostr address references
|
||||
if (word.startsWith('nostr:naddr1') || word.startsWith('naddr1')) {
|
||||
const naddr = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
const decoded = nip19.decode(naddr).data as AddressPointer;
|
||||
return word.replace(word, `<MentionUser pubkey='${decoded.pubkey}' />`);
|
||||
}
|
||||
|
||||
// lightning invoice
|
||||
if (word.startsWith('lnbc') && word.length > 60) {
|
||||
return word.replace(word, `<Invoice invoice='${word}' />`);
|
||||
}
|
||||
|
||||
// nostr note references
|
||||
if (word.startsWith('nostr:note1') || word.startsWith('note1')) {
|
||||
const note = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
richContent.notes.push(nip19.decode(note).data as string);
|
||||
return word.replace(word, '');
|
||||
}
|
||||
|
||||
// nostr event references
|
||||
if (word.startsWith('nostr:nevent1') || word.startsWith('nevent1')) {
|
||||
const nevent = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
const decoded = nip19.decode(nevent).data as EventPointer;
|
||||
richContent.notes.push(decoded.id);
|
||||
return word.replace(word, '');
|
||||
}
|
||||
|
||||
return word;
|
||||
});
|
||||
|
||||
// update content with parsed version
|
||||
richContent.parsed = parsed.join(' ').trim();
|
||||
return richContent;
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import {
|
||||
AddressPointer,
|
||||
EventPointer,
|
||||
ProfilePointer,
|
||||
} from 'nostr-tools/lib/types/nip19';
|
||||
import { Link } from 'react-router-dom';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
import { Boost, Hashtag, Invoice, MentionUser } from '@shared/notes';
|
||||
|
||||
import { RichContent } from '@utils/types';
|
||||
|
||||
function isURL(string: string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
if (url.protocol.length > 0) {
|
||||
if (url.protocol === 'https:' || url.protocol === 'http:') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function parser(eventContent: string) {
|
||||
const content: RichContent = {
|
||||
parsed: null,
|
||||
images: [],
|
||||
videos: [],
|
||||
links: [],
|
||||
notes: [],
|
||||
};
|
||||
|
||||
const parsed = eventContent.split(/\s/gm).map((word) => {
|
||||
// nostr note references
|
||||
if (word.startsWith('nostr:note1') || word.startsWith('note1')) {
|
||||
const note = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
content.notes.push(nip19.decode(note).data as string);
|
||||
return word.replace(word, ' ');
|
||||
}
|
||||
|
||||
// nostr event references
|
||||
if (word.startsWith('nostr:nevent1') || word.startsWith('nevent1')) {
|
||||
const nevent = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
const decoded = nip19.decode(nevent).data as EventPointer;
|
||||
content.notes.push(decoded.id);
|
||||
return word.replace(word, ' ');
|
||||
}
|
||||
|
||||
// url
|
||||
if (isURL(word)) {
|
||||
const url = new URL(word);
|
||||
url.search = '';
|
||||
|
||||
if (url.pathname.match(/\.(jpg|jpeg|gif|png|webp|avif)$/)) {
|
||||
// image url
|
||||
content.images.push(word);
|
||||
// remove url from original content
|
||||
return word.replace(word, ' ');
|
||||
}
|
||||
|
||||
if (url.pathname.match(/\.(mp4|mov|webm|wmv|flv|mts|avi|ogv|mkv|mp3|m3u8)$/)) {
|
||||
// video url
|
||||
content.videos.push(word);
|
||||
// remove url from original content
|
||||
return word.replace(word, ' ');
|
||||
}
|
||||
|
||||
// normal url
|
||||
if (content.links.length < 1) {
|
||||
content.links.push(url.toString());
|
||||
return word.replace(word, ' ');
|
||||
} else {
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<Link key={match + i} to={word} target="_blank" rel="noreferrer">
|
||||
{word}
|
||||
</Link>{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// hashtag
|
||||
if (word.startsWith('#') && word.length > 1) {
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<Hashtag key={match + i} tag={match} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// boost
|
||||
if (word.startsWith('$prism') && word.length > 1) {
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<Boost key={match + i} boost={match} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// nostr account references (depreciated)
|
||||
if (word.startsWith('@npub1')) {
|
||||
const npub = word.replace('@', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<MentionUser key={match + i} pubkey={nip19.decode(npub).data as string} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// nostr account references
|
||||
if (word.startsWith('nostr:npub1') || word.startsWith('npub1')) {
|
||||
const npub = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<MentionUser key={match + i} pubkey={nip19.decode(npub).data as string} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// nostr profile references
|
||||
if (word.startsWith('nostr:nprofile1') || word.startsWith('nprofile1')) {
|
||||
const nprofile = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
const decoded = nip19.decode(nprofile).data as ProfilePointer;
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<MentionUser key={match + i} pubkey={decoded.pubkey} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// nostr address references
|
||||
if (word.startsWith('nostr:naddr1') || word.startsWith('naddr1')) {
|
||||
const naddr = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');
|
||||
const decoded = nip19.decode(naddr).data as AddressPointer;
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<MentionUser key={match + i} pubkey={decoded.pubkey} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// lightning invoice
|
||||
if (word.startsWith('lnbc') && word.length > 60) {
|
||||
return reactStringReplace(word, word, (match, i) => (
|
||||
<>
|
||||
{' '}
|
||||
<Invoice key={match + i} invoice={word} />{' '}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
// normal word
|
||||
return ' ' + word + ' ';
|
||||
});
|
||||
|
||||
// update content with parsed version
|
||||
content.parsed = parsed;
|
||||
return content;
|
||||
}
|
||||
3
src/utils/types.d.ts
vendored
3
src/utils/types.d.ts
vendored
@@ -1,9 +1,8 @@
|
||||
import { type NDKEvent, type NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||
import { type Response } from '@tauri-apps/plugin-http';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface RichContent {
|
||||
parsed: string | ReactNode[];
|
||||
parsed: string;
|
||||
images: string[];
|
||||
videos: string[];
|
||||
links: string[];
|
||||
|
||||
Reference in New Issue
Block a user