From 12763cd87b16d21f186d3cd30a8335fcf0047a89 Mon Sep 17 00:00:00 2001 From: Cesar Augusto Date: Fri, 12 Jul 2024 12:59:45 +0800 Subject: [PATCH 01/26] =?UTF-8?q?=F0=9F=94=AA=F0=9F=94=AA=F0=9F=94=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elements/Content/AutoMatcherExtension.ts | 337 ------------------ src/components/elements/Content/Bubble.tsx | 59 --- .../elements/Content/Link/LinkExtension.ts | 28 -- .../Content/Mention/MentionExtension.tsx | 34 -- .../elements/Content/Note/NoteExtension.ts | 33 -- src/components/elements/Content/Paragraph.tsx | 29 -- .../elements/Content/Tag/TagExtension.ts | 27 -- .../elements/Content/Tweet/TweetExtension.tsx | 25 -- .../elements/Content/Video/VideoExtension.ts | 33 -- .../elements/Content/__tests__/parser.test.ts | 307 ---------------- src/components/elements/Content/parser.ts | 49 --- src/components/elements/Content/types.ts | 150 -------- src/components/elements/Posts/PostList.tsx | 103 ------ .../elements/QRCode/QRCodeDialog.tsx | 63 ---- src/components/elements/Relays/RelayChip.tsx | 54 --- src/components/elements/Relays/RelayForm.tsx | 0 src/components/elements/Relays/RelayStats.tsx | 0 src/components/elements/User/UserVerified.tsx | 2 + src/components/modules/FeedModule.tsx | 26 -- src/hooks/useStore.ts | 8 - src/stores/auth/auth.store.ts | 77 ---- src/stores/core/__tests__/filter.test.ts | 224 ------------ .../core/__tests__/filterRelays.test.ts | 271 -------------- src/stores/core/__tests__/imeta.test.ts | 73 ---- src/stores/core/__tests__/pool.test.ts | 14 - src/stores/core/__tests__/relay.test.ts | 31 -- src/stores/core/__tests__/relayHints.test.ts | 235 ------------ .../core/__tests__/subscription.test.ts | 218 ----------- src/stores/core/batcher.ts | 49 --- src/stores/core/filter.ts | 115 ------ src/stores/core/filterRelay.ts | 149 -------- src/stores/core/imeta.ts | 80 ----- src/stores/core/operators.ts | 98 ----- src/stores/core/pool.ts | 54 --- src/stores/core/relay.ts | 137 ------- src/stores/core/relayHints.ts | 86 ----- src/stores/core/subscription.ts | 142 -------- src/stores/db/__tests__/db.atom.test.ts | 28 -- src/stores/db/__tests__/observabledb.test.ts | 135 ------- src/stores/db/database.store.ts | 57 --- src/stores/db/db.atom.ts | 43 --- src/stores/db/db.batcher.ts | 44 --- src/stores/db/observabledb.store.ts | 171 --------- src/stores/index.ts | 2 - .../modules/__tests__/feed.store.test.ts | 77 ---- .../modules/__tests__/note.store.test.ts | 258 -------------- src/stores/modules/feed.store.ts | 149 -------- src/stores/modules/note.store.ts | 273 -------------- src/stores/modules/user.store.ts | 91 ----- .../nostr/__test__/contact.store.test.ts | 85 ----- src/stores/nostr/__test__/nostr.store.test.ts | 12 - src/stores/nostr/__test__/notes.store.test.ts | 89 ----- .../nostr/__test__/reactions.store.test.ts | 88 ----- .../nostr/__test__/userRelay.store.test.ts | 111 ------ src/stores/nostr/__test__/users.store.test.ts | 72 ---- src/stores/nostr/contacts.store.ts | 47 --- src/stores/nostr/nostr.store.ts | 78 ---- src/stores/nostr/subscriptions.store.ts | 31 -- src/stores/nostr/userRelay.store.ts | 103 ------ src/stores/root.store.ts | 80 ----- src/stores/ui/setting.store.ts | 40 --- src/utils/__tests__/utils.test.ts | 54 --- src/utils/references.ts | 135 ------- src/utils/utils.ts | 56 --- 64 files changed, 2 insertions(+), 5827 deletions(-) delete mode 100644 src/components/elements/Content/AutoMatcherExtension.ts delete mode 100644 src/components/elements/Content/Bubble.tsx delete mode 100644 src/components/elements/Content/Link/LinkExtension.ts delete mode 100644 src/components/elements/Content/Mention/MentionExtension.tsx delete mode 100644 src/components/elements/Content/Note/NoteExtension.ts delete mode 100644 src/components/elements/Content/Paragraph.tsx delete mode 100644 src/components/elements/Content/Tag/TagExtension.ts delete mode 100644 src/components/elements/Content/Tweet/TweetExtension.tsx delete mode 100644 src/components/elements/Content/Video/VideoExtension.ts delete mode 100644 src/components/elements/Content/__tests__/parser.test.ts delete mode 100644 src/components/elements/Content/parser.ts delete mode 100644 src/components/elements/Content/types.ts delete mode 100644 src/components/elements/Posts/PostList.tsx delete mode 100644 src/components/elements/QRCode/QRCodeDialog.tsx delete mode 100644 src/components/elements/Relays/RelayChip.tsx delete mode 100644 src/components/elements/Relays/RelayForm.tsx delete mode 100644 src/components/elements/Relays/RelayStats.tsx delete mode 100644 src/components/modules/FeedModule.tsx delete mode 100644 src/hooks/useStore.ts delete mode 100644 src/stores/auth/auth.store.ts delete mode 100644 src/stores/core/__tests__/filter.test.ts delete mode 100644 src/stores/core/__tests__/filterRelays.test.ts delete mode 100644 src/stores/core/__tests__/imeta.test.ts delete mode 100644 src/stores/core/__tests__/pool.test.ts delete mode 100644 src/stores/core/__tests__/relay.test.ts delete mode 100644 src/stores/core/__tests__/relayHints.test.ts delete mode 100644 src/stores/core/__tests__/subscription.test.ts delete mode 100644 src/stores/core/batcher.ts delete mode 100644 src/stores/core/filter.ts delete mode 100644 src/stores/core/filterRelay.ts delete mode 100644 src/stores/core/imeta.ts delete mode 100644 src/stores/core/operators.ts delete mode 100644 src/stores/core/pool.ts delete mode 100644 src/stores/core/relay.ts delete mode 100644 src/stores/core/relayHints.ts delete mode 100644 src/stores/core/subscription.ts delete mode 100644 src/stores/db/__tests__/db.atom.test.ts delete mode 100644 src/stores/db/__tests__/observabledb.test.ts delete mode 100644 src/stores/db/database.store.ts delete mode 100644 src/stores/db/db.atom.ts delete mode 100644 src/stores/db/db.batcher.ts delete mode 100644 src/stores/db/observabledb.store.ts delete mode 100644 src/stores/index.ts delete mode 100644 src/stores/modules/__tests__/feed.store.test.ts delete mode 100644 src/stores/modules/__tests__/note.store.test.ts delete mode 100644 src/stores/modules/feed.store.ts delete mode 100644 src/stores/modules/note.store.ts delete mode 100644 src/stores/modules/user.store.ts delete mode 100644 src/stores/nostr/__test__/contact.store.test.ts delete mode 100644 src/stores/nostr/__test__/nostr.store.test.ts delete mode 100644 src/stores/nostr/__test__/notes.store.test.ts delete mode 100644 src/stores/nostr/__test__/reactions.store.test.ts delete mode 100644 src/stores/nostr/__test__/userRelay.store.test.ts delete mode 100644 src/stores/nostr/__test__/users.store.test.ts delete mode 100644 src/stores/nostr/contacts.store.ts delete mode 100644 src/stores/nostr/nostr.store.ts delete mode 100644 src/stores/nostr/subscriptions.store.ts delete mode 100644 src/stores/nostr/userRelay.store.ts delete mode 100644 src/stores/root.store.ts delete mode 100644 src/stores/ui/setting.store.ts delete mode 100644 src/utils/__tests__/utils.test.ts delete mode 100644 src/utils/references.ts delete mode 100644 src/utils/utils.ts diff --git a/src/components/elements/Content/AutoMatcherExtension.ts b/src/components/elements/Content/AutoMatcherExtension.ts deleted file mode 100644 index 90558138..00000000 --- a/src/components/elements/Content/AutoMatcherExtension.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { Extension, combineTransactionSteps, getChangedRanges, type Editor, type Range } from '@tiptap/core' -import extractDomain from 'extract-domain' -import * as linkifyjs from 'linkifyjs' -import { undoDepth } from 'prosemirror-history' -import type { Node } from 'prosemirror-model' -import { Plugin, PluginKey, type EditorState, type Transaction } from 'prosemirror-state' -import type { IMeta } from 'stores/core/imeta' -import tlds from 'tlds' -import { parseReferences, type NostrReference } from 'utils/references' - -interface MatchBase extends Range { - text: string -} - -interface MatchLinks extends MatchBase { - kind: 'text' | 'image' | 'video' | 'tweet' | 'youtube' - href: string -} - -interface MatchTag extends MatchBase { - kind: 'tag' -} - -interface MatchNostr extends MatchBase { - kind: 'nostr' - ref: NostrReference -} - -type Matches = MatchLinks | MatchNostr | MatchTag - -interface GetMarkRange { - from: number - to: number - text: string -} - -interface NodeWithPosition { - node: Node - pos: number -} - -const IMAGE_EXTENSIONS = /.(jpg|jpeg|gif|png|bmp|svg|webp)$/ -const VIDEO_EXTENSIONS = /.(webm|mp4|ogg|mov)$/ -const REGEX_TAG = /(#\w+)/g - -type Storage = { - event: Event - imeta?: IMeta -} - -export const AutoMatcherExtension = Extension.create({ - name: 'autoMatcher', - - addProseMirrorPlugins() { - return [new AutoMatcherPlugin(this.editor).plugin] - }, -}) - -class AutoMatcherPlugin { - editor: Editor - plugin: Plugin - - constructor(editor: Editor) { - this.editor = editor - - const linkType = editor.schema.marks.link - - this.plugin = new Plugin({ - key: new PluginKey('link'), - - appendTransaction: (transactions, oldState, newState) => { - const isUndo = undoDepth(oldState) - undoDepth(newState) === 1 - - if (isUndo) { - return - } - - const docChanges = transactions.some((transaction) => transaction.docChanged) - - if (!docChanges) { - return - } - - const transform = combineTransactionSteps(oldState.doc, [...transactions]) - const changes = getChangedRanges(transform) - - const { tr, doc } = newState - const { mapping } = transform - - changes.forEach(({ oldRange, newRange }) => { - const { from, to } = newRange - const isNodeSeparated = to - from === 2 - - const prevMarks = this.getLinkMarksInRange(oldState.doc, oldRange.from, oldRange.to).map((mark) => ({ - mappedFrom: mapping.map(mark.from), - mappedTo: mapping.map(mark.to), - text: mark.text, - from: mark.from, - to: mark.to, - })) - - prevMarks.forEach(({ mappedFrom: newFrom, mappedTo: newTo, from: prevMarkFrom, to: prevMarkTo }, i) => { - this.getLinkMarksInRange(doc, newFrom, newTo).forEach((newMark) => { - const prevLinkText = oldState.doc.textBetween(prevMarkFrom, prevMarkTo, undefined, ' ') - const newLinkText = doc.textBetween(newMark.from, newMark.to + 1, undefined, ' ').trim() - - const wasLink = this.isValidTLD(prevLinkText) - const isLink = this.isValidTLD(newLinkText) - - if (isLink) { - return - } - - if (wasLink) { - tr.removeMark(newMark.from, newMark.to, linkType) - prevMarks.splice(i, 1) - } - - if (isNodeSeparated) { - return - } - - // Check newLinkText for a remaining valid link - if (from === to) { - this.findMatches(newLinkText, newFrom).forEach((match) => { - this.replace(tr, newState, match) - }) - } - }) - }) - - this.findTextBlocksInRange(doc, { from, to }).forEach(({ text, positionStart }) => { - this.findMatches(text, positionStart + 1) - .filter((range) => { - const fromIsInRange = range.from >= from && range.from <= to - const toIsInRange = range.to >= from && range.to <= to - return fromIsInRange || toIsInRange || isNodeSeparated - }) - .filter(({ from, text }) => !prevMarks.some((prev) => prev.mappedFrom === from && prev.text === text)) - .forEach((link) => { - this.replace(tr, newState, link) - }) - }) - }) - - if (tr.steps.length === 0) { - return - } - - return tr - }, - - props: { - clipboardTextSerializer(slice) { - let text = '' - slice.content.descendants((node) => { - if (node.type.name === 'paragraph') { - return - } - text += node.textContent - if (node.type.name === 'mention' || node.type.name === 'note') { - text += node.attrs.text - } - }) - return text - }, - }, - }) - } - - private replace(tr: Transaction, state: EditorState, match: Matches) { - const { kind, text, from, to } = match - const { nodes, marks } = state.schema - switch (kind) { - case 'text': { - tr.addMark(from, to, marks.link.create({ href: match.href })) - return true - } - case 'image': { - tr.replaceWith(from, to, nodes.image.create({ src: match.href })) - return true - } - case 'youtube': { - tr.replaceWith(from, to, nodes.youtube.create({ src: match.href })) - return true - } - case 'tweet': { - tr.replaceWith(from, to, nodes.tweet.create({ src: match.href })) - return true - } - case 'video': { - tr.replaceWith(from, to, nodes.video.create({ src: match.href })) - return true - } - case 'tag': { - tr.addMark(from, to, marks.tag.create({ tag: match.text })) - return true - } - case 'nostr': { - const { ref } = match - switch (ref.prefix) { - case 'npub': - case 'nprofile': { - tr.replaceWith(from, to, nodes.mention.create({ ...ref.profile, text })) - return true - } - case 'note': - case 'nevent': { - tr.replaceWith(from, to, nodes.note.create(ref.event)) - return true - } - default: { - return false - } - } - } - default: { - return false - } - } - } - - private findTextBlocksInRange(node: Node, range: Range): Array<{ text: string; positionStart: number }> { - const nodesWithPos: NodeWithPosition[] = [] - - // define a placeholder for leaf nodes to calculate link position - node.nodesBetween(range.from, range.to, (node, pos) => { - if (!node.isTextblock || !node.type.allowsMarkType(this.editor.schema.marks.link)) { - return - } - - nodesWithPos.push({ node, pos }) - }) - - return nodesWithPos.map((textBlock) => ({ - text: node.textBetween(textBlock.pos, textBlock.pos + textBlock.node.nodeSize, undefined, ' '), - positionStart: textBlock.pos, - })) - } - - private findMatches(text: string, positionStart: number): Matches[] { - const links = this.findLinks(text) - const tags = this.findTags(text) - const refs = this.findNostrRefs(text) - return [...links, ...tags, ...refs] - .map((match) => ({ - ...match, - from: positionStart + match.from, - to: positionStart + match.to, - })) - .sort((a, b) => (a.from > b.from ? -1 : 1)) - } - - private findLinks(text: string): Matches[] { - const links: Matches[] = [] - - for (const { start: from, end: to, value, href } of linkifyjs.find(text) || []) { - const kind = this.getLinkKind(value, href) - - if (!this.isValidTLD(href) && !href.startsWith('tel:')) { - continue - } - - links.push({ text: value, href, kind, from, to }) - } - - return links - } - - private findTags(text: string): Matches[] { - const tags: Matches[] = [] - for (const match of text.matchAll(REGEX_TAG)) { - const text = match[0] - const from = match.index || 0 - const to = from + text.length - tags.push({ text: match[0], kind: 'tag', from, to }) - } - return tags - } - - private findNostrRefs(text: string): Matches[] { - const refs: Matches[] = [] - const parsed = this.editor.storage.reference || parseReferences({ content: text }) - for (const ref of parsed) { - const from = ref.index - const to = from + ref.text.length - refs.push({ kind: 'nostr', from, to, text: ref.text, ref }) - } - return refs - } - - private getLinkMarksInRange(doc: EditorState['doc'], from: number, to: number) { - const linkMarks: GetMarkRange[] = [] - - doc.nodesBetween(from, to, (node, pos) => { - const marks = node.marks ?? [] - const mark = marks.find((mark) => mark.type === this.editor.schema.marks.link) - - if (mark) { - linkMarks.push({ - from: pos, - to: pos + node.nodeSize, - text: node.textContent, - }) - } - }) - return linkMarks - } - - private getLinkKind(url: string, href: string): MatchLinks['kind'] { - const mimetype = this.editor.storage.imeta?.getMimeType(url)?.split('/')[0] - if (mimetype && ['image', 'video'].includes(mimetype)) { - return mimetype as 'image' | 'video' - } else if (/youtube|youtu.be/.test(url)) { - return 'youtube' - } else if (/^https?:\/\/(twitter|x)\.com\/(?:#!\/)?(\w+)\/status(es)?\/(\d+)/.test(url)) { - return 'tweet' - } else { - const { pathname } = new URL(href) - return IMAGE_EXTENSIONS.test(pathname) ? 'image' : VIDEO_EXTENSIONS.test(pathname) ? 'video' : 'text' - } - } - - private isValidTLD(str: string): boolean { - const domain = extractDomain(str) - - if (domain === '') { - // Not a domain - return true - } - - const parts = domain?.toString().split('.') || [] - const tld = parts[parts.length - 1] - - return tlds.includes(tld) - } -} diff --git a/src/components/elements/Content/Bubble.tsx b/src/components/elements/Content/Bubble.tsx deleted file mode 100644 index 9e52dfa4..00000000 --- a/src/components/elements/Content/Bubble.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, styled } from '@mui/material' -import { observer } from 'mobx-react-lite' -import { useStore } from 'stores' -import type { Note } from 'stores/modules/note.store' -import { Row } from '../Layouts/Flex' -import { UserHeaderDate } from '../User/UserHeader' -import UserName from '../User/UserName' -import { TextContent } from './Text' -import type { ParagraphNode } from './types' - -type Props = { - node: ParagraphNode - note: Note - renderUserHeader?: boolean -} - -export const BubbleContainer = styled(Box)(({ theme }) => - theme.unstable_sx({ - px: 1.5, - pt: 0.2, - pb: 0.6, - borderRadius: 1.5, - backgroundColor: 'var(--mui-palette-FilledInput-bg)', - display: 'inline-block', - }), -) - -const Container = styled('div')(({ theme }) => - theme.unstable_sx({ - lineHeight: 1.5, - wordBreak: 'break-word', - height: 'auto', - }), -) - -export const ReplyUserHeader = observer((props: { note: Note }) => { - const store = useStore() - const user = store.users.getUserById(props.note.event?.pubkey) - return ( - - - - - ) -}) - -function Bubble(props: Props) { - const { note, node, renderUserHeader } = props - return ( - - - {renderUserHeader && } - - - - ) -} - -export default Bubble diff --git a/src/components/elements/Content/Link/LinkExtension.ts b/src/components/elements/Content/Link/LinkExtension.ts deleted file mode 100644 index 18d92d64..00000000 --- a/src/components/elements/Content/Link/LinkExtension.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Mark } from '@tiptap/core' - -export interface LinkProtocolOptions { - scheme: string - optionalSlashes?: boolean -} - -export type LinkAttributes = { - href: string -} - -export const LinkExtension = Mark.create({ - name: 'link' as const, - - inclusive: false, - - excludes: '_', - - addAttributes() { - return { - href: { default: null }, - } - }, - - renderHTML({ HTMLAttributes }) { - return ['a', HTMLAttributes, 0] - }, -}) diff --git a/src/components/elements/Content/Mention/MentionExtension.tsx b/src/components/elements/Content/Mention/MentionExtension.tsx deleted file mode 100644 index 0cf3d97f..00000000 --- a/src/components/elements/Content/Mention/MentionExtension.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Node } from '@tiptap/core' -import { ReactNodeViewRenderer } from '@tiptap/react' -import { MentionEditor } from './MentionEditor' - -export type MentionExtensionAttributes = { - id: string - pubkey: string -} - -export const MentionExtension = Node.create({ - name: 'mention', - - inline: true, - - inclusive: true, - - group: 'inline', - - addNodeView() { - return ReactNodeViewRenderer(MentionEditor) - }, - - renderHTML(p) { - return ['span', { ...p.node.attrs }, 'mention'] - }, - - addAttributes() { - return { - text: { default: null }, - pubkey: { default: null }, - relays: { default: null }, - } - }, -}) diff --git a/src/components/elements/Content/Note/NoteExtension.ts b/src/components/elements/Content/Note/NoteExtension.ts deleted file mode 100644 index fd9509e0..00000000 --- a/src/components/elements/Content/Note/NoteExtension.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Node } from '@tiptap/core' -import { ReactNodeViewRenderer } from '@tiptap/react' -import NoteEditor from './NoteEditor' - -export interface NoteExtensionAttributes { - id: string - author: string - relays: string[] -} - -export const NoteExtension = Node.create({ - name: 'note', - - group: 'block', - - atom: true, - - addAttributes() { - return { - id: { default: null }, - author: { default: null }, - relays: { default: null }, - } - }, - - addNodeView() { - return ReactNodeViewRenderer(NoteEditor) - }, - - renderHTML() { - return ['div', {}, 0] - }, -}) diff --git a/src/components/elements/Content/Paragraph.tsx b/src/components/elements/Content/Paragraph.tsx deleted file mode 100644 index 59afcba3..00000000 --- a/src/components/elements/Content/Paragraph.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { styled } from '@mui/material' -import type { ParagraphNode } from 'components/elements/Content/types' -import { TextContent } from './Text' - -const shouldForwardProp = (prop: string) => prop !== 'dense' - -export const Container = styled('div', { shouldForwardProp })<{ dense: boolean }>(({ dense, theme }) => - theme.unstable_sx({ - px: dense ? 0 : 2, - py: dense ? 0 : 0, - lineHeight: dense ? 1.5 : 1.7, - wordBreak: 'break-word', - height: 'auto', - }), -) - -type Props = { - node: ParagraphNode - dense?: boolean -} - -export function Paragraph(props: Props) { - const { dense = false } = props - return ( - - - - ) -} diff --git a/src/components/elements/Content/Tag/TagExtension.ts b/src/components/elements/Content/Tag/TagExtension.ts deleted file mode 100644 index aabebfde..00000000 --- a/src/components/elements/Content/Tag/TagExtension.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Mark } from '@tiptap/core' - -export interface TagAttributes { - tag: string -} - -export const TagExtension = Mark.create({ - name: 'tag', - - inline: true, - - selectable: true, - - inclusive: false, - - group: 'inline', - - renderHTML(p) { - return ['span', { ...p.mark.attrs, style: 'border:1px solid red;' }, 0] - }, - - addAttributes() { - return { - tag: { default: null }, - } - }, -}) diff --git a/src/components/elements/Content/Tweet/TweetExtension.tsx b/src/components/elements/Content/Tweet/TweetExtension.tsx deleted file mode 100644 index dd86151e..00000000 --- a/src/components/elements/Content/Tweet/TweetExtension.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Node } from '@tiptap/core' -import { ReactNodeViewRenderer } from '@tiptap/react' -import TweetEditor from './TweetEditor' - -export const TweetExtension = Node.create({ - name: 'tweet', - - group: 'block', - - atom: true, - - addAttributes() { - return { - src: { default: null }, - } - }, - - addNodeView() { - return ReactNodeViewRenderer(TweetEditor) - }, - - renderHTML() { - return ['div', {}, 0] - }, -}) diff --git a/src/components/elements/Content/Video/VideoExtension.ts b/src/components/elements/Content/Video/VideoExtension.ts deleted file mode 100644 index 83febb8e..00000000 --- a/src/components/elements/Content/Video/VideoExtension.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Node } from '@tiptap/core' -import { ReactNodeViewRenderer } from '@tiptap/react' -import { VideoEditor } from './VideoEditor' - -export interface VideoExtensionAttributes { - src: string -} - -export const VideoExtension = Node.create({ - name: 'video', - - inline: false, - - inclusive: false, - - group: 'block', - - atom: true, - - addAttributes() { - return { - src: { default: null }, - } - }, - - addNodeView() { - return ReactNodeViewRenderer(VideoEditor) - }, - - renderHTML() { - return ['a', {}, 0] - }, -}) diff --git a/src/components/elements/Content/__tests__/parser.test.ts b/src/components/elements/Content/__tests__/parser.test.ts deleted file mode 100644 index d865f190..00000000 --- a/src/components/elements/Content/__tests__/parser.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { Kind } from 'constants/kinds' -import { nip19 } from 'nostr-tools' -import { fakeNote, fakeSignature } from 'utils/faker' -import { test } from 'utils/fixtures' -import { parseNote } from '../parser' - -describe('ContentParser', () => { - test('Should assert the content with a image url with imeta', ({ createNote }) => { - const note = createNote({ - content: 'http://host.com/image http://host.com/video https://simplelink.com', - tags: [ - ['imeta', 'url http://host.com/image', 'm image/jpg'], - ['imeta', 'url http://host.com/video', 'm video/mp4'], - ], - }) - expect(parseNote(note.event, note.references, note.imeta)).toMatchInlineSnapshot(` - { - "content": [ - { - "type": "paragraph", - }, - { - "attrs": { - "alt": null, - "src": "http://host.com/image", - "title": null, - }, - "type": "image", - }, - { - "content": [ - { - "text": " ", - "type": "text", - }, - ], - "type": "paragraph", - }, - { - "attrs": { - "src": "http://host.com/video", - }, - "type": "video", - }, - { - "content": [ - { - "text": " ", - "type": "text", - }, - { - "marks": [ - { - "attrs": { - "href": "https://simplelink.com", - }, - "type": "link", - }, - ], - "text": "https://simplelink.com", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "doc", - } - `) - }) - - test('Should assert the content with multiple nodes', ({ createNote }) => { - const ref = fakeSignature( - fakeNote({ - id: '1a8232732307c9190d8d87f9692527ed34c388687247490f9fa4d0ba38db539c', - content: 'related', - created_at: 1, - pubkey: '1', - }), - ) - const nevent = nip19.neventEncode({ id: ref.id, relays: [], author: ref.pubkey }) - const note = createNote({ - content: `Hi! https://google.com #tag nostr:${nevent} Hi nostr:nprofile1qqsvvcpmpuwvlmrztkwq3d6nunmhf6hh688jw6fzxyjmtl2d5u5qr8spz3mhxue69uhhyetvv9ujuerpd46hxtnfdufzkeuj check this out https://nostr.com/img.jpg https://v.nostr.build/g6BQ.mp4`, - }) - expect(parseNote(note.event)).toMatchInlineSnapshot(` - { - "content": [ - { - "content": [ - { - "text": "Hi! ", - "type": "text", - }, - { - "marks": [ - { - "attrs": { - "href": "https://google.com", - }, - "type": "link", - }, - ], - "text": "https://google.com", - "type": "text", - }, - { - "text": " ", - "type": "text", - }, - { - "marks": [ - { - "attrs": { - "tag": "#tag", - }, - "type": "tag", - }, - ], - "text": "#tag", - "type": "text", - }, - { - "text": " ", - "type": "text", - }, - ], - "type": "paragraph", - }, - { - "attrs": { - "author": "e384fed7cd372bfa9422ad5d0714580fbfbe524a32db4836452857aa1322f88b", - "id": "2968872fe8fae19ebbcc2dbedf90099381519d0801c9d4c3b7139c7bd75a90ea", - "relays": [], - }, - "type": "note", - }, - { - "content": [ - { - "text": " Hi ", - "type": "text", - }, - { - "attrs": { - "pubkey": "c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e", - "relays": [ - "wss://relay.damus.io", - ], - "text": "nostr:nprofile1qqsvvcpmpuwvlmrztkwq3d6nunmhf6hh688jw6fzxyjmtl2d5u5qr8spz3mhxue69uhhyetvv9ujuerpd46hxtnfdufzkeuj", - }, - "type": "mention", - }, - { - "text": " check this out ", - "type": "text", - }, - ], - "type": "paragraph", - }, - { - "attrs": { - "alt": null, - "src": "https://nostr.com/img.jpg", - "title": null, - }, - "type": "image", - }, - { - "content": [ - { - "text": " ", - "type": "text", - }, - ], - "type": "paragraph", - }, - { - "attrs": { - "src": "https://v.nostr.build/g6BQ.mp4", - }, - "type": "video", - }, - ], - "type": "doc", - } - `) - }) - - test('Should assert markdown content', ({ createNote }) => { - const note = createNote({ - kind: Kind.Article, - content: ` -# Title - -* list 1 -* list 2 -* list 3 - -text **bold** *italic* [link](https://google.com) -`, - }) - expect(parseNote(note.event)).toMatchInlineSnapshot(` - { - "content": [ - { - "attrs": { - "level": 1, - }, - "content": [ - { - "text": "Title", - "type": "text", - }, - ], - "type": "heading", - }, - { - "attrs": { - "tight": true, - }, - "content": [ - { - "content": [ - { - "content": [ - { - "text": "list 1", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "listItem", - }, - { - "content": [ - { - "content": [ - { - "text": "list 2", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "listItem", - }, - { - "content": [ - { - "content": [ - { - "text": "list 3", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "listItem", - }, - ], - "type": "bulletList", - }, - { - "content": [ - { - "text": "text ", - "type": "text", - }, - { - "marks": [ - { - "type": "bold", - }, - ], - "text": "bold", - "type": "text", - }, - { - "text": " ", - "type": "text", - }, - { - "marks": [ - { - "type": "italic", - }, - ], - "text": "italic", - "type": "text", - }, - { - "text": " link", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "doc", - } - `) - }) -}) diff --git a/src/components/elements/Content/parser.ts b/src/components/elements/Content/parser.ts deleted file mode 100644 index 00df845c..00000000 --- a/src/components/elements/Content/parser.ts +++ /dev/null @@ -1,49 +0,0 @@ -import ImageExtension from '@tiptap/extension-image' -import YoutubeExtension from '@tiptap/extension-youtube' -import StarterKit from '@tiptap/starter-kit' - -import { Editor, type Extensions } from '@tiptap/react' -import { Kind } from 'constants/kinds' -import type { Event as NostrEvent } from 'nostr-tools' -import type { IMeta } from 'stores/core/imeta' -import { Markdown as MarkdownExtension } from 'tiptap-markdown' -import type { NostrReference } from 'utils/references' -import { AutoMatcherExtension } from './AutoMatcherExtension' -import { addImageNodeView } from './Image/ImageEditor' -import { LinkExtension } from './Link/LinkExtension' -import { MentionExtension } from './Mention/MentionExtension' -import { NoteExtension } from './Note/NoteExtension' -import { TagExtension } from './Tag/TagExtension' -import { TweetExtension } from './Tweet/TweetExtension' -import { VideoExtension } from './Video/VideoExtension' -import type { StateJSONSchema } from './types' - -const extensions: Extensions = [ - StarterKit.configure({ history: false }), - TagExtension, - LinkExtension, - NoteExtension, - ImageExtension.extend(addImageNodeView()), - VideoExtension, - TweetExtension, - YoutubeExtension, - MentionExtension, - AutoMatcherExtension, -] - -const editor = new Editor({ extensions }) -const editorMarkdown = new Editor({ extensions: [...extensions, MarkdownExtension] }) - -export function parseNote(event: NostrEvent, references?: NostrReference[], imeta?: IMeta): StateJSONSchema { - const _editor = event.kind === Kind.Article ? editorMarkdown : editor - _editor.storage.imeta = imeta - _editor.storage.references = references - _editor.commands.setContent(event.kind === Kind.Text ? event.content.replace(/\n+/g, '
') : event.content) - return _editor.getJSON() as StateJSONSchema -} - -export function parseUserAbout(about: string): StateJSONSchema { - editor.storage.references = null - editor.commands.setContent(about) - return editor.getJSON() as StateJSONSchema -} diff --git a/src/components/elements/Content/types.ts b/src/components/elements/Content/types.ts deleted file mode 100644 index dd2f5a2c..00000000 --- a/src/components/elements/Content/types.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { ImageOptions } from '@tiptap/extension-image' -import type { StarterKitOptions } from '@tiptap/starter-kit' -import type { LinkAttributes } from './Link/LinkExtension' -import type { MentionExtensionAttributes } from './Mention/MentionExtension' -import type { NoteExtensionAttributes } from './Note/NoteExtension' -import type { TagAttributes } from './Tag/TagExtension' -import type { VideoExtensionAttributes } from './Video/VideoExtension' - -type Extensions = T - -export type ParagraphNode = { - type: Extensions<'paragraph'> - content?: Node[] -} - -export type TextNode = { - type: Extensions<'text'> - marks?: Mark[] - text: string -} - -type HardBreak = { - type: Extensions<'hardBreak'> -} - -type TagMark = { - type: 'tag' - attrs: TagAttributes -} - -type LinkMark = { - type: 'link' - attrs: LinkAttributes -} - -type CodeMark = { - type: 'code' -} - -type ItalicMark = { - type: 'italic' -} - -type BoldMark = { - type: 'bold' -} - -type StrikeMark = { - type: 'strike' -} - -type MentionNode = { - type: 'mention' - attrs: MentionExtensionAttributes -} - -type NoteNode = { - type: 'note' - attrs: NoteExtensionAttributes -} - -type ImageNode = { - type: 'image' - attrs: ImageOptions['HTMLAttributes'] -} - -type VideoNode = { - type: 'video' - attrs: VideoExtensionAttributes -} - -export type HeadingNode = { - type: Extensions<'heading'> - content: Node[] - attrs: { - level: number - } -} - -type ListItemNode = { - type: Extensions<'listItem'> - content: Node[] - attrs: { - closed: boolean - nested: boolean - } -} - -export type BulletListNode = { - type: Extensions<'bulletList'> - content: ListItemNode[] -} - -export type OrderedListNode = { - type: Extensions<'orderedList'> - content: ListItemNode[] -} - -export type CodeBlockNode = { - type: Extensions<'codeBlock'> - content: Node[] - attrs: { - language: string - } -} - -export type BlockQuoteNode = { - type: Extensions<'blockquote'> - content: Node[] -} - -type TweetNode = { - type: 'tweet' - attrs: { - src: string - } -} - -type YoutubeNode = { - type: 'youtube' - attrs: { - src: string - width?: number - height?: number - start?: number - } -} - -export type Mark = TagMark | LinkMark | CodeMark | ItalicMark | BoldMark | StrikeMark - -export type Node = - | ParagraphNode - | TextNode - | HardBreak - | MentionNode - | NoteNode - | ImageNode - | VideoNode - | HeadingNode - | CodeBlockNode - | BulletListNode - | OrderedListNode - | BlockQuoteNode - | TweetNode - | YoutubeNode - -export type StateJSONSchema = { - type: 'doc' - content: Node[] -} diff --git a/src/components/elements/Posts/PostList.tsx b/src/components/elements/Posts/PostList.tsx deleted file mode 100644 index 8c5b96ca..00000000 --- a/src/components/elements/Posts/PostList.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { values } from 'mobx' -import { observer } from 'mobx-react-lite' -import { useCallback, useLayoutEffect, useMemo, useRef } from 'react' -import type { FeedStore } from 'stores/modules/feed.store' -import { WVList, type CacheSnapshot, type WVListHandle } from 'virtua' -import Post from './Post' -import PostLoading from './PostLoading' - -type Props = { - feed: FeedStore -} - -function Footer() { - return ( - <> - - - - ) -} - -const PostList = observer(function PostWindow(props: Props) { - const feed = values(props.feed.feed) - - const cacheKey = `window-list-cache-${props.feed.options.name}` - - const ref = useRef(null) - const seenRefs = useRef(new Set()).current - - const [offset, cache] = useMemo(() => { - const serialized = sessionStorage.getItem(cacheKey) - if (!serialized) return [] - try { - return JSON.parse(serialized) as [number, CacheSnapshot] - } catch (e) { - return [] - } - }, [cacheKey]) - - useLayoutEffect(() => { - if (!ref.current) { - return - } - const handle = ref.current - - window.scrollTo(0, offset ?? 0) - - let scrollY = 0 - const onScroll = () => { - scrollY = window.scrollY - } - window.addEventListener('scroll', onScroll) - onScroll() - - return () => { - window.removeEventListener('scroll', onScroll) - sessionStorage.setItem(cacheKey, JSON.stringify([scrollY, handle.cache])) - } - }, [cacheKey, offset]) - - const handleScrollForReactions = useCallback( - (start: number, end: number) => { - const range = [...Array(1 + end - start).keys()].map((x) => start + x) - const newKeys = range.filter((index) => !seenRefs.has(index)) - range.forEach((index) => seenRefs.add(index)) - props.feed.reactions$.next(newKeys.map((index) => feed[index]?.event.id)) - }, - [seenRefs, props.feed, feed], - ) - - const handleScrollForPagination = useCallback( - (end: number) => { - const { size } = props.feed.feed - if (size > 0 && end >= size - 5) { - props.feed.paginate$.next() - } - }, - [props.feed], - ) - - const handleRangeChange = useCallback( - (start: number, end: number) => { - if (start >= 0 && end >= 0) { - handleScrollForReactions(start, end) - handleScrollForPagination(end) - } - }, - [handleScrollForPagination, handleScrollForReactions], - ) - - return ( - <> - - {feed.map((note) => ( - - ))} - -