update channel
This commit is contained in:
@@ -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 (
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user