Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

comments: Add proposal author updates. #2549

Merged
merged 50 commits into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c538acd
enable author updates on approved proposals
amass01 Aug 26, 2021
abda44a
bug fixes & cleanup
amass01 Aug 26, 2021
d81b762
allow replies only on latest author update
amass01 Aug 26, 2021
60395bb
allow replies only on the latest author update thread
amass01 Aug 28, 2021
d3c40cf
if author updates allowed display comment form only to record author
amass01 Aug 28, 2021
1ed1331
add an XXX as a reminder to consider the billing status when it's
amass01 Aug 28, 2021
b9ba6d9
enable comment votes only on latest author update thread
amass01 Aug 28, 2021
087bb2d
visually indicate author updates.
amass01 Aug 30, 2021
ebb26c7
[wip] display each author update in a separate comments section
amass01 Sep 1, 2021
e1ad910
styling, cleanup & bug fixes
amass01 Sep 2, 2021
3c9502a
yarn prettify
amass01 Sep 2, 2021
19a4e29
Merge branch 'master' into authoruodates
amass01 Sep 3, 2021
7c08970
single thread tweaks and fixes
amass01 Sep 3, 2021
812efeb
fix document.getElementById is undefined in scrollToElement.
amass01 Sep 3, 2021
108d345
add new author updates to the top
amass01 Sep 3, 2021
b2b3fce
map backend `extra data not allowed` error code
amass01 Sep 6, 2021
ffdba8a
replace `Comment $numofcomments` with `$updatetitle $numofcomments` for
amass01 Sep 6, 2021
cc91d16
display a tooltip next to the author update title with
amass01 Sep 6, 2021
56189bc
tooltip tweaks
amass01 Sep 6, 2021
e4a3cac
yarn prettify
amass01 Sep 6, 2021
e31a2fe
push missing title tooltip icon alignment changes
amass01 Sep 6, 2021
c5f6246
add `Proposal Update` as title and the icon next to it
amass01 Sep 7, 2021
6274530
hide comments section while processing comments
amass01 Sep 7, 2021
4c5e620
cleanup
amass01 Sep 7, 2021
5554f2c
add confirmation modal when the user is submitting subsequent updates
amass01 Sep 7, 2021
dd4d4ea
yarn prettify
amass01 Sep 7, 2021
a7f2f02
typo
amass01 Sep 7, 2021
6f94200
map error code, adding margin to form error message & fix author update
amass01 Sep 7, 2021
39e94ae
yarn prettify
amass01 Sep 7, 2021
82e5550
reset form bug fix
amass01 Sep 7, 2021
cc4c941
tiny cleanup
amass01 Sep 7, 2021
5ca9087
Merge branch 'master' into authoruodates
amass01 Sep 8, 2021
e321288
[wip] fix comments dep in Detail container which causes extra
amass01 Sep 10, 2021
29d6dae
fix flickering and infinite re-renders & mem optimaization & cleanup
amass01 Sep 10, 2021
e22d49f
enhance useScrollTo
amass01 Sep 11, 2021
15145c1
Normalize comments redux state.
amass01 Sep 13, 2021
b1056b2
show comments related errors in a separate row
amass01 Sep 13, 2021
6ffd9ae
yarn prettify
amass01 Sep 13, 2021
74c8ba4
re-add optimistic comment vote.
amass01 Sep 13, 2021
8d12c8f
Merge branch 'master' into authoruodates
amass01 Sep 13, 2021
491cb7a
move useComments to global hooks dir
amass01 Sep 14, 2021
95a7f44
typo
amass01 Sep 14, 2021
0faa540
Merge branch 'master' into authoruodates
amass01 Sep 14, 2021
dca90eb
add entity dirs for e2e tests.
amass01 Sep 14, 2021
c02c8ff
e2e: Add author update thread e2e tests.
amass01 Sep 15, 2021
562792c
opps
amass01 Sep 15, 2021
0f885ec
tiny cleanup
amass01 Sep 15, 2021
1fb3fe3
add summaries mock as middleware
amass01 Sep 15, 2021
0b63edc
code review nits
amass01 Sep 18, 2021
83446c9
cleanup unnecessary useMemo usage.
amass01 Sep 19, 2021
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
91 changes: 71 additions & 20 deletions src/actions/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ import {
ARCHIVED,
PROPOSAL_METADATA_FILENAME,
VOTE_METADATA_FILENAME,
PROPOSAL_STATE_UNVETTED
} from "../constants";
PROPOSAL_STATE_UNVETTED,
PROPOSAL_UPDATE_HINT,
PROPOSAL_MAIN_THREAD_KEY
} from "src/constants";
import {
parseReceivedProposalsMap,
parseRawProposal,
parseRawProposalsBatch
} from "src/helpers";
parseRawProposalsBatch,
shortRecordToken
} from "../helpers";

export const onResetNewUser = act.RESET_NEW_USER;

Expand Down Expand Up @@ -1021,34 +1024,57 @@ export const onSubmitEditedInvoice = (
});
});

export const onCommentVote = (currentUserID, token, commentid, vote, state) =>
export const onCommentVote = (
currentUserID,
token,
commentid,
vote,
state,
sectionId
) =>
withCsrf((dispatch, _, csrf) => {
if (!currentUserID) {
return;
}
dispatch(act.RECEIVE_LIKE_COMMENT({ token, commentid, vote }));
dispatch(act.REQUEST_LIKE_COMMENT({ commentid, token, vote, sectionId }));
return Promise.resolve(api.makeCommentVote(state, token, vote, commentid))
.then((comment) => api.signCommentVote(currentUserID, comment))
.then((comment) => api.commentVote(csrf, comment))
.then(() => {
dispatch(act.RECEIVE_LIKE_COMMENT_SUCCESS({ token, commentid, vote }));
dispatch(
act.RECEIVE_LIKE_COMMENT_SUCCESS({
token,
commentid,
vote,
sectionId
})
);
})
.catch((error) => {
dispatch(act.RECEIVE_LIKE_COMMENT(null, error));
});
});

export const onCensorComment = (userid, token, commentid, state, reason) => {
export const onCensorComment = (
userid,
token,
commentid,
state,
sectionId,
reason
) => {
return withCsrf((dispatch, _, csrf) => {
dispatch(act.REQUEST_CENSOR_COMMENT({ commentid, token, state }));
dispatch(
act.REQUEST_CENSOR_COMMENT({ commentid, token, state, sectionId })
);
return Promise.resolve(
api.makeCensoredComment(state, token, reason, commentid)
)
.then((comment) => api.signCensorComment(userid, comment))
.then((comment) => api.censorComment(csrf, comment))
.then(({ comment: { receipt, commentid, token } }) => {
if (receipt) {
dispatch(act.RECEIVE_CENSOR_COMMENT({ commentid, token }, null));
dispatch(act.RECEIVE_CENSOR_COMMENT({ commentid, token, sectionId }));
}
})
.catch((error) => {
Expand All @@ -1063,16 +1089,30 @@ export const onSubmitComment = (
token,
commentText,
parentid,
state
state,
extraData,
extraDataHint,
sectionId
) =>
withCsrf((dispatch, getState, csrf) => {
const comment = api.makeComment(token, commentText, parentid, state);
const comment = api.makeComment(
token,
commentText,
parentid,
state,
extraData,
extraDataHint
);
dispatch(act.REQUEST_NEW_COMMENT(comment));
return Promise.resolve(api.signComment(currentUserID, comment))
.then((comment) => {
// make sure this is not a duplicate comment by comparing to the existent
// comments signatures
const comments = sel.commentsByToken(getState())[token];
// make sure this is not a duplicate comment by comparing to the
// existent comments signatures.
const shortToken = shortRecordToken(token);
const { comments: commentsMap } =
sel.commentsInfoByToken(getState())[shortToken] || {};
// Flatten all comments in one array.
const comments = commentsMap && Object.values(commentsMap).flat();
const signatureFound =
comments && comments.find((cm) => cm.signature === comment.signature);
if (signatureFound) {
Expand All @@ -1082,8 +1122,19 @@ export const onSubmitComment = (
})
.then((comment) => api.newComment(csrf, comment))
.then((response) => {
const responsecomment = response.comment;
return dispatch(act.RECEIVE_NEW_COMMENT(responsecomment));
const responsecomment = response.comment || {};
const { commentid } = responsecomment;
const isAuthorUpdate = extraDataHint === PROPOSAL_UPDATE_HINT;
return dispatch(
act.RECEIVE_NEW_COMMENT({
...responsecomment,
sectionId: sectionId
? sectionId
: isAuthorUpdate
? commentid
: PROPOSAL_MAIN_THREAD_KEY
})
);
})
.catch((error) => {
dispatch(act.RECEIVE_NEW_COMMENT(null, error));
Expand Down Expand Up @@ -1694,9 +1745,9 @@ export const onSubmitDccComment = (currentUserID, token, comment, parentid) =>
return Promise.resolve(api.makeDccComment(token, comment, parentid))
.then((comment) => api.signDccComment(currentUserID, comment))
.then((comment) => {
// make sure this is not a duplicate comment by comparing to the existent
// comments signatures
const comments = sel.commentsByToken(getState())[token];
// make sure this is not a duplicate comment by comparing to the
// existent comments signatures.
const comments = sel.commentsInfoByToken(getState())[token].comments;
const signatureFound =
comments && comments.find((cm) => cm.signature === comment.signature);
if (signatureFound) {
Expand Down
13 changes: 11 additions & 2 deletions src/actions/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,20 @@ export const onEditProposal =
};

export const onSaveNewComment =
({ comment, token, parentID, state }) =>
({ comment, token, parentID, state, extraData, extraDataHint, sectionId }) =>
(dispatch, getState) => {
const userid = sel.currentUserID(getState());
return dispatch(
onSubmitCommentApi(userid, token, comment, parentID, state)
onSubmitCommentApi(
userid,
token,
comment,
parentID,
state,
extraData,
extraDataHint,
sectionId
)
);
};

Expand Down
10 changes: 6 additions & 4 deletions src/actions/tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,8 @@ describe("test api actions (actions/api.js)", () => {
const keys = await pki.generateKeys(FAKE_USER.id);
await pki.loadKeys(FAKE_USER.id, keys);

// this needs a custom assertion for success response as the common one doesn't work for this case
// this needs a custom assertion for success response as the common
// one doesn't work for this case.
setPostSuccessResponse(path);
const store = getMockedStore();
await store.dispatch(api.onSubmitComment.apply(null, params));
Expand Down Expand Up @@ -659,12 +660,13 @@ describe("test api actions (actions/api.js)", () => {
//const down_action = -1;
const params = [FAKE_USER.id, FAKE_PROPOSAL_TOKEN, commentid, up_action];

// this needs a custom assertion for success response as the common one doesn't work for this case
// this needs a custom assertion for success response as the common one
// doesn't work for this case.
setPostSuccessResponse(path);
const store = getMockedStore();
await store.dispatch(api.onCommentVote.apply(null, params));
const dispatchedActions = store.getActions();
expect(dispatchedActions[0].type).toEqual(act.RECEIVE_LIKE_COMMENT);
expect(dispatchedActions[0].type).toEqual(act.REQUEST_LIKE_COMMENT);
expect(dispatchedActions[1].type).toEqual(act.RECEIVE_LIKE_COMMENT_SUCCESS);

const keys = await pki.generateKeys(FAKE_USER.id);
Expand All @@ -678,7 +680,7 @@ describe("test api actions (actions/api.js)", () => {
{
error: false,
payload: { commentid, token: FAKE_PROPOSAL_TOKEN, vote: up_action },
type: act.RECEIVE_LIKE_COMMENT
type: act.REQUEST_LIKE_COMMENT
},
{
error: true,
Expand Down
126 changes: 103 additions & 23 deletions src/components/CommentForm/CommentForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@ import React from "react";
import PropTypes from "prop-types";
import { Formik } from "formik";
import FormikPersist from "src/components/FormikPersist";
import { Button, Message } from "pi-ui";
import {
H4,
Button,
Message,
BoxTextInput,
Tooltip,
Icon,
useMediaQuery,
Text
} from "pi-ui";
import { Row } from "../layout";
import MarkdownEditor from "src/components/MarkdownEditor";
import validationSchema from "./validation";
import { usePolicy } from "src/hooks";
import useModalContext from "src/hooks/utils/useModalContext";
import ModalConfirm from "src/components/ModalConfirm";
import styles from "./CommentForm.module.css";

const forbiddenCommentsMdElements = ["h1", "h2", "h3", "h4", "h5", "h6"];

Expand All @@ -15,17 +28,44 @@ const CommentForm = ({
onCommentSubmitted,
disableSubmit,
persistKey,
className
className,
isAuthorUpdate,
hasAuthorUpdates
}) => {
const {
policyPi: { namesupportedchars, namelengthmax, namelengthmin }
} = usePolicy();
const [handleOpenModal, handleCloseModal] = useModalContext();
const smallTablet = useMediaQuery("(max-width: 685px)");
async function handleSubmit(
values,
{ comment, title },
{ resetForm, setSubmitting, setFieldError }
) {
try {
await onSubmit(values.comment.trim());
setSubmitting(false);
resetForm();
onCommentSubmitted && onCommentSubmitted();
if (title && hasAuthorUpdates) {
handleOpenModal(ModalConfirm, {
title: "New author update",
message:
"Submitting a new update will lock the previous update thread. Are you sure you want to continue?",
successTitle: "Author Update posted",
successMessage: <Text>The update has been successfully posted!</Text>,
onClose: () => {
setSubmitting(false);
handleCloseModal();
},
onSubmit: async () => {
await onSubmit({ comment: comment.trim(), title });
setSubmitting(false);
resetForm();
onCommentSubmitted && onCommentSubmitted();
}
});
} else {
await onSubmit({ comment: comment.trim(), title });
setSubmitting(false);
resetForm();
onCommentSubmitted && onCommentSubmitted();
}
} catch (e) {
setSubmitting(false);
setFieldError("global", e);
Expand All @@ -34,28 +74,68 @@ const CommentForm = ({
return (
<Formik
initialValues={{
title: isAuthorUpdate ? "" : null,
comment: ""
}}
loading={!validationSchema}
validationSchema={validationSchema}
validationSchema={validationSchema({
namesupportedchars,
namelengthmax,
namelengthmin,
isAuthorUpdate
})}
onSubmit={handleSubmit}>
{(formikProps) => {
const {
values,
handleBlur,
handleSubmit,
isSubmitting,
setFieldValue,
errors,
isValid
} = formikProps;
function handleCommentChange(v) {
setFieldValue("comment", v);
}
{({
values,
handleBlur,
handleSubmit,
handleChange,
isSubmitting,
setFieldTouched,
setFieldValue,
errors,
isValid,
touched
}) => {
const handleTitleChangeWithTouched = (e) => {
setFieldTouched("title", true);
handleChange(e);
};

const handleCommentChange = (v) => setFieldValue("comment", v);

return (
<form onSubmit={handleSubmit} className={className}>
{errors && errors.global && (
<Message kind="error">{errors.global.toString()}</Message>
<Message className="margin-bottom-m" kind="error">
{errors.global.toString()}
</Message>
)}
{isAuthorUpdate && (
<>
<Row noMargin align="center" wrap={smallTablet}>
<H4 className="margin-bottom-s">Proposal Update</H4>
<Tooltip
contentClassName={styles.updateTitleTooltip}
className={styles.titleTooltipWrapper}
placement="right"
content="The proposal author is allowed to give periodic updates on the status of their proposal. You can start an update thread by submitting a new comment. Users
will only be able to reply to your most recent update thread.">
<div>
<Icon type="info" size={smallTablet ? "md" : "lg"} />
</div>
</Tooltip>
</Row>
<BoxTextInput
placeholder="Update title"
name="title"
data-testid="update-title"
tabIndex={1}
value={values.title}
onChange={handleTitleChangeWithTouched}
error={touched.title && errors.title}
/>
</>
)}
<MarkdownEditor
allowImgs={false}
Expand All @@ -66,7 +146,7 @@ const CommentForm = ({
onChange={handleCommentChange}
onBlur={handleBlur}
disallowedElements={forbiddenCommentsMdElements}
placeholder={"Write a comment"}
placeholder="Write a comment"
/>
<Row justify="right" topMarginSize="s">
{!!onCancel && (
Expand Down
11 changes: 11 additions & 0 deletions src/components/CommentForm/CommentForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.updateTitleTooltip {
font-size: var(--font-size-small);
width: 50rem;
}

.titleTooltipWrapper {
flex-basis: 4rem;
margin-top: -0.5rem;
display: flex;
justify-content: center;
}
Loading