smal fixes and update article layout

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

View File

@@ -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 (
<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'
)}
>
{!richContent ? (
{!decryptedContent ? (
<p>Decrypting...</p>
) : (
<div>
<p className="select-text whitespace-pre-line">{richContent.parsed}</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>
<p className="select-text whitespace-pre-line">{decryptedContent}</p>
</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 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() {
<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">
<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 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 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') && (
<div className="relative max-w-lg">
<MentionNote id={searchParams.get('id')} />
<MentionNote id={searchParams.get('id')} editing />
<button
type="button"
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>
</div>
)}
</div>
<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">
{editor?.storage?.characterCount.characters()}
{editor?.storage?.characterCount.characters()} characters
</span>
<div className="flex items-center">
<div className="inline-flex items-center gap-2">

View File

@@ -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 (
<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="flex flex-col items-end gap-4">
<button
@@ -48,52 +41,46 @@ export function ArticleNoteScreen() {
>
<ArrowLeftIcon className="h-5 w-5" />
</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
type="button"
onClick={share}
className="inline-flex h-12 w-12 items-center justify-center rounded-t-xl"
>
{isCopy ? (
<CheckCircleIcon className="h-5 w-5 text-teal-500" />
) : (
<ShareIcon className="h-5 w-5" />
)}
</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>
<button
type="button"
onClick={share}
className="inline-flex h-12 w-12 items-center justify-center rounded-t-xl"
>
{isCopy ? (
<CheckCircleIcon className="h-5 w-5 text-teal-500" />
) : (
<ShareIcon className="h-5 w-5" />
)}
</button>
</div>
</div>
<div className="relative col-span-6 flex flex-col overflow-y-auto">
<div className="mx-auto w-full max-w-2xl">
{status === 'pending' ? (
<div className="px-3 py-1.5">Loading...</div>
) : (
<div className="flex h-min w-full flex-col px-3">
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
<div className="mt-3">{data.content}</div>
<div className="mt-3">
<NoteActions id={id} pubkey={data.pubkey} extraButtons={false} />
</div>
</div>
</div>
)}
<div ref={replyRef} className="px-3">
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
<NoteReplyForm eventId={id} />
</div>
<ReplyList eventId={id} />
</div>
</div>
<div className="col-span-7 overflow-y-auto px-3 xl:col-span-8">
{status === 'pending' ? (
<div className="px-3 py-1.5">Loading...</div>
) : (
<Markdown
options={{
overrides: {
a: {
props: {
className: 'text-blue-500 hover:text-blue-600',
target: '_blank',
},
},
},
}}
className="break-p prose-lg prose-neutral dark:prose-invert prose-ul:list-disc"
>
{data.content}
</Markdown>
)}
</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">
<NoteReplyForm eventId={id} />
</div>
<ReplyList eventId={id} />
</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 { 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 <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:
return <UnknownNote event={event} />;
}