mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 15:47:53 -05:00
Merge pull request #4616 from picklesrus/mute-behavior
Skeleton of code to show mute modal and comment status.
This commit is contained in:
commit
2005b23777
2 changed files with 288 additions and 68 deletions
|
@ -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 (
|
||||
<div
|
||||
className="flex-row comment"
|
||||
>
|
||||
<a href={`/users/${this.props.user.username}`}>
|
||||
<Avatar src={this.props.user.thumbnailUrl} />
|
||||
</a>
|
||||
<FlexRow className="compose-comment column">
|
||||
{this.state.error ? (
|
||||
<FlexRow className="compose-error-row">
|
||||
<div className="compose-error-tip">
|
||||
<FormattedMessage
|
||||
id={`comments.${this.state.error}`}
|
||||
values={{
|
||||
appealId: this.state.appealId
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FlexRow>
|
||||
) : null}
|
||||
<Formsy className="full-width-form">
|
||||
<InplaceInput
|
||||
className={classNames('compose-input',
|
||||
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||
'compose-valid' : 'compose-invalid')}
|
||||
handleUpdate={onUpdate}
|
||||
name="compose-comment"
|
||||
type="textarea"
|
||||
value={this.state.message}
|
||||
onInput={this.handleInput}
|
||||
/>
|
||||
<FlexRow className="compose-bottom-row">
|
||||
<Button
|
||||
className="compose-post"
|
||||
disabled={this.state.status === ComposeStatus.SUBMITTING}
|
||||
onClick={this.handlePost}
|
||||
>
|
||||
{this.state.status === ComposeStatus.SUBMITTING ? (
|
||||
<FormattedMessage id="comments.posting" />
|
||||
) : (
|
||||
<FormattedMessage id="comments.post" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="compose-cancel"
|
||||
onClick={this.handleCancel}
|
||||
>
|
||||
<FormattedMessage id="comments.cancel" />
|
||||
</Button>
|
||||
<span
|
||||
className={classNames('compose-limit',
|
||||
<React.Fragment>
|
||||
{this.state.status === ComposeStatus.REJECTED_MUTE ? (
|
||||
<FlexRow className="comment">
|
||||
<CommentingStatus>
|
||||
<p>Scratch thinks your comment was disrespectful.</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p className="bottom-text">For more information,
|
||||
<a
|
||||
href="#comment"
|
||||
onClick={this.handleMuteOpen}
|
||||
> click here</a>.</p>
|
||||
</CommentingStatus>
|
||||
</FlexRow>
|
||||
) : null }
|
||||
<div
|
||||
className="flex-row comment"
|
||||
>
|
||||
<a href={`/users/${this.props.user.username}`}>
|
||||
<Avatar src={this.props.user.thumbnailUrl} />
|
||||
</a>
|
||||
<FlexRow className="compose-comment column">
|
||||
{this.state.error && this.state.status !== ComposeStatus.REJECTED_MUTE ? (
|
||||
<FlexRow className="compose-error-row">
|
||||
<div className="compose-error-tip">
|
||||
<FormattedMessage
|
||||
id={`comments.${this.state.error}`}
|
||||
values={{
|
||||
appealId: this.state.appealId
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FlexRow>
|
||||
) : null}
|
||||
<Formsy className="full-width-form">
|
||||
<InplaceInput
|
||||
className={classNames('compose-input',
|
||||
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||
'compose-valid' : 'compose-invalid')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="comments.lengthWarning"
|
||||
values={{
|
||||
remainingCharacters: MAX_COMMENT_LENGTH - this.state.message.length
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</FlexRow>
|
||||
</Formsy>
|
||||
</FlexRow>
|
||||
</div>
|
||||
handleUpdate={onUpdate}
|
||||
name="compose-comment"
|
||||
type="textarea"
|
||||
value={this.state.message}
|
||||
onInput={this.handleInput}
|
||||
/>
|
||||
<FlexRow className="compose-bottom-row">
|
||||
<Button
|
||||
className="compose-post"
|
||||
disabled={this.state.status === ComposeStatus.SUBMITTING}
|
||||
onClick={this.handlePost}
|
||||
>
|
||||
{this.state.status === ComposeStatus.SUBMITTING ? (
|
||||
<FormattedMessage id="comments.posting" />
|
||||
) : (
|
||||
<FormattedMessage id="comments.post" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="compose-cancel"
|
||||
onClick={this.handleCancel}
|
||||
>
|
||||
<FormattedMessage id="comments.cancel" />
|
||||
</Button>
|
||||
<span
|
||||
className={classNames('compose-limit',
|
||||
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||
'compose-valid' : 'compose-invalid')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="comments.lengthWarning"
|
||||
values={{
|
||||
remainingCharacters: MAX_COMMENT_LENGTH - this.state.message.length
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</FlexRow>
|
||||
</Formsy>
|
||||
</FlexRow>
|
||||
{this.state.muteOpen ? (
|
||||
<MuteModal
|
||||
isOpen
|
||||
showCloseButton
|
||||
useStandardSizes
|
||||
className="mod-mute"
|
||||
shouldCloseOnOverlayClick={false}
|
||||
timeMuted={this.state.muteExpiresAt}
|
||||
onRequestClose={this.handleMuteClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
133
test/unit/components/compose-comment.test.jsx
Normal file
133
test/unit/components/compose-comment.test.jsx
Normal file
|
@ -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(
|
||||
<ComposeComment
|
||||
{...defaultProps()}
|
||||
{...props}
|
||||
|
||||
/>
|
||||
, {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;
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in a new issue