You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
5.4 KiB

import { useQuery } from "@tanstack/react-query";
import NextLink from "next/link";
import { FC, useMemo, useState } from "react";
import {
FiHeart as HeartIcon,
FiThumbsUp as LikeIcon,
FiLock as PinnedIcon,
FiCornerDownRight as ShowRepliesIcon,
FiSlash as SlashIcon,
FiCheck as UploaderIcon
} from "react-icons/fi";
import { Avatar } from "@nextui-org/avatar";
import { Button } from "@nextui-org/button";
import { Chip } from "@nextui-org/chip";
import { Divider } from "@nextui-org/divider";
import { Link } from "@nextui-org/link";
import { CircularProgress } from "@nextui-org/progress";
import { Tooltip } from "@nextui-org/tooltip";
import { useClient } from "@/hooks/useClient";
import { Author } from "@/client/typings/author";
import {
Comment as CommentProps,
Comments as CommentsProps
} from "@/client/typings/comment";
import formatBigNumber from "@/utils/formatBigNumber";
import formatUploadedTime from "@/utils/formatUploadedTime";
import { highlight } from "@/utils/highlight";
import { channelUrl } from "@/utils/urls";
import { HighlightRenderer } from "./HighlightRenderer";
const Comment: FC<{
data: CommentProps;
videoUploader: Author;
videoId: string;
}> = ({ data, videoUploader, videoId }) => {
const message = useMemo(() => highlight(data.message), [data.message]);
const client = useClient();
const [showReplies, setShowReplies] = useState(false);
const {
data: replies,
error: repliesError,
refetch: refetchReplies,
isLoading: isLoadingReplies
} = useQuery({
queryKey: ["replies", videoId, data.repliesToken],
queryFn: () => {
return client.getComments(videoId, data.repliesToken);
},
enabled: showReplies && !!data.repliesToken
});
const userUrl = data.author.id ? channelUrl(data.author.id) : "#";
return (
<div className="flex flex-row gap-4">
<div>
<Link as={NextLink} href={userUrl}>
<Avatar
isBordered
size="lg"
showFallback
src={data.author.avatar}
name={data.author.name}
/>
</Link>
</div>
<div className="flex flex-col gap-2 w-full">
<div className="flex flex-row gap-2">
<Link as={NextLink} href={userUrl}>
<p className="font-semibold text-default-foreground">
{data.author.name}
</p>
</Link>
{data.author.id === videoUploader.id && (
<Chip
className="pl-2"
startContent={<UploaderIcon />}
color="primary"
>
Uploader
</Chip>
)}
{data.pinned && (
<Chip
className="pl-2"
startContent={<PinnedIcon />}
color="primary"
>
Pinned
</Chip>
)}
</div>
<p>
<HighlightRenderer highlighted={message} />
</p>
<div className="flex flex-row gap-4 items-center">
<div className="flex flex-row tracking-tight text-default-500 items-center gap-1">
<LikeIcon />
<p>{formatBigNumber(data.likes)} likes</p>
</div>
<div className="tracking-tight text-default-500">
<p>{formatUploadedTime(data.written)}</p>
</div>
{data.videoUploaderLiked && (
<div className="flex items-center">
<Tooltip content="Uploader liked" showArrow>
<p className="text-danger text-xl">
<HeartIcon />
</p>
</Tooltip>
</div>
)}
{data.videoUploaderReplied && (
<div>
<Avatar
size="sm"
src={videoUploader.avatar}
name={videoUploader.name}
showFallback
/>
</div>
)}
{data.edited && (
<p className="tracking-tight text-default-500">(edited)</p>
)}
{data.repliesToken && (
<Button
startContent={<ShowRepliesIcon />}
variant="light"
onClick={() => setShowReplies((state) => !state)}
>
{showReplies ? "Hide replies" : "Show replies"}
</Button>
)}
</div>
{showReplies && (
<div className="flex flex-col gap-4">
<Comments
data={replies}
isLoading={isLoadingReplies}
error={repliesError}
refetch={refetchReplies}
videoUploader={videoUploader}
videoId={videoId}
/>
</div>
)}
</div>
</div>
);
};
export const Comments: FC<{
data?: CommentsProps;
isLoading: boolean;
error: Error | null;
refetch: () => void;
videoUploader: Author;
videoId: string;
}> = ({ data, isLoading, error, refetch, videoUploader, videoId }) => {
return (
<>
{data && (
<>
<p className="text-xl">
{data.count && formatBigNumber(data.count)} Comments
</p>
<Divider orientation="horizontal" />
<div className="flex flex-col gap-4">
{data.enabled && (
<>
{data.data.map((comment) => (
<Comment
key={comment.id}
data={comment}
videoUploader={videoUploader}
videoId={videoId}
/>
))}
</>
)}
{!data.enabled && (
<div className="flex flex-row gap-2 items-center">
<SlashIcon />
<p>Comments on this video are disabled</p>
</div>
)}
</div>
</>
)}
{!data && isLoading && (
<div className="h-24 w-full justify-center items-center flex">
<CircularProgress aria-label="Loading comments..." />
</div>
)}
{error && (
<div className="flex flex-col gap-2">
<p className="text-lg font-semibold">Failed to load comments:</p>
{error.toString()}
<div>
<Button color="primary" onClick={() => refetch()}>
Retry
</Button>
</div>
</div>
)}
</>
);
};