mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
wip
This commit is contained in:
parent
ee3e8eb105
commit
ce864c8990
@ -2,13 +2,13 @@ import { memo } from 'react';
|
||||
|
||||
import { useRichContent } from '@utils/hooks/useRichContent';
|
||||
|
||||
export function TextKind({ content, truncate }: { content: string; truncate?: boolean }) {
|
||||
const { parsedContent } = useRichContent(content);
|
||||
export function TextKind({ content, textmode }: { content: string; textmode?: boolean }) {
|
||||
const { parsedContent } = useRichContent(content, textmode);
|
||||
|
||||
if (truncate) {
|
||||
if (textmode) {
|
||||
return (
|
||||
<div className="break-p select-text whitespace-pre-line leading-normal text-neutral-900 dark:text-neutral-100">
|
||||
{content}
|
||||
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||
{parsedContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { PlusIcon } from '@shared/icons';
|
||||
import {
|
||||
MemoizedArticleKind,
|
||||
MemoizedFileKind,
|
||||
@ -22,7 +21,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
const renderKind = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <MemoizedTextKind content={event.content} />;
|
||||
return <MemoizedTextKind content={event.content} textmode />;
|
||||
case NDKKind.Article:
|
||||
return <MemoizedArticleKind id={event.id} tags={event.tags} />;
|
||||
case 1063:
|
||||
@ -42,10 +41,25 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
|
||||
return (
|
||||
<div className="my-2 flex w-full cursor-default flex-col gap-1 rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||
<div className="px-3 pt-3">
|
||||
<div className="mt-3 px-3">
|
||||
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
||||
</div>
|
||||
<div>{renderKind(data)}</div>
|
||||
<div className="mt-1 px-3 pb-3">
|
||||
{renderKind(data)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
addWidget.mutate({
|
||||
kind: WidgetKinds.local.thread,
|
||||
title: 'Thread',
|
||||
content: data.id,
|
||||
})
|
||||
}
|
||||
className="mt-2 text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -2,7 +2,12 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ShareIcon } from '@shared/icons';
|
||||
import { MemoizedArticleKind, MemoizedFileKind, NoteSkeleton } from '@shared/notes';
|
||||
import {
|
||||
MemoizedArticleKind,
|
||||
MemoizedFileKind,
|
||||
MemoizedTextKind,
|
||||
NoteSkeleton,
|
||||
} from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { WidgetKinds } from '@stores/constants';
|
||||
@ -21,11 +26,7 @@ export function NotifyNote({ event }: { event: NDKEvent }) {
|
||||
const renderKind = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return (
|
||||
<div className="break-p line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||
{event.content}
|
||||
</div>
|
||||
);
|
||||
return <MemoizedTextKind key={event.id} content={event.content} textmode />;
|
||||
case NDKKind.Article:
|
||||
return <MemoizedArticleKind key={event.id} id={event.id} tags={event.tags} />;
|
||||
case 1063:
|
||||
|
@ -3,19 +3,15 @@ import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ReplyMediaUploader } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function NoteReplyForm({ id }: { id: string }) {
|
||||
const { db } = useStorage();
|
||||
export function NoteReplyForm({ eventId }: { eventId: string }) {
|
||||
const { ndk, relayUrls } = useNDK();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const submit = async () => {
|
||||
const tags = [['e', id, relayUrls[0], 'root']];
|
||||
const tags = [['e', eventId, relayUrls[0], 'root']];
|
||||
|
||||
// publish event
|
||||
const event = new NDKEvent(ndk);
|
||||
@ -31,17 +27,15 @@ export function NoteReplyForm({ id }: { id: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex gap-3">
|
||||
<User pubkey={db.account.pubkey} variant="miniavatar" />
|
||||
<div className="relative flex flex-1 flex-col rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
||||
<div className="mt-3 flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Reply to this thread..."
|
||||
className="relative h-36 w-full resize-none bg-transparent px-5 py-4 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||
placeholder="Reply to this post..."
|
||||
className="h-28 w-full resize-none rounded-t-xl bg-neutral-100 px-5 py-4 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="inline-flex items-center justify-end gap-2 rounded-b-xl border-t border-neutral-200 p-2 dark:border-neutral-800">
|
||||
<div className="inline-flex items-center justify-end gap-2 rounded-b-xl p-2">
|
||||
<ReplyMediaUploader setValue={setValue} />
|
||||
<button
|
||||
onClick={() => submit()}
|
||||
@ -52,6 +46,5 @@ export function NoteReplyForm({ id }: { id: string }) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { NavArrowDownIcon } from '@shared/icons';
|
||||
import { MemoizedTextNote, NoteActions, SubReply } from '@shared/notes';
|
||||
import { MemoizedTextKind, NoteActions, SubReply } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { NDKEventWithReplies } from '@utils/types';
|
||||
@ -12,13 +12,11 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative flex flex-col">
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||
<div className="-mt-4 flex items-start gap-3">
|
||||
<div className="w-10 shrink-0" />
|
||||
<div className="relative z-10 flex-1">
|
||||
<MemoizedTextNote content={event.content} />
|
||||
<MemoizedTextKind content={event.content} />
|
||||
<div className="-ml-1">
|
||||
<NoteActions
|
||||
id={event.id}
|
||||
pubkey={event.pubkey}
|
||||
@ -27,8 +25,7 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-[48px]">
|
||||
<div className="pl-4">
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
{event.replies?.length > 0 ? (
|
||||
<div>
|
||||
|
@ -6,7 +6,7 @@ import { Reply } from '@shared/notes';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { NDKEventWithReplies } from '@utils/types';
|
||||
|
||||
export function ReplyList({ id }: { id: string }) {
|
||||
export function ReplyList({ eventId }: { eventId: string }) {
|
||||
const { fetchAllReplies, sub } = useNostr();
|
||||
const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
|
||||
|
||||
@ -14,31 +14,32 @@ export function ReplyList({ id }: { id: string }) {
|
||||
let isCancelled = false;
|
||||
|
||||
async function fetchRepliesAndSub() {
|
||||
const events = await fetchAllReplies(id);
|
||||
const events = await fetchAllReplies(eventId);
|
||||
if (!isCancelled) {
|
||||
setData(events);
|
||||
}
|
||||
// subscribe for new replies
|
||||
sub(
|
||||
{
|
||||
'#e': [id],
|
||||
'#e': [eventId],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
(event: NDKEventWithReplies) => setData((prev) => [event, ...prev]),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
fetchRepliesAndSub();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
}, [eventId]);
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div className="flex h-16 items-center justify-center rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||
<div className="flex h-16 items-center justify-center rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
@ -52,12 +53,12 @@ export function ReplyList({ id }: { id: string }) {
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||
<h3 className="text-3xl">👋</h3>
|
||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
||||
Share your thought on it...
|
||||
Be the first to Reply!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
data.map((event) => <Reply key={event.id} event={event} root={id} />)
|
||||
data.map((event) => <Reply key={event.id} event={event} root={eventId} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,19 +1,16 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
import { MemoizedTextNote, NoteActions } from '@shared/notes';
|
||||
import { MemoizedTextKind, NoteActions } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function SubReply({ event }: { event: NDKEvent }) {
|
||||
return (
|
||||
<div className="mb-3 flex flex-col">
|
||||
<div className="flex flex-col gap-2">
|
||||
<User pubkey={event.pubkey} time={event.created_at} eventId={event.id} />
|
||||
<div className="-mt-4 flex items-start gap-3">
|
||||
<div className="w-10 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<MemoizedTextNote content={event.content} />
|
||||
<MemoizedTextKind content={event.content} />
|
||||
<div className="-ml-1">
|
||||
<NoteActions id={event.id} pubkey={event.pubkey} extraButtons={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,11 +4,15 @@ import { memo } from 'react';
|
||||
import { ChildNote, NoteActions } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { WidgetKinds } from '@stores/constants';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { useRichContent } from '@utils/hooks/useRichContent';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function TextNote({ event }: { event: NDKEvent }) {
|
||||
const { parsedContent } = useRichContent(event.content);
|
||||
const { addWidget } = useWidget();
|
||||
const { getEventThread } = useNostr();
|
||||
|
||||
const thread = getEventThread(event);
|
||||
@ -22,6 +26,19 @@ export function TextNote({ event }: { event: NDKEvent }) {
|
||||
<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}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
addWidget.mutate({
|
||||
kind: WidgetKinds.local.thread,
|
||||
title: 'Thread',
|
||||
content: thread.rootEventId,
|
||||
})
|
||||
}
|
||||
className="self-start text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show full thread
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -102,7 +102,7 @@ export const User = memo(function User({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
@ -308,14 +308,16 @@ export const User = memo(function User({
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="absolute left-2 top-2 font-semibold leading-tight">
|
||||
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
|
||||
<div className="w-full max-w-[10rem] truncate">
|
||||
{user?.display_name ||
|
||||
user?.name ||
|
||||
user?.displayName ||
|
||||
displayNpub(pubkey, 16)}{' '}
|
||||
<span className="font-normal text-neutral-700 dark:text-neutral-300">
|
||||
</div>
|
||||
<div className="font-normal text-neutral-700 dark:text-neutral-300">
|
||||
{subtext}:
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -440,20 +442,20 @@ export const User = memo(function User({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-16 items-center gap-3 px-3">
|
||||
<Avatar.Root className="h-10 w-10 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg"
|
||||
className="h-10 w-10 rounded-lg ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||
className="h-10 w-10 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
|
@ -4,13 +4,11 @@ import { WVList } from 'virtua';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
MemoizedArticleNote,
|
||||
MemoizedFileNote,
|
||||
MemoizedTextNote,
|
||||
MemoizedArticleKind,
|
||||
MemoizedFileKind,
|
||||
MemoizedTextKind,
|
||||
NoteActions,
|
||||
NoteReplyForm,
|
||||
NoteStats,
|
||||
UnknownNote,
|
||||
} from '@shared/notes';
|
||||
import { ReplyList } from '@shared/notes/replies/list';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
@ -27,13 +25,13 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
|
||||
(event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <MemoizedTextNote content={event.content} />;
|
||||
return <MemoizedTextKind content={event.content} />;
|
||||
case NDKKind.Article:
|
||||
return <MemoizedArticleNote event={event} />;
|
||||
return <MemoizedArticleKind id={event.id} tags={event.tags} />;
|
||||
case 1063:
|
||||
return <MemoizedFileNote event={event} />;
|
||||
return <MemoizedFileKind tags={event.tags} />;
|
||||
default:
|
||||
return <UnknownNote event={event} />;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[data]
|
||||
@ -42,23 +40,22 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
|
||||
return (
|
||||
<WidgetWrapper>
|
||||
<TitleBar id={params.id} title={params.title} />
|
||||
<WVList className="flex-1 overflow-y-auto px-3">
|
||||
<WVList className="flex-1 overflow-y-auto px-3 pb-5">
|
||||
{status === 'pending' ? (
|
||||
<div className="flex h-16 items-center justify-center rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||
<div className="flex h-16 items-center justify-center rounded-xl bg-neutral-50 px-3 py-3 dark:bg-neutral-950">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl bg-neutral-100 px-3 py-3 dark:bg-neutral-900">
|
||||
<>
|
||||
<div className="flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||
<User pubkey={data.pubkey} time={data.created_at} variant="thread" />
|
||||
<div className="mt-2">{renderKind(data)}</div>
|
||||
<NoteActions id={params.content} pubkey={data.pubkey} extraButtons={false} />
|
||||
{renderKind(data)}
|
||||
<NoteActions id={data.id} pubkey={data.pubkey} />
|
||||
</div>
|
||||
<NoteReplyForm eventId={params.content} />
|
||||
<ReplyList eventId={data.id} />
|
||||
</>
|
||||
)}
|
||||
<NoteStats id={params.content} />
|
||||
<hr className="my-4 h-px w-full border-none bg-neutral-100" />
|
||||
<NoteReplyForm id={params.content} />
|
||||
<ReplyList id={params.content} />
|
||||
<div className="h-10" />
|
||||
</WVList>
|
||||
</WidgetWrapper>
|
||||
);
|
||||
|
@ -42,17 +42,23 @@ const VIDEOS = [
|
||||
'.m3u8',
|
||||
];
|
||||
|
||||
export function useRichContent(content: string) {
|
||||
export function useRichContent(content: string, textmode: boolean = false) {
|
||||
let parsedContent: string | ReactNode[] = content;
|
||||
let linkPreview: string;
|
||||
let images: string[] = [];
|
||||
let videos: string[] = [];
|
||||
let events: string[] = [];
|
||||
|
||||
const text = content;
|
||||
const words = text.split(/(\s+)/);
|
||||
|
||||
const images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
|
||||
const videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
|
||||
if (!textmode) {
|
||||
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
|
||||
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
|
||||
events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el)));
|
||||
}
|
||||
|
||||
const hashtags = words.filter((word) => word.startsWith('#'));
|
||||
const events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el)));
|
||||
const mentions = words.filter((word) =>
|
||||
NOSTR_MENTIONS.some((el) => word.startsWith(el))
|
||||
);
|
||||
@ -127,7 +133,7 @@ export function useRichContent(content: string) {
|
||||
const url = new URL(match);
|
||||
url.search = '';
|
||||
|
||||
if (!linkPreview) {
|
||||
if (!linkPreview && !textmode) {
|
||||
linkPreview = match;
|
||||
return <LinkPreview key={match + i} url={url.toString()} />;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user