diff --git a/client/coral-embed-stream/src/Comment.css b/client/coral-embed-stream/src/Comment.css
index 24124f9ae..0429b18b3 100644
--- a/client/coral-embed-stream/src/Comment.css
+++ b/client/coral-embed-stream/src/Comment.css
@@ -13,6 +13,15 @@
pointer-events: none;
}
+.bylineSecondary {
+ color: #696969;
+ font-size: 12px;
+}
+
+.editedMarker {
+ font-style: italic;
+}
+
/* element in the top right of the Comment */
.topRight {
float: right;
diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js
index cdbd9c978..ef40c2a64 100644
--- a/client/coral-embed-stream/src/Comment.js
+++ b/client/coral-embed-stream/src/Comment.js
@@ -39,6 +39,9 @@ class Comment extends React.Component {
constructor(props) {
super(props);
+
+ // timeout to keep track of Comment edit window expiration
+ this.editWindowExpiryTimeout = null;
this.onClickEdit = this.onClickEdit.bind(this);
this.state = {
@@ -92,7 +95,13 @@ class Comment extends React.Component {
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
- }).isRequired
+ }).isRequired,
+ editing: PropTypes.shape({
+ edited: PropTypes.bool,
+
+ // ISO8601
+ editableUntil: PropTypes.string,
+ })
}).isRequired,
// given a comment, return whether it should be rendered as ignored
@@ -116,6 +125,26 @@ class Comment extends React.Component {
this.setState({isEditing: true});
}
+ componentDidMount() {
+ if (this.editWindowExpiryTimeout) {
+ this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
+ }
+
+ // if still in the edit window, set a timeout to re-render once it expires
+ const msLeftToEdit = editWindowRemainingMs(this.props.comment);
+ if (msLeftToEdit > 0) {
+ this.editWindowExpiryTimeout = setTimeout(() => {
+
+ // re-render
+ this.setState(this.state);
+ }, msLeftToEdit);
+ }
+ }
+ componentWillUnmount() {
+ if (this.editWindowExpiryTimeout) {
+ this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
+ }
+ }
render () {
const {
comment,
@@ -193,7 +222,14 @@ class Comment extends React.Component {
{ commentIsBest(comment)
?
: null }
-
+
+
+ {
+ (comment.editing && comment.editing.edited)
+ ? (Edited)
+ : null
+ }
+
{ (currentUser &&
@@ -201,9 +237,12 @@ class Comment extends React.Component {
/* User can edit/delete their own comment for a short window after posting */
?
- Edit
+ {
+ commentIsStillEditable(comment) &&
+ Edit
+ }
/* TopRightMenu allows currentUser to ignore other users' comments */
@@ -346,3 +385,28 @@ class Comment extends React.Component {
}
export default Comment;
+
+// return a Date instance representing end of edit window for comment
+const getEditableUntilDate = (comment) => {
+ const editing = comment && comment.editing;
+ const editableUntil = editing && editing.editableUntil && new Date(Date.parse(editing.editableUntil));
+ return editableUntil;
+};
+
+// return whether the comment is editable
+function commentIsStillEditable (comment) {
+ const editing = comment && comment.editing;
+ if ( ! editing) {return false;}
+ const editableUntil = getEditableUntilDate(comment);
+ const editWindowExpired = (editableUntil - new Date) < 0;
+ return ! editWindowExpired;
+}
+
+// return number of milliseconds before edit window expires
+function editWindowRemainingMs (comment) {
+ const editableUntil = getEditableUntilDate(comment);
+ if ( ! editableUntil) {return;}
+ const now = new Date();
+ const editWindowRemainingMs = (editableUntil - now);
+ return editWindowRemainingMs;
+}
diff --git a/client/coral-embed-stream/src/EditableCommentContent.js b/client/coral-embed-stream/src/EditableCommentContent.js
index f792985ec..6ac223844 100644
--- a/client/coral-embed-stream/src/EditableCommentContent.js
+++ b/client/coral-embed-stream/src/EditableCommentContent.js
@@ -5,6 +5,12 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
+const getEditableUntilDate = (comment) => {
+ const editing = comment && comment.editing;
+ const editableUntil = editing && editing.editableUntil && new Date(Date.parse(editing.editableUntil));
+ return editableUntil;
+};
+
/**
* Renders a Comment's body in such a way that the end-user can edit it and save changes
*/
@@ -23,7 +29,13 @@ export class EditableCommentContent extends React.Component {
// comment that is being edited
comment: PropTypes.shape({
- body: PropTypes.string
+ body: PropTypes.string,
+ editing: PropTypes.shape({
+ edited: PropTypes.bool,
+
+ // ISO8601
+ editableUntil: PropTypes.string,
+ })
}).isRequired,
// logged in user
@@ -41,6 +53,22 @@ export class EditableCommentContent extends React.Component {
constructor(props) {
super(props);
this.editComment = this.editComment.bind(this);
+ this.editWindowExpiryTimeout = null;
+ }
+ componentDidMount() {
+ const editableUntil = getEditableUntilDate(this.props.comment);
+ const now = new Date();
+ const editWindowRemainingMs = editableUntil && (editableUntil - now);
+ if (editWindowRemainingMs > 0) {
+ this.editWindowExpiryTimeout = setTimeout(() => {
+ this.forceUpdate();
+ }, editWindowRemainingMs);
+ }
+ }
+ componentWillUnmount() {
+ if (this.editWindowExpiryTimeout) {
+ this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
+ }
}
async editComment(edit) {
const {editComment, addNotification, stopEditing} = this.props;
@@ -74,6 +102,8 @@ export class EditableCommentContent extends React.Component {
}
render() {
const originalBody = this.props.comment.body;
+ const editableUntil = getEditableUntilDate(this.props.comment);
+ const editWindowExpired = (editableUntil - new Date()) < 0;
return (
+ {
+ editWindowExpired
+ ?
You can no longer edit this comment. The time window to do so has expired. Why not post another one?
+ :
You have to save this Edit. You may save this edit now to reset the clock.
+ }
);
}
}
+
+/**
+ * Countdown the number of seconds until a given Date
+ */
+class CountdownSeconds extends React.Component {
+ static propTypes = {
+ until: PropTypes.instanceOf(Date).isRequired
+ }
+ constructor(props) {
+ super(props);
+ this.countdownInterval = null;
+ }
+ componentDidMount() {
+ const {until} = this.props;
+ const now = new Date();
+ if (until - now > 0) {
+ this.countdownInterval = setInterval(() => {
+
+ // re-render
+ this.forceUpdate();
+ }, 1000);
+ }
+ }
+ componentWillUnmount() {
+ if (this.countdownInterval) {
+ this.countdownInterval = clearInterval(this.countdownInterval);
+ }
+ }
+ render() {
+ const now = new Date();
+ const {until} = this.props;
+ const msRemaining = until - now;
+ const secRemaining = msRemaining / 1000;
+ const wholeSecRemaining = Math.floor(secRemaining);
+ const plural = secRemaining !== 1;
+ return {`${wholeSecRemaining} second${plural ? 's' : ''}`};
+ }
+}
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css
index 087859e89..76993725f 100644
--- a/client/coral-embed-stream/style/default.css
+++ b/client/coral-embed-stream/style/default.css
@@ -317,9 +317,7 @@ button.comment__action-button[disabled],
}
.coral-plugin-pubdate-text {
- color: #696969;
display: inline-block;
- font-size: .75rem;
margin-left: 5px;
}
diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql
index 0ed5e00b8..76f4c8caa 100644
--- a/client/coral-framework/graphql/fragments/commentView.graphql
+++ b/client/coral-framework/graphql/fragments/commentView.graphql
@@ -15,4 +15,8 @@ fragment commentView on Comment {
action_summaries {
...actionSummaryView
}
+ editing {
+ edited
+ editableUntil
+ }
}
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index 19ea11efe..b52361f40 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -1,3 +1,5 @@
+const CommentsService = require('../../services/comments');
+
const Comment = {
parent({parent_id}, _, {loaders: {Comments}}) {
if (parent_id == null) {
@@ -45,6 +47,14 @@ const Comment = {
},
asset({asset_id}, _, {loaders: {Assets}}) {
return Assets.getByID.load(asset_id);
+ },
+ editing(comment) {
+ const editableUntil = CommentsService.getEditableUntilDate(comment);
+ const edited = comment.body_history.length > 1;
+ return {
+ edited,
+ editableUntil,
+ };
}
};
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index a88baa2e8..c28e38d00 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -166,6 +166,11 @@ input CommentCountQuery {
tag: [String]
}
+type EditInfo {
+ edited: Boolean!
+ editableUntil: Date
+}
+
# Comment is the base representation of user interaction in Talk.
type Comment {
@@ -207,6 +212,9 @@ type Comment {
# The time when the comment was created
created_at: Date!
+
+ # describes how the comment can be edited
+ editing: EditInfo
}
################################################################################
diff --git a/services/comments.js b/services/comments.js
index 40f0ccce0..c87fc313a 100644
--- a/services/comments.js
+++ b/services/comments.js
@@ -61,13 +61,12 @@ module.exports = class CommentsService {
// it's an id
comment = await this.findById(comment);
}
- const lastEditDate = (comment) => {
- const {created_at, body_history} = comment;
- const lastEdit = body_history[body_history.length - 1];
- return lastEdit.created_at || created_at;
+ const editWindowExpired = (comment) => {
+ const now = new Date;
+ const editableUntil = this.getEditableUntilDate(comment);
+ return now > editableUntil;
};
- const editWindowExpired = (new Date() - lastEditDate(comment)) > EDIT_WINDOW_MS;
- if (( ! ignoreEditWindow) && editWindowExpired) {
+ if (( ! ignoreEditWindow) && editWindowExpired(comment)) {
throw Object.assign(new Error('Edit window is over.'), {
name: 'EditWindowExpired'
});
@@ -97,7 +96,20 @@ module.exports = class CommentsService {
case 0:
throw new Error(`Couldn't edit comment. There is no Comment with id "${id}"`);
}
+ }
+ /**
+ * Until when can the provided comment be edited?
+ * @param {Comment} comment - comment to check last edit date of
+ * @returns {Date} last date at which comment can be edited
+ */
+ static getEditableUntilDate(comment) {
+ const mostRecentEditDate = (comment) => {
+ const {created_at, body_history} = comment;
+ const lastEdit = body_history[body_history.length - 1];
+ return (lastEdit && lastEdit.created_at) || created_at;
+ };
+ return new Date(Number(mostRecentEditDate(comment)) + EDIT_WINDOW_MS);
}
/**
diff --git a/test/server/graph/mutations/editComment.js b/test/server/graph/mutations/editComment.js
index cd213ac35..39256d17b 100644
--- a/test/server/graph/mutations/editComment.js
+++ b/test/server/graph/mutations/editComment.js
@@ -63,6 +63,7 @@ describe('graph.mutations.editComment', () => {
console.error(response.errors);
}
expect(response.errors).to.be.empty;
+ expect(response.data.editComment.errors).to.be.null;
// assert body has changed
const commentAfterEdit = await CommentsService.findById(comment.id);