smal fixes and update article layout

This commit is contained in:
reya 2023-11-13 15:44:58 +07:00
parent d5647d7452
commit fee4ad7b98
13 changed files with 165 additions and 248 deletions

View File

@ -65,6 +65,7 @@
"markdown-to-jsx": "^7.3.2", "markdown-to-jsx": "^7.3.2",
"media-chrome": "^1.5.2", "media-chrome": "^1.5.2",
"minidenticons": "^4.2.0", "minidenticons": "^4.2.0",
"nanoid": "^5.0.3",
"nostr-fetch": "^0.13.1", "nostr-fetch": "^0.13.1",
"nostr-tools": "^1.17.0", "nostr-tools": "^1.17.0",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",

View File

@ -146,6 +146,9 @@ dependencies:
minidenticons: minidenticons:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
nanoid:
specifier: ^5.0.3
version: 5.0.3
nostr-fetch: nostr-fetch:
specifier: ^0.13.1 specifier: ^0.13.1
version: 0.13.1 version: 0.13.1
@ -4696,6 +4699,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true 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: /natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true dev: true

View File

@ -10,6 +10,10 @@
word-wrap: break-word; word-wrap: break-word;
overflow-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 { html {
@ -64,119 +68,3 @@ input::-ms-clear {
.ProseMirror img.ProseMirror-selectednode { .ProseMirror img.ProseMirror-selectednode {
@apply outline-blue-500; @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;
}
}

View File

@ -3,13 +3,8 @@ import { twMerge } from 'tailwind-merge';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage'; 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 }) { export function ChatMessage({ message, self }: { message: NDKEvent; self: boolean }) {
const decryptedContent = useDecryptMessage(message); const decryptedContent = useDecryptMessage(message);
const richContent = parser(decryptedContent) ?? null;
return ( return (
<div <div
@ -20,20 +15,11 @@ export function ChatMessage({ message, self }: { message: NDKEvent; self: boolea
: 'rounded-r-xl bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : 'rounded-r-xl bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
)} )}
> >
{!richContent ? ( {!decryptedContent ? (
<p>Decrypting...</p> <p>Decrypting...</p>
) : ( ) : (
<div> <div>
<p className="select-text whitespace-pre-line">{richContent.parsed}</p> <p className="select-text whitespace-pre-line">{decryptedContent}</p>
<div>
{richContent.images.length > 0 && <ImagePreview urls={richContent.images} />}
{richContent.videos.length > 0 && <VideoPreview urls={richContent.videos} />}
{richContent.links.length > 0 && <LinkPreview urls={richContent.links} />}
{richContent.notes.length > 0 &&
richContent.notes.map((note: string) => (
<MentionNote key={note} id={note} />
))}
</div>
</div> </div>
)} )}
</div> </div>

View File

@ -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 CharacterCount from '@tiptap/extension-character-count';
import Image from '@tiptap/extension-image'; import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder'; import Placeholder from '@tiptap/extension-placeholder';
@ -71,7 +71,7 @@ export function NewArticleScreen() {
const content = editor.storage.markdown.getMarkdown(); const content = editor.storage.markdown.getMarkdown();
// define tags // define tags
const tags: string[][] = [ const tags: NDKTag[] = [
['d', ident], ['d', ident],
['title', title], ['title', title],
['image', cover], ['image', cover],
@ -85,17 +85,20 @@ export function NewArticleScreen() {
tags.push(['t', tag.replace('#', '')]); tags.push(['t', tag.replace('#', '')]);
}); });
// publish message
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
event.content = content; event.content = content;
event.kind = NDKKind.Article; event.kind = NDKKind.Article;
event.tags = tags; event.tags = tags;
// publish
const publishedRelays = await event.publish(); const publishedRelays = await event.publish();
if (publishedRelays) { if (publishedRelays) {
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
// update state // update state
setLoading(false); setLoading(false);
// reset editor // reset editor
editor.commands.clearContent(); editor.commands.clearContent();
localStorage.setItem('editor-article', '{}'); localStorage.setItem('editor-article', '{}');
@ -235,7 +238,7 @@ export function NewArticleScreen() {
<div className="flex h-16 w-full items-center justify-between border-t border-neutral-100 dark:border-neutral-900"> <div className="flex h-16 w-full items-center justify-between border-t border-neutral-100 dark:border-neutral-900">
<div className="inline-flex items-center gap-3"> <div className="inline-flex items-center gap-3">
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400"> <span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
{editor?.storage?.characterCount.characters()} {editor?.storage?.characterCount.characters()} characters
</span> </span>
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400"> <span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
- -

View File

@ -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 CharacterCount from '@tiptap/extension-character-count';
import Image from '@tiptap/extension-image'; import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder'; import Placeholder from '@tiptap/extension-placeholder';
@ -16,8 +16,13 @@ import { useNDK } from '@libs/ndk/provider';
import { CancelIcon, LoaderIcon } from '@shared/icons'; import { CancelIcon, LoaderIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes'; import { MentionNote } from '@shared/notes';
import { WIDGET_KIND } from '@stores/constants';
import { useWidget } from '@utils/hooks/useWidget';
export function NewPostScreen() { export function NewPostScreen() {
const { ndk, relayUrls } = useNDK(); const { ndk, relayUrls } = useNDK();
const { addWidget } = useWidget();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -67,11 +72,11 @@ export function NewPostScreen() {
}); });
// define tags // define tags
let tags: string[][] = []; let tags: NDKTag[] = [];
// add reply to tags if present // add reply to tags if present
if (reply.id && reply.pubkey) { if (reply.id && reply.pubkey) {
if (reply.root && reply.root.length > 1) { if (reply.root) {
tags = [ tags = [
['e', reply.root, relayUrls[0], 'root'], ['e', reply.root, relayUrls[0], 'root'],
['e', reply.id, relayUrls[0], 'reply'], ['e', reply.id, relayUrls[0], 'reply'],
@ -89,24 +94,35 @@ export function NewPostScreen() {
const hashtags = serializedContent const hashtags = serializedContent
.split(/\s/gm) .split(/\s/gm)
.filter((s: string) => s.startsWith('#')); .filter((s: string) => s.startsWith('#'));
hashtags?.forEach((tag: string) => { hashtags?.forEach((tag: string) => {
tags.push(['t', tag.replace('#', '')]); tags.push(['t', tag.replace('#', '')]);
}); });
// publish message
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
event.content = serializedContent; event.content = serializedContent;
event.kind = NDKKind.Text; event.kind = NDKKind.Text;
event.tags = tags; event.tags = tags;
// publish event
const publishedRelays = await event.publish(); const publishedRelays = await event.publish();
if (publishedRelays) { if (publishedRelays) {
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`); toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
// update state // update state
setLoading(false); setLoading(false);
// reset editor
setSearchParams({}); 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(); editor.commands.clearContent();
localStorage.setItem('editor-post', '{}'); localStorage.setItem('editor-post', '{}');
} }
@ -132,20 +148,20 @@ export function NewPostScreen() {
/> />
{searchParams.get('id') && ( {searchParams.get('id') && (
<div className="relative max-w-lg"> <div className="relative max-w-lg">
<MentionNote id={searchParams.get('id')} /> <MentionNote id={searchParams.get('id')} editing />
<button <button
type="button" type="button"
onClick={() => setSearchParams({})} onClick={() => setSearchParams({})}
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-300 px-2 dark:bg-neutral-700" className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-200 px-2 dark:bg-neutral-800"
> >
<CancelIcon className="h-4 w-4" /> <CancelIcon className="h-5 w-5" />
</button> </button>
</div> </div>
)} )}
</div> </div>
<div className="flex h-16 w-full items-center justify-between border-t border-neutral-100 dark:border-neutral-900"> <div className="flex h-16 w-full items-center justify-between border-t border-neutral-100 dark:border-neutral-900">
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400"> <span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
{editor?.storage?.characterCount.characters()} {editor?.storage?.characterCount.characters()} characters
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">

View File

@ -1,24 +1,21 @@
import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import Markdown from 'markdown-to-jsx';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { AddressPointer, EventPointer } from 'nostr-tools/lib/types/nip19'; import { EventPointer } from 'nostr-tools/lib/types/nip19';
import { useRef, useState } from 'react'; import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons'; import { ArrowLeftIcon, CheckCircleIcon, ShareIcon } from '@shared/icons';
import { NoteActions, NoteReplyForm } from '@shared/notes'; import { NoteReplyForm } from '@shared/notes';
import { ReplyList } from '@shared/notes/replies/list'; import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
export function ArticleNoteScreen() { export function ArticleNoteScreen() {
const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const replyRef = useRef(null);
const naddr = id.startsWith('naddr') ? (nip19.decode(id).data as AddressPointer) : null; const { id } = useParams();
const { status, data } = useEvent(id, naddr); const { status, data } = useEvent(id);
const [isCopy, setIsCopy] = useState(false); const [isCopy, setIsCopy] = useState(false);
@ -33,12 +30,8 @@ export function ArticleNoteScreen() {
setTimeout(() => setIsCopy(false), 2000); setTimeout(() => setIsCopy(false), 2000);
}; };
const scrollToReply = () => {
replyRef.current.scrollIntoView();
};
return ( return (
<div className="container mx-auto grid grid-cols-8 scroll-smooth px-4"> <div className="grid grid-cols-12 gap-4 scroll-smooth px-4">
<div className="col-span-1"> <div className="col-span-1">
<div className="flex flex-col items-end gap-4"> <div className="flex flex-col items-end gap-4">
<button <button
@ -48,7 +41,6 @@ export function ArticleNoteScreen() {
> >
<ArrowLeftIcon className="h-5 w-5" /> <ArrowLeftIcon className="h-5 w-5" />
</button> </button>
<div className="flex flex-col divide-y divide-neutral-200 rounded-xl bg-neutral-100 dark:divide-neutral-800 dark:bg-neutral-900">
<button <button
type="button" type="button"
onClick={share} onClick={share}
@ -60,40 +52,35 @@ export function ArticleNoteScreen() {
<ShareIcon className="h-5 w-5" /> <ShareIcon className="h-5 w-5" />
)} )}
</button> </button>
<button
type="button"
onClick={scrollToReply}
className="inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
>
<ReplyIcon className="h-5 w-5" />
</button>
</div> </div>
</div> </div>
</div> <div className="col-span-7 overflow-y-auto px-3 xl:col-span-8">
<div className="relative col-span-6 flex flex-col overflow-y-auto">
<div className="mx-auto w-full max-w-2xl">
{status === 'pending' ? ( {status === 'pending' ? (
<div className="px-3 py-1.5">Loading...</div> <div className="px-3 py-1.5">Loading...</div>
) : ( ) : (
<div className="flex h-min w-full flex-col px-3"> <Markdown
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900"> options={{
<User pubkey={data.pubkey} time={data.created_at} variant="thread" /> overrides: {
<div className="mt-3">{data.content}</div> a: {
<div className="mt-3"> props: {
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} /> className: 'text-blue-500 hover:text-blue-600',
</div> target: '_blank',
</div> },
</div> },
},
}}
className="break-p prose-lg prose-neutral dark:prose-invert prose-ul:list-disc"
>
{data.content}
</Markdown>
)} )}
<div ref={replyRef} className="px-3"> </div>
<div className="col-span-4 border-l border-neutral-100 px-3 dark:border-neutral-900 xl:col-span-3">
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900"> <div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
<NoteReplyForm eventId={id} /> <NoteReplyForm eventId={id} />
</div> </div>
<ReplyList eventId={id} /> <ReplyList eventId={id} />
</div> </div>
</div> </div>
</div>
<div className="col-span-1" />
</div>
); );
} }

View File

@ -6,19 +6,27 @@ import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons'; 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 { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user'; import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
export function TextNoteScreen() { export function TextNoteScreen() {
const { id } = useParams();
const { status, data } = useEvent(id);
const navigate = useNavigate(); const navigate = useNavigate();
const replyRef = useRef(null); const replyRef = useRef(null);
const { id } = useParams();
const { status, data } = useEvent(id);
const { getEventThread } = useNostr();
const [isCopy, setIsCopy] = useState(false); const [isCopy, setIsCopy] = useState(false);
const share = async () => { const share = async () => {
@ -37,9 +45,24 @@ export function TextNoteScreen() {
}; };
const renderKind = (event: NDKEvent) => { const renderKind = (event: NDKEvent) => {
const thread = getEventThread(event.tags);
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return <MemoizedTextKind content={event.content} />; return (
<>
{thread ? (
<div className="mb-2 w-full px-3">
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
{thread.rootEventId ? (
<ChildNote id={thread.rootEventId} isRoot />
) : null}
{thread.replyEventId ? <ChildNote id={thread.replyEventId} /> : null}
</div>
</div>
) : null}
<MemoizedTextKind content={event.content} />
</>
);
default: default:
return <UnknownNote event={event} />; return <UnknownNote event={event} />;
} }

View File

@ -30,20 +30,12 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
</button> </button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-300 bg-neutral-200 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800"> <DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:bg-neutral-900">
<DropdownMenu.Item asChild>
<Link
to={`/notes/text/${id}`}
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-700"
>
Focus
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={() => copyLink()} onClick={() => copyLink()}
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-700" className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
> >
Copy shareable link Copy shareable link
</button> </button>
@ -52,7 +44,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
<button <button
type="button" type="button"
onClick={() => copyID()} onClick={() => copyID()}
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-700" className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
> >
Copy ID Copy ID
</button> </button>
@ -60,7 +52,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<Link <Link
to={`/users/${pubkey}`} to={`/users/${pubkey}`}
className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-700" className="inline-flex h-10 items-center px-4 text-sm text-neutral-900 hover:bg-neutral-200 focus:outline-none dark:text-neutral-100 dark:hover:bg-neutral-800"
> >
View profile View profile
</Link> </Link>

View File

@ -14,7 +14,13 @@ import { WIDGET_KIND } from '@stores/constants';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
import { useWidget } from '@utils/hooks/useWidget'; 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 { status, data } = useEvent(id);
const { addWidget } = useWidget(); const { addWidget } = useWidget();
@ -46,6 +52,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
</div> </div>
<div className="mt-1 px-3 pb-3"> <div className="mt-1 px-3 pb-3">
{renderKind(data)} {renderKind(data)}
{!editing ? (
<button <button
type="button" type="button"
onClick={() => onClick={() =>
@ -59,6 +66,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
> >
Show more Show more
</button> </button>
) : null}
</div> </div>
</div> </div>
); );

View File

@ -12,11 +12,11 @@ export function LinkPreview({ url }: { url: string }) {
if (status === 'pending') { if (status === 'pending') {
return ( return (
<div className="mt-2 flex w-full flex-col rounded-lg border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"> <div className="my-2 flex w-full flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900">
<div className="h-44 w-full animate-pulse bg-neutral-400 dark:bg-neutral-600" /> <div className="h-48 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col gap-2 px-3 py-3"> <div className="flex flex-col gap-2 px-3 py-3">
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" /> <div className="h-3 w-2/3 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-3 w-3/4 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" /> <div className="h-3 w-3/4 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400"> <span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
{domain.hostname} {domain.hostname}
</span> </span>

View File

@ -1,18 +1,19 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { AddressPointer } from 'nostr-tools/lib/types/nip19'; import { AddressPointer } from 'nostr-tools/lib/types/nip19';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
export function useEvent( export function useEvent(id: undefined | string, embed?: undefined | string) {
id: undefined | string,
naddr?: undefined | AddressPointer,
embed?: undefined | string
) {
const { ndk } = useNDK(); const { ndk } = useNDK();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['event', id], queryKey: ['event', id],
queryFn: async () => { queryFn: async () => {
const naddr = id.startsWith('naddr')
? (nip19.decode(id).data as AddressPointer)
: null;
// return event refer from naddr // return event refer from naddr
if (naddr) { if (naddr) {
const rEvents = await ndk.fetchEvents({ const rEvents = await ndk.fetchEvents({
@ -20,8 +21,10 @@ export function useEvent(
'#d': [naddr.identifier], '#d': [naddr.identifier],
authors: [naddr.pubkey], authors: [naddr.pubkey],
}); });
const rEvent = [...rEvents].slice(-1)[0]; const rEvent = [...rEvents].slice(-1)[0];
if (!rEvent) return Promise.reject(new Error('event not found')); if (!rEvent) return Promise.reject(new Error('event not found'));
return rEvent; return rEvent;
} }

View File

@ -1,3 +1,4 @@
import { nanoid } from 'nanoid';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -43,13 +44,13 @@ const VIDEOS = [
]; ];
export function useRichContent(content: string, textmode: boolean = false) { 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 linkPreview: string;
let images: string[] = []; let images: string[] = [];
let videos: string[] = []; let videos: string[] = [];
let events: string[] = []; let events: string[] = [];
const text = content.replace(/\n+/g, '\n'); const text = parsedContent;
const words = text.split(/( |\n)/); const words = text.split(/( |\n)/);
if (!textmode) { if (!textmode) {
@ -151,8 +152,8 @@ export function useRichContent(content: string, textmode: boolean = false) {
); );
}); });
parsedContent = reactStringReplace(parsedContent, '\n', (match, i) => { parsedContent = reactStringReplace(parsedContent, '\n', () => {
return <div key={'n-' + i} className="h-2" />; return <div key={nanoid()} className="h-3" />;
}); });
if (typeof parsedContent[0] === 'string') { if (typeof parsedContent[0] === 'string') {