Skip to content
Open
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
46 changes: 45 additions & 1 deletion front_end/src/app/(main)/components/comments_feed_provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { PostWithForecasts, ProjectPermissions } from "@/types/post";
import { VoteDirection } from "@/types/votes";
import { parseComment } from "@/utils/comments";
import { logError } from "@/utils/core/errors";

type ErrorType = Error & { digest?: string };

Expand Down Expand Up @@ -53,6 +54,9 @@ export type CommentsFeedContextType = {
parentId: number,
text: string
) => Promise<number>;
ensureCommentLoaded: (id: number) => Promise<boolean>;
refreshComment: (id: number) => Promise<void>;
updateComment: (id: number, changes: Partial<CommentType>) => void;
};

const COMMENTS_PER_PAGE = 10;
Expand Down Expand Up @@ -295,6 +299,43 @@ const CommentsFeedProvider: FC<
}
};

const refreshComment = async (id: number): Promise<void> => {
try {
const response = await ClientCommentsApi.getComments({
post: postData?.id,
author: profileId,
limit: 50,
use_root_comments_pagination: rootCommentStructure,
sort,
focus_comment_id: String(id),
});
const results = response.results as unknown as BECommentType[];
const found = results.find((c) => c.id === id);
if (found) {
const parsed = parseComment(found);
setComments((prev) => {
if (findById(prev, id)) {
return replaceById(prev, id, parsed);
}
// Not in feed yet — insert the full focused page
const focusedPage = parseCommentsArray(results, rootCommentStructure);
const merged = [...focusedPage, ...prev].sort((a, b) => b.id - a.id);
return uniqueById(merged);
});
}
} catch (e) {
logError(e);
}
};

const updateComment = (id: number, changes: Partial<CommentType>) => {
setComments((prev) => {
const existing = findById(prev, id);
if (!existing) return prev;
return replaceById(prev, id, { ...existing, ...changes });
});
};

const optimisticallyAddReplyEnsuringParent = async (
parentId: number,
text: string
Expand Down Expand Up @@ -326,6 +367,9 @@ const CommentsFeedProvider: FC<
finalizeReply,
removeTempReply,
optimisticallyAddReplyEnsuringParent,
ensureCommentLoaded,
refreshComment,
updateComment,
}}
>
{children}
Expand All @@ -346,7 +390,7 @@ export const useCommentsFeed = () => {
return context;
};

function findById(list: CommentType[], id: number): CommentType | null {
export function findById(list: CommentType[], id: number): CommentType | null {
for (const c of list) {
if (c.id === id) return c;
const kids = c.children ?? [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"use client";

import { useLocale, useTranslations } from "next-intl";
import { FC } from "react";

import CommentActionBar from "@/components/comment_feed/comment_action_bar";
import MarkdownEditor from "@/components/markdown_editor";
import { BECommentType, KeyFactor } from "@/types/comment";
import { PostWithForecasts } from "@/types/post";
import { VoteDirection } from "@/types/votes";
import { parseUserMentions } from "@/utils/comments";
import { formatDate } from "@/utils/formatters/date";

import { KeyFactorItem } from "./item_view";

type Props = {
keyFactor: KeyFactor;
relatedKeyFactors: KeyFactor[];
post: PostWithForecasts;
comment: BECommentType | null;
isLoading: boolean;
onScrollToComment: () => void;
onSelectKeyFactor: (keyFactor: KeyFactor) => void;
onVoteChange: (voteScore: number, userVote: VoteDirection | null) => void;
onCmmToggle: (enabled: boolean) => void;
};

const CommentDetailPanel: FC<Props> = ({
keyFactor,
relatedKeyFactors,
post,
comment,
isLoading,
onScrollToComment,
onSelectKeyFactor,
onVoteChange,
onCmmToggle,
}) => {
const t = useTranslations();
const locale = useLocale();

return (
<div
className="relative min-w-0 flex-1"
onClick={(e) => e.stopPropagation()}
>
<div className="flex max-h-[50dvh] flex-col gap-3.5 overflow-hidden rounded-xl bg-gray-0 p-5 shadow-2xl dark:bg-gray-0-dark lg:max-h-[calc(100dvh-10rem)]">
<div className="flex shrink-0 items-center gap-1.5">
<span className="text-base font-bold text-gray-800 dark:text-gray-800-dark">
{keyFactor.author.username}
</span>
<span className="flex items-center gap-2">
<span className="size-[2px] shrink-0 rounded-full bg-gray-500 dark:bg-gray-500-dark" />
<span
className="text-base text-gray-500 dark:text-gray-500-dark"
suppressHydrationWarning
>
{t("onDate", {
date: formatDate(locale, new Date(keyFactor.created_at)),
})}
</span>
</span>
</div>

<div className="min-h-0 flex-1 overflow-y-auto text-base leading-6 text-gray-700 dark:text-gray-700-dark">
{isLoading && (
<div className="animate-pulse space-y-[10px]">
<div className="h-[1.5em] rounded bg-gray-200 dark:bg-gray-700-dark" />
<div className="h-[1.5em] rounded bg-gray-200 dark:bg-gray-700-dark" />
<div className="h-[1.5em] w-4/5 rounded bg-gray-200 dark:bg-gray-700-dark" />
<div className="h-[1.5em] w-1/2 rounded bg-gray-200 dark:bg-gray-700-dark" />
</div>
)}
{comment && (
<MarkdownEditor
mode="read"
markdown={parseUserMentions(
comment.text,
comment.mentioned_users
)}
contentEditableClassName="!text-base !leading-6 !text-gray-700 dark:!text-gray-700-dark"
withUgcLinks
withCodeBlocks
/>
)}
</div>

{relatedKeyFactors.length > 0 && (
<div className="flex shrink-0 flex-col gap-2">
<span className="text-[10px] font-medium uppercase leading-3 text-gray-500 dark:text-gray-500-dark">
{t("keyFactors")}
</span>
<div className="flex gap-1">
{relatedKeyFactors.map((kf) => (
<KeyFactorItem
key={kf.id}
keyFactor={kf}
linkToComment={false}
isCompact
projectPermission={post.user_permission}
className="w-[190px]"
onClick={() => onSelectKeyFactor(kf)}
/>
))}
</div>
</div>
)}

{isLoading && (
<div className="flex shrink-0 animate-pulse items-center gap-3 text-sm leading-4">
<div className="inline-flex items-center gap-2 rounded-sm border border-blue-500/30 px-1 dark:border-blue-600/30">
<div className="size-6 rounded-sm bg-gray-200 dark:bg-gray-700-dark" />
<div className="h-3 w-4 rounded bg-gray-200 dark:bg-gray-700-dark" />
<div className="size-6 rounded-sm bg-gray-200 dark:bg-gray-700-dark" />
</div>
<div className="inline-flex items-center gap-0.5 rounded-sm border border-blue-400/30 py-0 pl-0.5 pr-2 dark:border-blue-600/30">
<div className="size-4 rounded-sm bg-gray-200 dark:bg-gray-700-dark" />
<div className="h-3 w-9 rounded bg-gray-200 dark:bg-gray-700-dark" />
</div>
<div className="inline-flex items-center gap-1 rounded-sm border border-blue-400/30 px-2 py-1 dark:border-blue-600/30">
<div className="size-4 rounded-full bg-gray-200 dark:bg-gray-700-dark" />
<div className="h-3 w-28 rounded bg-gray-200 dark:bg-gray-700-dark" />
</div>
</div>
)}
{comment && (
<CommentActionBar
comment={comment}
post={post}
onReply={onScrollToComment}
onScrollToLink={onScrollToComment}
onVoteChange={onVoteChange}
onCmmToggle={onCmmToggle}
/>
)}
</div>
</div>
);
};

export default CommentDetailPanel;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dynamic from "next/dynamic";
import { FC } from "react";

import { useCommentsFeed } from "@/app/(main)/components/comments_feed_provider";
import { ImpactMetadata, KeyFactor } from "@/types/comment";
import { ProjectPermissions } from "@/types/post";
import { getImpactDirectionFromMetadata } from "@/utils/key_factors";
Expand All @@ -25,6 +26,7 @@ type Props = {
className?: string;
projectPermission?: ProjectPermissions;
isSuggested?: boolean;
inlineVotePanels?: boolean;
};

function getImpactMetadata(keyFactor: KeyFactor): ImpactMetadata | null {
Expand All @@ -41,13 +43,18 @@ export const KeyFactorItem: FC<Props> = ({
className,
projectPermission,
isSuggested,
inlineVotePanels,
}) => {
const isFlagged = keyFactor.flagged_by_me;
const hasImpactBar = !keyFactor.base_rate;
const { combinedKeyFactors } = useCommentsFeed();
const liveKeyFactor =
combinedKeyFactors.find((kf) => kf.id === keyFactor.id) ?? keyFactor;

const isFlagged = liveKeyFactor.flagged_by_me;
const hasImpactBar = !liveKeyFactor.base_rate;
const impactDirection = hasImpactBar
? getImpactDirectionFromMetadata(getImpactMetadata(keyFactor))
? getImpactDirectionFromMetadata(getImpactMetadata(liveKeyFactor))
: undefined;
const impactStrength = keyFactor.vote?.score ?? 0;
const impactStrength = liveKeyFactor.vote?.score ?? 0;

const {
impactPanel,
Expand All @@ -71,9 +78,9 @@ export const KeyFactorItem: FC<Props> = ({
impactDirection={impactDirection}
impactStrength={impactStrength}
>
{keyFactor.driver && (
{liveKeyFactor.driver && (
<KeyFactorDriver
keyFactor={keyFactor}
keyFactor={liveKeyFactor}
mode={mode}
isCompact={isCompact}
onVotePanelToggle={handleUpvotePanelToggle}
Expand All @@ -82,9 +89,9 @@ export const KeyFactorItem: FC<Props> = ({
isMorePanelOpen={morePanel.showPanel}
/>
)}
{keyFactor.base_rate && (
{liveKeyFactor.base_rate && (
<KeyFactorBaseRate
keyFactor={keyFactor}
keyFactor={liveKeyFactor}
isCompact={isCompact}
mode={mode}
isSuggested={isSuggested}
Expand All @@ -94,9 +101,9 @@ export const KeyFactorItem: FC<Props> = ({
isMorePanelOpen={morePanel.showPanel}
/>
)}
{keyFactor.news && (
{liveKeyFactor.news && (
<KeyFactorNews
keyFactor={keyFactor}
keyFactor={liveKeyFactor}
mode={mode}
isCompact={isCompact}
onVotePanelToggle={handleUpvotePanelToggle}
Expand All @@ -113,7 +120,8 @@ export const KeyFactorItem: FC<Props> = ({
morePanel={morePanel}
anchorRef={impactPanel.anchorRef}
isCompact={isCompact}
keyFactor={keyFactor}
inline={inlineVotePanels}
keyFactor={liveKeyFactor}
projectPermission={projectPermission}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const KeyFactorCardContainer: FC<Props> = ({
id={id}
onClick={onClick}
className={cn(
"relative flex gap-3 rounded-xl p-5 [&:hover_.target]:visible",
"relative flex gap-3 overflow-hidden rounded-xl p-5 [&:hover_.target]:visible",
linkToComment
? "border border-blue-400 bg-gray-0 dark:border-blue-400-dark dark:bg-gray-0-dark"
: "bg-blue-200 dark:bg-blue-200-dark",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,26 +144,27 @@ const KeyFactorStrengthItem: FC<Props> = ({
)}
</div>

<div
className="flex items-end justify-between"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<ThumbVoteButtons
upCount={upCount}
downCount={downCount}
selected={selection}
disabled={submitting}
onClickUp={() => {
toggle(upScore);
onVotePanelToggle?.(selection !== "up");
}}
onClickDown={() => {
toggle(downScore);
onVotePanelToggle?.(false);
onDownvotePanelToggle?.(selection !== "down");
}}
/>
<div className="flex items-end justify-between">
<div
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<ThumbVoteButtons
upCount={upCount}
downCount={downCount}
selected={selection}
disabled={submitting}
onClickUp={() => {
toggle(upScore);
onVotePanelToggle?.(selection !== "up");
}}
onClickDown={() => {
toggle(downScore);
onVotePanelToggle?.(false);
onDownvotePanelToggle?.(selection !== "down");
}}
/>
</div>
{!isCompactConsumer && onMorePanelToggle && (
<button
aria-label="menu"
Expand All @@ -173,7 +174,11 @@ const KeyFactorStrengthItem: FC<Props> = ({
? "bg-blue-500 text-gray-0 dark:bg-blue-500-dark dark:text-gray-0-dark"
: "text-gray-500 hover:bg-gray-300 dark:text-gray-500-dark dark:hover:bg-gray-300-dark"
)}
onClick={() => onMorePanelToggle(!isMorePanelOpen)}
onClick={(e) => {
e.stopPropagation();
onMorePanelToggle(!isMorePanelOpen);
}}
onPointerDown={(e) => e.stopPropagation()}
>
<FontAwesomeIcon icon={faEllipsis} />
</button>
Expand Down
Loading
Loading