Edit Comment UI reflects expired edit comment window

This commit is contained in:
Benjamin Goering
2017-05-03 12:34:22 -07:00
parent b9a3466e2e
commit ae3cbe990b
9 changed files with 194 additions and 15 deletions
@@ -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;
+69 -5
View File
@@ -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)
? <TagLabel><BestIndicator /></TagLabel>
: null }
<PubDate created_at={comment.created_at} />
<span className={styles.bylineSecondary}>
<PubDate created_at={comment.created_at} />
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>(Edited)</span></span>
: null
}
</span>
<Slot fill="commentInfoBar" comment={comment} commentId={comment.id} inline/>
{ (currentUser &&
@@ -201,9 +237,12 @@ class Comment extends React.Component {
/* User can edit/delete their own comment for a short window after posting */
? <span className={classnames(styles.topRight)}>
<a
className={classnames(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
{
commentIsStillEditable(comment) &&
<a
className={classnames(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
/* 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;
}
@@ -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 (
<div style={{marginBottom: '10px'}}>
<CommentForm
@@ -84,7 +114,7 @@ export class EditableCommentContent extends React.Component {
// should be disabled if user hasn't actually changed their
// original comment
return comment.body !== originalBody;
return (comment.body !== originalBody) && ! editWindowExpired;
}}
saveComment={this.editComment}
bodyLabel={'Edit this comment' /* @TODO (bengo) i18n */}
@@ -92,7 +122,50 @@ export class EditableCommentContent extends React.Component {
submitText={'Save changes' /* @TODO (bengo) i18n */}
saveButtonCStyle="green"
/>
{
editWindowExpired
? <p>You can no longer edit this comment. The time window to do so has expired. Why not post another one?</p>
: <p>You have <CountdownSeconds until={editableUntil} /> to save this Edit. You may save this edit now to reset the clock.</p>
}
</div>
);
}
}
/**
* 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 <span>{`${wholeSecRemaining} second${plural ? 's' : ''}`}</span>;
}
}
@@ -317,9 +317,7 @@ button.comment__action-button[disabled],
}
.coral-plugin-pubdate-text {
color: #696969;
display: inline-block;
font-size: .75rem;
margin-left: 5px;
}
@@ -15,4 +15,8 @@ fragment commentView on Comment {
action_summaries {
...actionSummaryView
}
editing {
edited
editableUntil
}
}
+10
View File
@@ -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,
};
}
};
+8
View File
@@ -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
}
################################################################################
+18 -6
View File
@@ -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);
}
/**
@@ -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);