mirror of
https://github.com/lumehq/lume.git
synced 2025-09-27 02:06:27 +02:00
fix the mess I started
This commit is contained in:
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
56
src/app/newsfeed/components/contentParser.tsx
Normal file
56
src/app/newsfeed/components/contentParser.tsx
Normal 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;
|
||||
};
|
@@ -1,39 +1,41 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import { ImagePicker } from '@lume/shared/form/imagePicker';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
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';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export default function FormBase() {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
|
||||
export default function NoteForm() {
|
||||
const { account, isLoading, isError } = useActiveAccount();
|
||||
const [value, setValue] = useAtom(noteContentAtom);
|
||||
const resetValue = useResetAtom(noteContentAtom);
|
||||
|
||||
const submitEvent = () => {
|
||||
const event: any = {
|
||||
content: value,
|
||||
created_at: dateToUnix(),
|
||||
kind: 1,
|
||||
pubkey: activeAccount.pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, activeAccount.privkey);
|
||||
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');
|
||||
// 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 (
|
@@ -1,7 +1,6 @@
|
||||
import { NoteMetadata } from '@lume/shared/note/metadata';
|
||||
import { NoteParent } from '@lume/shared/note/parent';
|
||||
import { UserExtend } from '@lume/shared/user/extend';
|
||||
import { contentParser } from '@lume/utils/parser';
|
||||
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';
|
||||
@@ -40,19 +39,12 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||
{parentNote()}
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<div onClick={(e) => openUserPage(e)}>
|
||||
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||
<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]">
|
||||
<NoteMetadata
|
||||
eventID={event.event_id}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
24
src/app/newsfeed/components/note/comment.tsx
Normal file
24
src/app/newsfeed/components/note/comment.tsx
Normal 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>
|
||||
);
|
||||
});
|
@@ -1,15 +1,12 @@
|
||||
import { NoteMetadata } from '@lume/shared/note/metadata';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { UserExtend } from '@lume/shared/user/extend';
|
||||
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 { contentParser } from '@lume/utils/parser';
|
||||
|
||||
import { memo, useContext } from 'react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { memo } from 'react';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
const pool: any = useContext(RelayContext);
|
||||
|
||||
const { data, error } = useSWRSubscription(
|
||||
id
|
||||
? [
|
||||
@@ -20,6 +17,7 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
]
|
||||
: null,
|
||||
(key, { next }) => {
|
||||
const pool = new RelayPool(READONLY_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
key,
|
||||
READONLY_RELAYS,
|
||||
@@ -70,20 +68,13 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
<>
|
||||
<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">
|
||||
<UserExtend pubkey={data.pubkey} time={data.created_at} />
|
||||
<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]">
|
||||
<NoteMetadata
|
||||
eventID={data.event_id}
|
||||
eventPubkey={data.pubkey}
|
||||
eventContent={data.content}
|
||||
eventTime={data.created_at}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
@@ -1,14 +1,12 @@
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { UserExtend } from '@lume/shared/user/extend';
|
||||
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 { contentParser } from '@lume/utils/parser';
|
||||
|
||||
import { memo, useContext } from 'react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { memo } from 'react';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
||||
const pool: any = useContext(RelayContext);
|
||||
|
||||
const { data, error } = useSWRSubscription(
|
||||
id
|
||||
? [
|
||||
@@ -19,6 +17,7 @@ export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
||||
]
|
||||
: null,
|
||||
(key, { next }) => {
|
||||
const pool = new RelayPool(READONLY_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
key,
|
||||
READONLY_RELAYS,
|
||||
@@ -45,7 +44,7 @@ export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
||||
<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">
|
||||
<UserExtend pubkey={data.pubkey} time={data.created_at} />
|
||||
<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)}
|
@@ -1,5 +1,5 @@
|
||||
import { RootNote } from '@lume/shared/note/rootNote';
|
||||
import { UserQuoteRepost } from '@lume/shared/user/quoteRepost';
|
||||
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';
|
||||
@@ -11,7 +11,7 @@ export const NoteQuoteRepost = memo(function NoteQuoteRepost({ event }: { event:
|
||||
<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>
|
||||
<UserQuoteRepost pubkey={event.pubkey} time={event.created_at} />
|
||||
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
|
||||
</div>
|
||||
<RootNote id={rootID} />
|
||||
</div>
|
@@ -1,16 +1,13 @@
|
||||
import { NoteMetadata } from '@lume/shared/note/metadata';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { UserExtend } from '@lume/shared/user/extend';
|
||||
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 { contentParser } from '@lume/utils/parser';
|
||||
|
||||
import { memo, useContext } from 'react';
|
||||
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 pool: any = useContext(RelayContext);
|
||||
|
||||
const openThread = (e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
@@ -30,6 +27,7 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) {
|
||||
]
|
||||
: null,
|
||||
(key, { next }) => {
|
||||
const pool = new RelayPool(READONLY_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
key,
|
||||
READONLY_RELAYS,
|
||||
@@ -56,20 +54,13 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) {
|
||||
<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">
|
||||
<UserExtend pubkey={data.pubkey} time={data.created_at} />
|
||||
<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]">
|
||||
<NoteMetadata
|
||||
eventID={data.id}
|
||||
eventPubkey={data.pubkey}
|
||||
eventContent={data.content}
|
||||
eventTime={data.created_at}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
@@ -7,7 +7,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const UserExtend = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
export const NoteDefaultUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
return (
|
@@ -1,7 +1,7 @@
|
||||
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||
import { shortenKey } from '@lume/utils/shortenKey';
|
||||
|
||||
export const UserMention = ({ pubkey }: { pubkey: string }) => {
|
||||
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>
|
@@ -7,7 +7,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const UserQuoteRepost = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
export const NoteRepostUser = ({ pubkey, time }: { pubkey: string; time: number }) => {
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
return (
|
@@ -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
|
||||
|
@@ -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={{
|
||||
|
@@ -94,8 +94,9 @@ export function Page() {
|
||||
const submit = async () => {
|
||||
setLoading(true);
|
||||
|
||||
const followsIncludeSelf = follows.concat([onboarding.pubkey]);
|
||||
// insert to database
|
||||
createAccount(onboarding.pubkey, onboarding.privkey, onboarding.metadata, arrayToNIP02(follows), 1)
|
||||
createAccount(onboarding.pubkey, onboarding.privkey, onboarding.metadata, arrayToNIP02(followsIncludeSelf), 1)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
for (const tag of follows) {
|
||||
|
@@ -53,8 +53,9 @@ export function Page() {
|
||||
// show loading indicator
|
||||
setLoading(true);
|
||||
|
||||
const follows = onboarding.follows.concat([['p', pubkey]]);
|
||||
// insert to database
|
||||
createAccount(pubkey, onboarding.privkey, onboarding.metadata, onboarding.follows, 1)
|
||||
createAccount(pubkey, onboarding.privkey, onboarding.metadata, follows, 1)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
for (const tag of onboarding.follows) {
|
||||
|
@@ -45,9 +45,8 @@ export default function AppHeader({ collector }: { collector: boolean }) {
|
||||
</button>
|
||||
</div>
|
||||
<div data-tauri-drag-region className="flex h-full w-full items-center justify-between">
|
||||
<div className="flex h-full items-center divide-x divide-zinc-900 px-4 pt-px">
|
||||
{collector && <EventCollector />}
|
||||
</div>
|
||||
<div className="flex h-full items-center divide-x divide-zinc-900 px-4 pt-px"></div>
|
||||
{collector && <EventCollector />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,24 +1,26 @@
|
||||
import { AccountContext } from '@lume/shared/accountProvider';
|
||||
import { NetworkStatusIndicator } from '@lume/shared/networkStatusIndicator';
|
||||
import { RelayContext } from '@lume/shared/relaysProvider';
|
||||
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||
import { hasNewerNoteAtom } from '@lume/stores/note';
|
||||
import { dateToUnix } from '@lume/utils/getDate';
|
||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||
import { createChat, createNote, updateAccount } from '@lume/utils/storage';
|
||||
import { getParentID, nip02ToArray } from '@lume/utils/transform';
|
||||
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
import { useRef } from 'react';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
export default function EventCollector() {
|
||||
const pool: any = useContext(RelayContext);
|
||||
const activeAccount: any = useContext(AccountContext);
|
||||
|
||||
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
|
||||
const now = useRef(new Date());
|
||||
|
||||
const subscribe = useCallback(async () => {
|
||||
const follows = activeAccount.follows ? JSON.parse(activeAccount.follows) : [];
|
||||
const { account, isLoading, isError } = useActiveAccount();
|
||||
|
||||
useSWRSubscription(!isLoading && !isError ? account : null, () => {
|
||||
const follows = nip02ToArray(JSON.parse(account.follows));
|
||||
|
||||
const pool = new RelayPool(READONLY_RELAYS);
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
@@ -28,16 +30,16 @@ export default function EventCollector() {
|
||||
},
|
||||
{
|
||||
kinds: [0, 3],
|
||||
authors: [activeAccount.pubkey],
|
||||
authors: [account.pubkey],
|
||||
},
|
||||
{
|
||||
kinds: [4],
|
||||
'#p': [activeAccount.pubkey],
|
||||
'#p': [account.pubkey],
|
||||
since: dateToUnix(now.current),
|
||||
},
|
||||
],
|
||||
READONLY_RELAYS,
|
||||
(event: { kind: number; tags: string[]; id: string; pubkey: string; content: string; created_at: number }) => {
|
||||
(event: any) => {
|
||||
switch (event.kind) {
|
||||
// metadata
|
||||
case 0:
|
||||
@@ -48,7 +50,7 @@ export default function EventCollector() {
|
||||
const parentID = getParentID(event.tags, event.id);
|
||||
createNote(
|
||||
event.id,
|
||||
activeAccount.id,
|
||||
account.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.tags,
|
||||
@@ -61,21 +63,20 @@ export default function EventCollector() {
|
||||
break;
|
||||
// contacts
|
||||
case 3:
|
||||
const arr = nip02ToArray(event.tags);
|
||||
// update account's folllows with NIP-02 tag list
|
||||
updateAccount('follows', arr, event.pubkey);
|
||||
updateAccount('follows', event.tags, event.pubkey);
|
||||
break;
|
||||
// chat
|
||||
case 4:
|
||||
if (event.pubkey !== activeAccount.pubkey) {
|
||||
createChat(activeAccount.id, event.pubkey, event.created_at);
|
||||
if (event.pubkey !== account.pubkey) {
|
||||
createChat(account.id, event.pubkey, event.created_at);
|
||||
}
|
||||
break;
|
||||
// repost
|
||||
case 6:
|
||||
createNote(
|
||||
event.id,
|
||||
activeAccount.id,
|
||||
account.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.tags,
|
||||
@@ -93,19 +94,7 @@ export default function EventCollector() {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [activeAccount.id, activeAccount.pubkey, activeAccount.follows, pool, setHasNewerNote]);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
if (!ignore) {
|
||||
subscribe();
|
||||
}
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [subscribe]);
|
||||
});
|
||||
|
||||
return <NetworkStatusIndicator />;
|
||||
}
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import AppHeader from '@lume/shared/appHeader';
|
||||
import MultiAccounts from '@lume/shared/multiAccounts';
|
||||
import Navigation from '@lume/shared/navigation';
|
||||
|
||||
export default function ChannelLayout({ 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="flex h-screen w-full flex-col">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative h-11 shrink-0 border-b border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||
>
|
||||
<AppHeader collector={true} />
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-full flex-1">
|
||||
<div className="relative w-[68px] shrink-0 border-r border-zinc-900">
|
||||
<MultiAccounts />
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-4 xl:grid-cols-5">
|
||||
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className="col-span-3 m-3 overflow-hidden xl:col-span-4 xl:mr-1.5">
|
||||
<div className="h-full w-full rounded-lg">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
import AppHeader from '@lume/shared/appHeader';
|
||||
import MultiAccounts from '@lume/shared/multiAccounts';
|
||||
import Navigation from '@lume/shared/navigation';
|
||||
|
||||
export default function NewsfeedLayout({ 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="flex h-screen w-full flex-col">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative h-11 shrink-0 border-b border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||
>
|
||||
<AppHeader collector={true} />
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-full flex-1">
|
||||
<div className="relative w-[68px] shrink-0 border-r border-zinc-900">
|
||||
<MultiAccounts />
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-4 xl:grid-cols-5">
|
||||
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className="col-span-3 m-3 overflow-hidden xl:col-span-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
import AppHeader from '@lume/shared/appHeader';
|
||||
|
||||
export default function OnboardingLayout({ 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="flex h-screen w-full flex-col">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative h-11 shrink-0 border-b border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||
>
|
||||
<AppHeader collector={false} />
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-full flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -24,7 +24,7 @@ export default function Navigation() {
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="flex flex-col text-zinc-400">
|
||||
<ActiveLink
|
||||
href="/newsfeed/following"
|
||||
href="/app/newsfeed/following"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
>
|
||||
@@ -32,7 +32,7 @@ export default function Navigation() {
|
||||
<span>Following</span>
|
||||
</ActiveLink>
|
||||
<ActiveLink
|
||||
href="/newsfeed/circle"
|
||||
href="/app/newsfeed/circle"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
>
|
||||
|
@@ -1,82 +0,0 @@
|
||||
import { NoteMetadata } from '@lume/shared/note/metadata';
|
||||
import { ImagePreview } from '@lume/shared/note/preview/image';
|
||||
import { VideoPreview } from '@lume/shared/note/preview/video';
|
||||
import { NoteQuote } from '@lume/shared/note/quote';
|
||||
import { UserExtend } from '@lume/shared/user/extend';
|
||||
import { UserMention } from '@lume/shared/user/mention';
|
||||
|
||||
import destr from 'destr';
|
||||
import { memo, useMemo } from 'react';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
export const NoteComment = memo(function NoteComment({ event }: { event: any }) {
|
||||
const content = useMemo(() => {
|
||||
let parsedContent = event.content;
|
||||
// get data tags
|
||||
const tags = destr(event.tags);
|
||||
// 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(/(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/i)) {
|
||||
// youtube
|
||||
return <VideoPreview 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} 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.length > 0) {
|
||||
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||
if (tags[match][0] === 'p') {
|
||||
// @-mentions
|
||||
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||
} else if (tags[match][0] === 'e') {
|
||||
// note-quotes
|
||||
return <NoteQuote key={match + i} id={tags[match][1]} />;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return parsedContent;
|
||||
}, [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">
|
||||
<UserExtend 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]">
|
||||
<NoteMetadata
|
||||
eventID={event.event_id}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@@ -1,82 +0,0 @@
|
||||
import { NoteMetadata } from '@lume/shared/note/metadata';
|
||||
import { ImagePreview } from '@lume/shared/note/preview/image';
|
||||
import { VideoPreview } from '@lume/shared/note/preview/video';
|
||||
import { NoteQuote } from '@lume/shared/note/quote';
|
||||
import { UserLarge } from '@lume/shared/user/large';
|
||||
import { UserMention } from '@lume/shared/user/mention';
|
||||
|
||||
import destr from 'destr';
|
||||
import { memo, useMemo } from 'react';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
export const NoteExtend = memo(function NoteExtend({ event }: { event: any }) {
|
||||
const content = useMemo(() => {
|
||||
let parsedContent = event.content;
|
||||
// get data tags
|
||||
const tags = destr(event.tags);
|
||||
// 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(/(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/i)) {
|
||||
// youtube
|
||||
return <VideoPreview 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} 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.length > 0) {
|
||||
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||
if (tags[match][0] === 'p') {
|
||||
// @-mentions
|
||||
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||
} else if (tags[match][0] === 'e') {
|
||||
// note-quotes
|
||||
return <NoteQuote key={match + i} id={tags[match][1]} />;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return parsedContent;
|
||||
}, [event.content, event.tags]);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col">
|
||||
<div className="relative z-10 flex flex-col">
|
||||
<UserLarge pubkey={event.pubkey} time={event.created_at} />
|
||||
<div className="mt-2">
|
||||
<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 className="mt-5 flex items-center border-b border-t border-zinc-800 py-2">
|
||||
<NoteMetadata
|
||||
eventID={event.event_id}
|
||||
eventPubkey={event.pubkey}
|
||||
eventContent={event.content}
|
||||
eventTime={event.created_at}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
15
src/utils/hooks/useActiveAccount.tsx
Normal file
15
src/utils/hooks/useActiveAccount.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getActiveAccount } from '@lume/utils/storage';
|
||||
|
||||
import useSWR from 'swr';
|
||||
|
||||
const fetcher = () => getActiveAccount();
|
||||
|
||||
export const useActiveAccount = () => {
|
||||
const { data, error, isLoading } = useSWR('activeAcount', fetcher);
|
||||
|
||||
return {
|
||||
account: data,
|
||||
isLoading,
|
||||
isError: error,
|
||||
};
|
||||
};
|
@@ -1,7 +1,7 @@
|
||||
import destr from 'destr';
|
||||
|
||||
// convert NIP-02 to array of pubkey
|
||||
export const nip02ToArray = (tags: string[]) => {
|
||||
export const nip02ToArray = (tags: any) => {
|
||||
const arr = [];
|
||||
tags.forEach((item) => {
|
||||
arr.push(item[1]);
|
||||
|
Reference in New Issue
Block a user