Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions components/CommentsDrawer/CommentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Avatar, Group } from "@mantine/core";
import { useForm } from "@mantine/form";
import DraftEditor from "components/DraftEditor/DraftEditor";
import {
DraftEditorProvider,
draftEditorDecorator,
} from "components/DraftEditor/DraftEditorProvider";
import MentionMenu from "components/DraftEditor/MentionMenu";
import { EditorState } from "draft-js";
import useAuth from "hooks/useAuth";
import { useComments } from "ndk/NDKCommentsProvider";
import { useNDK } from "ndk/NDKProvider";
import useProfilePicSrc from "ndk/hooks/useProfilePicSrc";
import { useUser } from "ndk/hooks/useUser";
import { createKind1Event, getFormattedAtName } from "ndk/utils";
import { useState } from "react";
import { noop } from "utils/common";

type CommentFormValues = {
comment: string;
};

export default function CommentForm() {
const { ndk, stemstrRelaySet } = useNDK();
const { replyingTo, setReplyingTo, rootEvent, setHighlightedEvent } =
useComments();
const [editorState, setEditorState] = useState(() =>
EditorState.createEmpty(draftEditorDecorator)
);
const isShowingName = replyingTo?.id !== rootEvent?.id;
const replyingToUser = useUser(
isShowingName ? replyingTo?.pubkey : undefined
);
const formattedReplyingToName = isShowingName
? getFormattedAtName(replyingToUser)
: "";
const { authState } = useAuth();
const user = useUser(authState.pk);
const src = useProfilePicSrc(user);
const form = useForm<CommentFormValues>({
initialValues: {
comment: "",
},
validate: {
comment: (value) => (value ? null : "Comment required."),
},
});

const handleSubmit = (values: CommentFormValues) => {
if (ndk && replyingTo) {
const event = createKind1Event(ndk, values.comment, {
replyingTo: replyingTo.rawEvent(),
});
event
.publish(stemstrRelaySet)
.then(() => {
// Reset form
setHighlightedEvent(event);
setEditorState(EditorState.createEmpty(draftEditorDecorator));
if (rootEvent) {
setReplyingTo(rootEvent);
}
form.reset();
})
.catch(noop);
}
};

return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Group spacing="sm" py={8} px="md" pos="relative" noWrap>
<DraftEditorProvider
editorState={editorState}
setEditorState={setEditorState}
replyingTo={replyingTo}
>
<MentionMenu />
<Avatar
src={user?.profile?.image || src}
alt={user?.profile?.name}
size={36}
radius={18}
sx={(theme) => ({
border: `1px solid ${theme.colors.gray[4]}`,
})}
/>
<DraftEditor
onChange={(value) => {
form.setFieldValue("comment", value);
}}
replyingToName={formattedReplyingToName}
/>
</DraftEditorProvider>
</Group>
</form>
);
}
154 changes: 154 additions & 0 deletions components/CommentsDrawer/CommentGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Box, Button, DefaultProps, Group, Space } from "@mantine/core";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import NoteActionLike from "components/NoteAction/NoteActionLike";
import NoteActionMore from "components/NoteAction/NoteActionMore";
import NoteActionZap from "components/NoteActionZap/NoteActionZap";
import NoteContent from "components/NoteContent/NoteContent";
import {
RelativeTime,
UserDetailsAnchorWrapper,
UserDetailsAvatar,
UserDetailsDisplayName,
UserDetailsName,
} from "components/NoteHeader/NoteHeader";
import { Comment, useComments } from "ndk/NDKCommentsProvider";
import { EventProvider, useEvent } from "ndk/NDKEventProvider";
import { useEffect, useState } from "react";

type CommentProps = {
comment: Comment;
};

export default function CommentGroup({ comment }: CommentProps) {
return (
<Box pb={8}>
<EventProvider event={comment.event}>
<CommentView />
</EventProvider>
<CommentChildren events={comment.children} />
</Box>
);
}

const CommentChildren = ({ events }: { events: NDKEvent[] }) => {
const { highlightedEvent } = useComments();
const [isShowingReplies, setIsShowingReplies] = useState(false);

useEffect(() => {
if (events.find((event) => event.id === highlightedEvent?.id)) {
setIsShowingReplies(true);
}
}, [events, highlightedEvent]);

const handleShowRepliesClick = () => {
setIsShowingReplies(true);
};

if (!events.length) return null;

return isShowingReplies ? (
<Box px="md">
{events.map((event) => (
<EventProvider event={event}>
<CommentView isReply={true} />
</EventProvider>
))}
</Box>
) : (
<Button
onClick={handleShowRepliesClick}
variant="subtle"
size="xs"
ml={46}
ta="left"
p={0}
mih={0}
c="white"
fullWidth={false}
>
- View {events.length} more replies
</Button>
);
};

const CommentView = ({ isReply = false }: { isReply?: boolean }) => {
const { event } = useEvent();
const { highlightedEvent } = useComments();
const isHighlighted = event.id === highlightedEvent?.id;

return (
<Box
pr={isReply ? 8 : 16}
pl={isReply ? 46 : 16}
py={8}
bg={isHighlighted ? "dark.7" : undefined}
sx={{
borderRadius: isReply ? 8 : 0,
transition: ".5s background-color ease",
}}
>
<CommentHeader isReply={isReply} />
<Space h={isReply ? 12 : 8} />
<CommentContent isReply={isReply} />
<Space h={12} />
<CommentActions isReply={isReply} />
</Box>
);
};

const CommentHeader = ({
isReply,
...rest
}: DefaultProps & { isReply: boolean }) => {
return (
<Group position="apart" noWrap {...rest}>
<UserDetailsAnchorWrapper>
<UserDetailsAvatar size={isReply ? 28 : 36} />
<UserDetailsDisplayName ml={6} size="sm" />
<UserDetailsName truncate />
<RelativeTime />
</UserDetailsAnchorWrapper>
<NoteActionMore size={16} />
</Group>
);
};

const CommentContent = ({ isReply = false }: { isReply?: boolean }) => {
const { event } = useEvent();

return <NoteContent fz="sm" ml={isReply ? 0 : 46} content={event.content} />;
};

const CommentActions = ({ isReply = false }: { isReply?: boolean }) => {
return (
<Group spacing={12} ml={isReply ? 0 : 46}>
<CommentActionReply />
<NoteActionLike size={16} c="white" />
<NoteActionZap size={16} c="white" />
</Group>
);
};

const CommentActionReply = () => {
const { setReplyingTo, draftEditorRef } = useComments();
const { event } = useEvent();

const handleClick = () => {
setReplyingTo(event);
draftEditorRef?.current?.focus();
};

return (
<Button
onClick={handleClick}
fz={13}
variant="subtle"
c="purple.0"
py={4}
px={16}
mih={0}
>
Reply
</Button>
);
};
81 changes: 81 additions & 0 deletions components/CommentsDrawer/CommentsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Box, Group, Text } from "@mantine/core";
import Drawer, { DrawerProps } from "components/Drawer/Drawer";
import CommentsFeed from "./CommentsFeed";
import { useEvent } from "ndk/NDKEventProvider";
import { CommentsProvider, useComments } from "ndk/NDKCommentsProvider";
import CommentForm from "./CommentForm";
import { isMobile } from "react-device-detect";

export default function CommentsDrawer(props: DrawerProps) {
const { event } = useEvent();

return (
<Drawer
{...props}
styles={(theme) => ({
drawer: {
borderTopWidth: 1,
borderTopStyle: "solid",
borderTopColor: theme.colors.purple[4],
boxShadow: "0px -8px 32px 0px #2E1F4D",
},
})}
>
<CommentsProvider rootEvent={event} enabled={props.opened}>
<CommentsDrawerHeader />
{isMobile ? (
<>
<CommentForm />
<CommentsFeed />
</>
) : (
<>
<CommentsFeed />
<CommentForm />
</>
)}
</CommentsProvider>
</Drawer>
);
}

const CommentsDrawerHeader = () => {
const { comments } = useComments();
const commentsCount =
(comments?.length ?? 0) +
(comments?.reduce((prev, curr) => prev + curr.children.length, 0) ?? 0);

return (
<Box
pb="md"
sx={(theme) => ({
borderBottom: `1px solid ${theme.colors.gray[6]}`,
})}
>
<Group
position="center"
spacing={12}
c="white"
fz={20}
fw="bold"
ta="center"
>
Comments{" "}
{comments && (
<Text
fz="sm"
fw={500}
lh={1.16}
py={4}
px={8}
bg="gray.4"
sx={{ borderRadius: 12 }}
span
>
{commentsCount}
</Text>
)}
</Group>
</Box>
);
};
Loading