This commit is contained in:
reya 2023-11-12 08:41:47 +07:00
parent 5c48ebe103
commit a3632571ff
9 changed files with 99 additions and 73 deletions

View File

@ -80,7 +80,6 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-hotkeys-hook": "^4.4.1",
"react-medium-image-zoom": "^5.1.8",
"react-router-dom": "^6.18.0",
"react-string-replace": "^1.1.1",
"reactflow": "^11.10.1",

View File

@ -191,9 +191,6 @@ dependencies:
react-hotkeys-hook:
specifier: ^4.4.1
version: 4.4.1(react-dom@18.2.0)(react@18.2.0)
react-medium-image-zoom:
specifier: ^5.1.8
version: 5.1.8(react-dom@18.2.0)(react@18.2.0)
react-router-dom:
specifier: ^6.18.0
version: 6.18.0(react-dom@18.2.0)(react@18.2.0)
@ -5405,16 +5402,6 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-medium-image-zoom@5.1.8(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-2X4oLlEopIWg7qalR1Qpy4gPrU9CTF0DvJ7HNu5u/NwdyQWupEsje2vuMbjBz7+np8MmQ4DKJ6zGr1ofCuzB3g==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-remove-scroll-bar@2.3.4(@types/react@18.2.37)(react@18.2.0):
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
engines: {node: '>=10'}

View File

@ -87,9 +87,9 @@ export function ArticleNoteScreen() {
)}
<div ref={replyRef} className="px-3">
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
<NoteReplyForm id={id} />
<NoteReplyForm eventId={id} />
</div>
<ReplyList id={id} />
<ReplyList eventId={id} />
</div>
</div>
</div>

View File

@ -6,14 +6,7 @@ import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteActions,
NoteReplyForm,
TextNote,
UnknownNote,
} from '@shared/notes';
import { MemoizedTextKind, NoteActions, NoteReplyForm, UnknownNote } from '@shared/notes';
import { ReplyList } from '@shared/notes/replies/list';
import { User } from '@shared/user';
@ -46,11 +39,7 @@ export function TextNoteScreen() {
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
return <MemoizedTextKind content={event.content} />;
default:
return <UnknownNote event={event} />;
}
@ -106,9 +95,9 @@ export function TextNoteScreen() {
)}
<div ref={replyRef} className="px-3">
<div className="mb-3 border-b border-neutral-100 pb-3 dark:border-neutral-900">
<NoteReplyForm id={id} />
<NoteReplyForm eventId={id} />
</div>
<ReplyList id={id} />
<ReplyList eventId={id} />
</div>
</div>
</div>

View File

@ -83,7 +83,7 @@ export function FileNote({ event }: { event: NDKEvent }) {
<div className="mb-3 h-min w-full px-3">
<div className="relative flex flex-col gap-2 overflow-hidden rounded-xl bg-neutral-50 pt-3 dark:bg-neutral-950">
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
<div>{renderFileType()}</div>
<div className="relative mt-2">{renderFileType()}</div>
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>

View File

@ -1,15 +1,29 @@
import { downloadDir } from '@tauri-apps/api/path';
import { Window } from '@tauri-apps/api/window';
import { download } from '@tauri-apps/plugin-upload';
import { SyntheticEvent } from 'react';
import Zoom from 'react-medium-image-zoom';
import { SyntheticEvent, useState } from 'react';
import { CancelIcon, DownloadIcon } from '@shared/icons';
import { CheckCircleIcon, DownloadIcon } from '@shared/icons';
export function ImagePreview({ url }: { url: string }) {
const downloadImage = async (url: string) => {
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf('/') + 1);
return await download(url, downloadDirPath + `/${filename}`);
const [downloaded, setDownloaded] = useState(false);
const downloadImage = async (e: { stopPropagation: () => void }) => {
try {
e.stopPropagation();
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf('/') + 1);
await download(url, downloadDirPath + `/${filename}`);
setDownloaded(true);
} catch (e) {
console.error(e);
}
};
const open = () => {
return new Window('image-viewer', { url, title: 'Image Viewer' });
};
const fallback = (event: SyntheticEvent<HTMLImageElement, Event>) => {
@ -17,25 +31,28 @@ export function ImagePreview({ url }: { url: string }) {
};
return (
<Zoom key={url} zoomMargin={50} IconUnzoom={() => <CancelIcon className="h-4 w-4" />}>
<div className="group relative my-2">
<img
src={url}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
onError={fallback}
className="h-auto w-full rounded-lg border border-neutral-300/50 object-cover dark:border-neutral-700/50"
/>
<button
type="button"
onClick={() => downloadImage(url)}
className="absolute right-2 top-2 hidden h-10 w-10 items-center justify-center rounded-lg bg-black/50 backdrop-blur-xl group-hover:inline-flex hover:bg-blue-500"
>
<DownloadIcon className="h-4 w-4 text-white" />
</button>
</div>
</Zoom>
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div onClick={open} className="group relative my-2">
<img
src={url}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
onError={fallback}
className="h-auto w-full rounded-lg border border-neutral-300/50 object-cover dark:border-neutral-700/50"
/>
<button
type="button"
onClick={(e) => downloadImage(e)}
className="absolute right-2 top-2 z-10 hidden h-10 w-10 items-center justify-center rounded-lg bg-blue-500 group-hover:inline-flex hover:bg-blue-600"
>
{downloaded ? (
<CheckCircleIcon className="h-5 w-5 text-white" />
) : (
<DownloadIcon className="h-5 w-5 text-white" />
)}
</button>
</div>
);
}

View File

@ -15,7 +15,7 @@ export function TextNote({ event }: { event: NDKEvent }) {
const { addWidget } = useWidget();
const { getEventThread } = useNostr();
const thread = getEventThread(event);
const thread = getEventThread(event.tags);
return (
<div className="mb-3 h-min w-full px-3">

View File

@ -4,6 +4,7 @@ import { WVList } from 'virtua';
import { LoaderIcon } from '@shared/icons';
import {
ChildNote,
MemoizedArticleKind,
MemoizedFileKind,
MemoizedTextKind,
@ -16,16 +17,33 @@ import { User } from '@shared/user';
import { WidgetWrapper } from '@shared/widgets';
import { useEvent } from '@utils/hooks/useEvent';
import { useNostr } from '@utils/hooks/useNostr';
import { Widget } from '@utils/types';
export function ThreadWidget({ widget }: { widget: Widget }) {
const { status, data } = useEvent(widget.content);
const { getEventThread } = useNostr();
const renderKind = useCallback(
(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} />
</>
);
case NDKKind.Article:
return <MemoizedArticleKind id={event.id} tags={event.tags} />;
case 1063:

View File

@ -1,4 +1,10 @@
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
import {
NDKEvent,
NDKFilter,
NDKKind,
NDKSubscription,
NDKTag,
} from '@nostr-dev-kit/ndk';
import { open } from '@tauri-apps/plugin-dialog';
import { readBinaryFile } from '@tauri-apps/plugin-fs';
import { fetch } from '@tauri-apps/plugin-http';
@ -50,20 +56,30 @@ export function useNostr() {
}
};
const getEventThread = (event: NDKEvent) => {
let rootEventId: string;
let replyEventId: string;
const getEventThread = (tags: NDKTag[]) => {
let rootEventId: string = null;
let replyEventId: string = null;
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
rootEventId = event.tags[0][1];
const events = tags.filter((el) => el[0] === 'e');
if (!events.length) return null;
if (events.length === 1)
return {
rootEventId: events[0][1],
replyEventId: null,
};
if (events.length > 1) {
rootEventId = events.find((el) => el[3] === 'root')?.[1];
replyEventId = events.find((el) => el[3] === 'reply')?.[1];
if (!rootEventId && !replyEventId) {
rootEventId = events[0][1];
replyEventId = events[1][1];
}
}
rootEventId = event.tags.find((el) => el[3] === 'root')?.[1] || null;
// eslint-disable-next-line prefer-const
replyEventId = event.tags.find((el) => el[3] === 'reply')?.[1] || null;
if (!rootEventId && !replyEventId) return null;
return {
rootEventId,
replyEventId,