-
- {media}
+ const labelComponent = (
+
);
+
+ return (
+
+ );
}
}
diff --git a/app/javascript/mastodon/features/report/containers/status_check_box_container.js b/app/javascript/mastodon/features/report/containers/status_check_box_container.js
index 48cd0319b..65a7c11fd 100644
--- a/app/javascript/mastodon/features/report/containers/status_check_box_container.js
+++ b/app/javascript/mastodon/features/report/containers/status_check_box_container.js
@@ -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);
diff --git a/app/javascript/mastodon/features/report/rules.js b/app/javascript/mastodon/features/report/rules.js
new file mode 100644
index 000000000..f2db0d9e4
--- /dev/null
+++ b/app/javascript/mastodon/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 '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 (
+
+
+
+
+
+ {rules.map(item => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/report/statuses.js b/app/javascript/mastodon/features/report/statuses.js
new file mode 100644
index 000000000..5999a0e06
--- /dev/null
+++ b/app/javascript/mastodon/features/report/statuses.js
@@ -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 (
+
+
+
+
+
+ {availableStatusIds.union(selectedStatusIds).map(statusId => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/report/thanks.js b/app/javascript/mastodon/features/report/thanks.js
new file mode 100644
index 000000000..d169b1e32
--- /dev/null
+++ b/app/javascript/mastodon/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 '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 (
+
+ {submitted ? : }
+ {submitted ? : }
+
+ {account.getIn(['relationship', 'following']) && (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js
index f4f0a3884..744dd248b 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.js
@@ -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 = (
+
+ );
+ break;
+ case 'rules':
+ stepComponent = (
+
+ );
+ break;
+ case 'statuses':
+ stepComponent = (
+
+ );
+ break;
+ case 'comment':
+ stepComponent = (
+
+ );
+ break;
+ case 'thanks':
+ stepComponent = (
+
+ );
+ }
return (
-
+
{account.get('acct')} }} />
-
-
-
-
-
-
- {domain && (
-
- )}
-
-
-
-
-
-
- {statusIds.map(statusId => )}
-
-
+
+ {stepComponent}
);
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index af2ef595e..ce4ef991d 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -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,
diff --git a/app/javascript/mastodon/reducers/rules.js b/app/javascript/mastodon/reducers/rules.js
new file mode 100644
index 000000000..c1180b520
--- /dev/null
+++ b/app/javascript/mastodon/reducers/rules.js
@@ -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;
+ }
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 591f2fad1..6d30bea83 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -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;