Show comment context in thread
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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")};
|
||||||
|
|||||||
1
app/typings/styled-components.d.ts
vendored
1
app/typings/styled-components.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user