diff options
Diffstat (limited to 'app/javascript/flavours/glitch/features/report')
8 files changed, 531 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/report/category.js b/app/javascript/flavours/glitch/features/report/category.js new file mode 100644 index 000000000..55c43577b --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/category.js @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import Option from './components/option'; +import { List as ImmutableList } from 'immutable'; + +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 repetitive 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' }, +}); + +const mapStateToProps = state => ({ + rules: state.getIn(['server', 'server', 'rules'], ImmutableList()), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Category extends React.PureComponent { + + static propTypes = { + onNextStep: PropTypes.func.isRequired, + rules: ImmutablePropTypes.list, + 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, rules, intl } = this.props; + + const options = rules.size > 0 ? [ + 'spam', + 'violation', + 'other', + ] : [ + 'spam', + '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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/comment.js b/app/javascript/flavours/glitch/features/report/comment.js new file mode 100644 index 000000000..ec261afcb --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/comment.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/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} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button> + </div> + </React.Fragment> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/components/option.js b/app/javascript/flavours/glitch/features/report/components/option.js new file mode 100644 index 000000000..7e94f0654 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/components/option.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Check from 'flavours/glitch/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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js new file mode 100644 index 000000000..2231fc0ce --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import StatusContent from 'flavours/glitch/components/status_content'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import Option from './option'; +import MediaAttachments from 'flavours/glitch/components/media_attachments'; +import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; + +export default class StatusCheckBox extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + status: ImmutablePropTypes.map.isRequired, + checked: PropTypes.bool, + onToggle: PropTypes.func.isRequired, + }; + + handleStatusesToggle = (value, checked) => { + const { onToggle } = this.props; + onToggle(value, checked); + }; + + render () { + const { status, checked } = this.props; + + if (status.get('reblog')) { + return null; + } + + 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')} /> · <VisibilityIcon visibility={status.get('visibility')} /><RelativeTimestamp timestamp={status.get('created_at')} /></div> + </div> + + <StatusContent status={status} media={<MediaAttachments status={status} revealed={false} />} /> + </div> + ); + + return ( + <Option + name='status_ids' + value={status.get('id')} + checked={checked} + onToggle={this.handleStatusesToggle} + label={status.get('search_index')} + labelComponent={labelComponent} + multiple + /> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js new file mode 100644 index 000000000..aa34b3efd --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import StatusCheckBox from '../components/status_check_box'; +import { makeGetStatus } from 'flavours/glitch/selectors'; + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, { id }) => ({ + status: getStatus(state, { id }), + }); + + return mapStateToProps; +}; + +export default connect(makeMapStateToProps)(StatusCheckBox); diff --git a/app/javascript/flavours/glitch/features/report/rules.js b/app/javascript/flavours/glitch/features/report/rules.js new file mode 100644 index 000000000..efcdf1fcf --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/rules.js @@ -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 'flavours/glitch/components/button'; +import Option from './components/option'; + +const mapStateToProps = state => ({ + rules: state.getIn(['server', 'server', '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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/statuses.js b/app/javascript/flavours/glitch/features/report/statuses.js new file mode 100644 index 000000000..47d5ee863 --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/statuses.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container'; +import { OrderedSet } from 'immutable'; +import { FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; + +const mapStateToProps = (state, { accountId }) => ({ + availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])), + isLoading: state.getIn(['timelines', `account:${accountId}:with_replies`, 'isLoading']), +}); + +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, + isLoading: PropTypes.bool, + onToggle: PropTypes.func.isRequired, + }; + + handleNextClick = () => { + const { onNextStep } = this.props; + onNextStep('comment'); + }; + + render () { + const { availableStatusIds, selectedStatusIds, onToggle, isLoading } = 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'> + {isLoading ? <LoadingIndicator /> : 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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/thanks.js b/app/javascript/flavours/glitch/features/report/thanks.js new file mode 100644 index 000000000..454979f9f --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/thanks.js @@ -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 'flavours/glitch/components/button'; +import { connect } from 'react-redux'; +import { + unfollowAccount, + muteAccount, + blockAccount, +} from 'flavours/glitch/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> + ); + } + +} |