Merge branch 'timeline-word-filters' into 'develop'
Timeline word filters Closes #14 See merge request soapbox-pub/soapbox-fe!126
This commit is contained in:
commit
9819f1aa73
7 changed files with 368 additions and 12 deletions
|
@ -1,4 +1,5 @@
|
|||
import api from '../api';
|
||||
import { showAlert } from 'soapbox/actions/alerts';
|
||||
|
||||
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
||||
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||
|
@ -8,6 +9,10 @@ export const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
|||
export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
||||
export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
||||
|
||||
export const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||
export const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||
export const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||
|
||||
export const fetchFilters = () => (dispatch, getState) => {
|
||||
if (!getState().get('me')) return;
|
||||
|
||||
|
@ -31,13 +36,33 @@ export const fetchFilters = () => (dispatch, getState) => {
|
|||
}));
|
||||
};
|
||||
|
||||
export function createFilter(params) {
|
||||
export function createFilter(phrase, expires_at, context, whole_word, irreversible) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v1/filters', params).then(response => {
|
||||
return api(getState).post('/api/v1/filters', {
|
||||
phrase,
|
||||
context,
|
||||
irreversible,
|
||||
whole_word,
|
||||
expires_at,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
dispatch(showAlert('', 'Filter added'));
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function deleteFilter(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete('/api/v1/filters/'+id).then(response => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||
dispatch(showAlert('', 'Filter deleted'));
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -168,10 +168,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon id='times-circle' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
||||
</NavLink>
|
||||
{/* <NavLink className='sidebar-menu-item' to='/filters' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/filters' onClick={onClose}>
|
||||
<Icon id='filter' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.filters)}</span>
|
||||
</NavLink> */}
|
||||
</NavLink>
|
||||
{ isStaff && <a className='sidebar-menu-item' href={'/pleroma/admin/'} target='_blank' onClick={onClose}>
|
||||
<Icon id='shield' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.admin_settings)}</span>
|
||||
|
|
|
@ -76,7 +76,7 @@ class ActionBar extends React.PureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
// menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' });
|
||||
menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
||||
if (isStaff) {
|
||||
|
|
|
@ -4,16 +4,55 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import Column from '../ui/components/column';
|
||||
import { fetchFilters } from '../../actions/filters';
|
||||
import { fetchFilters, createFilter, deleteFilter } from '../../actions/filters';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import Button from 'soapbox/components/button';
|
||||
import {
|
||||
SimpleForm,
|
||||
SimpleInput,
|
||||
FieldsGroup,
|
||||
SelectDropdown,
|
||||
Checkbox,
|
||||
} from 'soapbox/features/forms';
|
||||
import { showAlert } from 'soapbox/actions/alerts';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
|
||||
subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
|
||||
keyword: { id: 'column.filters.keyword', defaultMessage: 'Keyword or phrase' },
|
||||
expires: { id: 'column.filters.expires', defaultMessage: 'Expire after' },
|
||||
expires_hint: { id: 'column.filters.expires_hint', defaultMessage: 'Expiration dates are not currently supported' },
|
||||
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
|
||||
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
|
||||
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
|
||||
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
|
||||
drop_header: { id: 'column.filters.drop_header', defaultMessage: 'Drop instead of hide' },
|
||||
drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
|
||||
whole_word_header: { id: 'column.filters.whole_word_header', defaultMessage: 'Whole word' },
|
||||
whole_word_hint: { id: 'column.filters.whole_word_hint', defaultMessage: 'When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word' },
|
||||
add_new: { id: 'column.filters.add_new', defaultMessage: 'Add New Filter' },
|
||||
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
|
||||
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
|
||||
subheading_filters: { id: 'column.filters.subheading_filters', defaultMessage: 'Current Filters' },
|
||||
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const expirations = {
|
||||
null: 'Never',
|
||||
// 3600: '30 minutes',
|
||||
// 21600: '1 hour',
|
||||
// 43200: '12 hours',
|
||||
// 86400 : '1 day',
|
||||
// 604800: '1 week',
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
filters: state.get('filters'),
|
||||
});
|
||||
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Filters extends ImmutablePureComponent {
|
||||
|
@ -24,17 +63,206 @@ class Filters extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
phrase: '',
|
||||
expires_at: '',
|
||||
home_timeline: true,
|
||||
public_timeline: false,
|
||||
notifications: false,
|
||||
conversations: false,
|
||||
irreversible: false,
|
||||
whole_word: true,
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFilters());
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
}
|
||||
|
||||
handleSelectChange = e => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
}
|
||||
|
||||
handleCheckboxChange = e => {
|
||||
this.setState({ [e.target.name]: e.target.checked });
|
||||
}
|
||||
|
||||
handleAddNew = e => {
|
||||
e.preventDefault();
|
||||
const { intl, dispatch } = this.props;
|
||||
const { phrase, whole_word, expires_at, irreversible } = this.state;
|
||||
const { home_timeline, public_timeline, notifications, conversations } = this.state;
|
||||
let context = [];
|
||||
|
||||
if (home_timeline) {
|
||||
context.push('home');
|
||||
};
|
||||
if (public_timeline) {
|
||||
context.push('public');
|
||||
};
|
||||
if (notifications) {
|
||||
context.push('notifications');
|
||||
};
|
||||
if (conversations) {
|
||||
context.push('thread');
|
||||
};
|
||||
|
||||
dispatch(createFilter(phrase, expires_at, context, whole_word, irreversible)).then(response => {
|
||||
return dispatch(fetchFilters());
|
||||
}).catch(error => {
|
||||
dispatch(showAlert('', intl.formatMessage(messages.create_error)));
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterDelete = e => {
|
||||
const { intl, dispatch } = this.props;
|
||||
dispatch(deleteFilter(e.currentTarget.dataset.value)).then(response => {
|
||||
return dispatch(fetchFilters());
|
||||
}).catch(error => {
|
||||
dispatch(showAlert('', intl.formatMessage(messages.delete_error)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { intl, filters } = this.props;
|
||||
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
||||
|
||||
return (
|
||||
<Column icon='filter' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
{emptyMessage}
|
||||
<Column className='filter-settings-panel' icon='filter' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading_add_new)} />
|
||||
<SimpleForm>
|
||||
<div className='filter-settings-panel'>
|
||||
<fieldset disabled={false}>
|
||||
<FieldsGroup>
|
||||
<div className='two-col'>
|
||||
<SimpleInput
|
||||
label={intl.formatMessage(messages.keyword)}
|
||||
required
|
||||
type='text'
|
||||
name='phrase'
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
<div className='input with_label required'>
|
||||
<SelectDropdown
|
||||
label={intl.formatMessage(messages.expires)}
|
||||
hint={intl.formatMessage(messages.expires_hint)}
|
||||
items={expirations}
|
||||
defaultValue={expirations.never}
|
||||
onChange={this.handleSelectChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FieldsGroup>
|
||||
|
||||
<FieldsGroup>
|
||||
<label className='checkboxes required'>
|
||||
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
||||
</label>
|
||||
<span className='hint'>
|
||||
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
||||
</span>
|
||||
<div className='two-col'>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.home_timeline)}
|
||||
name='home_timeline'
|
||||
checked={this.state.home_timeline}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.public_timeline)}
|
||||
name='public_timeline'
|
||||
checked={this.state.public_timeline}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.notifications)}
|
||||
name='notifications'
|
||||
checked={this.state.notifications}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.conversations)}
|
||||
name='conversations'
|
||||
checked={this.state.conversations}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</FieldsGroup>
|
||||
|
||||
<FieldsGroup>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.drop_header)}
|
||||
hint={intl.formatMessage(messages.drop_hint)}
|
||||
name='irreversible'
|
||||
checked={this.state.irreversible}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.whole_word_header)}
|
||||
hint={intl.formatMessage(messages.whole_word_hint)}
|
||||
name='whole_word'
|
||||
checked={this.state.whole_word}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
</FieldsGroup>
|
||||
</fieldset>
|
||||
|
||||
<Button className='button button-primary setup' text={intl.formatMessage(messages.add_new)} onClick={this.handleAddNew} />
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading_filters)} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{filters.map((filter, i) => (
|
||||
<div key={i} className='filter__container'>
|
||||
<div className='filter__details'>
|
||||
<div className='filter__phrase'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' /></span>
|
||||
<span className='filter__list-value'>{filter.get('phrase')}</span>
|
||||
</div>
|
||||
<div className='filter__contexts'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.get('context').map((context, i) => (
|
||||
<span key={i} className='context'>{context}</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
<div className='filter__details'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_details_label' defaultMessage='Filter settings:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.get('irreversible') ?
|
||||
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
|
||||
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
|
||||
}
|
||||
{filter.get('whole_word') &&
|
||||
<span><FormattedMessage id='filters.filters_list_whole-word' defaultMessage='Whole word' /></span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='filter__delete' role='button' tabIndex='0' onClick={this.handleFilterDelete} data-value={filter.get('id')} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='filter__delete-icon' id='times' size={40} />
|
||||
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
</div>
|
||||
</SimpleForm>
|
||||
|
||||
|
||||
|
||||
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ InputContainer.propTypes = {
|
|||
extraClass: PropTypes.string,
|
||||
};
|
||||
|
||||
export const LabelInputContainer = ({ label, children, ...props }) => {
|
||||
export const LabelInputContainer = ({ label, hint, children, ...props }) => {
|
||||
const [id] = useState(uuidv4());
|
||||
const childrenWithProps = React.Children.map(children, child => (
|
||||
React.cloneElement(child, { id: id, key: id })
|
||||
|
@ -48,12 +48,14 @@ export const LabelInputContainer = ({ label, children, ...props }) => {
|
|||
<div className='label_input__wrapper'>
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
{hint && <span className='hint'>{hint}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LabelInputContainer.propTypes = {
|
||||
label: FormPropTypes.label.isRequired,
|
||||
hint: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
|
@ -223,11 +225,12 @@ export class SelectDropdown extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
label: FormPropTypes.label,
|
||||
hint: PropTypes.node,
|
||||
items: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, items, ...props } = this.props;
|
||||
const { label, hint, items, ...props } = this.props;
|
||||
|
||||
const optionElems = Object.keys(items).map(item => (
|
||||
<option key={item} value={item}>{items[item]}</option>
|
||||
|
@ -236,7 +239,7 @@ export class SelectDropdown extends ImmutablePureComponent {
|
|||
const selectElem = <select {...props}>{optionElems}</select>;
|
||||
|
||||
return label ? (
|
||||
<LabelInputContainer label={label}>{selectElem}</LabelInputContainer>
|
||||
<LabelInputContainer label={label} hint={hint}>{selectElem}</LabelInputContainer>
|
||||
) : selectElem;
|
||||
}
|
||||
|
||||
|
|
|
@ -72,4 +72,5 @@
|
|||
@import 'components/video-player';
|
||||
@import 'components/audio-player';
|
||||
@import 'components/profile_hover_card';
|
||||
@import 'components/filters';
|
||||
@import 'components/mfa_form';
|
||||
|
|
99
app/styles/components/filters.scss
Normal file
99
app/styles/components/filters.scss
Normal file
|
@ -0,0 +1,99 @@
|
|||
.filter-settings-panel {
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 400;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.item-list article {
|
||||
border-bottom: 1px solid var(--primary-text-color--faint);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fields-group .two-col {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
div.input {
|
||||
width: 45%;
|
||||
margin-right: 20px;
|
||||
|
||||
.label_input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 485px){
|
||||
div.input {
|
||||
width: 100%;
|
||||
margin-right: 5px;
|
||||
|
||||
.label_input {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter__container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
|
||||
.filter__phrase, .filter__contexts, .filter__details {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
span.filter__list-label {
|
||||
padding-right: 5px;
|
||||
color: var(--primary-text-color--faint);
|
||||
}
|
||||
|
||||
span.filter__list-value span {
|
||||
padding-right: 5px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&::after {
|
||||
content: ',';
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
&::after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter__delete {
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
|
||||
span.filter__delete-label {
|
||||
color: var(--primary-text-color--faint);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.filter__delete-icon {
|
||||
background: none;
|
||||
color: var(--primary-text-color--faint);
|
||||
padding: 0 5px;
|
||||
margin: 0 auto;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue