update channel

This commit is contained in:
Ren Amamiya 2023-06-16 18:41:44 +07:00
parent f8de44fe9f
commit 0a6865431d
28 changed files with 240 additions and 489 deletions

View File

@ -80,15 +80,13 @@ export function ChannelCreateModal() {
<button <button
type="button" type="button"
onClick={() => openModal()} onClick={() => openModal()}
className="group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900" className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
> >
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800"> <div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
<PlusIcon width={12} height={12} className="text-zinc-500" /> <PlusIcon width={12} height={12} className="text-zinc-500" />
</div> </div>
<div> <div>
<h5 className="font-semibold text-zinc-400 group-hover:text-zinc-200"> <h5 className="font-medium text-zinc-400">Add a new channel</h5>
Add a new channel
</h5>
</div> </div>
</button> </button>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>

View File

@ -3,7 +3,7 @@ import { usePageContext } from "@utils/hooks/usePageContext";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export function ChannelsListItem({ data }: { data: any }) { export function ChannelsListItem({ data }: { data: any }) {
const channel: any = useChannelProfile(data.event_id, data.pubkey); const channel: any = useChannelProfile(data.event_id);
const pageContext = usePageContext(); const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search; const searchParams: any = pageContext.urlParsed.search;
@ -11,20 +11,16 @@ export function ChannelsListItem({ data }: { data: any }) {
return ( return (
<a <a
href={`/app/channel?id=${data.event_id}&channelpub=${data.pubkey}`} href={`/app/channel?id=${data.event_id}`}
className={twMerge( className={twMerge(
"group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900", "inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
pageID === data.event_id pageID === data.event_id ? "bg-zinc-900 text-white" : "",
? "dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
: "",
)} )}
> >
<div <div
className={twMerge( className={twMerge(
"inline-flex shrink-0 h-5 w-5 items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800", "inline-flex shrink-0 h-5 w-5 items-center justify-center rounded bg-zinc-900",
pageID === data.event_id pageID === data.event_id ? "bg-zinc-800" : "",
? "dark:bg-zinc-800 group-hover:dark:bg-zinc-700"
: "",
)} )}
> >
<span className="text-xs text-zinc-100">#</span> <span className="text-xs text-zinc-100">#</span>

View File

@ -12,14 +12,14 @@ export function ChannelsList() {
}, [fetchChannels]); }, [fetchChannels]);
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-0.5">
{!channels ? ( {!channels ? (
<> <>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
</div> </div>

View File

@ -28,7 +28,7 @@ export function ChannelMessageList() {
<div className="h-full w-full"> <div className="h-full w-full">
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
data={messages} data={[]}
itemContent={itemContent} itemContent={itemContent}
components={{ components={{
Header: () => ( Header: () => (

View File

@ -2,44 +2,21 @@ import { MessageHideButton } from "@app/channel/components/messages/hideButton";
import { MessageMuteButton } from "@app/channel/components/messages/muteButton"; import { MessageMuteButton } from "@app/channel/components/messages/muteButton";
import { MessageReplyButton } from "@app/channel/components/messages/replyButton"; import { MessageReplyButton } from "@app/channel/components/messages/replyButton";
import { ChannelMessageUser } from "@app/channel/components/messages/user"; import { ChannelMessageUser } from "@app/channel/components/messages/user";
import { ChannelMessageUserMute } from "@app/channel/components/messages/userMute"; import { NDKEvent } from "@nostr-dev-kit/ndk";
import { MentionNote } from "@shared/notes/mentions/note"; import { MentionNote } from "@shared/notes/mentions/note";
import { ImagePreview } from "@shared/notes/preview/image"; import { ImagePreview } from "@shared/notes/preview/image";
import { VideoPreview } from "@shared/notes/preview/video"; import { VideoPreview } from "@shared/notes/preview/video";
import { parser } from "@utils/parser"; import { parser } from "@utils/parser";
import { useMemo, useState } from "react"; import { useMemo } from "react";
export function ChannelMessageItem({ data }: { data: any }) { export function ChannelMessageItem({ data }: { data: NDKEvent }) {
const content = useMemo(() => parser(data), [data]); const content = useMemo(() => parser(data), [data]);
const [hide, setHide] = useState(data.hide);
const toggleHide = () => {
setHide((prev) => !prev);
};
if (data.mute)
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">
<ChannelMessageUserMute pubkey={data.pubkey} />
</div>
);
return ( 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="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"> <div className="flex flex-col">
<ChannelMessageUser pubkey={data.pubkey} time={data.created_at} /> <ChannelMessageUser pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[20px] pl-[49px]"> <div className="-mt-[20px] pl-[49px]">
{hide ? (
<>
<p className="leading-tight italic text-zinc-400">
[hided message]
</p>
<button type="button" onClick={() => toggleHide()}>
show
</button>
</>
) : (
<>
<p className="whitespace-pre-line break-words text-base leading-tight"> <p className="whitespace-pre-line break-words text-base leading-tight">
{content.parsed} {content.parsed}
</p> </p>
@ -60,8 +37,6 @@ export function ChannelMessageItem({ data }: { data: any }) {
) : ( ) : (
<></> <></>
)} )}
</>
)}
</div> </div>
</div> </div>
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex"> <div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">

View File

@ -4,11 +4,8 @@ import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
export function ChannelMetadata({ export function ChannelMetadata({ id }: { id: string }) {
id, const metadata = useChannelProfile(id);
pubkey,
}: { id: string; pubkey: string }) {
const metadata = useChannelProfile(id, pubkey);
const noteID = id ? nip19.noteEncode(id) : null; const noteID = id ? nip19.noteEncode(id) : null;
const copyNoteID = async () => { const copyNoteID = async () => {
@ -22,7 +19,7 @@ export function ChannelMetadata({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="relative shrink-0 rounded-md h-11 w-11"> <div className="relative shrink-0 rounded-md h-11 w-11">
<Image <Image
src={metadata?.image || DEFAULT_AVATAR} src={metadata?.picture || DEFAULT_AVATAR}
alt={id} alt={id}
className="h-11 w-11 rounded-md object-contain bg-zinc-900" className="h-11 w-11 rounded-md object-contain bg-zinc-900"
/> />

View File

@ -1,267 +0,0 @@
import { Dialog, Transition } from "@headlessui/react";
import { getChannel } from "@libs/storage";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { AvatarUploader } from "@shared/avatarUploader";
import { CancelIcon, EditIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { DEFAULT_AVATAR } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { Fragment, useContext, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
export function ChannelUpdateModal({ id }: { id: string }) {
const ndk = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(DEFAULT_AVATAR);
const [loading, setLoading] = useState(false);
const closeModal = () => {
setIsOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
const {
register,
handleSubmit,
reset,
setValue,
formState: { isDirty, isValid },
} = useForm({
defaultValues: async () => {
const channel = await getChannel(id);
const metadata = JSON.parse(channel.metadata);
// update image state
setImage(metadata.image);
// set default values
return metadata;
},
});
const onSubmit = (data: any) => {
setLoading(true);
try {
const signer = new NDKPrivateKeySigner(account.privkey);
ndk.signer = signer;
const event = new NDKEvent(ndk);
// build event
event.content = JSON.stringify(data);
event.kind = 41;
event.created_at = dateToUnix();
event.pubkey = account.pubkey;
event.tags = [["e", id]];
// publish event
event.publish();
// reset form
reset();
// close modal
setIsOpen(false);
} catch (e) {
console.log("error: ", e);
}
};
useEffect(() => {
setValue("picture", image);
}, [setValue, image]);
return (
<>
<button
type="button"
onClick={() => openModal()}
className="group inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800 focus:outline-none"
>
<EditIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-white"
/>
</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" />
</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">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-white"
>
Update channel
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
New metadata will be published on all relays, and will be
immediately available to all users, so please carefully.
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4"
>
<input
type={"hidden"}
{...register("picture")}
value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 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"
/>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Picture
</label>
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image
src={image}
alt="channel picture"
className="relative z-10 h-11 w-11 rounded-md"
/>
<div className="absolute bottom-3 right-3 z-10">
<AvatarUploader valueState={setImage} />
</div>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Channel name *
</label>
<div className="relative 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">
<input
type={"text"}
{...register("name", {
required: true,
minLength: 4,
})}
spellCheck={false}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 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"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Description
</label>
<div className="relative h-20 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">
<textarea
{...register("about")}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 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"
/>
</div>
</div>
<div className="flex h-14 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
<div className="flex flex-col gap-0.5">
<div className="inline-flex items-center gap-1">
<span className="text-base font-bold leading-none text-white">
Make Private
</span>
<div className="inline-flex items-center rounded-md bg-zinc-400/10 px-2 py-0.5 text-base font-medium ring-1 ring-inset ring-zinc-400/20">
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
Coming soon
</span>
</div>
</div>
<p className="text-base leading-none text-zinc-400">
Private channels can only be viewed by member
</p>
</div>
<div>
<button
type="button"
disabled
className="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent bg-zinc-900 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2"
role="switch"
aria-checked="false"
>
<span className="pointer-events-none inline-block h-5 w-5 translate-x-0 transform rounded-full bg-zinc-600 shadow ring-0 transition duration-200 ease-in-out" />
</button>
</div>
</div>
<div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<svg
className="h-4 w-4 animate-spin text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
) : (
"Update channel"
)}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
}

View File

@ -1,7 +1,7 @@
import { getChannel, updateChannelMetadata } from "@libs/storage"; import { getChannel, updateChannelMetadata } from "@libs/storage";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useContext } from "react"; import { useContext } from "react";
import useSWR, { useSWRConfig } from "swr"; import useSWR from "swr";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
const fetcher = async ([, id]) => { const fetcher = async ([, id]) => {
@ -13,20 +13,15 @@ const fetcher = async ([, id]) => {
} }
}; };
export function useChannelProfile(id: string, channelPubkey: string) { export function useChannelProfile(id: string) {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
const { data, mutate } = useSWR(["channel-metadata", id], fetcher);
const { mutate } = useSWRConfig(); useSWRSubscription(data ? ["channel-metadata", id] : null, () => {
const { data, isLoading } = useSWR(["channel-metadata", id], fetcher);
useSWRSubscription(
!isLoading && data ? ["channel-metadata", id] : null,
([, key]) => {
// subscribe to channel // subscribe to channel
const sub = ndk.subscribe( const sub = ndk.subscribe(
{ {
"#e": [key], "#e": [id],
authors: [channelPubkey],
kinds: [41], kinds: [41],
}, },
{ {
@ -36,16 +31,15 @@ export function useChannelProfile(id: string, channelPubkey: string) {
sub.addListener("event", (event: { content: string }) => { sub.addListener("event", (event: { content: string }) => {
// update in local database // update in local database
updateChannelMetadata(key, event.content); updateChannelMetadata(id, event.content);
// revaildate // revaildate
mutate(["channel-metadata", key]); mutate();
}); });
return () => { return () => {
sub.stop(); sub.stop();
}; };
}, });
);
return data; return data;
} }

View File

@ -1,98 +1,98 @@
import { ChannelBlackList } from "@app/channel/components/blacklist"; import { ChannelMessageItem } from "../components/messages/item";
import { ChannelMembers } from "@app/channel/components/members"; import { ChannelMembers } from "@app/channel/components/members";
import { ChannelMessageList } from "@app/channel/components/messageList";
import { ChannelMessageForm } from "@app/channel/components/messages/form"; import { ChannelMessageForm } from "@app/channel/components/messages/form";
import { ChannelMetadata } from "@app/channel/components/metadata"; import { ChannelMetadata } from "@app/channel/components/metadata";
import { ChannelUpdateModal } from "@app/channel/components/updateModal";
import { getActiveBlacklist, getBlacklist } from "@libs/storage";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { useChannelMessages } from "@stores/channels"; import { useChannelMessages } from "@stores/channels";
import { useVirtualizer } from "@tanstack/react-virtual";
import { dateToUnix, getHourAgo } from "@utils/date"; import { dateToUnix, getHourAgo } from "@utils/date";
import { usePageContext } from "@utils/hooks/usePageContext"; import { usePageContext } from "@utils/hooks/usePageContext";
import { arrayObjToPureArr } from "@utils/transform"; import { useCallback, useContext, useEffect, useRef } from "react";
import { useContext, useRef } from "react";
import useSWR from "swr";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
const fetchMuted = async ([, id]) => { const now = new Date();
const res = await getBlacklist(id, 44); const since = dateToUnix(getHourAgo(24, now));
const array = arrayObjToPureArr(res);
return { original: res, array: array };
};
const fetchHided = async ([, id]) => {
const res = await getActiveBlacklist(id, 43);
const array = arrayObjToPureArr(res);
return array;
};
export function Page() { export function Page() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
const pageContext = usePageContext(); const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search; const searchParams: any = pageContext.urlParsed.search;
const channelID = searchParams.id; const channelID = searchParams.id;
const channelPubkey = searchParams.channelpub;
const account: any = useActiveAccount((state: any) => state.account); const [messages, addMessage, fetchMessages, clearMessages]: any =
const [addMessage, clear] = useChannelMessages((state: any) => [ useChannelMessages((state: any) => [
state.add, state.messages,
state.addMessage,
state.fetch,
state.clear, state.clear,
]); ]);
const { data: muted } = useSWR( useSWRSubscription(["channelMessagesSubscribe", channelID], () => {
account ? ["muted", account.id] : null,
fetchMuted,
);
const { data: hided } = useSWR(
account ? ["hided", account.id] : null,
fetchHided,
);
const now = useRef(new Date());
useSWRSubscription(
account && channelID && muted && hided ? ["channel", channelID] : null,
() => {
// subscribe to channel // subscribe to channel
const sub = ndk.subscribe({ const sub = ndk.subscribe({
"#e": [channelID], "#e": [channelID],
kinds: [42], kinds: [42],
since: dateToUnix(getHourAgo(24, now.current)), since: dateToUnix(),
limit: 20,
}); });
sub.addListener("event", (event: NDKEvent) => { sub.addListener("event", (event) => {
const message: NDKEvent = event; addMessage(event);
// handle hide message
if (hided.includes(event.id)) {
message["hide"] = true;
} else {
message["hide"] = false;
}
// handle mute user
if (muted.array.includes(event.pubkey)) {
message["mute"] = true;
} else {
message["mute"] = false;
}
// add to store
addMessage(message);
}); });
return () => { return () => {
sub.stop(); sub.stop();
clear();
}; };
}, });
);
if (!account) return <div>Fuck SSR</div>; useEffect(() => {
fetchMessages(ndk, channelID, since);
return () => {
clearMessages();
};
}, [fetchMessages]);
const count = messages.length;
const reverseIndex = useCallback((index) => count - 1 - index, [count]);
const parentRef = useRef();
const virtualizerRef = useRef(null);
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];
return ( return (
<div className="h-full w-full grid grid-cols-3"> <div className="h-full w-full grid grid-cols-3">
@ -104,9 +104,41 @@ export function Page() {
<h3 className="font-semibold text-zinc-100">Public Channel</h3> <h3 className="font-semibold text-zinc-100">Public Channel</h3>
</div> </div>
<div className="w-full flex-1 p-3"> <div className="w-full flex-1 p-3">
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900 shadow-input shadow-black/20"> <div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
<ChannelMessageList /> <div
<div className="inline-flex shrink-0 p-3"> 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">
<ChannelMessageForm channelID={channelID} /> <ChannelMessageForm channelID={channelID} />
</div> </div>
</div> </div>
@ -118,12 +150,8 @@ export function Page() {
className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900" className="h-11 w-full shrink-0 inline-flex items-center justify-center border-b border-zinc-900"
/> />
<div className="p-3 flex flex-col gap-3"> <div className="p-3 flex flex-col gap-3">
<ChannelMetadata id={channelID} pubkey={channelPubkey} /> <ChannelMetadata id={channelID} />
<ChannelMembers /> <ChannelMembers />
{muted && <ChannelBlackList blacklist={muted.original} />}
{account && account.pubkey === channelPubkey && (
<ChannelUpdateModal id={channelID} />
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -16,7 +16,7 @@ export function ChatsListItem({ data }: { data: any }) {
<> <>
{isError && <div>error</div>} {isError && <div>error</div>}
{isLoading && !user ? ( {isLoading && !user ? (
<div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div> <div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" /> <div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
@ -26,10 +26,8 @@ export function ChatsListItem({ data }: { data: any }) {
<a <a
href={`/app/chat?pubkey=${data.sender_pubkey}`} href={`/app/chat?pubkey=${data.sender_pubkey}`}
className={twMerge( className={twMerge(
"group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900", "inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
pagePubkey === data.sender_pubkey pagePubkey === data.sender_pubkey ? "bg-zinc-900 text-white" : "",
? "dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
: "",
)} )}
> >
<div className="relative h-5 w-5 shrink-0 rounded"> <div className="relative h-5 w-5 shrink-0 rounded">
@ -41,7 +39,7 @@ export function ChatsListItem({ data }: { data: any }) {
</div> </div>
<div className="w-full inline-flex items-center justify-between"> <div className="w-full inline-flex items-center justify-between">
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200 group-hover:text-white"> <h5 className="max-w-[9rem] truncate font-medium text-zinc-200">
{user?.nip05 || {user?.nip05 ||
user?.displayName || user?.displayName ||
shortenKey(data.sender_pubkey)} shortenKey(data.sender_pubkey)}

View File

@ -16,12 +16,12 @@ export function ChatsList() {
if (!account) if (!account)
return ( return (
<div className="flex flex-col"> <div className="flex flex-col gap-0.5">
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
</div> </div>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
</div> </div>
@ -29,15 +29,15 @@ export function ChatsList() {
); );
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-0.5">
<ChatsListSelfItem data={account} /> <ChatsListSelfItem data={account} />
{!chats ? ( {!chats ? (
<> <>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
</div> </div>
<div className="inline-flex h-8 items-center gap-2 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
</div> </div>

View File

@ -10,13 +10,12 @@ export function ChatsListSelfItem({ data }: { data: any }) {
const searchParams: any = pageContext.urlParsed.search; const searchParams: any = pageContext.urlParsed.search;
const pagePubkey = searchParams.pubkey; const pagePubkey = searchParams.pubkey;
const { user, isError, isLoading } = useProfile(data.pubkey); const { user, isLoading } = useProfile(data.pubkey);
return ( return (
<> <>
{isError && <div>error</div>}
{isLoading && !user ? ( {isLoading && !user ? (
<div className="inline-flex h-8 items-center gap-2.5 rounded-md px-2.5"> <div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" /> <div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div> <div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" /> <div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
@ -26,10 +25,8 @@ export function ChatsListSelfItem({ data }: { data: any }) {
<a <a
href={`/app/chat?pubkey=${data.pubkey}`} href={`/app/chat?pubkey=${data.pubkey}`}
className={twMerge( className={twMerge(
"group inline-flex h-8 items-center gap-2.5 rounded-md px-2.5 hover:bg-zinc-900", "inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
pagePubkey === data.pubkey pagePubkey === data.pubkey ? "bg-zinc-900 text-white" : "",
? "dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
: "",
)} )}
> >
<div className="relative h-5 w-5 shrink-0 rounded"> <div className="relative h-5 w-5 shrink-0 rounded">
@ -40,7 +37,7 @@ export function ChatsListSelfItem({ data }: { data: any }) {
/> />
</div> </div>
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200 group-hover:text-white"> <h5 className="max-w-[9rem] truncate font-medium text-zinc-200">
{user?.nip05 || user?.name || shortenKey(data.pubkey)} {user?.nip05 || user?.name || shortenKey(data.pubkey)}
</h5> </h5>
<span className="text-zinc-500">(you)</span> <span className="text-zinc-500">(you)</span>

View File

@ -61,7 +61,7 @@ export function Page() {
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3> <h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
</div> </div>
<div className="w-full flex-1 p-3"> <div className="w-full flex-1 p-3">
<div className="flex h-full flex-col justify-between rounded-md bg-zinc-900 shadow-input shadow-black/20"> <div className="flex h-full flex-col justify-between rounded-md bg-zinc-900">
<ChatMessageList /> <ChatMessageList />
<div className="shrink-0 px-5 p-3 border-t border-zinc-800"> <div className="shrink-0 px-5 p-3 border-t border-zinc-800">
<ChatMessageForm <ChatMessageForm

View File

@ -18,7 +18,7 @@ export function FeedBlock({ params }: { params: any }) {
const removeBlock = useActiveAccount((state: any) => state.removeBlock); const removeBlock = useActiveAccount((state: any) => state.removeBlock);
const close = () => { const close = () => {
removeBlock(params.id); removeBlock(params.id, true);
}; };
const getKey = (pageIndex, previousPageData) => { const getKey = (pageIndex, previousPageData) => {

View File

@ -6,7 +6,7 @@ export function ImageBlock({ params }: { params: any }) {
const removeBlock = useActiveAccount((state: any) => state.removeBlock); const removeBlock = useActiveAccount((state: any) => state.removeBlock);
const close = () => { const close = () => {
removeBlock(params.id); removeBlock(params.id, true);
}; };
return ( return (

View File

@ -0,0 +1 @@
export { LayoutUser as Layout } from "./layout";

14
src/app/user/layout.tsx Normal file
View File

@ -0,0 +1,14 @@
import { MultiAccounts } from "@shared/multiAccounts";
import { Navigation } from "@shared/navigation";
export function LayoutUser({ children }: { children: React.ReactNode }) {
return (
<div className="flex w-screen h-screen">
<div className="relative flex flex-row shrink-0">
<MultiAccounts />
<Navigation />
</div>
<div className="w-full h-full">{children}</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
import { usePageContext } from "@utils/hooks/usePageContext";
export function Page() {
const pageContext = usePageContext();
const searchParams: any = pageContext.urlParsed.search;
const pubkey = searchParams.pubkey;
return (
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
<p>{pubkey}</p>
</div>
);
}

View File

@ -299,7 +299,7 @@ export async function updateChannelMetadata(event_id: string, value: string) {
return await db.execute( return await db.execute(
"UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;", "UPDATE channels SET name = ?, picture = ?, about = ? WHERE event_id = ?;",
[data.name, data.image, data.about, event_id], [data.name, data.picture, data.about, event_id],
); );
} }

View File

@ -29,8 +29,8 @@ export function Navigation() {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<ActiveLink <ActiveLink
href="/app/space" href="/app/space"
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-zinc-200 hover:text-white" className="flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200"
activeClassName="bg-zinc-900/50 hover:bg-zinc-900" activeClassName="bg-zinc-900/50"
> >
<span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900"> <span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
<SpaceIcon width={12} height={12} className="text-white" /> <SpaceIcon width={12} height={12} className="text-white" />
@ -39,8 +39,8 @@ export function Navigation() {
</ActiveLink> </ActiveLink>
<ActiveLink <ActiveLink
href="/app/trending" href="/app/trending"
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-zinc-200 hover:text-white" className="flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200"
activeClassName="bg-zinc-900/50 hover:bg-zinc-900" activeClassName="bg-zinc-900/50"
> >
<span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900"> <span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
<TrendingIcon width={12} height={12} className="text-white" /> <TrendingIcon width={12} height={12} className="text-white" />

View File

@ -11,7 +11,7 @@ import { useMemo } from "react";
export function NoteBase({ export function NoteBase({
event, event,
block, block,
metadata, metadata = true,
}: { event: LumeEvent; block?: number; metadata?: boolean }) { }: { event: LumeEvent; block?: number; metadata?: boolean }) {
const content = useMemo(() => parser(event), [event]); const content = useMemo(() => parser(event), [event]);
const checkParentID = isTagsIncludeID(event.parent_id, event.tags); const checkParentID = isTagsIncludeID(event.parent_id, event.tags);

View File

@ -22,7 +22,7 @@ export function NoteReply({
<button <button
type="button" type="button"
onClick={(e) => openThread(e, id)} onClick={(e) => openThread(e, id)}
className="w-14 group inline-flex items-center gap-1.5" className="w-20 group inline-flex items-center gap-1.5"
> >
<ReplyIcon <ReplyIcon
width={16} width={16}

View File

@ -44,7 +44,7 @@ export function NoteRepost({
<button <button
type="button" type="button"
onClick={(e) => submitEvent(e)} onClick={(e) => submitEvent(e)}
className="w-14 group inline-flex items-center gap-1.5" className="w-20 group inline-flex items-center gap-1.5"
> >
<RepostIcon <RepostIcon
width={16} width={16}

View File

@ -5,7 +5,7 @@ export function NoteZap({ zaps }: { zaps: number }) {
return ( return (
<button <button
type="button" type="button"
className="w-14 group inline-flex items-center gap-1.5" className="w-20 group inline-flex items-center gap-1.5"
> >
<ZapIcon <ZapIcon
width={16} width={16}

View File

@ -10,7 +10,7 @@ export function Reply({ data }: { data: any }) {
<div className="flex h-min min-h-min w-full select-text flex-col px-3 pt-5 mb-3 rounded-md bg-zinc-900"> <div className="flex h-min min-h-min w-full select-text flex-col px-3 pt-5 mb-3 rounded-md bg-zinc-900">
<div className="flex flex-col"> <div className="flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} /> <User pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-[20px] pl-[47px]"> <div className="-mt-[20px] pl-[50px]">
<Kind1 content={content} /> <Kind1 content={content} />
<NoteMetadata id={data.id} eventPubkey={data.pubkey} /> <NoteMetadata id={data.id} eventPubkey={data.pubkey} />
</div> </div>

View File

@ -56,11 +56,7 @@ export function User({
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl"> <Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl">
<div <div className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5">
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5"
>
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3"> <div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
<Image <Image
src={user?.image || DEFAULT_AVATAR} src={user?.image || DEFAULT_AVATAR}
@ -90,7 +86,7 @@ export function User({
href={`/app/user?pubkey=${pubkey}`} href={`/app/user?pubkey=${pubkey}`}
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium" className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
> >
View full profile View profile
</a> </a>
<a <a
href={`/app/chat?pubkey=${pubkey}`} href={`/app/chat?pubkey=${pubkey}`}

View File

@ -1,4 +1,5 @@
import { getChannels } from "@libs/storage"; import { getChannels } from "@libs/storage";
import NDK, { NDKFilter } from "@nostr-dev-kit/ndk";
import { create } from "zustand"; import { create } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
@ -37,8 +38,20 @@ export const useChannelMessages = create(
immer((set) => ({ immer((set) => ({
messages: [], messages: [],
replyTo: { id: null, pubkey: null, content: null }, replyTo: { id: null, pubkey: null, content: null },
fetch: async (ndk: NDK, id: string, since: number) => {
const filter: NDKFilter = {
"#e": [id],
kinds: [42],
since: since,
};
const events = await ndk.fetchEvents(filter);
const array = [...events];
set({ messages: array });
},
add: (message: any) => { add: (message: any) => {
set((state: any) => ({ messages: [message, ...state.messages] })); set((state: any) => {
state.messages.push(message);
});
}, },
openReply: (id: string, pubkey: string, content: string) => { openReply: (id: string, pubkey: string, content: string) => {
set(() => ({ replyTo: { id, pubkey, content } })); set(() => ({ replyTo: { id, pubkey, content } }));

View File

@ -58,9 +58,7 @@ export function parser(event: any) {
const event = item.event; const event = item.event;
if (event) { if (event) {
content.notes.push(event.id); content.notes.push(event.id);
content.parsed = reactStringReplace(content.parsed, item.text, () => ( content.parsed = content.parsed.replace(item.text, "");
<></>
));
} }
if (profile) { if (profile) {
content.parsed = reactStringReplace( content.parsed = reactStringReplace(