mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-02 18:00:47 +00:00
smal fixes and update article layout
This commit is contained in:
parent
d5647d7452
commit
fee4ad7b98
@ -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",
|
||||||
|
@ -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
|
||||||
|
120
src/app.css
120
src/app.css
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
-
|
-
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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} />;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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') {
|
||||||
|
Loading…
Reference in New Issue
Block a user