Change report modal to include category selection in web UI (#17565)
* Change report modal to include category selection in web UI * Various fixes and improvements - Change thank you text to be different based on category - Change starting headline to be different for account and status reports - Change toggle components to have a checkmark when checked - Fix report dialog being cut off on small screens - Fix thank you screen offering mute or block if already muted or blocked - Refactor toggle components in report dialog into one component * Change wording on final screen * Change checkboxes to be square when multiple options are possible
This commit is contained in:
		
							parent
							
								
									1c3e5e44e2
								
							
						
					
					
						commit
						a9a43de6d1
					
				
					 15 changed files with 954 additions and 232 deletions
				
			
		|  | @ -1,89 +1,38 @@ | |||
| import api from '../api'; | ||||
| import { openModal, closeModal } from './modal'; | ||||
| 
 | ||||
| export const REPORT_INIT   = 'REPORT_INIT'; | ||||
| export const REPORT_CANCEL = 'REPORT_CANCEL'; | ||||
| import { openModal } from './modal'; | ||||
| 
 | ||||
| export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; | ||||
| export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; | ||||
| export const REPORT_SUBMIT_FAIL    = 'REPORT_SUBMIT_FAIL'; | ||||
| 
 | ||||
| export const REPORT_STATUS_TOGGLE  = 'REPORT_STATUS_TOGGLE'; | ||||
| export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; | ||||
| export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE'; | ||||
| export const initReport = (account, status) => dispatch => | ||||
|   dispatch(openModal('REPORT', { | ||||
|     accountId: account.get('id'), | ||||
|     statusId: status.get('id'), | ||||
|   })); | ||||
| 
 | ||||
| export function initReport(account, status) { | ||||
|   return dispatch => { | ||||
|     dispatch({ | ||||
|       type: REPORT_INIT, | ||||
|       account, | ||||
|       status, | ||||
|     }); | ||||
| export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { | ||||
|   dispatch(submitReportRequest()); | ||||
| 
 | ||||
|     dispatch(openModal('REPORT')); | ||||
|   }; | ||||
|   api(getState).post('/api/v1/reports', params).then(response => { | ||||
|     dispatch(submitReportSuccess(response.data)); | ||||
|     if (onSuccess) onSuccess(); | ||||
|   }).catch(error => { | ||||
|     dispatch(submitReportFail(error)); | ||||
|     if (onFail) onFail(); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export function cancelReport() { | ||||
|   return { | ||||
|     type: REPORT_CANCEL, | ||||
|   }; | ||||
| }; | ||||
| export const submitReportRequest = () => ({ | ||||
|   type: REPORT_SUBMIT_REQUEST, | ||||
| }); | ||||
| 
 | ||||
| export function toggleStatusReport(statusId, checked) { | ||||
|   return { | ||||
|     type: REPORT_STATUS_TOGGLE, | ||||
|     statusId, | ||||
|     checked, | ||||
|   }; | ||||
| }; | ||||
| export const submitReportSuccess = report => ({ | ||||
|   type: REPORT_SUBMIT_SUCCESS, | ||||
|   report, | ||||
| }); | ||||
| 
 | ||||
| export function submitReport() { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(submitReportRequest()); | ||||
| 
 | ||||
|     api(getState).post('/api/v1/reports', { | ||||
|       account_id: getState().getIn(['reports', 'new', 'account_id']), | ||||
|       status_ids: getState().getIn(['reports', 'new', 'status_ids']), | ||||
|       comment: getState().getIn(['reports', 'new', 'comment']), | ||||
|       forward: getState().getIn(['reports', 'new', 'forward']), | ||||
|     }).then(response => { | ||||
|       dispatch(closeModal()); | ||||
|       dispatch(submitReportSuccess(response.data)); | ||||
|     }).catch(error => dispatch(submitReportFail(error))); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function submitReportRequest() { | ||||
|   return { | ||||
|     type: REPORT_SUBMIT_REQUEST, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function submitReportSuccess(report) { | ||||
|   return { | ||||
|     type: REPORT_SUBMIT_SUCCESS, | ||||
|     report, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function submitReportFail(error) { | ||||
|   return { | ||||
|     type: REPORT_SUBMIT_FAIL, | ||||
|     error, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeReportComment(comment) { | ||||
|   return { | ||||
|     type: REPORT_COMMENT_CHANGE, | ||||
|     comment, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeReportForward(forward) { | ||||
|   return { | ||||
|     type: REPORT_FORWARD_CHANGE, | ||||
|     forward, | ||||
|   }; | ||||
| }; | ||||
| export const submitReportFail = error => ({ | ||||
|   type: REPORT_SUBMIT_FAIL, | ||||
|   error, | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										27
									
								
								app/javascript/mastodon/actions/rules.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/javascript/mastodon/actions/rules.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import api from '../api'; | ||||
| 
 | ||||
| export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST'; | ||||
| export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS'; | ||||
| export const RULES_FETCH_FAIL    = 'RULES_FETCH_FAIL'; | ||||
| 
 | ||||
| export const fetchRules = () => (dispatch, getState) => { | ||||
|   dispatch(fetchRulesRequest()); | ||||
| 
 | ||||
|   api(getState) | ||||
|     .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules))) | ||||
|     .catch(err => dispatch(fetchRulesFail(err))); | ||||
| }; | ||||
| 
 | ||||
| const fetchRulesRequest = () => ({ | ||||
|   type: RULES_FETCH_REQUEST, | ||||
| }); | ||||
| 
 | ||||
| const fetchRulesSuccess = rules => ({ | ||||
|   type: RULES_FETCH_SUCCESS, | ||||
|   rules, | ||||
| }); | ||||
| 
 | ||||
| const fetchRulesFail = error => ({ | ||||
|   type: RULES_FETCH_FAIL, | ||||
|   error, | ||||
| }); | ||||
							
								
								
									
										9
									
								
								app/javascript/mastodon/components/check.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/javascript/mastodon/components/check.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| import React from 'react'; | ||||
| 
 | ||||
| const Check = () => ( | ||||
|   <svg width='14' height='11' viewBox='0 0 14 11'> | ||||
|     <path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' /> | ||||
|   </svg> | ||||
| ); | ||||
| 
 | ||||
| export default Check; | ||||
							
								
								
									
										93
									
								
								app/javascript/mastodon/features/report/category.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								app/javascript/mastodon/features/report/category.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import Button from 'mastodon/components/button'; | ||||
| import Option from './components/option'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, | ||||
|   dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' }, | ||||
|   spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' }, | ||||
|   spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' }, | ||||
|   violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' }, | ||||
|   violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' }, | ||||
|   other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' }, | ||||
|   other_description: { id: 'report.reasons.other_description', defaultMessage: 'The issue does not fit into other categories' }, | ||||
|   status: { id: 'report.category.title_status', defaultMessage: 'post' }, | ||||
|   account: { id: 'report.category.title_account', defaultMessage: 'profile' }, | ||||
| }); | ||||
| 
 | ||||
| export default @injectIntl | ||||
| class Category extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     onNextStep: PropTypes.func.isRequired, | ||||
|     category: PropTypes.string, | ||||
|     onChangeCategory: PropTypes.func.isRequired, | ||||
|     startedFrom: PropTypes.oneOf(['status', 'account']), | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleNextClick = () => { | ||||
|     const { onNextStep, category } = this.props; | ||||
| 
 | ||||
|     switch(category) { | ||||
|     case 'dislike': | ||||
|       onNextStep('thanks'); | ||||
|       break; | ||||
|     case 'violation': | ||||
|       onNextStep('rules'); | ||||
|       break; | ||||
|     default: | ||||
|       onNextStep('statuses'); | ||||
|       break; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   handleCategoryToggle = (value, checked) => { | ||||
|     const { onChangeCategory } = this.props; | ||||
| 
 | ||||
|     if (checked) { | ||||
|       onChangeCategory(value); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { category, startedFrom, intl } = this.props; | ||||
| 
 | ||||
|     const options = [ | ||||
|       'dislike', | ||||
|       'spam', | ||||
|       'violation', | ||||
|       'other', | ||||
|     ]; | ||||
| 
 | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         <h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3> | ||||
|         <p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p> | ||||
| 
 | ||||
|         <div> | ||||
|           {options.map(item => ( | ||||
|             <Option | ||||
|               key={item} | ||||
|               name='category' | ||||
|               value={item} | ||||
|               checked={category === item} | ||||
|               onToggle={this.handleCategoryToggle} | ||||
|               label={intl.formatMessage(messages[item])} | ||||
|               description={intl.formatMessage(messages[`${item}_description`])} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='flex-spacer' /> | ||||
| 
 | ||||
|         <div className='report-dialog-modal__actions'> | ||||
|           <Button onClick={this.handleNextClick} disabled={category === null}><FormattedMessage id='report.next' defaultMessage='Next' /></Button> | ||||
|         </div> | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										83
									
								
								app/javascript/mastodon/features/report/comment.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/javascript/mastodon/features/report/comment.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; | ||||
| import Button from 'mastodon/components/button'; | ||||
| import Toggle from 'react-toggle'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' }, | ||||
| }); | ||||
| 
 | ||||
| export default @injectIntl | ||||
| class Comment extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     onSubmit: PropTypes.func.isRequired, | ||||
|     comment: PropTypes.string.isRequired, | ||||
|     onChangeComment: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     isSubmitting: PropTypes.bool, | ||||
|     forward: PropTypes.bool, | ||||
|     isRemote: PropTypes.bool, | ||||
|     domain: PropTypes.string, | ||||
|     onChangeForward: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleClick = () => { | ||||
|     const { onSubmit } = this.props; | ||||
|     onSubmit(); | ||||
|   }; | ||||
| 
 | ||||
|   handleChange = e => { | ||||
|     const { onChangeComment } = this.props; | ||||
|     onChangeComment(e.target.value); | ||||
|   }; | ||||
| 
 | ||||
|   handleKeyDown = e => { | ||||
|     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { | ||||
|       this.handleClick(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   handleForwardChange = e => { | ||||
|     const { onChangeForward } = this.props; | ||||
|     onChangeForward(e.target.checked); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         <h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3> | ||||
| 
 | ||||
|         <textarea | ||||
|           className='report-dialog-modal__textarea' | ||||
|           placeholder={intl.formatMessage(messages.placeholder)} | ||||
|           value={comment} | ||||
|           onChange={this.handleChange} | ||||
|           onKeyDown={this.handleKeyDown} | ||||
|           disabled={isSubmitting} | ||||
|         /> | ||||
| 
 | ||||
|         {isRemote && ( | ||||
|           <React.Fragment> | ||||
|             <p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p> | ||||
| 
 | ||||
|             <label className='report-dialog-modal__toggle'> | ||||
|               <Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} /> | ||||
|               <FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /> | ||||
|             </label> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
| 
 | ||||
|         <div className='flex-spacer' /> | ||||
| 
 | ||||
|         <div className='report-dialog-modal__actions'> | ||||
|           <Button onClick={this.handleClick}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button> | ||||
|         </div> | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										60
									
								
								app/javascript/mastodon/features/report/components/option.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/javascript/mastodon/features/report/components/option.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import classNames from 'classnames'; | ||||
| import Check from 'mastodon/components/check'; | ||||
| 
 | ||||
| export default class Option extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     name: PropTypes.string.isRequired, | ||||
|     value: PropTypes.string.isRequired, | ||||
|     checked: PropTypes.bool, | ||||
|     label: PropTypes.node, | ||||
|     description: PropTypes.node, | ||||
|     onToggle: PropTypes.func, | ||||
|     multiple: PropTypes.bool, | ||||
|     labelComponent: PropTypes.node, | ||||
|   }; | ||||
| 
 | ||||
|   handleKeyPress = e => { | ||||
|     const { value, checked, onToggle } = this.props; | ||||
| 
 | ||||
|     if (e.key === 'Enter' || e.key === ' ') { | ||||
|       e.stopPropagation(); | ||||
|       e.preventDefault(); | ||||
|       onToggle(value, !checked); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleChange = e => { | ||||
|     const { value, onToggle } = this.props; | ||||
|     onToggle(value, e.target.checked); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { name, value, checked, label, labelComponent, description, multiple } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <label className='dialog-option poll__option selectable'> | ||||
|         <input type={multiple ? 'checkbox' : 'radio'} name={name} value={value} checked={checked} onChange={this.handleChange} /> | ||||
| 
 | ||||
|         <span | ||||
|           className={classNames('poll__input', { active: checked, checkbox: multiple })} | ||||
|           tabIndex='0' | ||||
|           role='radio' | ||||
|           onKeyPress={this.handleKeyPress} | ||||
|           aria-checked={checked} | ||||
|           aria-label={label} | ||||
|         >{checked && <Check />}</span> | ||||
| 
 | ||||
|         {labelComponent ? labelComponent : ( | ||||
|           <span className='poll__option__text'> | ||||
|             <strong>{label}</strong> | ||||
|             {description} | ||||
|           </span> | ||||
|         )} | ||||
|       </label> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,23 +1,32 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import Toggle from 'react-toggle'; | ||||
| import noop from 'lodash/noop'; | ||||
| import StatusContent from '../../../components/status_content'; | ||||
| import { MediaGallery, Video } from '../../ui/util/async-components'; | ||||
| import Bundle from '../../ui/components/bundle'; | ||||
| import StatusContent from 'mastodon/components/status_content'; | ||||
| import { MediaGallery, Video } from 'mastodon/features/ui/util/async-components'; | ||||
| import Bundle from 'mastodon/features/ui/components/bundle'; | ||||
| import Avatar from 'mastodon/components/avatar'; | ||||
| import DisplayName from 'mastodon/components/display_name'; | ||||
| import RelativeTimestamp from 'mastodon/components/relative_timestamp'; | ||||
| import Option from './option'; | ||||
| 
 | ||||
| export default class StatusCheckBox extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     id: PropTypes.string.isRequired, | ||||
|     status: ImmutablePropTypes.map.isRequired, | ||||
|     checked: PropTypes.bool, | ||||
|     onToggle: PropTypes.func.isRequired, | ||||
|     disabled: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   handleStatusesToggle = (value, checked) => { | ||||
|     const { onToggle } = this.props; | ||||
|     onToggle(value, checked); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { status, checked, onToggle, disabled } = this.props; | ||||
|     const { status, checked } = this.props; | ||||
| 
 | ||||
|     let media = null; | ||||
| 
 | ||||
|     if (status.get('reblog')) { | ||||
|  | @ -50,24 +59,46 @@ export default class StatusCheckBox extends React.PureComponent { | |||
|       } else { | ||||
|         media = ( | ||||
|           <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > | ||||
|             {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />} | ||||
|             {Component => ( | ||||
|               <Component | ||||
|                 media={status.get('media_attachments')} | ||||
|                 sensitive={status.get('sensitive')} | ||||
|                 height={110} | ||||
|                 onOpenMedia={noop} | ||||
|               /> | ||||
|             )} | ||||
|           </Bundle> | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='status-check-box'> | ||||
|         <div className='status-check-box__status'> | ||||
|           <StatusContent status={status} /> | ||||
|           {media} | ||||
|     const labelComponent = ( | ||||
|       <div className='status-check-box__status poll__option__text'> | ||||
|         <div className='detailed-status__display-name'> | ||||
|           <div className='detailed-status__display-avatar'> | ||||
|             <Avatar account={status.get('account')} size={46} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='status-check-box-toggle'> | ||||
|           <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> | ||||
|         </div> | ||||
|         <StatusContent status={status} /> | ||||
| 
 | ||||
|         {media} | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <Option | ||||
|         name='status_ids' | ||||
|         value={status.get('id')} | ||||
|         checked={checked} | ||||
|         onToggle={this.handleStatusesToggle} | ||||
|         label={status.get('search_index')} | ||||
|         labelComponent={labelComponent} | ||||
|         multiple | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,19 +1,15 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import StatusCheckBox from '../components/status_check_box'; | ||||
| import { toggleStatusReport } from '../../../actions/reports'; | ||||
| import { Set as ImmutableSet } from 'immutable'; | ||||
| import { makeGetStatus } from 'mastodon/selectors'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { id }) => ({ | ||||
|   status: state.getIn(['statuses', id]), | ||||
|   checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id), | ||||
| }); | ||||
| const makeMapStateToProps = () => { | ||||
|   const getStatus = makeGetStatus(); | ||||
| 
 | ||||
| const mapDispatchToProps = (dispatch, { id }) => ({ | ||||
|   const mapStateToProps = (state, { id }) => ({ | ||||
|     status: getStatus(state, { id }), | ||||
|   }); | ||||
| 
 | ||||
|   onToggle (e) { | ||||
|     dispatch(toggleStatusReport(id, e.target.checked)); | ||||
|   }, | ||||
|   return mapStateToProps; | ||||
| }; | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox); | ||||
| export default connect(makeMapStateToProps)(StatusCheckBox); | ||||
|  |  | |||
							
								
								
									
										64
									
								
								app/javascript/mastodon/features/report/rules.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/javascript/mastodon/features/report/rules.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import Button from 'mastodon/components/button'; | ||||
| import Option from './components/option'; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   rules: state.get('rules'), | ||||
| }); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
| class Rules extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     onNextStep: PropTypes.func.isRequired, | ||||
|     rules: ImmutablePropTypes.list, | ||||
|     selectedRuleIds: ImmutablePropTypes.set.isRequired, | ||||
|     onToggle: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleNextClick = () => { | ||||
|     const { onNextStep } = this.props; | ||||
|     onNextStep('statuses'); | ||||
|   }; | ||||
| 
 | ||||
|   handleRulesToggle = (value, checked) => { | ||||
|     const { onToggle } = this.props; | ||||
|     onToggle(value, checked); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { rules, selectedRuleIds } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         <h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3> | ||||
|         <p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p> | ||||
| 
 | ||||
|         <div> | ||||
|           {rules.map(item => ( | ||||
|             <Option | ||||
|               key={item.get('id')} | ||||
|               name='rule_ids' | ||||
|               value={item.get('id')} | ||||
|               checked={selectedRuleIds.includes(item.get('id'))} | ||||
|               onToggle={this.handleRulesToggle} | ||||
|               label={item.get('text')} | ||||
|               multiple | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='flex-spacer' /> | ||||
| 
 | ||||
|         <div className='report-dialog-modal__actions'> | ||||
|           <Button onClick={this.handleNextClick} disabled={selectedRuleIds.size < 1}><FormattedMessage id='report.next' defaultMessage='Next' /></Button> | ||||
|         </div> | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										58
									
								
								app/javascript/mastodon/features/report/statuses.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								app/javascript/mastodon/features/report/statuses.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { connect } from 'react-redux'; | ||||
| import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container'; | ||||
| import { OrderedSet } from 'immutable'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import Button from 'mastodon/components/button'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { accountId }) => ({ | ||||
|   availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])), | ||||
| }); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
| class Statuses extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     onNextStep: PropTypes.func.isRequired, | ||||
|     accountId: PropTypes.string.isRequired, | ||||
|     availableStatusIds: ImmutablePropTypes.set.isRequired, | ||||
|     selectedStatusIds: ImmutablePropTypes.set.isRequired, | ||||
|     onToggle: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleNextClick = () => { | ||||
|     const { onNextStep } = this.props; | ||||
|     onNextStep('comment'); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { availableStatusIds, selectedStatusIds, onToggle } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         <h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3> | ||||
|         <p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p> | ||||
| 
 | ||||
|         <div className='report-dialog-modal__statuses'> | ||||
|           {availableStatusIds.union(selectedStatusIds).map(statusId => ( | ||||
|             <StatusCheckBox | ||||
|               id={statusId} | ||||
|               key={statusId} | ||||
|               checked={selectedStatusIds.includes(statusId)} | ||||
|               onToggle={onToggle} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='flex-spacer' /> | ||||
| 
 | ||||
|         <div className='report-dialog-modal__actions'> | ||||
|           <Button onClick={this.handleNextClick}><FormattedMessage id='report.next' defaultMessage='Next' /></Button> | ||||
|         </div> | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										84
									
								
								app/javascript/mastodon/features/report/thanks.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								app/javascript/mastodon/features/report/thanks.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import Button from 'mastodon/components/button'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { | ||||
|   unfollowAccount, | ||||
|   muteAccount, | ||||
|   blockAccount, | ||||
| } from 'mastodon/actions/accounts'; | ||||
| 
 | ||||
| const mapStateToProps = () => ({}); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
| class Thanks extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     submitted: PropTypes.bool, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleCloseClick = () => { | ||||
|     const { onClose } = this.props; | ||||
|     onClose(); | ||||
|   }; | ||||
| 
 | ||||
|   handleUnfollowClick = () => { | ||||
|     const { dispatch, account, onClose } = this.props; | ||||
|     dispatch(unfollowAccount(account.get('id'))); | ||||
|     onClose(); | ||||
|   }; | ||||
| 
 | ||||
|   handleMuteClick = () => { | ||||
|     const { dispatch, account, onClose } = this.props; | ||||
|     dispatch(muteAccount(account.get('id'))); | ||||
|     onClose(); | ||||
|   }; | ||||
| 
 | ||||
|   handleBlockClick = () => { | ||||
|     const { dispatch, account, onClose } = this.props; | ||||
|     dispatch(blockAccount(account.get('id'))); | ||||
|     onClose(); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, submitted } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         <h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3> | ||||
|         <p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p> | ||||
| 
 | ||||
|         {account.getIn(['relationship', 'following']) && ( | ||||
|           <React.Fragment> | ||||
|             <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='report.unfollow' defaultMessage='Unfollow @{name}' values={{ name: account.get('username') }} /></h4> | ||||
|             <p className='report-dialog-modal__lead'><FormattedMessage id='report.unfollow_explanation' defaultMessage='You are following this account. To not see their posts in your home feed anymore, unfollow them.' /></p> | ||||
|             <Button secondary onClick={this.handleUnfollowClick}><FormattedMessage id='account.unfollow' defaultMessage='Unfollow' /></Button> | ||||
|             <hr /> | ||||
|           </React.Fragment> | ||||
|         )} | ||||
| 
 | ||||
|         <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.mute' defaultMessage='Mute @{name}' values={{ name: account.get('username') }} /></h4> | ||||
|         <p className='report-dialog-modal__lead'><FormattedMessage id='report.mute_explanation' defaultMessage='You will not see their posts. They can still follow you and see your posts and will not know that they are muted.' /></p> | ||||
|         <Button secondary onClick={this.handleMuteClick}>{!account.getIn(['relationship', 'muting']) ? <FormattedMessage id='report.mute' defaultMessage='Mute' /> : <FormattedMessage id='account.muted' defaultMessage='Muted' />}</Button> | ||||
| 
 | ||||
|         <hr /> | ||||
| 
 | ||||
|         <h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.block' defaultMessage='Block @{name}' values={{ name: account.get('username') }} /></h4> | ||||
|         <p className='report-dialog-modal__lead'><FormattedMessage id='report.block_explanation' defaultMessage='You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.' /></p> | ||||
|         <Button secondary onClick={this.handleBlockClick}>{!account.getIn(['relationship', 'blocking']) ? <FormattedMessage id='report.block' defaultMessage='Block' /> : <FormattedMessage id='account.blocked' defaultMessage='Blocked' />}</Button> | ||||
| 
 | ||||
|         <div className='flex-spacer' /> | ||||
| 
 | ||||
|         <div className='report-dialog-modal__actions'> | ||||
|           <Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button> | ||||
|         </div> | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,38 +1,31 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports'; | ||||
| import { expandAccountTimeline } from '../../../actions/timelines'; | ||||
| import { submitReport } from 'mastodon/actions/reports'; | ||||
| import { expandAccountTimeline } from 'mastodon/actions/timelines'; | ||||
| import { fetchRules } from 'mastodon/actions/rules'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { makeGetAccount } from '../../../selectors'; | ||||
| import { makeGetAccount } from 'mastodon/selectors'; | ||||
| import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; | ||||
| import StatusCheckBox from '../../report/containers/status_check_box_container'; | ||||
| import { OrderedSet } from 'immutable'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import Button from '../../../components/button'; | ||||
| import Toggle from 'react-toggle'; | ||||
| import IconButton from '../../../components/icon_button'; | ||||
| import IconButton from 'mastodon/components/icon_button'; | ||||
| import Category from 'mastodon/features/report/category'; | ||||
| import Statuses from 'mastodon/features/report/statuses'; | ||||
| import Rules from 'mastodon/features/report/rules'; | ||||
| import Comment from 'mastodon/features/report/comment'; | ||||
| import Thanks from 'mastodon/features/report/thanks'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
|   placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, | ||||
|   submit: { id: 'report.submit', defaultMessage: 'Submit' }, | ||||
| }); | ||||
| 
 | ||||
| const makeMapStateToProps = () => { | ||||
|   const getAccount = makeGetAccount(); | ||||
| 
 | ||||
|   const mapStateToProps = state => { | ||||
|     const accountId = state.getIn(['reports', 'new', 'account_id']); | ||||
| 
 | ||||
|     return { | ||||
|       isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), | ||||
|       account: getAccount(state, accountId), | ||||
|       comment: state.getIn(['reports', 'new', 'comment']), | ||||
|       forward: state.getIn(['reports', 'new', 'forward']), | ||||
|       statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), | ||||
|     }; | ||||
|   }; | ||||
|   const mapStateToProps = (state, { accountId }) => ({ | ||||
|     account: getAccount(state, accountId), | ||||
|   }); | ||||
| 
 | ||||
|   return mapStateToProps; | ||||
| }; | ||||
|  | @ -42,92 +35,182 @@ export default @connect(makeMapStateToProps) | |||
| class ReportModal extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     isSubmitting: PropTypes.bool, | ||||
|     account: ImmutablePropTypes.map, | ||||
|     statusIds: ImmutablePropTypes.orderedSet.isRequired, | ||||
|     comment: PropTypes.string.isRequired, | ||||
|     forward: PropTypes.bool, | ||||
|     accountId: PropTypes.string.isRequired, | ||||
|     statusId: PropTypes.string, | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleCommentChange = e => { | ||||
|     this.props.dispatch(changeReportComment(e.target.value)); | ||||
|   } | ||||
| 
 | ||||
|   handleForwardChange = e => { | ||||
|     this.props.dispatch(changeReportForward(e.target.checked)); | ||||
|   } | ||||
|   state = { | ||||
|     step: 'category', | ||||
|     selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []), | ||||
|     comment: '', | ||||
|     category: null, | ||||
|     selectedRuleIds: OrderedSet(), | ||||
|     forward: true, | ||||
|     isSubmitting: false, | ||||
|     isSubmitted: false, | ||||
|   }; | ||||
| 
 | ||||
|   handleSubmit = () => { | ||||
|     this.props.dispatch(submitReport()); | ||||
|   } | ||||
|     const { dispatch, accountId } = this.props; | ||||
|     const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state; | ||||
| 
 | ||||
|   handleKeyDown = e => { | ||||
|     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { | ||||
|       this.handleSubmit(); | ||||
|     this.setState({ isSubmitting: true }); | ||||
| 
 | ||||
|     dispatch(submitReport({ | ||||
|       account_id: accountId, | ||||
|       status_ids: selectedStatusIds.toArray(), | ||||
|       comment, | ||||
|       forward, | ||||
|       category, | ||||
|       rule_ids: selectedRuleIds.toArray(), | ||||
|     }, this.handleSuccess, this.handleFail)); | ||||
|   }; | ||||
| 
 | ||||
|   handleSuccess = () => { | ||||
|     this.setState({ isSubmitting: false, isSubmitted: true, step: 'thanks' }); | ||||
|   }; | ||||
| 
 | ||||
|   handleFail = () => { | ||||
|     this.setState({ isSubmitting: false }); | ||||
|   }; | ||||
| 
 | ||||
|   handleStatusToggle = (statusId, checked) => { | ||||
|     const { selectedStatusIds } = this.state; | ||||
| 
 | ||||
|     if (checked) { | ||||
|       this.setState({ selectedStatusIds: selectedStatusIds.add(statusId) }); | ||||
|     } else { | ||||
|       this.setState({ selectedStatusIds: selectedStatusIds.remove(statusId) }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   handleRuleToggle = (ruleId, checked) => { | ||||
|     const { selectedRuleIds } = this.state; | ||||
| 
 | ||||
|     if (checked) { | ||||
|       this.setState({ selectedRuleIds: selectedRuleIds.add(ruleId) }); | ||||
|     } else { | ||||
|       this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleChangeCategory = category => { | ||||
|     this.setState({ category }); | ||||
|   }; | ||||
| 
 | ||||
|   handleChangeComment = comment => { | ||||
|     this.setState({ comment }); | ||||
|   }; | ||||
| 
 | ||||
|   handleChangeForward = forward => { | ||||
|     this.setState({ forward }); | ||||
|   }; | ||||
| 
 | ||||
|   handleNextStep = step => { | ||||
|     this.setState({ step }); | ||||
|   }; | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); | ||||
|   } | ||||
|     const { dispatch, accountId } = this.props; | ||||
| 
 | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (this.props.account !== nextProps.account && nextProps.account) { | ||||
|       this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); | ||||
|     } | ||||
|     dispatch(expandAccountTimeline(accountId, { withReplies: true })); | ||||
|     dispatch(fetchRules()); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props; | ||||
|     const { | ||||
|       accountId, | ||||
|       account, | ||||
|       intl, | ||||
|       onClose, | ||||
|     } = this.props; | ||||
| 
 | ||||
|     if (!account) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     const domain = account.get('acct').split('@')[1]; | ||||
|     const { | ||||
|       step, | ||||
|       selectedStatusIds, | ||||
|       selectedRuleIds, | ||||
|       comment, | ||||
|       forward, | ||||
|       category, | ||||
|       isSubmitting, | ||||
|       isSubmitted, | ||||
|     } = this.state; | ||||
| 
 | ||||
|     const domain   = account.get('acct').split('@')[1]; | ||||
|     const isRemote = !!domain; | ||||
| 
 | ||||
|     let stepComponent; | ||||
| 
 | ||||
|     switch(step) { | ||||
|     case 'category': | ||||
|       stepComponent = ( | ||||
|         <Category | ||||
|           onNextStep={this.handleNextStep} | ||||
|           startedFrom={this.props.statusId ? 'status' : 'account'} | ||||
|           category={category} | ||||
|           onChangeCategory={this.handleChangeCategory} | ||||
|         /> | ||||
|       ); | ||||
|       break; | ||||
|     case 'rules': | ||||
|       stepComponent = ( | ||||
|         <Rules | ||||
|           onNextStep={this.handleNextStep} | ||||
|           selectedRuleIds={selectedRuleIds} | ||||
|           onToggle={this.handleRuleToggle} | ||||
|         /> | ||||
|       ); | ||||
|       break; | ||||
|     case 'statuses': | ||||
|       stepComponent = ( | ||||
|         <Statuses | ||||
|           onNextStep={this.handleNextStep} | ||||
|           accountId={accountId} | ||||
|           selectedStatusIds={selectedStatusIds} | ||||
|           onToggle={this.handleStatusToggle} | ||||
|         /> | ||||
|       ); | ||||
|       break; | ||||
|     case 'comment': | ||||
|       stepComponent = ( | ||||
|         <Comment | ||||
|           onSubmit={this.handleSubmit} | ||||
|           isSubmitting={isSubmitting} | ||||
|           isRemote={isRemote} | ||||
|           comment={comment} | ||||
|           forward={forward} | ||||
|           domain={domain} | ||||
|           onChangeComment={this.handleChangeComment} | ||||
|           onChangeForward={this.handleChangeForward} | ||||
|         /> | ||||
|       ); | ||||
|       break; | ||||
|     case 'thanks': | ||||
|       stepComponent = ( | ||||
|         <Thanks | ||||
|           submitted={isSubmitted} | ||||
|           account={account} | ||||
|           onClose={onClose} | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='modal-root__modal report-modal'> | ||||
|       <div className='modal-root__modal report-dialog-modal'> | ||||
|         <div className='report-modal__target'> | ||||
|           <IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} /> | ||||
|           <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='report-modal__container'> | ||||
|           <div className='report-modal__comment'> | ||||
|             <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p> | ||||
| 
 | ||||
|             <textarea | ||||
|               className='setting-text light' | ||||
|               placeholder={intl.formatMessage(messages.placeholder)} | ||||
|               value={comment} | ||||
|               onChange={this.handleCommentChange} | ||||
|               onKeyDown={this.handleKeyDown} | ||||
|               disabled={isSubmitting} | ||||
|               autoFocus | ||||
|             /> | ||||
| 
 | ||||
|             {domain && ( | ||||
|               <div> | ||||
|                 <p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p> | ||||
| 
 | ||||
|                 <div className='setting-toggle'> | ||||
|                   <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} /> | ||||
|                   <label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label> | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className='report-modal__statuses'> | ||||
|             <div> | ||||
|               {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} | ||||
|             </div> | ||||
|           </div> | ||||
|         <div className='report-dialog-modal__container'> | ||||
|           {stepComponent} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -17,7 +17,8 @@ import status_lists from './status_lists'; | |||
| import mutes from './mutes'; | ||||
| import blocks from './blocks'; | ||||
| import boosts from './boosts'; | ||||
| import reports from './reports'; | ||||
| // import reports from './reports';
 | ||||
| import rules from './rules'; | ||||
| import contexts from './contexts'; | ||||
| import compose from './compose'; | ||||
| import search from './search'; | ||||
|  | @ -61,7 +62,8 @@ const reducers = { | |||
|   mutes, | ||||
|   blocks, | ||||
|   boosts, | ||||
|   reports, | ||||
|   // reports,
 | ||||
|   rules, | ||||
|   contexts, | ||||
|   compose, | ||||
|   search, | ||||
|  |  | |||
							
								
								
									
										13
									
								
								app/javascript/mastodon/reducers/rules.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/javascript/mastodon/reducers/rules.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| import { RULES_FETCH_SUCCESS } from 'mastodon/actions/rules'; | ||||
| import { List as ImmutableList, fromJS } from 'immutable'; | ||||
| 
 | ||||
| const initialState = ImmutableList(); | ||||
| 
 | ||||
| export default function rules(state = initialState, action) { | ||||
|   switch (action.type) { | ||||
|   case RULES_FETCH_SUCCESS: | ||||
|     return fromJS(action.rules); | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
| } | ||||
|  | @ -50,16 +50,14 @@ | |||
|   cursor: pointer; | ||||
|   display: inline-block; | ||||
|   font-family: inherit; | ||||
|   font-size: 14px; | ||||
|   font-size: 17px; | ||||
|   font-weight: 500; | ||||
|   height: 36px; | ||||
|   letter-spacing: 0; | ||||
|   line-height: 36px; | ||||
|   line-height: 22px; | ||||
|   overflow: hidden; | ||||
|   padding: 0 16px; | ||||
|   padding: 7px 18px; | ||||
|   position: relative; | ||||
|   text-align: center; | ||||
|   text-transform: uppercase; | ||||
|   text-decoration: none; | ||||
|   text-overflow: ellipsis; | ||||
|   transition: all 100ms ease-in; | ||||
|  | @ -100,17 +98,6 @@ | |||
|     outline: 0 !important; | ||||
|   } | ||||
| 
 | ||||
|   &.button-primary, | ||||
|   &.button-alternative, | ||||
|   &.button-secondary, | ||||
|   &.button-alternative-2 { | ||||
|     font-size: 16px; | ||||
|     line-height: 36px; | ||||
|     height: auto; | ||||
|     text-transform: none; | ||||
|     padding: 4px 16px; | ||||
|   } | ||||
| 
 | ||||
|   &.button-alternative { | ||||
|     color: $inverted-text-color; | ||||
|     background: $ui-primary-color; | ||||
|  | @ -135,7 +122,7 @@ | |||
|   &.button-secondary { | ||||
|     color: $darker-text-color; | ||||
|     background: transparent; | ||||
|     padding: 3px 15px; | ||||
|     padding: 6px 17px; | ||||
|     border: 1px solid $ui-primary-color; | ||||
| 
 | ||||
|     &:active, | ||||
|  | @ -1114,42 +1101,39 @@ | |||
|   font-size: 15px; | ||||
| } | ||||
| 
 | ||||
| .status-check-box { | ||||
|   border-bottom: 1px solid $ui-secondary-color; | ||||
|   display: flex; | ||||
| .status-check-box__status { | ||||
|   display: block; | ||||
|   box-sizing: border-box; | ||||
|   width: 100%; | ||||
|   padding: 0 10px; | ||||
| 
 | ||||
|   .status-check-box__status { | ||||
|     margin: 10px 0 10px 10px; | ||||
|     flex: 1; | ||||
|     overflow: hidden; | ||||
|   .detailed-status__display-name { | ||||
|     color: lighten($inverted-text-color, 16%); | ||||
| 
 | ||||
|     .media-gallery { | ||||
|       max-width: 250px; | ||||
|     span { | ||||
|       display: inline; | ||||
|     } | ||||
| 
 | ||||
|     .status__content { | ||||
|       padding: 0; | ||||
|       white-space: normal; | ||||
|     } | ||||
| 
 | ||||
|     .video-player, | ||||
|     .audio-player { | ||||
|       margin-top: 8px; | ||||
|       max-width: 250px; | ||||
|     } | ||||
| 
 | ||||
|     .media-gallery__item-thumbnail { | ||||
|       cursor: default; | ||||
|     &:hover strong { | ||||
|       text-decoration: none; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .status-check-box-toggle { | ||||
|   align-items: center; | ||||
|   display: flex; | ||||
|   flex: 0 0 auto; | ||||
|   justify-content: center; | ||||
|   padding: 10px; | ||||
|   .media-gallery, | ||||
|   .audio-player, | ||||
|   .video-player { | ||||
|     margin-top: 8px; | ||||
|     max-width: 250px; | ||||
|   } | ||||
| 
 | ||||
|   .status__content { | ||||
|     padding: 0; | ||||
|     white-space: normal; | ||||
|   } | ||||
| 
 | ||||
|   .media-gallery__item-thumbnail { | ||||
|     cursor: default; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .status__prepend { | ||||
|  | @ -5103,6 +5087,192 @@ a.status-card.compact:hover { | |||
|   max-width: 700px; | ||||
| } | ||||
| 
 | ||||
| .report-dialog-modal { | ||||
|   max-width: 90vw; | ||||
|   width: 480px; | ||||
|   height: 80vh; | ||||
|   background: lighten($ui-secondary-color, 8%); | ||||
|   color: $inverted-text-color; | ||||
|   border-radius: 8px; | ||||
|   overflow: hidden; | ||||
|   position: relative; | ||||
|   flex-direction: column; | ||||
|   display: flex; | ||||
| 
 | ||||
|   &__container { | ||||
|     box-sizing: border-box; | ||||
|     border-top: 1px solid $ui-secondary-color; | ||||
|     padding: 20px; | ||||
|     flex-grow: 1; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     min-height: 0; | ||||
|     overflow: auto; | ||||
|   } | ||||
| 
 | ||||
|   &__title { | ||||
|     font-size: 28px; | ||||
|     line-height: 33px; | ||||
|     font-weight: 700; | ||||
|     margin-bottom: 15px; | ||||
| 
 | ||||
|     @media screen and (max-height: 800px) { | ||||
|       font-size: 22px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &__subtitle { | ||||
|     font-size: 17px; | ||||
|     font-weight: 600; | ||||
|     line-height: 22px; | ||||
|     margin-bottom: 4px; | ||||
|   } | ||||
| 
 | ||||
|   &__lead { | ||||
|     font-size: 17px; | ||||
|     line-height: 22px; | ||||
|     color: lighten($inverted-text-color, 16%); | ||||
|     margin-bottom: 30px; | ||||
|   } | ||||
| 
 | ||||
|   &__actions { | ||||
|     margin-top: 30px; | ||||
|     display: flex; | ||||
| 
 | ||||
|     .button { | ||||
|       flex: 1 1 auto; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &__statuses { | ||||
|     flex-grow: 1; | ||||
|     min-height: 0; | ||||
|     overflow: auto; | ||||
|   } | ||||
| 
 | ||||
|   .status__content a { | ||||
|     color: $highlight-text-color; | ||||
|   } | ||||
| 
 | ||||
|   .status__content, | ||||
|   .status__content p { | ||||
|     color: $inverted-text-color; | ||||
|   } | ||||
| 
 | ||||
|   .dialog-option .poll__input { | ||||
|     border-color: $inverted-text-color; | ||||
|     color: $ui-secondary-color; | ||||
|     display: inline-flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| 
 | ||||
|     svg { | ||||
|       width: 8px; | ||||
|       height: auto; | ||||
|     } | ||||
| 
 | ||||
|     &:active, | ||||
|     &:focus, | ||||
|     &:hover { | ||||
|       border-color: lighten($inverted-text-color, 15%); | ||||
|       border-width: 4px; | ||||
|     } | ||||
| 
 | ||||
|     &.active { | ||||
|       border-color: $inverted-text-color; | ||||
|       background: $inverted-text-color; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .poll__option.dialog-option { | ||||
|     padding: 15px 0; | ||||
|     flex: 0 0 auto; | ||||
|     border-bottom: 1px solid $ui-secondary-color; | ||||
| 
 | ||||
|     &:last-child { | ||||
|       border-bottom: 0; | ||||
|     } | ||||
| 
 | ||||
|     & > .poll__option__text { | ||||
|       font-size: 13px; | ||||
|       color: lighten($inverted-text-color, 16%); | ||||
| 
 | ||||
|       strong { | ||||
|         font-size: 17px; | ||||
|         font-weight: 500; | ||||
|         line-height: 22px; | ||||
|         color: $inverted-text-color; | ||||
|         display: block; | ||||
|         margin-bottom: 4px; | ||||
| 
 | ||||
|         &:last-child { | ||||
|           margin-bottom: 0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .flex-spacer { | ||||
|     background: transparent; | ||||
|   } | ||||
| 
 | ||||
|   &__textarea { | ||||
|     display: block; | ||||
|     box-sizing: border-box; | ||||
|     width: 100%; | ||||
|     margin: 0; | ||||
|     color: $inverted-text-color; | ||||
|     background: $simple-background-color; | ||||
|     padding: 10px; | ||||
|     font-family: inherit; | ||||
|     font-size: 17px; | ||||
|     line-height: 22px; | ||||
|     resize: vertical; | ||||
|     border: 0; | ||||
|     outline: 0; | ||||
|     border-radius: 4px; | ||||
|     margin: 20px 0; | ||||
| 
 | ||||
|     &::placeholder { | ||||
|       color: $dark-text-color; | ||||
|     } | ||||
| 
 | ||||
|     &:focus { | ||||
|       outline: 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &__toggle { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| 
 | ||||
|     & > span { | ||||
|       font-size: 17px; | ||||
|       font-weight: 500; | ||||
|       margin-left: 10px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .button.button-secondary { | ||||
|     border-color: $inverted-text-color; | ||||
|     color: $inverted-text-color; | ||||
|     flex: 0 0 auto; | ||||
| 
 | ||||
|     &:hover, | ||||
|     &:focus, | ||||
|     &:active { | ||||
|       border-color: lighten($inverted-text-color, 15%); | ||||
|       color: lighten($inverted-text-color, 15%); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   hr { | ||||
|     border: 0; | ||||
|     background: transparent; | ||||
|     margin: 15px 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .report-modal__container { | ||||
|   display: flex; | ||||
|   border-top: 1px solid $ui-secondary-color; | ||||
|  |  | |||
		Reference in a new issue