mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 18:49:28 +08:00
Edit Comment UI reflects expired edit comment window
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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> <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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user