mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 09:35:56 -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 Avatar = require('../../../components/avatar/avatar.jsx');
|
||||||
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
|
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
|
||||||
const Button = require('../../../components/forms/button.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;
|
const connect = require('react-redux').connect;
|
||||||
|
|
||||||
|
@ -24,7 +26,8 @@ const MAX_COMMENT_LENGTH = 500;
|
||||||
const ComposeStatus = keyMirror({
|
const ComposeStatus = keyMirror({
|
||||||
EDITING: null,
|
EDITING: null,
|
||||||
SUBMITTING: null,
|
SUBMITTING: null,
|
||||||
REJECTED: null
|
REJECTED: null,
|
||||||
|
REJECTED_MUTE: null
|
||||||
});
|
});
|
||||||
|
|
||||||
class ComposeComment extends React.Component {
|
class ComposeComment extends React.Component {
|
||||||
|
@ -33,13 +36,17 @@ class ComposeComment extends React.Component {
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handlePost',
|
'handlePost',
|
||||||
'handleCancel',
|
'handleCancel',
|
||||||
'handleInput'
|
'handleInput',
|
||||||
|
'handleMuteClose',
|
||||||
|
'handleMuteOpen'
|
||||||
|
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
message: '',
|
message: '',
|
||||||
status: ComposeStatus.EDITING,
|
status: ComposeStatus.EDITING,
|
||||||
error: null,
|
error: null,
|
||||||
appealId: null
|
appealId: null,
|
||||||
|
muteOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
handleInput (event) {
|
handleInput (event) {
|
||||||
|
@ -67,13 +74,24 @@ class ComposeComment extends React.Component {
|
||||||
if (err || res.statusCode !== 200) {
|
if (err || res.statusCode !== 200) {
|
||||||
body = {rejected: 'error'};
|
body = {rejected: 'error'};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) {
|
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
|
// Note: does not reset the message state
|
||||||
this.setState({
|
this.setState({
|
||||||
status: ComposeStatus.REJECTED,
|
status: rejectedStatus,
|
||||||
error: body.rejected,
|
error: body.rejected,
|
||||||
appealId: body.appealId
|
appealId: body.appealId,
|
||||||
|
muteOpen: muteOpen,
|
||||||
|
muteExpiresAt: muteExpiresAt
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -92,6 +110,44 @@ class ComposeComment extends React.Component {
|
||||||
this.props.onAddComment(body);
|
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 () {
|
handleCancel () {
|
||||||
this.setState({
|
this.setState({
|
||||||
message: '',
|
message: '',
|
||||||
|
@ -103,70 +159,101 @@ class ComposeComment extends React.Component {
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div
|
<React.Fragment>
|
||||||
className="flex-row comment"
|
{this.state.status === ComposeStatus.REJECTED_MUTE ? (
|
||||||
>
|
<FlexRow className="comment">
|
||||||
<a href={`/users/${this.props.user.username}`}>
|
<CommentingStatus>
|
||||||
<Avatar src={this.props.user.thumbnailUrl} />
|
<p>Scratch thinks your comment was disrespectful.</p>
|
||||||
</a>
|
<p>
|
||||||
<FlexRow className="compose-comment column">
|
For the next {this.state.muteExpiresAt} you
|
||||||
{this.state.error ? (
|
won't be able to post comments.
|
||||||
<FlexRow className="compose-error-row">
|
Once {this.state.muteExpiresAt} have passed,
|
||||||
<div className="compose-error-tip">
|
you will be able to comment again.
|
||||||
<FormattedMessage
|
</p>
|
||||||
id={`comments.${this.state.error}`}
|
<p className="bottom-text">For more information,
|
||||||
values={{
|
<a
|
||||||
appealId: this.state.appealId
|
href="#comment"
|
||||||
}}
|
onClick={this.handleMuteOpen}
|
||||||
/>
|
> click here</a>.</p>
|
||||||
</div>
|
</CommentingStatus>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
) : null}
|
) : null }
|
||||||
<Formsy className="full-width-form">
|
<div
|
||||||
<InplaceInput
|
className="flex-row comment"
|
||||||
className={classNames('compose-input',
|
>
|
||||||
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
<a href={`/users/${this.props.user.username}`}>
|
||||||
'compose-valid' : 'compose-invalid')}
|
<Avatar src={this.props.user.thumbnailUrl} />
|
||||||
handleUpdate={onUpdate}
|
</a>
|
||||||
name="compose-comment"
|
<FlexRow className="compose-comment column">
|
||||||
type="textarea"
|
{this.state.error && this.state.status !== ComposeStatus.REJECTED_MUTE ? (
|
||||||
value={this.state.message}
|
<FlexRow className="compose-error-row">
|
||||||
onInput={this.handleInput}
|
<div className="compose-error-tip">
|
||||||
/>
|
<FormattedMessage
|
||||||
<FlexRow className="compose-bottom-row">
|
id={`comments.${this.state.error}`}
|
||||||
<Button
|
values={{
|
||||||
className="compose-post"
|
appealId: this.state.appealId
|
||||||
disabled={this.state.status === ComposeStatus.SUBMITTING}
|
}}
|
||||||
onClick={this.handlePost}
|
/>
|
||||||
>
|
</div>
|
||||||
{this.state.status === ComposeStatus.SUBMITTING ? (
|
</FlexRow>
|
||||||
<FormattedMessage id="comments.posting" />
|
) : null}
|
||||||
) : (
|
<Formsy className="full-width-form">
|
||||||
<FormattedMessage id="comments.post" />
|
<InplaceInput
|
||||||
)}
|
className={classNames('compose-input',
|
||||||
</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 ?
|
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
|
||||||
'compose-valid' : 'compose-invalid')}
|
'compose-valid' : 'compose-invalid')}
|
||||||
>
|
handleUpdate={onUpdate}
|
||||||
<FormattedMessage
|
name="compose-comment"
|
||||||
id="comments.lengthWarning"
|
type="textarea"
|
||||||
values={{
|
value={this.state.message}
|
||||||
remainingCharacters: MAX_COMMENT_LENGTH - this.state.message.length
|
onInput={this.handleInput}
|
||||||
}}
|
/>
|
||||||
/>
|
<FlexRow className="compose-bottom-row">
|
||||||
</span>
|
<Button
|
||||||
</FlexRow>
|
className="compose-post"
|
||||||
</Formsy>
|
disabled={this.state.status === ComposeStatus.SUBMITTING}
|
||||||
</FlexRow>
|
onClick={this.handlePost}
|
||||||
</div>
|
>
|
||||||
|
{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