diff --git a/package.json b/package.json
index 4fbc7a85..00997e7a 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"markdown-to-jsx": "^7.3.2",
"media-chrome": "^1.5.2",
"minidenticons": "^4.2.0",
+ "nanoid": "^5.0.3",
"nostr-fetch": "^0.13.1",
"nostr-tools": "^1.17.0",
"qrcode.react": "^3.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fd55430b..6ec56e9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -146,6 +146,9 @@ dependencies:
minidenticons:
specifier: ^4.2.0
version: 4.2.0
+ nanoid:
+ specifier: ^5.0.3
+ version: 5.0.3
nostr-fetch:
specifier: ^0.13.1
version: 0.13.1
@@ -4696,6 +4699,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ /nanoid@5.0.3:
+ resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+ dev: false
+
/natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
diff --git a/src/app.css b/src/app.css
index d7f5d15b..cba4eba0 100644
--- a/src/app.css
+++ b/src/app.css
@@ -10,6 +10,10 @@
word-wrap: break-word;
overflow-wrap: break-word;
}
+
+ .prose :where(iframe):not(:where([class~="not-prose"] *)) {
+ @apply aspect-video w-full h-auto mx-auto;
+ }
}
html {
@@ -64,119 +68,3 @@ input::-ms-clear {
.ProseMirror img.ProseMirror-selectednode {
@apply outline-blue-500;
}
-
-[data-rmiz] {
- position: relative;
-}
-
-[data-rmiz-ghost] {
- position: absolute;
- pointer-events: none;
-}
-
-[data-rmiz-btn-zoom],
-[data-rmiz-btn-unzoom] {
- background-color: rgba(0, 0, 0, 0.7);
- border-radius: 50%;
- border: none;
- box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
- color: #fff;
- height: 40px;
- margin: 0;
- outline-offset: 2px;
- padding: 9px;
- touch-action: manipulation;
- width: 40px;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-[data-rmiz-btn-zoom]:not(:focus):not(:active) {
- position: absolute;
- clip: rect(0 0 0 0);
- clip-path: inset(50%);
- height: 1px;
- overflow: hidden;
- pointer-events: none;
- white-space: nowrap;
- width: 1px;
-}
-
-[data-rmiz-btn-zoom] {
- position: absolute;
- inset: 10px 10px auto auto;
- cursor: zoom-in;
-}
-
-[data-rmiz-btn-unzoom] {
- position: absolute;
- inset: 20px 20px auto auto;
- cursor: zoom-out;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-[data-rmiz-content="found"] img,
-[data-rmiz-content="found"] svg,
-[data-rmiz-content="found"] [role="img"],
-[data-rmiz-content="found"] [data-zoom] {
- cursor: zoom-in;
-}
-
-[data-rmiz-modal]::backdrop {
- display: none;
-}
-
-[data-rmiz-modal][open] {
- position: fixed;
- width: 100vw;
- width: 100dvw;
- height: 100vh;
- height: 100dvh;
- max-width: none;
- max-height: none;
- margin: 0;
- padding: 0;
- border: 0;
- background: transparent;
- overflow: hidden;
-}
-
-[data-rmiz-modal-overlay] {
- position: absolute;
- inset: 0;
- transition: background-color 0.3s;
-}
-
-[data-rmiz-modal-overlay="hidden"] {
- background-color: rgba(255, 255, 255, 0);
-}
-
-[data-rmiz-modal-overlay="visible"] {
- background-color: rgba(255, 255, 255, 0.5);
- backdrop-filter: blur(4px);
-}
-
-[data-rmiz-modal-content] {
- position: relative;
- width: 100%;
- height: 100%;
-}
-
-[data-rmiz-modal-img] {
- position: absolute;
- cursor: zoom-out;
- image-rendering: high-quality;
- transform-origin: top left;
- transition: transform 0.3s;
-}
-
-@media (prefers-reduced-motion: reduce) {
- [data-rmiz-modal-overlay],
- [data-rmiz-modal-img] {
- transition-duration: 0.01ms !important;
- }
-}
diff --git a/src/app/chats/components/message.tsx b/src/app/chats/components/message.tsx
index 3dedeac2..f12843c9 100644
--- a/src/app/chats/components/message.tsx
+++ b/src/app/chats/components/message.tsx
@@ -3,13 +3,8 @@ import { twMerge } from 'tailwind-merge';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
-import { ImagePreview, LinkPreview, MentionNote, VideoPreview } from '@shared/notes';
-
-import { parser } from '@utils/parser';
-
export function ChatMessage({ message, self }: { message: NDKEvent; self: boolean }) {
const decryptedContent = useDecryptMessage(message);
- const richContent = parser(decryptedContent) ?? null;
return (
- {!richContent ? (
+ {!decryptedContent ? (
Decrypting...
) : (
-
{richContent.parsed}
-
- {richContent.images.length > 0 && }
- {richContent.videos.length > 0 && }
- {richContent.links.length > 0 && }
- {richContent.notes.length > 0 &&
- richContent.notes.map((note: string) => (
-
- ))}
-
+
{decryptedContent}
)}
diff --git a/src/app/new/article.tsx b/src/app/new/article.tsx
index 38998e35..16b20bf4 100644
--- a/src/app/new/article.tsx
+++ b/src/app/new/article.tsx
@@ -1,4 +1,4 @@
-import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
+import { NDKEvent, NDKKind, NDKTag } from '@nostr-dev-kit/ndk';
import CharacterCount from '@tiptap/extension-character-count';
import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder';
@@ -71,7 +71,7 @@ export function NewArticleScreen() {
const content = editor.storage.markdown.getMarkdown();
// define tags
- const tags: string[][] = [
+ const tags: NDKTag[] = [
['d', ident],
['title', title],
['image', cover],
@@ -85,17 +85,20 @@ export function NewArticleScreen() {
tags.push(['t', tag.replace('#', '')]);
});
- // publish message
const event = new NDKEvent(ndk);
event.content = content;
event.kind = NDKKind.Article;
event.tags = tags;
+ // publish
const publishedRelays = await event.publish();
+
if (publishedRelays) {
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
+
// update state
setLoading(false);
+
// reset editor
editor.commands.clearContent();
localStorage.setItem('editor-article', '{}');
@@ -235,7 +238,7 @@ export function NewArticleScreen() {
- {editor?.storage?.characterCount.characters()}
+ {editor?.storage?.characterCount.characters()} characters
-
diff --git a/src/app/new/post.tsx b/src/app/new/post.tsx
index afb92439..a77e5f74 100644
--- a/src/app/new/post.tsx
+++ b/src/app/new/post.tsx
@@ -1,4 +1,4 @@
-import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
+import { NDKEvent, NDKKind, NDKTag } from '@nostr-dev-kit/ndk';
import CharacterCount from '@tiptap/extension-character-count';
import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder';
@@ -16,8 +16,13 @@ import { useNDK } from '@libs/ndk/provider';
import { CancelIcon, LoaderIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes';
+import { WIDGET_KIND } from '@stores/constants';
+
+import { useWidget } from '@utils/hooks/useWidget';
+
export function NewPostScreen() {
const { ndk, relayUrls } = useNDK();
+ const { addWidget } = useWidget();
const [loading, setLoading] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
@@ -67,11 +72,11 @@ export function NewPostScreen() {
});
// define tags
- let tags: string[][] = [];
+ let tags: NDKTag[] = [];
// add reply to tags if present
if (reply.id && reply.pubkey) {
- if (reply.root && reply.root.length > 1) {
+ if (reply.root) {
tags = [
['e', reply.root, relayUrls[0], 'root'],
['e', reply.id, relayUrls[0], 'reply'],
@@ -89,24 +94,35 @@ export function NewPostScreen() {
const hashtags = serializedContent
.split(/\s/gm)
.filter((s: string) => s.startsWith('#'));
-
hashtags?.forEach((tag: string) => {
tags.push(['t', tag.replace('#', '')]);
});
- // publish message
const event = new NDKEvent(ndk);
event.content = serializedContent;
event.kind = NDKKind.Text;
event.tags = tags;
+ // publish event
const publishedRelays = await event.publish();
+
if (publishedRelays) {
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
+
// update state
setLoading(false);
- // reset editor
setSearchParams({});
+
+ // open new widget with this event id
+ if (!reply.id && !reply.pubkey) {
+ addWidget.mutate({
+ title: 'Thread',
+ content: event.id,
+ kind: WIDGET_KIND.thread,
+ });
+ }
+
+ // reset editor
editor.commands.clearContent();
localStorage.setItem('editor-post', '{}');
}
@@ -132,20 +148,20 @@ export function NewPostScreen() {
/>
{searchParams.get('id') && (
-
+
)}
- {editor?.storage?.characterCount.characters()}
+ {editor?.storage?.characterCount.characters()} characters
diff --git a/src/app/notes/article.tsx b/src/app/notes/article.tsx
index 1df8bedf..4f746f55 100644
--- a/src/app/notes/article.tsx
+++ b/src/app/notes/article.tsx
@@ -1,24 +1,21 @@
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
+import Markdown from 'markdown-to-jsx';
import { nip19 } from 'nostr-tools';
-import { AddressPointer, EventPointer } from 'nostr-tools/lib/types/nip19';
-import { useRef, useState } from 'react';
+import { EventPointer } from 'nostr-tools/lib/types/nip19';
+import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
-import { NoteActions, NoteReplyForm } from '@shared/notes';
+import { ArrowLeftIcon, CheckCircleIcon, ShareIcon } from '@shared/icons';
+import { NoteReplyForm } from '@shared/notes';
import { ReplyList } from '@shared/notes/replies/list';
-import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent';
export function ArticleNoteScreen() {
- const { id } = useParams();
-
const navigate = useNavigate();
- const replyRef = useRef(null);
- const naddr = id.startsWith('naddr') ? (nip19.decode(id).data as AddressPointer) : null;
- const { status, data } = useEvent(id, naddr);
+ const { id } = useParams();
+ const { status, data } = useEvent(id);
const [isCopy, setIsCopy] = useState(false);
@@ -33,12 +30,8 @@ export function ArticleNoteScreen() {
setTimeout(() => setIsCopy(false), 2000);
};
- const scrollToReply = () => {
- replyRef.current.scrollIntoView();
- };
-
return (
-
+
-
-
-
-
+
-
-
- {status === 'pending' ? (
-
Loading...
- ) : (
-
-
-
-
{data.content}
-
-
-
-
-
- )}
-
-
+
+ {status === 'pending' ? (
+
Loading...
+ ) : (
+
+ {data.content}
+
+ )}
+
+
-
);
}
diff --git a/src/app/notes/text.tsx b/src/app/notes/text.tsx
index 85695b6e..861a0861 100644
--- a/src/app/notes/text.tsx
+++ b/src/app/notes/text.tsx
@@ -6,19 +6,27 @@ import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
-import { MemoizedTextKind, NoteActions, NoteReplyForm, UnknownNote } from '@shared/notes';
+import {
+ ChildNote,
+ MemoizedTextKind,
+ NoteActions,
+ NoteReplyForm,
+ UnknownNote,
+} from '@shared/notes';
import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent';
+import { useNostr } from '@utils/hooks/useNostr';
export function TextNoteScreen() {
- const { id } = useParams();
- const { status, data } = useEvent(id);
-
const navigate = useNavigate();
const replyRef = useRef(null);
+ const { id } = useParams();
+ const { status, data } = useEvent(id);
+ const { getEventThread } = useNostr();
+
const [isCopy, setIsCopy] = useState(false);
const share = async () => {
@@ -37,9 +45,24 @@ export function TextNoteScreen() {
};
const renderKind = (event: NDKEvent) => {
+ const thread = getEventThread(event.tags);
switch (event.kind) {
case NDKKind.Text:
- return
;
+ return (
+ <>
+ {thread ? (
+
+
+ {thread.rootEventId ? (
+
+ ) : null}
+ {thread.replyEventId ? : null}
+
+
+ ) : null}
+
+ >
+ );
default:
return
;
}
diff --git a/src/shared/notes/actions/more.tsx b/src/shared/notes/actions/more.tsx
index cd588544..d2f4d9e9 100644
--- a/src/shared/notes/actions/more.tsx
+++ b/src/shared/notes/actions/more.tsx
@@ -30,20 +30,12 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
-
-
-
- Focus
-
-
+
@@ -52,7 +44,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
@@ -60,7 +52,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
View profile
diff --git a/src/shared/notes/mentions/note.tsx b/src/shared/notes/mentions/note.tsx
index 8b624685..07024a0f 100644
--- a/src/shared/notes/mentions/note.tsx
+++ b/src/shared/notes/mentions/note.tsx
@@ -14,7 +14,13 @@ import { WIDGET_KIND } from '@stores/constants';
import { useEvent } from '@utils/hooks/useEvent';
import { useWidget } from '@utils/hooks/useWidget';
-export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
+export const MentionNote = memo(function MentionNote({
+ id,
+ editing,
+}: {
+ id: string;
+ editing?: boolean;
+}) {
const { status, data } = useEvent(id);
const { addWidget } = useWidget();
@@ -46,19 +52,21 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
{renderKind(data)}
-
+ {!editing ? (
+
+ ) : null}
);
diff --git a/src/shared/notes/preview/link.tsx b/src/shared/notes/preview/link.tsx
index 2eb7aabf..1d7a5296 100644
--- a/src/shared/notes/preview/link.tsx
+++ b/src/shared/notes/preview/link.tsx
@@ -12,11 +12,11 @@ export function LinkPreview({ url }: { url: string }) {
if (status === 'pending') {
return (
-
-
+
+
-
-
+
+
{domain.hostname}
diff --git a/src/utils/hooks/useEvent.ts b/src/utils/hooks/useEvent.ts
index b4b00dab..8b27b993 100644
--- a/src/utils/hooks/useEvent.ts
+++ b/src/utils/hooks/useEvent.ts
@@ -1,18 +1,19 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
+import { nip19 } from 'nostr-tools';
import { AddressPointer } from 'nostr-tools/lib/types/nip19';
import { useNDK } from '@libs/ndk/provider';
-export function useEvent(
- id: undefined | string,
- naddr?: undefined | AddressPointer,
- embed?: undefined | string
-) {
+export function useEvent(id: undefined | string, embed?: undefined | string) {
const { ndk } = useNDK();
const { status, data } = useQuery({
queryKey: ['event', id],
queryFn: async () => {
+ const naddr = id.startsWith('naddr')
+ ? (nip19.decode(id).data as AddressPointer)
+ : null;
+
// return event refer from naddr
if (naddr) {
const rEvents = await ndk.fetchEvents({
@@ -20,8 +21,10 @@ export function useEvent(
'#d': [naddr.identifier],
authors: [naddr.pubkey],
});
+
const rEvent = [...rEvents].slice(-1)[0];
if (!rEvent) return Promise.reject(new Error('event not found'));
+
return rEvent;
}
diff --git a/src/utils/hooks/useRichContent.tsx b/src/utils/hooks/useRichContent.tsx
index 681cd039..1e188ad7 100644
--- a/src/utils/hooks/useRichContent.tsx
+++ b/src/utils/hooks/useRichContent.tsx
@@ -1,3 +1,4 @@
+import { nanoid } from 'nanoid';
import { nip19 } from 'nostr-tools';
import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
@@ -43,13 +44,13 @@ const VIDEOS = [
];
export function useRichContent(content: string, textmode: boolean = false) {
- let parsedContent: string | ReactNode[] = content;
+ let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
let linkPreview: string;
let images: string[] = [];
let videos: string[] = [];
let events: string[] = [];
- const text = content.replace(/\n+/g, '\n');
+ const text = parsedContent;
const words = text.split(/( |\n)/);
if (!textmode) {
@@ -151,8 +152,8 @@ export function useRichContent(content: string, textmode: boolean = false) {
);
});
- parsedContent = reactStringReplace(parsedContent, '\n', (match, i) => {
- return
;
+ parsedContent = reactStringReplace(parsedContent, '\n', () => {
+ return
;
});
if (typeof parsedContent[0] === 'string') {