fix the mess I started

This commit is contained in:
Ren Amamiya
2023-04-28 09:52:12 +07:00
parent 233a5bd2ad
commit a71502d19e
35 changed files with 197 additions and 380 deletions

View File

@@ -4,7 +4,7 @@ import Navigation from '@lume/shared/navigation';
export function LayoutChannel({ children }: { children: React.ReactNode }) {
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-white">
<div className="flex h-screen w-full flex-col">
<div
data-tauri-drag-region

View File

@@ -1,9 +1,7 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { ChannelBlackList } from '@lume/shared/channels/channelBlackList';
import { ChannelProfile } from '@lume/shared/channels/channelProfile';
import { UpdateChannelModal } from '@lume/shared/channels/updateChannelModal';
import { FormChannel } from '@lume/shared/form/channel';
import { RelayContext } from '@lume/shared/relaysProvider';
import { channelMessagesAtom, channelReplyAtom } from '@lume/stores/channel';
import { FULL_RELAYS } from '@lume/stores/constants';
import { dateToUnix, hoursAgo } from '@lume/utils/getDate';
@@ -12,18 +10,20 @@ import { arrayObjToPureArr } from '@lume/utils/transform';
import { useSetAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { Suspense, lazy, useContext, useRef } from 'react';
import { RelayPool } from 'nostr-relaypool';
import { Suspense, lazy, useRef } from 'react';
import useSWRSubscription from 'swr/subscription';
const ChannelMessages = lazy(() => import('@lume/shared/channels/messages'));
let mutedList: any = [];
let activeAccount: any = {};
let activeMutedList: any = [];
let activeHidedList: any = [];
if (typeof window !== 'undefined') {
const { getBlacklist, getActiveBlacklist, getActiveAccount } = await import('@lume/utils/storage');
const activeAccount = await getActiveAccount();
activeAccount = await getActiveAccount();
activeHidedList = await getActiveBlacklist(activeAccount.id, 43);
activeMutedList = await getActiveBlacklist(activeAccount.id, 44);
mutedList = await getBlacklist(activeAccount.id, 44);
@@ -33,12 +33,9 @@ export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const id = searchParams.id;
const channelID = searchParams.id;
const channelPubkey = searchParams.pubkey;
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const setChannelMessages = useSetAtom(channelMessagesAtom);
const resetChannelMessages = useResetAtom(channelMessagesAtom);
const resetChannelReply = useResetAtom(channelReplyAtom);
@@ -47,16 +44,17 @@ export function Page() {
const hided = arrayObjToPureArr(activeHidedList);
const muted = arrayObjToPureArr(activeMutedList);
useSWRSubscription(id, () => {
useSWRSubscription(channelID, () => {
// reset channel reply
resetChannelReply();
// reset channel messages
resetChannelMessages();
// subscribe for new messages
const pool = new RelayPool(FULL_RELAYS);
const unsubscribe = pool.subscribe(
[
{
'#e': [id],
'#e': [channelID],
kinds: [42],
since: dateToUnix(hoursAgo(48, now.current)),
},
@@ -82,11 +80,11 @@ export function Page() {
<div className="flex h-full flex-col justify-between gap-2">
<div className="flex h-11 w-full shrink-0 items-center justify-between">
<div>
<ChannelProfile id={id} pubkey={channelPubkey} />
<ChannelProfile id={channelID} pubkey={channelPubkey} />
</div>
<div className="flex items-center gap-2">
<ChannelBlackList blacklist={mutedList} />
{activeAccount.pubkey === channelPubkey && <UpdateChannelModal id={id} />}
{activeAccount.pubkey === channelPubkey && <UpdateChannelModal id={activeAccount} />}
</div>
</div>
<div className="relative flex w-full flex-1 flex-col justify-between rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
@@ -94,7 +92,7 @@ export function Page() {
<ChannelMessages />
</Suspense>
<div className="shrink-0 p-3">
<FormChannel eventId={id} />
<FormChannel eventId={channelID} />
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@ import Navigation from '@lume/shared/navigation';
export function LayoutChat({ children }: { children: React.ReactNode }) {
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-white">
<div className="flex h-screen w-full flex-col">
<div
data-tauri-drag-region

View File

@@ -0,0 +1,56 @@
import { ImagePreview } from '@lume/app/newsfeed/components/note/preview/image';
import { VideoPreview } from '@lume/app/newsfeed/components/note/preview/video';
import { YoutubePreview } from '@lume/app/newsfeed/components/note/preview/youtube';
import { NoteQuote } from '@lume/app/newsfeed/components/note/quote';
import { NoteMentionUser } from '@lume/app/newsfeed/components/user/mention';
import destr from 'destr';
import reactStringReplace from 'react-string-replace';
export const contentParser = (noteContent: any, noteTags: any) => {
let parsedContent = noteContent.trim();
// get data tags
const tags = destr(noteTags);
// handle urls
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
if (match.match(/\.(jpg|jpeg|gif|png|webp)$/i)) {
// image url
return <ImagePreview key={match + i} url={match} size="large" />;
} else if (match.match(/(http:|https:)?(\/\/)?(www\.)?(youtube.com|youtu.be)\/(watch|embed)?(\?v=|\/)?(\S+)?/)) {
// youtube
return <YoutubePreview key={match + i} url={match} />;
} else if (match.match(/\.(mp4|webm)$/i)) {
// video
return <VideoPreview key={match + i} url={match} />;
} else {
return (
<a key={match + i} href={match} className="cursor-pointer text-fuchsia-500" target="_blank" rel="noreferrer">
{match}
</a>
);
}
});
// handle #-hashtags
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
<span key={match + i} className="cursor-pointer text-fuchsia-500">
#{match}
</span>
));
// handle mentions
if (tags && tags.length > 0) {
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
if (tags[match][0] === 'p') {
// @-mentions
return <NoteMentionUser key={tags[match][1] + i} pubkey={tags[match][1]} />;
} else if (tags[match][0] === 'e') {
// note-quotes
return <NoteQuote key={tags[match][1] + i} id={tags[match][1]} />;
} else {
return;
}
});
}
return parsedContent;
};

View File

@@ -0,0 +1,73 @@
import { ImagePicker } from '@lume/shared/form/imagePicker';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { noteContentAtom } from '@lume/stores/note';
import { dateToUnix } from '@lume/utils/getDate';
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
import { useAtom } from 'jotai';
import { useResetAtom } from 'jotai/utils';
import { RelayPool } from 'nostr-relaypool';
import { getEventHash, signEvent } from 'nostr-tools';
export default function NoteForm() {
const { account, isLoading, isError } = useActiveAccount();
const [value, setValue] = useAtom(noteContentAtom);
const resetValue = useResetAtom(noteContentAtom);
const submitEvent = () => {
if (!isLoading && !isError && account) {
const pool = new RelayPool(WRITEONLY_RELAYS);
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: account.pubkey,
tags: [],
};
event.id = getEventHash(event);
event.sig = signEvent(event, account.privkey);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset form
resetValue();
// send notification
// sendNotification('Note has been published successfully');
} else {
console.log('Cannot publish note');
}
};
return (
<div className="p-3">
<div className="relative h-32 w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<div>
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
spellCheck={false}
placeholder="What's up?"
className="relative h-32 w-full resize-none rounded-lg border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<div className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700">
<ImagePicker type="note" />
<div className="flex items-center gap-2 pl-2"></div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false}
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
import { NoteParent } from '@lume/app/newsfeed/components/note/parent';
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
import { memo } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
const content = contentParser(event.content, event.tags);
const parentNote = () => {
if (event.parent_id) {
if (event.parent_id !== event.event_id && !event.content.includes('#[0]')) {
return <NoteParent key={event.parent_id} id={event.parent_id} />;
}
}
return <></>;
};
const openUserPage = (e) => {
e.stopPropagation();
navigate(`/user?pubkey=${event.pubkey}`);
};
const openThread = (e) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
navigate(`/newsfeed/note?id=${event.parent_id}`);
} else {
e.stopPropagation();
}
};
return (
<div
onClick={(e) => openThread(e)}
className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20"
>
{parentNote()}
<div className="relative z-10 flex flex-col">
<div onClick={(e) => openUserPage(e)}>
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
</div>
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
</div>
</div>
);
});

View File

@@ -0,0 +1,24 @@
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
import { memo } from 'react';
export const NoteComment = memo(function NoteComment({ event }: { event: any }) {
const content = contentParser(event.content, event.tags);
return (
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20">
<div className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
<div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-2">
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
{content}
</div>
</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
</div>
</div>
);
});

View File

@@ -0,0 +1,158 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { RelayContext } from '@lume/shared/relaysProvider';
import { UserExtend } from '@lume/shared/user/extend';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { Dialog, Transition } from '@headlessui/react';
import { ChatLines, OpenNewWindow } from 'iconoir-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { Fragment, useContext, useState } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export const NoteComment = ({
count,
eventID,
eventPubkey,
eventContent,
eventTime,
}: {
count: number;
eventID: string;
eventPubkey: string;
eventTime: number;
eventContent: any;
}) => {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState('');
const profile = activeAccount ? JSON.parse(activeAccount.metadata) : null;
const closeModal = () => {
setIsOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
const openThread = () => {
navigate(`/newsfeed/note?id=${eventID}`);
};
const submitEvent = () => {
const event: any = {
content: value,
created_at: dateToUnix(),
kind: 1,
pubkey: activeAccount.pubkey,
tags: [['e', eventID]],
};
event.id = getEventHash(event);
event.sig = signEvent(event, activeAccount.privkey);
pool.publish(event, WRITEONLY_RELAYS);
setIsOpen(false);
};
return (
<>
<button
type="button"
onClick={() => openModal()}
className="group flex w-16 items-center gap-1 text-sm text-zinc-500"
>
<div className="rounded-md p-1 group-hover:bg-zinc-800">
<ChatLines width={20} height={20} className="text-zinc-500" />
</div>
<span>{count}</span>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md data-[state=open]:animate-overlayShow" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900">
{/* root note */}
<div className="relative z-10 flex flex-col pb-6">
<div className="relative z-10">
<UserExtend pubkey={eventPubkey} time={eventTime} />
</div>
<div className="-mt-5 pl-[52px]">
<div className="prose prose-zinc max-w-none break-words leading-tight dark:prose-invert prose-headings:mb-2 prose-headings:mt-3 prose-p:m-0 prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-ul:mt-2 prose-li:my-1">
{eventContent}
</div>
</div>
{/* divider */}
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
</div>
{/* comment form */}
<div className="flex gap-2">
<div>
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
<img src={profile?.picture} alt="user's avatar" className="h-11 w-11 rounded-md object-cover" />
</div>
</div>
<div className="relative h-36 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<div>
<textarea
name="content"
onChange={(e) => setValue(e.target.value)}
placeholder="Send your comment"
className="relative h-36 w-full resize-none rounded-md border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
spellCheck={false}
/>
</div>
<div className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700">
<button
onClick={() => openThread()}
className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-zinc-700"
>
<OpenNewWindow width={16} height={16} className="text-zinc-400" />
</button>
<div className="flex items-center gap-2 pl-2"></div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => submitEvent()}
disabled={value.length === 0 ? true : false}
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-md shadow-fuchsia-900/50 hover:bg-fuchsia-600"
>
<span className="text-white drop-shadow">Send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
};

View File

@@ -0,0 +1,69 @@
import { AccountContext } from '@lume/shared/accountProvider';
import { RelayContext } from '@lume/shared/relaysProvider';
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
import { dateToUnix } from '@lume/utils/getDate';
import { Heart } from 'iconoir-react';
import { getEventHash, signEvent } from 'nostr-tools';
import { useContext, useEffect, useState } from 'react';
export const NoteReaction = ({
count,
liked,
eventID,
eventPubkey,
}: {
count: number;
liked: boolean;
eventID: string;
eventPubkey: string;
}) => {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const [isReact, setIsReact] = useState(false);
const [like, setLike] = useState(0);
const handleLike = (e: any) => {
e.stopPropagation();
if (liked === false || isReact === false) {
const event: any = {
content: '+',
kind: 7,
tags: [
['e', eventID],
['p', eventPubkey],
],
created_at: dateToUnix(),
pubkey: activeAccount.pubkey,
};
event.id = getEventHash(event);
event.sig = signEvent(event, activeAccount.privkey);
// publish event to all relays
pool.publish(event, WRITEONLY_RELAYS);
// update state to change icon to filled heart
setIsReact(true);
// update counter
setLike((like) => (like += 1));
}
};
useEffect(() => {
setIsReact(liked);
setLike(count);
}, [count, liked]);
return (
<button onClick={(e) => handleLike(e)} className="group flex w-16 items-center gap-1 text-sm text-zinc-500">
<div className="rounded-md p-1 group-hover:bg-zinc-800">
{isReact ? (
<Heart width={20} height={20} className="fill-red-500 text-transparent" />
) : (
<Heart width={20} height={20} className="text-zinc-500" />
)}
</div>
<span>{like}</span>
</button>
);
};

View File

@@ -0,0 +1,5 @@
import { memo } from 'react';
export const NoteMetadata = memo(function NoteMetadata() {
return <div className="relative z-10 -ml-1 flex items-center gap-8"></div>;
});

View File

@@ -0,0 +1,83 @@
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
import { READONLY_RELAYS } from '@lume/stores/constants';
import { RelayPool } from 'nostr-relaypool';
import { memo } from 'react';
import useSWRSubscription from 'swr/subscription';
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
const { data, error } = useSWRSubscription(
id
? [
{
ids: [id],
kinds: [1],
},
]
: null,
(key, { next }) => {
const pool = new RelayPool(READONLY_RELAYS);
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
}
);
return () => {
unsubscribe();
};
}
);
return (
<div className="relative pb-5">
{error && <div>failed to load</div>}
{!data ? (
<div className="animated-pulse">
<div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
</div>
</div>
</div>
<div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-6">
<div className="h-16 w-full rounded bg-zinc-700" />
<div className="flex items-center gap-8">
<div className="h-4 w-12 rounded bg-zinc-700" />
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
</div>
</div>
</div>
) : (
<>
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
<div className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentParser(data.content, data.tags)}
</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
</div>
</>
)}
</div>
);
});

View File

@@ -0,0 +1,27 @@
export const Placeholder = () => {
return (
<div className="relative z-10 flex h-min animate-pulse select-text flex-col px-3 py-5">
<div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
</div>
</div>
</div>
<div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-6">
<div className="h-16 w-full rounded bg-zinc-700" />
<div className="flex items-center gap-8">
<div className="h-4 w-12 rounded bg-zinc-700" />
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,14 @@
export const ImagePreview = ({ url, size }: { url: string; size: string }) => {
return (
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
<img
src={url}
alt={url}
className="h-auto w-full rounded-lg object-cover"
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
/>
</div>
);
};

View File

@@ -0,0 +1,11 @@
import { MediaOutlet, MediaPlayer } from '@vidstack/react';
export const VideoPreview = ({ url }: { url: string }) => {
return (
<div onClick={(e) => e.stopPropagation()} className="relative mt-2 flex flex-col overflow-hidden rounded-lg">
<MediaPlayer src={url} poster="" controls>
<MediaOutlet />
</MediaPlayer>
</div>
);
};

View File

@@ -0,0 +1,16 @@
import YouTube from 'react-youtube';
function getVideoId(url: string) {
const regex = /(youtu.*be.*)\/(watch\?v=|embed\/|v|shorts|)(.*?((?=[&#?])|$))/gm;
return regex.exec(url)[3];
}
export const YoutubePreview = ({ url }: { url: string }) => {
const id = getVideoId(url);
return (
<div onClick={(e) => e.stopPropagation()} className="relative mt-2 flex flex-col overflow-hidden rounded-lg">
<YouTube videoId={id} className="aspect-video xl:w-2/3" opts={{ width: '100%', height: '100%' }} />
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
import { READONLY_RELAYS } from '@lume/stores/constants';
import { RelayPool } from 'nostr-relaypool';
import { memo } from 'react';
import useSWRSubscription from 'swr/subscription';
export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
const { data, error } = useSWRSubscription(
id
? [
{
ids: [id],
kinds: [1],
},
]
: null,
(key, { next }) => {
const pool = new RelayPool(READONLY_RELAYS);
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
}
);
return () => {
unsubscribe();
};
}
);
return (
<div className="relative mb-2 mt-3 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3">
{error && <div>failed to load</div>}
{!data ? (
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
) : (
<div className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentParser(data.content, data.tags)}
</div>
</div>
</div>
)}
</div>
);
});

View File

@@ -0,0 +1,19 @@
import { RootNote } from '@lume/app/newsfeed/components/note/rootNote';
import { NoteRepostUser } from '@lume/app/newsfeed/components/user/repost';
import { getQuoteID } from '@lume/utils/transform';
import { memo } from 'react';
export const NoteQuoteRepost = memo(function NoteQuoteRepost({ event }: { event: any }) {
const rootID = getQuoteID(event.tags);
return (
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20">
<div className="relative z-10 flex flex-col pb-5">
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
</div>
<RootNote id={rootID} />
</div>
);
});

View File

@@ -0,0 +1,68 @@
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
import { READONLY_RELAYS } from '@lume/stores/constants';
import { RelayPool } from 'nostr-relaypool';
import { memo } from 'react';
import useSWRSubscription from 'swr/subscription';
import { navigate } from 'vite-plugin-ssr/client/router';
export const RootNote = memo(function RootNote({ id }: { id: string }) {
const openThread = (e) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
navigate(`/newsfeed/note?id=${id}`);
} else {
e.stopPropagation();
}
};
const { data, error } = useSWRSubscription(
id
? [
{
ids: [id],
kinds: [1],
},
]
: null,
(key, { next }) => {
const pool = new RelayPool(READONLY_RELAYS);
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
}
);
return () => {
unsubscribe();
};
}
);
return (
<>
{error && <div>failed to load</div>}
{!data ? (
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
) : (
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentParser(data.content, data.tags)}
</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
</div>
)}
</>
);
});

View File

@@ -0,0 +1,39 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const NoteDefaultUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
const profile = useProfile(pubkey);
return (
<div className="group flex h-11 items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-baseline gap-2">
<h5 className="text-sm font-semibold leading-none group-hover:underline">
{profile?.display_name || profile?.name || shortenKey(pubkey)}
</h5>
<span className="text-sm leading-none text-zinc-700"></span>
</div>
<span className="text-sm leading-none text-zinc-500">
{profile?.nip05 || shortenKey(pubkey)} {dayjs().to(dayjs.unix(time))}
</span>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
export const NoteMentionUser = ({ pubkey }: { pubkey: string }) => {
const profile = useProfile(pubkey);
return (
<span className="cursor-pointer text-fuchsia-500">@{profile?.name || profile?.username || shortenKey(pubkey)}</span>
);
};

View File

@@ -0,0 +1,36 @@
import { DEFAULT_AVATAR } from '@lume/stores/constants';
import { useProfile } from '@lume/utils/hooks/useProfile';
import { shortenKey } from '@lume/utils/shortenKey';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
export const NoteRepostUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
const profile = useProfile(pubkey);
return (
<div className="group flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
<img
src={profile?.picture || DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-md object-cover"
loading="lazy"
fetchpriority="high"
/>
</div>
<div className="flex items-baseline gap-2 text-sm">
<h5 className="font-semibold leading-tight group-hover:underline">
{profile?.display_name || profile?.name || shortenKey(pubkey)}{' '}
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
reposted
</span>
</h5>
<span className="leading-tight text-zinc-500">·</span>
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
</div>
</div>
);
};

View File

@@ -4,7 +4,7 @@ import Navigation from '@lume/shared/navigation';
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-white">
<div className="flex h-screen w-full flex-col">
<div
data-tauri-drag-region

View File

@@ -1,7 +1,7 @@
import FormBase from '@lume/shared/form/base';
import { NoteBase } from '@lume/shared/note/base';
import { Placeholder } from '@lume/shared/note/placeholder';
import { NoteQuoteRepost } from '@lume/shared/note/quoteRepost';
import NoteForm from '@lume/app/newsfeed/components/form';
import { NoteBase } from '@lume/app/newsfeed/components/note/base';
import { Placeholder } from '@lume/app/newsfeed/components/note/placeholder';
import { NoteQuoteRepost } from '@lume/app/newsfeed/components/note/quoteRepost';
import { hasNewerNoteAtom } from '@lume/stores/note';
import { countTotalNotes, getNotes } from '@lume/utils/storage';
@@ -73,7 +73,7 @@ export function Page() {
<div>{error.message}</div>
) : (
<div ref={parentRef} className="scrollbar-hide h-full w-full overflow-y-auto" style={{ contain: 'strict' }}>
<FormBase />
<NoteForm />
<div
className="relative w-full"
style={{