Show comment context in thread

This commit is contained in:
Tom Moor
2024-01-04 22:30:22 -05:00
parent 63eae352ee
commit 47c13c9916
6 changed files with 55 additions and 17 deletions

View File

@@ -11,6 +11,7 @@ import { ProsemirrorData } from "@shared/types";
import Comment from "~/models/Comment"; import Comment from "~/models/Comment";
import Document from "~/models/Document"; import Document from "~/models/Document";
import Avatar from "~/components/Avatar"; import Avatar from "~/components/Avatar";
import { useDocumentContext } from "~/components/DocumentContext";
import Fade from "~/components/Fade"; import Fade from "~/components/Fade";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer"; import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
@@ -63,6 +64,7 @@ function CommentThread({
recessed, recessed,
focused, focused,
}: Props) { }: Props) {
const { editor } = useDocumentContext();
const { comments } = useStores(); const { comments } = useStores();
const topRef = React.useRef<HTMLDivElement>(null); const topRef = React.useRef<HTMLDivElement>(null);
const user = useCurrentUser(); const user = useCurrentUser();
@@ -75,6 +77,11 @@ function CommentThread({
}); });
const can = usePolicy(document); const can = usePolicy(document);
const highlightedCommentMarks = editor
?.getComments()
.filter((comment) => comment.id === thread.id);
const highlightedText = highlightedCommentMarks?.map((c) => c.text).join("");
const commentsInThread = comments const commentsInThread = comments
.inThread(thread.id) .inThread(thread.id)
.filter((comment) => !comment.isNew); .filter((comment) => !comment.isNew);
@@ -167,7 +174,9 @@ function CommentThread({
return ( return (
<CommentThreadItem <CommentThreadItem
highlightedText={index === 0 ? highlightedText : undefined}
comment={comment} comment={comment}
onDelete={() => editor?.removeComment(comment.id)}
key={comment.id} key={comment.id}
firstOfThread={index === 0} firstOfThread={index === 0}
lastOfThread={index === commentsInThread.length - 1 && !draft} lastOfThread={index === commentsInThread.length - 1 && !draft}

View File

@@ -14,13 +14,12 @@ import { Minute } from "@shared/utils/time";
import Comment from "~/models/Comment"; import Comment from "~/models/Comment";
import Avatar from "~/components/Avatar"; import Avatar from "~/components/Avatar";
import ButtonSmall from "~/components/ButtonSmall"; import ButtonSmall from "~/components/ButtonSmall";
import { useDocumentContext } from "~/components/DocumentContext";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import Text from "~/components/Text"; import Text from "~/components/Text";
import Time from "~/components/Time"; import Time from "~/components/Time";
import useBoolean from "~/hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
import CommentMenu from "~/menus/CommentMenu"; import CommentMenu from "~/menus/CommentMenu";
import { hover } from "~/styles"; import { hover, truncateMultiline } from "~/styles";
import CommentEditor from "./CommentEditor"; import CommentEditor from "./CommentEditor";
/** /**
@@ -74,6 +73,10 @@ type Props = {
previousCommentCreatedAt?: string; previousCommentCreatedAt?: string;
/** Whether the user can reply in the thread */ /** Whether the user can reply in the thread */
canReply: boolean; canReply: boolean;
/** Callback when the comment has been deleted */
onDelete: () => void;
/** Text to highlight at the top of the comment */
highlightedText?: string;
}; };
function CommentThreadItem({ function CommentThreadItem({
@@ -84,8 +87,9 @@ function CommentThreadItem({
dir, dir,
previousCommentCreatedAt, previousCommentCreatedAt,
canReply, canReply,
onDelete,
highlightedText,
}: Props) { }: Props) {
const { editor } = useDocumentContext();
const { t } = useTranslation(); const { t } = useTranslation();
const [forceRender, setForceRender] = React.useState(0); const [forceRender, setForceRender] = React.useState(0);
const [data, setData] = React.useState(toJS(comment.data)); const [data, setData] = React.useState(toJS(comment.data));
@@ -120,10 +124,6 @@ function CommentThreadItem({
} }
}; };
const handleDelete = () => {
editor?.removeComment(comment.id);
};
const handleCancel = () => { const handleCancel = () => {
setData(toJS(comment.data)); setData(toJS(comment.data));
setReadOnly(); setReadOnly();
@@ -174,6 +174,9 @@ function CommentThreadItem({
)} )}
</Meta> </Meta>
)} )}
{highlightedText && (
<HighlightedText>{highlightedText}</HighlightedText>
)}
<Body ref={formRef} onSubmit={handleSubmit}> <Body ref={formRef} onSubmit={handleSubmit}>
<StyledCommentEditor <StyledCommentEditor
key={`${forceRender}`} key={`${forceRender}`}
@@ -198,7 +201,7 @@ function CommentThreadItem({
<Menu <Menu
comment={comment} comment={comment}
onEdit={setEditing} onEdit={setEditing}
onDelete={handleDelete} onDelete={onDelete}
dir={dir} dir={dir}
/> />
)} )}
@@ -237,6 +240,28 @@ const Body = styled.form`
border-radius: 2px; border-radius: 2px;
`; `;
const HighlightedText = styled(Text)`
position: relative;
color: ${s("textSecondary")};
font-size: 14px;
padding: 0 8px;
margin: 4px 0;
display: inline-block;
${truncateMultiline(3)}
&:after {
content: "";
width: 2px;
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
background: ${s("commentMarkBackground")};
border-radius: 2px;
}
`;
const Menu = styled(CommentMenu)<{ dir?: "rtl" | "ltr" }>` const Menu = styled(CommentMenu)<{ dir?: "rtl" | "ltr" }>`
position: absolute; position: absolute;
left: ${(props) => (props.dir !== "rtl" ? "auto" : "4px")}; left: ${(props) => (props.dir !== "rtl" ? "auto" : "4px")};

View File

@@ -134,6 +134,7 @@ declare module "styled-components" {
textDiffDeleted: string; textDiffDeleted: string;
textDiffDeletedBackground: string; textDiffDeletedBackground: string;
placeholder: string; placeholder: string;
commentMarkBackground: string;
commentBackground: string; commentBackground: string;
sidebarBackground: string; sidebarBackground: string;
sidebarActiveBackground: string; sidebarActiveBackground: string;

View File

@@ -788,13 +788,13 @@ h6 {
} }
.comment-marker { .comment-marker {
border-bottom: 2px solid ${transparentize(0.5, props.theme.brand.marine)}; border-bottom: 2px solid ${props.theme.commentMarkBackground};
transition: background 100ms ease-in-out; transition: background 100ms ease-in-out;
border-radius: 2px; border-radius: 2px;
&:hover { &:hover {
${props.readOnly ? "cursor: var(--pointer);" : ""} ${props.readOnly ? "cursor: var(--pointer);" : ""}
background: ${transparentize(0.5, props.theme.brand.marine)}; background: ${props.theme.commentMarkBackground};
} }
} }

View File

@@ -69,6 +69,7 @@ const buildBaseTheme = (input: Partial<Colors>) => {
selected: colors.accent, selected: colors.accent,
textHighlight: "#FDEA9B", textHighlight: "#FDEA9B",
textHighlightForeground: colors.almostBlack, textHighlightForeground: colors.almostBlack,
commentMarkBackground: transparentize(0.5, "#2BC2FF"),
code: colors.lightBlack, code: colors.lightBlack,
codeComment: "#6a737d", codeComment: "#6a737d",
codePunctuation: "#5e6687", codePunctuation: "#5e6687",

View File

@@ -16,6 +16,8 @@ export type CommentMark = {
id: string; id: string;
/* The id of the user who created the comment */ /* The id of the user who created the comment */
userId: string; userId: string;
/* The text of the comment */
text: string;
}; };
export type Task = { export type Task = {
@@ -88,8 +90,7 @@ export default class ProsemirrorHelper {
} }
/** /**
* Returns true if the trimmed content of the passed document is an empty * Returns true if the trimmed content of the passed document is an empty string.
* string.
* *
* @returns True if the editor is empty * @returns True if the editor is empty
*/ */
@@ -98,8 +99,7 @@ export default class ProsemirrorHelper {
} }
/** /**
* Iterates through the document to find all of the comments that exist as * Iterates through the document to find all of the comments that exist as marks.
* marks.
* *
* @param doc Prosemirror document node * @param doc Prosemirror document node
* @returns Array<CommentMark> * @returns Array<CommentMark>
@@ -110,7 +110,10 @@ export default class ProsemirrorHelper {
doc.descendants((node) => { doc.descendants((node) => {
node.marks.forEach((mark) => { node.marks.forEach((mark) => {
if (mark.type.name === "comment") { if (mark.type.name === "comment") {
comments.push(mark.attrs as CommentMark); comments.push({
...mark.attrs,
text: node.textContent,
} as CommentMark);
} }
}); });
@@ -121,8 +124,7 @@ export default class ProsemirrorHelper {
} }
/** /**
* Iterates through the document to find all of the tasks and their completion * Iterates through the document to find all of the tasks and their completion state.
* state.
* *
* @param doc Prosemirror document node * @param doc Prosemirror document node
* @returns Array<Task> * @returns Array<Task>