From e5f97d1f185832b9d0e5bbb6c606e0f5cb2ba303 Mon Sep 17 00:00:00 2001 From: picklesrus Date: Mon, 9 Nov 2020 11:01:42 -0500 Subject: [PATCH] Skeleton of code to show mute modal and comment status. Still todo: - l10n - Time needs formatting - Compose box needs to be shown/formatted correctly based on mute status - Blue comment status box need to be sticky --- src/views/preview/comment/compose-comment.jsx | 223 ++++++++++++------ test/unit/components/compose-comment.test.jsx | 133 +++++++++++ 2 files changed, 288 insertions(+), 68 deletions(-) create mode 100644 test/unit/components/compose-comment.test.jsx diff --git a/src/views/preview/comment/compose-comment.jsx b/src/views/preview/comment/compose-comment.jsx index a1d2e85db..9de5d1e59 100644 --- a/src/views/preview/comment/compose-comment.jsx +++ b/src/views/preview/comment/compose-comment.jsx @@ -10,6 +10,8 @@ const FlexRow = require('../../../components/flex-row/flex-row.jsx'); const Avatar = require('../../../components/avatar/avatar.jsx'); const InplaceInput = require('../../../components/forms/inplace-input.jsx'); const Button = require('../../../components/forms/button.jsx'); +const CommentingStatus = require('../../../components/commenting-status/commenting-status.jsx'); +const MuteModal = require('../../../components/modal/mute/modal.jsx'); const connect = require('react-redux').connect; @@ -24,7 +26,8 @@ const MAX_COMMENT_LENGTH = 500; const ComposeStatus = keyMirror({ EDITING: null, SUBMITTING: null, - REJECTED: null + REJECTED: null, + REJECTED_MUTE: null }); class ComposeComment extends React.Component { @@ -33,13 +36,17 @@ class ComposeComment extends React.Component { bindAll(this, [ 'handlePost', 'handleCancel', - 'handleInput' + 'handleInput', + 'handleMuteClose', + 'handleMuteOpen' + ]); this.state = { message: '', status: ComposeStatus.EDITING, error: null, - appealId: null + appealId: null, + muteOpen: false }; } handleInput (event) { @@ -67,13 +74,24 @@ class ComposeComment extends React.Component { if (err || res.statusCode !== 200) { body = {rejected: 'error'}; } - if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) { + let muteOpen = false; + let muteExpiresAt = 0; + let rejectedStatus = ComposeStatus.REJECTED; + if (body.status && body.status.mute_status) { + muteExpiresAt = body.status.mute_status.muteExpiresAt; + rejectedStatus = ComposeStatus.REJECTED_MUTE; + if (this.shouldShowMuteModal(body.status.mute_status.offenses)) { + muteOpen = true; + } + } // Note: does not reset the message state this.setState({ - status: ComposeStatus.REJECTED, + status: rejectedStatus, error: body.rejected, - appealId: body.appealId + appealId: body.appealId, + muteOpen: muteOpen, + muteExpiresAt: muteExpiresAt }); return; } @@ -92,6 +110,44 @@ class ComposeComment extends React.Component { this.props.onAddComment(body); }); } + + handleMuteClose () { + this.setState({ + muteOpen: false + }); + } + + handleMuteOpen () { + this.setState({ + muteOpen: true + }); + } + + shouldShowMuteModal (offensesList) { + // We should show the mute modal whne the user is newly muted or hasn't seen it for a while. + // We don't want to show it more than about once a week. + // A newly muted user has only 1 offense and it happened in the last coulpe of minutes. + // If a user has more than 1 offense, it means that they have have been muted in the + // last week. + // Assumption: The offenses list is ordered by time with the most recent at the end. + + // This check is here just in case we somehow get bad data back from a backend. + if (!offensesList) { + return false; + } + + const numOffenses = offensesList.length; + // This isn't intended to be called if there are no offenses, but + // say no just in case. + if (numOffenses === 0) { + return false; + } + + const mostRecent = offensesList[numOffenses - 1]; + const creationTimeMinutesAgo = (Date.now() - (mostRecent.createdAt * 1000)) / (60 * 1000); + return creationTimeMinutesAgo < 2 && numOffenses === 1; + } + handleCancel () { this.setState({ message: '', @@ -103,70 +159,101 @@ class ComposeComment extends React.Component { } render () { return ( -
- - - - - {this.state.error ? ( - -
- -
-
- ) : null} - - = 0 ? - 'compose-valid' : 'compose-invalid')} - handleUpdate={onUpdate} - name="compose-comment" - type="textarea" - value={this.state.message} - onInput={this.handleInput} - /> - - - - + {this.state.status === ComposeStatus.REJECTED_MUTE ? ( + + +

Scratch thinks your comment was disrespectful.

+

+ For the next {this.state.muteExpiresAt} you + won't be able to post comments. + Once {this.state.muteExpiresAt} have passed, + you will be able to comment again. +

+

For more information, + click here.

+
+
+ ) : null } +
+ + + + + {this.state.error && this.state.status !== ComposeStatus.REJECTED_MUTE ? ( + +
+ +
+
+ ) : null} + + = 0 ? 'compose-valid' : 'compose-invalid')} - > - - -
- - -
+ handleUpdate={onUpdate} + name="compose-comment" + type="textarea" + value={this.state.message} + onInput={this.handleInput} + /> + + + + = 0 ? + 'compose-valid' : 'compose-invalid')} + > + + + +
+
+ {this.state.muteOpen ? ( + + ) : null} +
+ ); } } diff --git a/test/unit/components/compose-comment.test.jsx b/test/unit/components/compose-comment.test.jsx new file mode 100644 index 000000000..0d35d191d --- /dev/null +++ b/test/unit/components/compose-comment.test.jsx @@ -0,0 +1,133 @@ +const React = require('react'); +const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); +const ComposeComment = require('../../../src/views/preview/comment/compose-comment.jsx'); +import configureStore from 'redux-mock-store'; + + +describe('Compose Comment test', () => { + const mockStore = configureStore(); + const defaultProps = () =>({ + user: { + thumbnailUrl: 'scratch.mit.edu', + username: 'auser' + } + }); + + let store; + beforeEach(() => { + store = mockStore({ + session: { + session: { + user: {} + } + + } + }); + }); + + const getComposeCommentWrapper = props => { + const wrapper = shallowWithIntl( + + , {context: {store}} + ); + return wrapper.dive(); // unwrap redux connect(injectIntl(JoinFlow)) + }; + + test('Modal & Comment status do not show ', () => { + const component = getComposeCommentWrapper({}); + // Comment compsoe box is there + expect(component.find('FlexRow.compose-comment').exists()).toEqual(true); + // No error message + expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false); + expect(component.find('MuteModal').exists()).toEqual(false); + expect(component.find('CommentingStatus').exists()).toEqual(false); + }); + + test('Error messages shows when comment rejected ', () => { + const component = getComposeCommentWrapper({}); + const commentInstance = component.instance(); + commentInstance.setState({error: 'isFlood'}); + component.update(); + expect(component.find('FlexRow.compose-error-row').exists()).toEqual(true); + }); + + test('No error message shows when comment rejected because user muted ', () => { + const component = getComposeCommentWrapper({}); + const commentInstance = component.instance(); + commentInstance.setState({ + error: 'isMuted', + status: 'REJECTED_MUTE' + }); + component.update(); + expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false); + }); + + test('Comment Status shows when state is REJECTED_MUTE ', () => { + const component = getComposeCommentWrapper({}); + const commentInstance = component.instance(); + commentInstance.setState({status: 'REJECTED_MUTE'}); + component.update(); + expect(component.find('FlexRow.compose-comment').exists()).toEqual(true); + expect(component.find('MuteModal').exists()).toEqual(false); + expect(component.find('CommentingStatus').exists()).toEqual(true); + }); + + test('Mute Modal shows when muteOpen is true ', () => { + const component = getComposeCommentWrapper({}); + const commentInstance = component.instance(); + commentInstance.setState({muteOpen: true}); + component.update(); + expect(component.find('FlexRow.compose-comment').exists()).toEqual(true); + expect(component.find('MuteModal').exists()).toEqual(true); + }); + + test('shouldShowMuteModal is false when list is undefined ', () => { + const commentInstance = getComposeCommentWrapper({}).instance(); + expect(commentInstance.shouldShowMuteModal()).toBe(false); + }); + + test('shouldShowMuteModal is false when list empty ', () => { + const offenses = []; + const commentInstance = getComposeCommentWrapper({}).instance(); + expect(commentInstance.shouldShowMuteModal(offenses)).toBe(false); + }); + + test('shouldShowMuteModal is true when only 1 recent offesnse ', () => { + const offenses = []; + const realDateNow = Date.now.bind(global.Date); + global.Date.now = () => 0; + // Since Date.now mocked to 0 above, we just need a small number to make + // it look like it was created < 2 minutes ago. + const offense = { + expiresAt: '1000', + createdAt: '-60' // ~1 ago min given shouldShowMuteModal's conversions, + }; + offenses.push(offense); + const commentInstance = getComposeCommentWrapper({}).instance(); + expect(commentInstance.shouldShowMuteModal(offenses)).toBe(true); + global.Date.now = realDateNow; + }); + + test('shouldShowMuteModal is false when multiple offenses, even if 1 is recent ', () => { + const offenses = []; + const realDateNow = Date.now.bind(global.Date); + global.Date.now = () => 0; + // Since Date.now mocked to 0 above, we just need a small number to make + // it look like it was created more than 2 minutes ago. + let offense = { + expiresAt: '1000', + createdAt: '-119' // just shy of two min ago + }; + offenses.push(offense); + offense.createdAt = '-180'; // 3 minutes ago; + offenses.push(offense); + const commentInstance = getComposeCommentWrapper({}).instance(); + expect(commentInstance.shouldShowMuteModal(offenses)).toBe(false); + global.Date.now = realDateNow; + }); + +});