refactor channel
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
import { ChannelMessageItem } from "@app/channel/components/messages/item";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
import { getHourAgo } from "@utils/date";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
|
||||
export function ChannelMessageList() {
|
||||
const now = useRef(new Date());
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
const messages = useChannelMessages((state: any) => state.messages);
|
||||
|
||||
const itemContent: any = useCallback(
|
||||
(index: string | number) => {
|
||||
return <ChannelMessageItem data={messages[index]} />;
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
const computeItemKey = useCallback(
|
||||
(index: string | number) => {
|
||||
return messages[index].id;
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={[]}
|
||||
itemContent={itemContent}
|
||||
components={{
|
||||
Header: () => (
|
||||
<div className="relative py-4">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-zinc-800" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-base font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
|
||||
{getHourAgo(24, now.current).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
EmptyPlaceholder: () => (
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h3 className="text-base font-semibold leading-none text-white">
|
||||
Nothing to see here yet
|
||||
</h3>
|
||||
<p className="text-base leading-none text-zinc-400">
|
||||
Be the first to share a message in this channel.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UserReply } from "@app/channel/components/messages/userReply";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { CancelIcon } from "@shared/icons";
|
||||
import { CancelIcon, EnterIcon } from "@shared/icons";
|
||||
import { MediaUploader } from "@shared/mediaUploader";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
@@ -17,7 +18,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
||||
state.closeReply,
|
||||
]);
|
||||
|
||||
const submitEvent = () => {
|
||||
const submit = () => {
|
||||
let tags: string[][];
|
||||
|
||||
if (replyTo.id !== null) {
|
||||
@@ -51,7 +52,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
||||
const handleEnterPress = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submitEvent();
|
||||
submit();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,11 +61,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${
|
||||
replyTo.id ? "h-36" : "h-24"
|
||||
} w-full overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] 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 className={`relative w-full ${replyTo.id ? "h-36" : "h-24"}`}>
|
||||
{replyTo.id && (
|
||||
<div className="absolute left-0 top-0 z-10 h-16 w-full p-[2px]">
|
||||
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
|
||||
@@ -92,23 +89,19 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
||||
placeholder="Message"
|
||||
className={`relative ${
|
||||
replyTo.id ? "h-36 pt-16" : "h-24 pt-3"
|
||||
} w-full resize-none rounded-lg border border-black/5 px-3.5 pb-3 text-base shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500`}
|
||||
} w-full resize-none rounded-md px-5 !outline-none bg-zinc-800 placeholder:text-zinc-500`}
|
||||
/>
|
||||
<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">
|
||||
<div className="flex items-center gap-2 pl-2" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="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-base font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute right-2 bottom-0 h-11">
|
||||
<div className="h-full flex gap-3 items-center justify-end text-zinc-500">
|
||||
<MediaUploader setState={setValue} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
className="inline-flex items-center gap-1 text-sm leading-none"
|
||||
>
|
||||
<EnterIcon width={14} height={14} className="" />
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function MessageHideButton({ id }: { id: string }) {
|
||||
onClick={openModal}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<HideIcon width={16} height={16} className="text-white" />
|
||||
<HideIcon width={16} height={16} className="text-zinc-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
||||
@@ -2,22 +2,22 @@ import { MessageHideButton } from "@app/channel/components/messages/hideButton";
|
||||
import { MessageMuteButton } from "@app/channel/components/messages/muteButton";
|
||||
import { MessageReplyButton } from "@app/channel/components/messages/replyButton";
|
||||
import { ChannelMessageUser } from "@app/channel/components/messages/user";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { MentionNote } from "@shared/notes/mentions/note";
|
||||
import { ImagePreview } from "@shared/notes/preview/image";
|
||||
import { LinkPreview } from "@shared/notes/preview/link";
|
||||
import { VideoPreview } from "@shared/notes/preview/video";
|
||||
import { parser } from "@utils/parser";
|
||||
import { useMemo } from "react";
|
||||
import { LumeEvent } from "@utils/types";
|
||||
|
||||
export function ChannelMessageItem({ data }: { data: NDKEvent }) {
|
||||
const content = useMemo(() => parser(data), [data]);
|
||||
export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
||||
const content = parser(data);
|
||||
|
||||
return (
|
||||
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
||||
<div className="flex flex-col">
|
||||
<ChannelMessageUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-[20px] pl-[49px]">
|
||||
<p className="whitespace-pre-line break-words text-base leading-tight">
|
||||
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
{content.parsed}
|
||||
</p>
|
||||
{Array.isArray(content.images) && content.images.length ? (
|
||||
@@ -30,6 +30,11 @@ export function ChannelMessageItem({ data }: { data: NDKEvent }) {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{Array.isArray(content.links) && content.links.length ? (
|
||||
<LinkPreview urls={content.links} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{Array.isArray(content.notes) && content.notes.length ? (
|
||||
content.notes.map((note: string) => (
|
||||
<MentionNote key={note} id={note} />
|
||||
|
||||
@@ -53,7 +53,7 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
||||
onClick={() => openModal()}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<MuteIcon width={16} height={16} className="text-white" />
|
||||
<MuteIcon width={16} height={16} className="text-zinc-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function MessageReplyButton({
|
||||
onClick={() => createReply()}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
|
||||
>
|
||||
<ReplyMessageIcon width={16} height={16} className="text-white" />
|
||||
<ReplyMessageIcon width={16} height={16} className="text-zinc-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -4,95 +4,73 @@ import { ChannelMessageForm } from "@app/channel/components/messages/form";
|
||||
import { ChannelMetadata } from "@app/channel/components/metadata";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { dateToUnix, getHourAgo } from "@utils/date";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
||||
import { LumeEvent } from "@utils/types";
|
||||
import { useCallback, useContext, useEffect, useRef } from "react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
|
||||
const now = new Date();
|
||||
const since = dateToUnix(getHourAgo(24, now));
|
||||
|
||||
export function Page() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const pageContext = usePageContext();
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
const channelID = searchParams.id;
|
||||
|
||||
const [messages, addMessage, fetchMessages, clearMessages]: any =
|
||||
const [messages, fetchMessages, addMessage, clearMessages] =
|
||||
useChannelMessages((state: any) => [
|
||||
state.messages,
|
||||
state.addMessage,
|
||||
state.fetch,
|
||||
state.add,
|
||||
state.clear,
|
||||
]);
|
||||
|
||||
useSWRSubscription(["channelMessagesSubscribe", channelID], () => {
|
||||
// subscribe to channel
|
||||
const sub = ndk.subscribe({
|
||||
"#e": [channelID],
|
||||
kinds: [42],
|
||||
since: dateToUnix(),
|
||||
});
|
||||
useSWRSubscription(
|
||||
channelID ? ["channelMessagesSubscribe", channelID] : null,
|
||||
() => {
|
||||
// subscribe to channel
|
||||
const sub = ndk.subscribe(
|
||||
{
|
||||
"#e": [channelID],
|
||||
kinds: [42],
|
||||
since: dateToUnix(),
|
||||
},
|
||||
{ closeOnEose: false },
|
||||
);
|
||||
|
||||
sub.addListener("event", (event) => {
|
||||
addMessage(event);
|
||||
});
|
||||
sub.addListener("event", (event: LumeEvent) => {
|
||||
addMessage(channelID, event);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.stop();
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
sub.stop();
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages(ndk, channelID, since);
|
||||
fetchMessages(channelID);
|
||||
|
||||
return () => {
|
||||
clearMessages();
|
||||
};
|
||||
}, [fetchMessages]);
|
||||
|
||||
const count = messages.length;
|
||||
const reverseIndex = useCallback((index) => count - 1 - index, [count]);
|
||||
const parentRef = useRef();
|
||||
const virtualizerRef = useRef(null);
|
||||
const itemContent: any = useCallback(
|
||||
(index: string | number) => {
|
||||
return <ChannelMessageItem data={messages[index]} />;
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
if (
|
||||
virtualizerRef.current &&
|
||||
count !== virtualizerRef.current.options.count
|
||||
) {
|
||||
const delta = count - virtualizerRef.current.options.count;
|
||||
const nextOffset = virtualizerRef.current.scrollOffset + delta * 200;
|
||||
|
||||
virtualizerRef.current.scrollOffset = nextOffset;
|
||||
virtualizerRef.current.scrollToOffset(nextOffset, { align: "start" });
|
||||
}
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 200,
|
||||
getItemKey: useCallback(
|
||||
(index) => messages[reverseIndex(index)].id,
|
||||
[messages, reverseIndex],
|
||||
),
|
||||
overscan: 5,
|
||||
scrollMargin: 50,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
virtualizerRef.current = virtualizer;
|
||||
}, []);
|
||||
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
const [paddingTop, paddingBottom] =
|
||||
items.length > 0
|
||||
? [
|
||||
Math.max(0, items[0].start - virtualizer.options.scrollMargin),
|
||||
Math.max(0, virtualizer.getTotalSize() - items[items.length - 1].end),
|
||||
]
|
||||
: [0, 0];
|
||||
const computeItemKey = useCallback(
|
||||
(index: string | number) => {
|
||||
return messages[index].event_id;
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full grid grid-cols-3">
|
||||
@@ -105,40 +83,23 @@ export function Page() {
|
||||
</div>
|
||||
<div className="w-full flex-1 p-3">
|
||||
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="scrollbar-hide overflow-y-auto h-full w-full"
|
||||
style={{ contain: "strict" }}
|
||||
>
|
||||
{!messages ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
overflowAnchor: "none",
|
||||
paddingTop,
|
||||
paddingBottom,
|
||||
}}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const index = reverseIndex(item.index);
|
||||
const message = messages[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
data-index={item.index}
|
||||
data-reverse-index={index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<ChannelMessageItem data={message} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full inline-flex shrink-0 border-t border-zinc-800">
|
||||
{!messages ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
alignToBottom={true}
|
||||
followOutput={true}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
className="scrollbar-hide overflow-y-auto h-full w-full"
|
||||
/>
|
||||
)}
|
||||
<div className="w-full inline-flex shrink-0 px-5 py-3 border-t border-zinc-800">
|
||||
<ChannelMessageForm channelID={channelID} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,11 +18,11 @@ export function Page() {
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
const pubkey = searchParams.pubkey;
|
||||
|
||||
const [fetchMessages, clear] = useChatMessages((state: any) => [
|
||||
const [add, fetchMessages, clear] = useChatMessages((state: any) => [
|
||||
state.add,
|
||||
state.fetch,
|
||||
state.clear,
|
||||
]);
|
||||
const add = useChatMessages((state: any) => state.add);
|
||||
|
||||
useSWRSubscription(account !== pubkey ? ["chat", pubkey] : null, () => {
|
||||
const sub = ndk.subscribe({
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { prefetchEvents } from "@libs/ndk";
|
||||
import { countTotalNotes, createChat, createNote } from "@libs/storage";
|
||||
import {
|
||||
countTotalNotes,
|
||||
createChannelMessage,
|
||||
createChat,
|
||||
createNote,
|
||||
getChannels,
|
||||
} from "@libs/storage";
|
||||
import { NDKFilter } from "@nostr-dev-kit/ndk";
|
||||
import { LumeIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
@@ -98,12 +104,52 @@ export function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchChannelMessages() {
|
||||
try {
|
||||
const ids = [];
|
||||
const channels: any = await getChannels(10, 0);
|
||||
channels.forEach((channel) => {
|
||||
ids.push(channel.event_id);
|
||||
});
|
||||
|
||||
const since =
|
||||
lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
|
||||
|
||||
const filter: NDKFilter = {
|
||||
"#e": ids,
|
||||
kinds: [42],
|
||||
since: since,
|
||||
};
|
||||
|
||||
const events = await prefetchEvents(ndk, filter);
|
||||
events.forEach((event) => {
|
||||
const channel_id = event.tags[0][1];
|
||||
if (channel_id) {
|
||||
createChannelMessage(
|
||||
channel_id,
|
||||
event.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.content,
|
||||
event.tags,
|
||||
event.created_at,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function prefetch() {
|
||||
const notes = await fetchNotes();
|
||||
if (notes) {
|
||||
const chats = await fetchChats();
|
||||
if (chats) {
|
||||
const channels = await fetchChannelMessages();
|
||||
if (chats && channels) {
|
||||
navigate("/app/space", { overwriteLastHistoryEntry: true });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user