From a17c5dfba54cad08e9ae012d2017e3dda35e4830 Mon Sep 17 00:00:00 2001 From: Ren Amamiya <123083837+reyamir@users.noreply.github.com> Date: Tue, 21 Mar 2023 09:24:40 +0700 Subject: [PATCH] added note content parser --- package.json | 1 + pnpm-lock.yaml | 14 +++++-- src/components/note/content/index.tsx | 56 +++++++++++++-------------- src/components/note/index.tsx | 18 ++++++--- src/components/note/root.tsx | 4 +- src/components/user/mention.tsx | 50 ++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 39 deletions(-) create mode 100644 src/components/user/mention.tsx diff --git a/package.json b/package.json index a28b9d4c..e24a9bd2 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.43.7", "react-player": "^2.12.0", + "react-string-replace": "^1.1.0", "react-virtuoso": "^4.1.0", "tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql", "unique-names-generator": "^4.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac5393a9..44f19c7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,7 @@ specifiers: react-dom: ^18.2.0 react-hook-form: ^7.43.7 react-player: ^2.12.0 + react-string-replace: ^1.1.0 react-virtuoso: ^4.1.0 tailwindcss: ^3.2.7 tauri-plugin-sql-api: github:tauri-apps/tauri-plugin-sql @@ -82,6 +83,7 @@ dependencies: react-dom: 18.2.0_react@18.2.0 react-hook-form: 7.43.7_react@18.2.0 react-player: 2.12.0_react@18.2.0 + react-string-replace: 1.1.0 react-virtuoso: 4.1.0_biqbaboplfbrettd7655fr4n2y tauri-plugin-sql-api: github.com/tauri-apps/tauri-plugin-sql/3a8b9a6b244df7512bc5ef8692cebdedbab3ccce unique-names-generator: 4.7.1 @@ -1682,7 +1684,7 @@ packages: '@uiw/copy-to-clipboard': 1.0.12 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - react-markdown: 8.0.5_pmekkgnqduwlme35zpnqhenc34 + react-markdown: 8.0.6_pmekkgnqduwlme35zpnqhenc34 rehype-attr: 2.1.4 rehype-autolink-headings: 6.1.1 rehype-ignore: 1.0.4 @@ -5240,9 +5242,9 @@ packages: { integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== } dev: false - /react-markdown/8.0.5_pmekkgnqduwlme35zpnqhenc34: + /react-markdown/8.0.6_pmekkgnqduwlme35zpnqhenc34: resolution: - { integrity: sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A== } + { integrity: sha512-KgPWsYgHuftdx510wwIzpwf+5js/iHqBR+fzxefv8Khk3mFbnioF1bmL2idHN3ler0LMQmICKeDrWnZrX9mtbQ== } peerDependencies: '@types/react': '>=16' react: '>=16' @@ -5319,6 +5321,12 @@ packages: use-sidecar: 1.1.2_pmekkgnqduwlme35zpnqhenc34 dev: false + /react-string-replace/1.1.0: + resolution: + { integrity: sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw== } + engines: { node: '>=0.12.0' } + dev: false + /react-style-singleton/2.2.1_pmekkgnqduwlme35zpnqhenc34: resolution: { integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== } diff --git a/src/components/note/content/index.tsx b/src/components/note/content/index.tsx index 742a3227..3b63599b 100644 --- a/src/components/note/content/index.tsx +++ b/src/components/note/content/index.tsx @@ -1,22 +1,35 @@ import NoteMetadata from '@components/note/content/metadata'; import NotePreview from '@components/note/content/preview'; import { UserExtend } from '@components/user/extend'; +import { UserMention } from '@components/user/mention'; -import dynamic from 'next/dynamic'; import { memo, useMemo } from 'react'; - -const MarkdownPreview = dynamic(() => import('@uiw/react-markdown-preview'), { - ssr: false, - loading: () =>
, -}); +import reactStringReplace from 'react-string-replace'; export const Content = memo(function Content({ data }: { data: any }) { - const content = useMemo( - () => - // remove all image urls - data.content.replace(/(https?:\/\/.*\.(jpg|jpeg|gif|png|webp)((\?.*)$|$))/i, ''), - [data.content] - ); + const content = useMemo(() => { + let parsedContent; + // get data tags + const tags = JSON.parse(data.tags); + // remove all image urls + parsedContent = data.content.replace(/(https?:\/\/.*\.(jpg|jpeg|gif|png|webp)((\?.*)$|$))/gim, ''); + // handle urls + parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => ( + + {match} + + )); + // handle hashtags + parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => ( + #{match} + )); + // handle mentions + parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => ( + + )); + + return parsedContent; + }, [data.content, data.tags]); return (
@@ -24,23 +37,8 @@ export const Content = memo(function Content({ data }: { data: any }) {
-
- +
+ {content}
diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index c74ddb0d..eb37999e 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -1,23 +1,31 @@ import { Content } from '@components/note/content'; import { RootNote } from '@components/note/root'; -import { memo, useEffect, useState } from 'react'; +import { memo, useMemo } from 'react'; export const Note = memo(function Note({ event }: { event: any }) { - const [root, setRoot] = useState(null); const tags = JSON.parse(event.tags); - useEffect(() => { + const fetchRootEvent = useMemo(() => { if (tags.length > 0) { if (tags[0][0] === 'e') { - setRoot(tags[0][1]); + return ; + } else { + tags.every((tag) => { + if (tag[2] === 'root') { + return ; + } + return <>; + }); } + } else { + return <>; } }, [tags]); return (
- {root && } + <>{fetchRootEvent}
); diff --git a/src/components/note/root.tsx b/src/components/note/root.tsx index 1c213e02..8282ef5c 100644 --- a/src/components/note/root.tsx +++ b/src/components/note/root.tsx @@ -75,7 +75,7 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) { } else { return (
-
+
@@ -88,7 +88,7 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) {
-
+
diff --git a/src/components/user/mention.tsx b/src/components/user/mention.tsx new file mode 100644 index 00000000..5b5ab538 --- /dev/null +++ b/src/components/user/mention.tsx @@ -0,0 +1,50 @@ +import { DatabaseContext } from '@components/contexts/database'; + +import { truncate } from '@utils/truncate'; + +import { memo, useCallback, useContext, useEffect, useState } from 'react'; + +export const UserMention = memo(function UserMention({ pubkey }: { pubkey: string }) { + const { db }: any = useContext(DatabaseContext); + const [profile, setProfile] = useState({ name: null }); + + const insertCacheProfile = useCallback( + async (event) => { + // insert to database + await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [pubkey, event.content]); + // update state + setProfile(JSON.parse(event.content)); + }, + [db, pubkey] + ); + + const getCacheProfile = useCallback(async () => { + const result: any = await db.select(`SELECT metadata FROM cache_profiles WHERE id = "${pubkey}"`); + return result[0]; + }, [db, pubkey]); + + useEffect(() => { + getCacheProfile() + .then((res) => { + if (res !== undefined) { + setProfile(JSON.parse(res.metadata)); + } else { + fetch(`https://rbr.bio/${pubkey}/metadata.json`, { redirect: 'follow' }) + .then((response) => { + if (response.ok) { + return response.json(); + } else if (response.status === 404) { + return Promise.reject('error 404'); + } else { + return Promise.reject('some other error: ' + response.status); + } + }) + .then((data) => insertCacheProfile(data)) + .catch((error) => console.log('error is', error)); + } + }) + .catch(console.error); + }, [getCacheProfile, insertCacheProfile, pubkey]); + + return @{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}; +});