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
1 change: 1 addition & 0 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,7 @@
"switchBackToSlidersHint": "přepněte zpět na posuvníky pro plynulé přizpůsobení",
"view": "Zobrazit",
"viewComment": "Zobrazit komentář",
"viewArticle": "Zobrazit článek",
"createdTimeAgoBy": "Vytvořeno {timeAgo} uživatelem @{author}",
"createdTimeAgo": "Vytvořeno {timeAgo}",
"direction": "Směr",
Expand Down
1 change: 1 addition & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,7 @@
"redundant": "Redundant",
"thanksForVoting": "Thanks for voting!",
"viewComment": "View comment",
"viewArticle": "View Article",
"createdTimeAgoBy": "Created {timeAgo} by @{author}",
"createdTimeAgo": "Created {timeAgo}",
"direction": "Direction",
Expand Down
1 change: 1 addition & 0 deletions front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,7 @@
"switchBackToSlidersHint": "vuelve a los deslizadores para un ajuste suave",
"view": "Ver",
"viewComment": "Ver comentario",
"viewArticle": "Ver artículo",
"createdTimeAgoBy": "Creado {timeAgo} por @{author}",
"createdTimeAgo": "Creado {timeAgo}",
"direction": "Dirección",
Expand Down
1 change: 1 addition & 0 deletions front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,7 @@
"switchBackToSlidersHint": "volte para os controles deslizantes para um ajuste suave",
"view": "Visualizar",
"viewComment": "Ver comentário",
"viewArticle": "Ver artigo",
"createdTimeAgoBy": "Criado {timeAgo} por @{author}",
"createdTimeAgo": "Criado {timeAgo}",
"direction": "Direção",
Expand Down
1 change: 1 addition & 0 deletions front_end/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,7 @@
"switchBackToSlidersHint": "切換回滑桿以平滑調整",
"view": "檢視",
"viewComment": "查看評論",
"viewArticle": "查看文章",
"createdTimeAgoBy": "由 @{author} 建立於 {timeAgo}",
"createdTimeAgo": "建立於 {timeAgo}",
"direction": "方向",
Expand Down
1 change: 1 addition & 0 deletions front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,7 @@
"switchBackToSlidersHint": "切回滑块以进行更精细的调整",
"view": "查看",
"viewComment": "查看评论",
"viewArticle": "查看文章",
"createdTimeAgoBy": "由 @{author} 创建于 {timeAgo}",
"createdTimeAgo": "创建于 {timeAgo}",
"direction": "方向",
Expand Down
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 @@ -296,6 +300,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 @@ -327,6 +368,9 @@ const CommentsFeedProvider: FC<
finalizeReply,
removeTempReply,
optimisticallyAddReplyEnsuringParent,
ensureCommentLoaded,
refreshComment,
updateComment,
}}
>
{children}
Expand All @@ -347,7 +391,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,143 @@
"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;
onReplyToComment: () => 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,
onReplyToComment,
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={onReplyToComment}
onScrollToLink={onScrollToComment}
onVoteChange={onVoteChange}
onCmmToggle={onCmmToggle}
/>
)}
</div>
</div>
);
};

export default CommentDetailPanel;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import useCoherenceLinksContext from "@/app/(main)/components/coherence_links_provider";
import { useCommentsFeed } from "@/app/(main)/components/comments_feed_provider";
import { useQuestionLayoutSafe } from "@/app/(main)/questions/[id]/components/question_layout/question_layout_context";
import {
addKeyFactorsToComment,
createComment,
Expand Down Expand Up @@ -485,6 +486,7 @@ export const useKeyFactorDelete = () => {
export const useKeyFactorModeration = () => {
const t = useTranslations();
const { setCurrentModal } = useModal();
const questionLayout = useQuestionLayoutSafe();
const { combinedKeyFactors, setCombinedKeyFactors } = useCommentsFeed();
const [doReportKeyFactor] = useServerAction(reportKeyFactor);

Expand Down Expand Up @@ -535,11 +537,13 @@ export const useKeyFactorModeration = () => {
optimisticallyAddReplyEnsuringParent(kf.comment_id, text),
onFinalize: finalizeReply,
onRemove: removeTempReply,
onSubmitted: () => questionLayout?.closeKeyFactorOverlay(),
},
});
},
[
setCurrentModal,
questionLayout,
optimisticallyAddReplyEnsuringParent,
finalizeReply,
removeTempReply,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { useTranslations } from "next-intl";
import { KeyFactor, KeyFactorVoteTypes } from "@/types/comment";
import cn from "@/utils/core/cn";

import KeyFactorStrengthItem from "../key_factor_strength_item";
import KeyFactorStrengthItem, {
ImpactVoteHandler,
} from "../key_factor_strength_item";
import KeyFactorText from "../key_factor_text";
import KeyFactorBaseRateFrequency from "./key_factor_base_rate_frequency";
import KeyFactorBaseRateTrend from "./key_factor_base_rate_trend";
Expand All @@ -15,6 +17,7 @@ type Props = {
mode?: "forecaster" | "consumer";
isCompact?: boolean;
isSuggested?: boolean;
impactVoteRef?: React.MutableRefObject<ImpactVoteHandler | null>;
onVotePanelToggle?: (open: boolean) => void;
onDownvotePanelToggle?: (open: boolean) => void;
onMorePanelToggle?: (open: boolean) => void;
Expand All @@ -26,6 +29,7 @@ const KeyFactorBaseRate: React.FC<Props> = ({
isCompact,
mode,
isSuggested,
impactVoteRef,
onVotePanelToggle,
onDownvotePanelToggle,
onMorePanelToggle,
Expand All @@ -45,6 +49,7 @@ const KeyFactorBaseRate: React.FC<Props> = ({
isCompact={isCompact}
mode={mode}
voteType={KeyFactorVoteTypes.DIRECTION}
impactVoteRef={impactVoteRef}
onVotePanelToggle={onVotePanelToggle}
onDownvotePanelToggle={onDownvotePanelToggle}
onMorePanelToggle={onMorePanelToggle}
Expand Down
Loading
Loading