Commit 55d69b0e authored by picklesrus's avatar picklesrus

merge

parents 47d63cb5 664ccc5d
This diff is collapsed.
...@@ -20,6 +20,10 @@ class MuteModal extends React.Component { ...@@ -20,6 +20,10 @@ class MuteModal extends React.Component {
'handleNext', 'handleNext',
'handlePrevious' 'handlePrevious'
]); ]);
this.numSteps = 2;
if (this.props.showWarning) {
this.numSteps++;
}
this.state = { this.state = {
step: 0 step: 0
}; };
...@@ -82,6 +86,23 @@ class MuteModal extends React.Component { ...@@ -82,6 +86,23 @@ class MuteModal extends React.Component {
/> />
</p> </p>
</MuteStep> </MuteStep>
{this.props.showWarning ? (
<MuteStep
bottomImg="/svgs/commenting/warning.svg"
bottomImgClass="bottom-img"
header={this.props.intl.formatMessage({id: 'comments.muted.warningBlocked'})}
>
<p>
<FormattedMessage
id="comments.muted.warningCareful"
values={{CommunityGuidelinesLink: (
<a href="/community_guidelines">
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
</a>
)}}
/>
</p>
</MuteStep>) : null}
</Progression> </Progression>
<FlexRow className={classNames('nav-divider')} /> <FlexRow className={classNames('nav-divider')} />
<FlexRow className={classNames('mute-nav')}> <FlexRow className={classNames('mute-nav')}>
...@@ -97,7 +118,7 @@ class MuteModal extends React.Component { ...@@ -97,7 +118,7 @@ class MuteModal extends React.Component {
</div> </div>
</Button> </Button>
) : null } ) : null }
{this.state.step >= 1 ? ( {this.state.step >= this.numSteps - 1 ? (
<Button <Button
className={classNames('close-button')} className={classNames('close-button')}
onClick={this.props.onRequestClose} onClick={this.props.onRequestClose}
...@@ -128,9 +149,10 @@ MuteModal.propTypes = { ...@@ -128,9 +149,10 @@ MuteModal.propTypes = {
muteModalMessages: PropTypes.shape({ muteModalMessages: PropTypes.shape({
commentType: PropTypes.string, commentType: PropTypes.string,
muteStepHeader: PropTypes.string, muteStepHeader: PropTypes.string,
muteStepContent: PropTypes.string muteStepContent: PropTypes.array
}), }),
onRequestClose: PropTypes.func, onRequestClose: PropTypes.func,
showWarning: PropTypes.bool,
timeMuted: PropTypes.string timeMuted: PropTypes.string
}; };
......
...@@ -109,17 +109,20 @@ class UsernameStep extends React.Component { ...@@ -109,17 +109,20 @@ class UsernameStep extends React.Component {
} }
api({ api({
host: '', uri: `/accounts/checkusername/${username}/`
uri: `/accounts/check_username/${username}/`
}, (err, body, res) => { }, (err, body, res) => {
if (err || res.statusCode !== 200) { if (err || res.statusCode !== 200) {
err = err || this.props.intl.formatMessage({id: 'general.error'}); err = err || this.props.intl.formatMessage({id: 'general.error'});
this.form.formsy.updateInputsWithError({all: err}); this.form.formsy.updateInputsWithError({all: err});
return callback(false); return callback(false);
} }
body = body[0]; // get the message in a way that will work for both scratchr2 and api
// versions of the checkusername endpoint
let msg = '';
if (body && body.msg) msg = body.msg;
else if (body && body[0]) msg = body[0].msg;
switch (body.msg) { switch (msg) {
case 'valid username': case 'valid username':
this.setState({ this.setState({
validUsername: 'pass' validUsername: 'pass'
......
...@@ -354,6 +354,9 @@ ...@@ -354,6 +354,9 @@
"comments.muted.moreInfoGuidelines": "If you would like more information, you can read the {CommunityGuidelinesLink}.", "comments.muted.moreInfoGuidelines": "If you would like more information, you can read the {CommunityGuidelinesLink}.",
"comments.muted.moreInfoModal": "For more information, {clickHereLink}.", "comments.muted.moreInfoModal": "For more information, {clickHereLink}.",
"comments.muted.clickHereLinkText": "click here", "comments.muted.clickHereLinkText": "click here",
"comments.muted.warningBlocked": "If you continue to post comments like this, it will cause you to be blocked from using Scratch",
"comments.muted.warningCareful": "We don't want that to happen, so please be careful and make sure you have read and understand the {CommunityGuidelinesLink} before you try to post again!",
"social.embedLabel": "Embed", "social.embedLabel": "Embed",
"social.copyEmbedLinkText": "Copy embed", "social.copyEmbedLinkText": "Copy embed",
......
...@@ -25,6 +25,8 @@ module.exports.validateUsernameRemotely = username => ( ...@@ -25,6 +25,8 @@ module.exports.validateUsernameRemotely = username => (
if (err || res.statusCode !== 200) { if (err || res.statusCode !== 200) {
resolve({requestSucceeded: false, valid: false, errMsgId: 'general.error'}); resolve({requestSucceeded: false, valid: false, errMsgId: 'general.error'});
} }
// get the message in a way that will work for both scratchr2 and api
// versions of the checkusername endpoint
let msg = ''; let msg = '';
if (body && body.msg) msg = body.msg; if (body && body.msg) msg = body.msg;
else if (body && body[0]) msg = body[0].msg; else if (body && body[0]) msg = body[0].msg;
......
...@@ -54,6 +54,11 @@ ...@@ -54,6 +54,11 @@
"userId": 2755634, "userId": 2755634,
"name": "Christan" "name": "Christan"
}, },
{
"userName": "floralsunset",
"userId": 64635632,
"name": "Cindy"
},
{ {
"userName": "codubee", "userName": "codubee",
"userId": 10866958, "userId": 10866958,
......
...@@ -50,7 +50,8 @@ class ComposeComment extends React.Component { ...@@ -50,7 +50,8 @@ class ComposeComment extends React.Component {
error: null, error: null,
appealId: null, appealId: null,
muteOpen: false, muteOpen: false,
muteExpiresAtMs: muteExpiresAtMs muteExpiresAtMs: muteExpiresAtMs,
showWarning: this.props.muteStatus.showWarning ? this.props.muteStatus.showWarning : false
}; };
if (this.isMuted()) { if (this.isMuted()) {
this.setupMuteExpirationTimeout(muteExpiresAtMs); this.setupMuteExpirationTimeout(muteExpiresAtMs);
...@@ -92,12 +93,14 @@ class ComposeComment extends React.Component { ...@@ -92,12 +93,14 @@ class ComposeComment extends React.Component {
let muteOpen = false; let muteOpen = false;
let muteExpiresAtMs = 0; let muteExpiresAtMs = 0;
let rejectedStatus = ComposeStatus.REJECTED; let rejectedStatus = ComposeStatus.REJECTED;
let showWarning = false;
if (body.status && body.status.mute_status) { if (body.status && body.status.mute_status) {
muteExpiresAtMs = body.status.mute_status.muteExpiresAt * 1000; // convert to ms muteExpiresAtMs = body.status.mute_status.muteExpiresAt * 1000; // convert to ms
rejectedStatus = ComposeStatus.REJECTED_MUTE; rejectedStatus = ComposeStatus.REJECTED_MUTE;
if (this.shouldShowMuteModal(body.status.mute_status.offenses)) { if (this.shouldShowMuteModal(body.status.mute_status)) {
muteOpen = true; muteOpen = true;
} }
showWarning = body.status.mute_status.showWarning;
this.setupMuteExpirationTimeout(muteExpiresAtMs); this.setupMuteExpirationTimeout(muteExpiresAtMs);
} }
// Note: does not reset the message state // Note: does not reset the message state
...@@ -106,7 +109,8 @@ class ComposeComment extends React.Component { ...@@ -106,7 +109,8 @@ class ComposeComment extends React.Component {
error: body.rejected, error: body.rejected,
appealId: body.appealId, appealId: body.appealId,
muteOpen: muteOpen, muteOpen: muteOpen,
muteExpiresAtMs: muteExpiresAtMs muteExpiresAtMs: muteExpiresAtMs,
showWarning: showWarning
}); });
return; return;
} }
...@@ -145,8 +149,9 @@ class ComposeComment extends React.Component { ...@@ -145,8 +149,9 @@ class ComposeComment extends React.Component {
muteOpen: true muteOpen: true
}); });
} }
shouldShowMuteModal (offensesList) { shouldShowMuteModal (muteStatus) {
// We should show the mute modal whne the user is newly muted or hasn't seen it for a while. // We should show the mute modal if the user is in danger of being blocked or
// when 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. // 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. // 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 // If a user has more than 1 offense, it means that they have have been muted in the
...@@ -154,10 +159,17 @@ class ComposeComment extends React.Component { ...@@ -154,10 +159,17 @@ class ComposeComment extends React.Component {
// Assumption: The offenses list is ordered by time with the most recent at the end. // 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. // This check is here just in case we somehow get bad data back from a backend.
if (!offensesList) { if (!muteStatus || !muteStatus.offenses) {
return false; return false;
} }
// If the backend tells us to show a warning about getting blocked, we should show the modal
// regardless of what the offenses list looks like.
if (muteStatus.showWarning) {
return true;
}
const offensesList = muteStatus.offenses;
const numOffenses = offensesList.length; const numOffenses = offensesList.length;
// This isn't intended to be called if there are no offenses, but // This isn't intended to be called if there are no offenses, but
// say no just in case. // say no just in case.
...@@ -300,6 +312,7 @@ class ComposeComment extends React.Component { ...@@ -300,6 +312,7 @@ class ComposeComment extends React.Component {
className="mod-mute" className="mod-mute"
muteModalMessages={this.getMuteMessageInfo()} muteModalMessages={this.getMuteMessageInfo()}
shouldCloseOnOverlayClick={false} shouldCloseOnOverlayClick={false}
showWarning={this.state.showWarning}
timeMuted={formatTime.formatRelativeTime(this.state.muteExpiresAtMs, window._locale)} timeMuted={formatTime.formatRelativeTime(this.state.muteExpiresAtMs, window._locale)}
onRequestClose={this.handleMuteClose} onRequestClose={this.handleMuteClose}
/> />
...@@ -313,7 +326,8 @@ ComposeComment.propTypes = { ...@@ -313,7 +326,8 @@ ComposeComment.propTypes = {
commenteeId: PropTypes.number, commenteeId: PropTypes.number,
muteStatus: PropTypes.shape({ muteStatus: PropTypes.shape({
offenses: PropTypes.array, offenses: PropTypes.array,
muteExpiresAt: PropTypes.number muteExpiresAt: PropTypes.number,
showWarning: PropTypes.bool
}), }),
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
...@@ -330,7 +344,7 @@ ComposeComment.propTypes = { ...@@ -330,7 +344,7 @@ ComposeComment.propTypes = {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
muteStatus: state.session.session.permissions.mute_status ? muteStatus: state.session.session.permissions.mute_status ?
state.session.session.permissions.mute_status : state.session.session.permissions.mute_status :
{muteExpiresAt: 0, offenses: []}, {muteExpiresAt: 0, offenses: [], showWarning: false},
user: state.session.session.user user: state.session.session.user
}); });
......
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 180"><defs><style>.cls-1,.cls-2{fill:#ff8c1a;}.cls-1{opacity:0.25;}.cls-3{fill:#fff;}.cls-4{fill:#6e7b8a;opacity:0.5;}</style></defs><path class="cls-1" d="M2.88,80.43c6.16,83.18,50.8,100.79,154.47,99C294.6,177,370.23,188.26,376.72,95.74c5.46-77.83-39.79-93.93-174-95.31C73.35-.9-2.91,2.13,2.88,80.43Z"/><path class="cls-2" d="M214.05,91.28l-118.4-.2H82.85a1.91,1.91,0,0,0-1.31,3.24l7.83,8a1.92,1.92,0,0,1,.54,1.34V122a8.93,8.93,0,0,0,8.81,9H212.2a8.92,8.92,0,0,0,8.8-9V98.42A7,7,0,0,0,214.05,91.28Z"/><path class="cls-3" d="M128.11,50.82l159-.28h17.18A2.69,2.69,0,0,1,306,55.08L295.53,66.33a2.73,2.73,0,0,0-.73,1.88V93.89c0,7-5.29,12.65-11.82,12.65H130.59c-6.53,0-11.82-5.66-11.82-12.65V60.82C118.77,55.3,123,50.82,128.11,50.82Z"/><path class="cls-4" d="M139.57,65.56H264.19a4.32,4.32,0,0,1,4.32,4.32v.69a4.32,4.32,0,0,1-4.32,4.32H138.88a4.32,4.32,0,0,1-4.32-4.32v0A5,5,0,0,1,139.57,65.56Z"/><path class="cls-4" d="M139.57,81.89h82.08a3.61,3.61,0,0,1,3.61,3.61v2.12a3.61,3.61,0,0,1-3.61,3.61H138.17a3.61,3.61,0,0,1-3.61-3.61V86.9a5,5,0,0,1,5-5Z"/></svg>
\ No newline at end of file
...@@ -105,7 +105,8 @@ describe('Compose Comment test', () => { ...@@ -105,7 +105,8 @@ describe('Compose Comment test', () => {
permissions: { permissions: {
mute_status: { mute_status: {
muteExpiresAt: 5, muteExpiresAt: 5,
offenses: [] offenses: [],
showWarning: true
} }
} }
} }
...@@ -117,6 +118,7 @@ describe('Compose Comment test', () => { ...@@ -117,6 +118,7 @@ describe('Compose Comment test', () => {
expect(commentInstance.state.muteExpiresAtMs).toEqual(5 * 1000); expect(commentInstance.state.muteExpiresAtMs).toEqual(5 * 1000);
// Check we setup a timeout to expire the widget when timeout reached. // Check we setup a timeout to expire the widget when timeout reached.
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5 * 1000); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5 * 1000);
expect(commentInstance.state.showWarning).toBe(true);
// Compose box should be hidden if muted unless they got muted due to a comment they just posted. // Compose box should be hidden if muted unless they got muted due to a comment they just posted.
expect(component.find('FlexRow.compose-comment').exists()).toEqual(false); expect(component.find('FlexRow.compose-comment').exists()).toEqual(false);
expect(component.find('MuteModal').exists()).toEqual(false); expect(component.find('MuteModal').exists()).toEqual(false);
...@@ -194,22 +196,61 @@ describe('Compose Comment test', () => { ...@@ -194,22 +196,61 @@ describe('Compose Comment test', () => {
commentInstance.setState({muteOpen: true}); commentInstance.setState({muteOpen: true});
component.update(); component.update();
expect(component.find('MuteModal').exists()).toEqual(true); expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showWarning).toBe(false);
global.Date.now = realDateNow; global.Date.now = realDateNow;
}); });
test('shouldShowMuteModal is false when list is undefined ', () => { test('Mute Modal gets showWarning props from state', () => {
const store = mockStore({
session: {
session: {
user: {},
permissions: {
mute_status: {}
}
}
}
});
const component = mountWithIntl(
<ComposeComment
{...defaultProps()}
/>
, {context: {store}}
);
// set state on the ComposeComment component, not the wrapper
const commentInstance = component.find('ComposeComment').instance();
commentInstance.setState({muteOpen: true});
component.update();
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showWarning).toBe(false);
commentInstance.setState({
muteOpen: true,
showWarning: true
});
component.update();
expect(component.find('MuteModal').props().showWarning).toBe(true);
});
test('shouldShowMuteModal is false when muteStatus is undefined ', () => {
const commentInstance = getComposeCommentWrapper({}).instance(); const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal()).toBe(false); expect(commentInstance.shouldShowMuteModal()).toBe(false);
}); });
test('shouldShowMuteModal is false when list is undefined ', () => {
const muteStatus = {};
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(false);
});
test('shouldShowMuteModal is false when list empty ', () => { test('shouldShowMuteModal is false when list empty ', () => {
const offenses = []; const muteStatus = {
offenses: []
};
const commentInstance = getComposeCommentWrapper({}).instance(); const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(offenses)).toBe(false); expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(false);
}); });
test('shouldShowMuteModal is true when only 1 recent offesnse ', () => { test('shouldShowMuteModal is true when only 1 recent offesnse ', () => {
const offenses = [];
const realDateNow = Date.now.bind(global.Date); const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0; global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make // Since Date.now mocked to 0 above, we just need a small number to make
...@@ -218,9 +259,11 @@ describe('Compose Comment test', () => { ...@@ -218,9 +259,11 @@ describe('Compose Comment test', () => {
expiresAt: '1000', expiresAt: '1000',
createdAt: '-60' // ~1 ago min given shouldShowMuteModal's conversions, createdAt: '-60' // ~1 ago min given shouldShowMuteModal's conversions,
}; };
offenses.push(offense); const muteStatus = {
offenses: [offense]
};
const commentInstance = getComposeCommentWrapper({}).instance(); const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(offenses)).toBe(true); expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(true);
global.Date.now = realDateNow; global.Date.now = realDateNow;
}); });
...@@ -237,8 +280,33 @@ describe('Compose Comment test', () => { ...@@ -237,8 +280,33 @@ describe('Compose Comment test', () => {
offenses.push(offense); offenses.push(offense);
offense.createdAt = '-180'; // 3 minutes ago; offense.createdAt = '-180'; // 3 minutes ago;
offenses.push(offense); offenses.push(offense);
const muteStatus = {
offenses: offenses
};
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(false);
global.Date.now = realDateNow;
});
test('shouldShowMuteModal is true when showWarning is true even with multiple offenses', () => {
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 muteStatus = {
offenses: offenses,
showWarning: true
};
const commentInstance = getComposeCommentWrapper({}).instance(); const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(offenses)).toBe(false); expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(true);
global.Date.now = realDateNow; global.Date.now = realDateNow;
}); });
......
...@@ -33,6 +33,20 @@ describe('MuteModalTest', () => { ...@@ -33,6 +33,20 @@ describe('MuteModalTest', () => {
expect(component.find('button.back-button').exists()).toEqual(false); expect(component.find('button.back-button').exists()).toEqual(false);
}); });
test('Mute Modal shows extra showWarning step', () => {
const component = mountWithIntl(
<MuteModal
showWarning
muteModalMessages={defaultMessages}
/>
);
component.find('MuteModal').instance()
.setState({step: 2});
component.update();
expect(component.find('MuteStep').prop('bottomImg')).toEqual('/svgs/commenting/warning.svg');
expect(component.find('MuteStep').prop('totalSteps')).toEqual(3);
});
test('Mute Modal shows back & close button on last step', () => { test('Mute Modal shows back & close button on last step', () => {
const component = mountWithIntl( const component = mountWithIntl(
<MuteModal muteModalMessages={defaultMessages} /> <MuteModal muteModalMessages={defaultMessages} />
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment