update channel

This commit is contained in:
Ren Amamiya
2023-05-29 22:04:50 +07:00
parent 0492729e7e
commit c8f643198e
18 changed files with 340 additions and 467 deletions

View File

@@ -4,7 +4,7 @@ import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
export default function MiniMember({ pubkey }: { pubkey: string }) {
export function Member({ pubkey }: { pubkey: string }) {
const { user, isError, isLoading } = useProfile(pubkey);
return (

View File

@@ -1,44 +1,13 @@
import MiniMember from "@app/channel/components/miniMember";
import { channelMembersAtom } from "@stores/channel";
import { useAtomValue } from "jotai";
export default function ChannelMembers() {
const membersAsSet = useAtomValue(channelMembersAtom);
const membersAsArray = [...membersAsSet];
const miniMembersList = membersAsArray.slice(0, 4);
const totalMembers =
membersAsArray.length > 0
? `+${Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
}).format(membersAsArray.length)}`
: 0;
import { Member } from "@app/channel/components/member";
import { useChannelMessages } from "@stores/channels";
import { useEffect } from "react";
export function ChannelMembers() {
return (
<div>
<div className="group flex -space-x-2 overflow-hidden hover:-space-x-1">
{miniMembersList.map((member, index) => (
<MiniMember key={`item-${index}`} pubkey={member} />
))}
{totalMembers ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 ring-2 ring-zinc-950 transition-all duration-150 ease-in-out group-hover:bg-zinc-800">
<span className="text-base font-medium text-zinc-400 group-hover:text-white">
{totalMembers}
</span>
</div>
) : (
<div>
<button
type="button"
className="inline-flex h-8 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-base text-white shadow-button"
>
Invite
</button>
</div>
)}
</div>
<div className="flex flex-wrap gap-1">
{[].map((member) => (
<Member key={member} pubkey={member} />
))}
</div>
);
}

View File

@@ -9,6 +9,7 @@ export function ChannelMessageList() {
const virtuosoRef = useRef(null);
const messages = useChannelMessages((state: any) => state.messages);
const sorted = messages;
const itemContent: any = useCallback(
(index: string | number) => {
@@ -33,10 +34,7 @@ export function ChannelMessageList() {
components={{
Header: () => (
<div className="relative py-4">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-800" />
</div>
<div className="relative flex justify-center">

View File

@@ -3,13 +3,11 @@ import CancelIcon from "@icons/cancel";
import { ImagePicker } from "@shared/form/imagePicker";
import { RelayContext } from "@shared/relayProvider";
import { useActiveAccount } from "@stores/accounts";
import { channelContentAtom, channelReplyAtom } from "@stores/channel";
import { useChannelMessages } from "@stores/channels";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useAtom, useAtomValue } from "jotai";
import { useResetAtom } from "jotai/utils";
import { getEventHash, getSignature } from "nostr-tools";
import { useContext } from "react";
import { useContext, useState } from "react";
export default function ChannelMessageForm({
channelID,
@@ -17,20 +15,20 @@ export default function ChannelMessageForm({
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const [value, setValue] = useAtom(channelContentAtom);
const resetValue = useResetAtom(channelContentAtom);
const channelReply = useAtomValue(channelReplyAtom);
const resetChannelReply = useResetAtom(channelReplyAtom);
const [value, setValue] = useState("");
const [replyTo, closeReply] = useChannelMessages((state: any) => [
state.replyTo,
state.closeReply,
]);
const submitEvent = () => {
let tags: any[][];
if (channelReply.id !== null) {
if (replyTo.id !== null) {
tags = [
["e", channelID, "", "root"],
["e", channelReply.id, "", "reply"],
["p", channelReply.pubkey, ""],
["e", replyTo.id, "", "reply"],
["p", replyTo.pubkey, ""],
];
} else {
tags = [["e", channelID, "", "root"]];
@@ -50,11 +48,6 @@ export default function ChannelMessageForm({
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// reset state
resetValue();
// reset channel reply
resetChannelReply();
} else {
console.log("error");
}
@@ -68,24 +61,22 @@ export default function ChannelMessageForm({
};
const stopReply = () => {
resetChannelReply();
closeReply();
};
return (
<div
className={`relative ${
channelReply.id ? "h-36" : "h-24"
replyTo.id ? "h-36" : "h-24"
} w-full 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`}
>
{channelReply.id && (
{replyTo.id && (
<div className="absolute left-0 top-0 z-10 h-14 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">
<div className="flex w-full flex-col">
<UserReply pubkey={channelReply.pubkey} />
<UserReply pubkey={replyTo.pubkey} />
<div className="-mt-3.5 pl-[32px]">
<div className="text-base text-white">
{channelReply.content}
</div>
<div className="text-base text-white">{replyTo.content}</div>
</div>
</div>
<button
@@ -105,13 +96,12 @@ export default function ChannelMessageForm({
spellCheck={false}
placeholder="Message"
className={`relative ${
channelReply.id ? "h-36 pt-16" : "h-24 pt-3"
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`}
/>
<div className="absolute bottom-2 w-full px-2">
<div className="flex w-full items-center justify-between bg-zinc-800">
<div className="flex items-center gap-2 divide-x divide-zinc-700">
<ImagePicker type="channel" />
<div className="flex items-center gap-2 pl-2" />
</div>
<div className="flex items-center gap-2">

View File

@@ -4,19 +4,18 @@ import HideIcon from "@icons/hide";
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip";
import { useActiveAccount } from "@stores/accounts";
import { channelMessagesAtom } from "@stores/channel";
import { useChannelMessages } from "@stores/channels";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useAtom } from "jotai";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react";
export default function MessageHideButton({ id }: { id: string }) {
export function MessageHideButton({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const hide = useChannelMessages((state: any) => state.hideMessage);
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useAtom(channelMessagesAtom);
const closeModal = () => {
setIsOpen(false);
@@ -27,34 +26,25 @@ export default function MessageHideButton({ id }: { id: string }) {
};
const hideMessage = () => {
if (account) {
const event: any = {
content: "",
created_at: dateToUnix(),
kind: 43,
pubkey: account.pubkey,
tags: [["e", id]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event: any = {
content: "",
created_at: dateToUnix(),
kind: 43,
pubkey: account.pubkey,
tags: [["e", id]],
};
// publish note
pool.publish(event, WRITEONLY_RELAYS);
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// update local state
const cloneMessages = [...messages];
const targetMessage = cloneMessages.findIndex(
(message) => message.id === id,
);
// publish note
pool.publish(event, WRITEONLY_RELAYS);
cloneMessages[targetMessage]["hide"] = true;
setMessages(cloneMessages);
// update state
hide(id);
// close modal
closeModal();
} else {
console.log("error");
}
// close modal
closeModal();
};
return (

View File

@@ -1,25 +1,61 @@
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 { 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 { MentionNote } from "@app/note/components/mentions/note";
import ImagePreview from "@app/note/components/preview/image";
import VideoPreview from "@app/note/components/preview/video";
import { noteParser } from "@utils/parser";
import { useMemo } from "react";
import { useMemo, useState } from "react";
export function ChannelMessageItem({ data }: { data: any }) {
const content = useMemo(() => noteParser(data), [data]);
const [hide, setHide] = useState(data.hide);
const toggleHide = () => {
setHide((prev) => !prev);
};
if (data.mute) return null;
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]">
<div className="whitespace-pre-line break-words text-base leading-tight">
{data.hide ? (
<span className="italic text-zinc-400">[hided message]</span>
) : (
content.parsed
)}
</div>
{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">
{content.parsed}
</p>
{Array.isArray(content.images) && content.images.length ? (
<ImagePreview urls={content.images} />
) : (
<></>
)}
{Array.isArray(content.videos) && content.videos.length ? (
<VideoPreview urls={content.videos} />
) : (
<></>
)}
{Array.isArray(content.notes) && content.notes.length ? (
content.notes.map((note: string) => (
<MentionNote key={note} id={note} />
))
) : (
<></>
)}
</>
)}
</div>
</div>
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">

View File

@@ -4,18 +4,17 @@ import MuteIcon from "@icons/mute";
import { RelayContext } from "@shared/relayProvider";
import { Tooltip } from "@shared/tooltip";
import { useActiveAccount } from "@stores/accounts";
import { channelMessagesAtom } from "@stores/channel";
import { useChannelMessages } from "@stores/channels";
import { WRITEONLY_RELAYS } from "@stores/constants";
import { dateToUnix } from "@utils/date";
import { useAtom } from "jotai";
import { getEventHash, getSignature } from "nostr-tools";
import { Fragment, useContext, useState } from "react";
export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
const pool: any = useContext(RelayContext);
const account = useActiveAccount((state: any) => state.account);
const mute = useChannelMessages((state: any) => state.muteUser);
const [messages, setMessages] = useAtom(channelMessagesAtom);
const [isOpen, setIsOpen] = useState(false);
const closeModal = () => {
@@ -27,31 +26,25 @@ export default function MessageMuteButton({ pubkey }: { pubkey: string }) {
};
const muteUser = () => {
if (account) {
const event: any = {
content: "",
created_at: dateToUnix(),
kind: 44,
pubkey: account.pubkey,
tags: [["p", pubkey]],
};
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
const event: any = {
content: "",
created_at: dateToUnix(),
kind: 44,
pubkey: account.pubkey,
tags: [["p", pubkey]],
};
// publish note
pool.publish(event, WRITEONLY_RELAYS);
event.id = getEventHash(event);
event.sig = getSignature(event, account.privkey);
// update local state
const cloneMessages = [...messages];
const finalMessages = cloneMessages.filter(
(message) => message.pubkey !== pubkey,
);
setMessages(finalMessages);
// close modal
closeModal();
} else {
console.log("error");
}
// publish note
pool.publish(event, WRITEONLY_RELAYS);
// update state
mute(pubkey);
// close modal
closeModal();
};
return (

View File

@@ -1,20 +1,16 @@
import { Tooltip } from "@shared/tooltip";
import ReplyMessageIcon from "@icons/replyMessage";
import { Tooltip } from "@shared/tooltip";
import { useChannelMessages } from "@stores/channels";
import { channelReplyAtom } from "@stores/channel";
import { useSetAtom } from "jotai";
export default function MessageReplyButton({
export function MessageReplyButton({
id,
pubkey,
content,
}: { id: string; pubkey: string; content: string }) {
const setChannelReplyAtom = useSetAtom(channelReplyAtom);
const openReply = useChannelMessages((state: any) => state.openReply);
const createReply = () => {
setChannelReplyAtom({ id: id, pubkey: pubkey, content: content });
openReply(id, pubkey, content);
};
return (

View File

@@ -10,11 +10,14 @@ dayjs.extend(relativeTime);
export function ChannelMessageUser({
pubkey,
time,
}: { pubkey: string; time: number }) {
}: {
pubkey: string;
time: number;
}) {
const { user, isError, isLoading } = useProfile(pubkey);
return (
<div className="group flex items-start gap-3">
<div className="flex items-start gap-3">
{isError || isLoading ? (
<>
<div className="relative h-11 w-11 shrink animate-pulse rounded-md bg-zinc-800" />
@@ -35,7 +38,7 @@ export function ChannelMessageUser({
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex items-baseline gap-2 text-base">
<span className="font-semibold leading-none text-white group-hover:underline">
<span className="font-semibold leading-none text-white">
{user?.nip05 || user?.name || shortenKey(pubkey)}
</span>
<span className="leading-none text-zinc-500">·</span>

View File

@@ -8,7 +8,7 @@ import { DEFAULT_AVATAR } from "@stores/constants";
import { nip19 } from "nostr-tools";
export default function ChannelMetadata({
export function ChannelMetadata({
id,
pubkey,
}: { id: string; pubkey: string }) {
@@ -23,24 +23,24 @@ export default function ChannelMetadata({
};
return (
<div className="inline-flex items-center gap-2">
<div className="relative shrink-0 rounded-md">
<div className="flex flex-col gap-2">
<div className="relative shrink-0 rounded-md h-11 w-11">
<Image
src={metadata?.picture || DEFAULT_AVATAR}
alt={id}
className="h-8 w-8 rounded bg-zinc-900 object-contain ring-2 ring-zinc-950"
className="h-11 w-11 rounded-md object-contain bg-zinc-900"
/>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<h5 className="truncate text-base font-medium leading-none text-white">
<div className="flex flex-col gap-2">
<div className="inline-flex items-center gap-1">
<h5 className="leading-none text-lg font-semibold">
{metadata?.name}
</h5>
<button type="button" onClick={() => copyNoteID()}>
<CopyIcon width={14} height={14} className="text-zinc-400" />
</button>
</div>
<p className="text-base leading-none text-zinc-400">
<p className="leading-tight text-zinc-400">
{metadata?.about || (noteID && `${noteID.substring(0, 24)}...`)}
</p>
</div>