Merge branch 'ts' into 'develop'
TypeScript, React.FC See merge request soapbox-pub/soapbox-fe!1524
This commit is contained in:
commit
7ff991f8b3
|
@ -10,7 +10,7 @@ describe('<AutosuggestEmoji />', () => {
|
||||||
colons: ':foobar:',
|
colons: ':foobar:',
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<AutosuggestEmoji emoji={emoji} />);
|
render(<AutosuggestEmoji emoji={emoji as any} />);
|
||||||
|
|
||||||
expect(screen.getByTestId('emoji')).toHaveTextContent('foobar');
|
expect(screen.getByTestId('emoji')).toHaveTextContent('foobar');
|
||||||
expect(screen.getByRole('img').getAttribute('src')).not.toBe('http://example.com/emoji.png');
|
expect(screen.getByRole('img').getAttribute('src')).not.toBe('http://example.com/emoji.png');
|
||||||
|
@ -24,7 +24,7 @@ describe('<AutosuggestEmoji />', () => {
|
||||||
colons: ':foobar:',
|
colons: ':foobar:',
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<AutosuggestEmoji emoji={emoji} />);
|
render(<AutosuggestEmoji emoji={emoji as any} />);
|
||||||
|
|
||||||
expect(screen.getByTestId('emoji')).toHaveTextContent('foobar');
|
expect(screen.getByTestId('emoji')).toHaveTextContent('foobar');
|
||||||
expect(screen.getByRole('img').getAttribute('src')).toBe('http://example.com/emoji.png');
|
expect(screen.getByRole('img').getAttribute('src')).toBe('http://example.com/emoji.png');
|
|
@ -68,9 +68,9 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||||
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.get<string | undefined>('accountId', undefined));
|
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined);
|
||||||
const account = useAppSelector(state => accountId && getAccount(state, accountId));
|
const account = useAppSelector(state => accountId && getAccount(state, accountId));
|
||||||
const targetRef = useAppSelector(state => state.profile_hover_card.getIn(['ref', 'current']) as Element | null);
|
const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current);
|
||||||
const badges = account ? getBadges(account) : [];
|
const badges = account ? getBadges(account) : [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ interface IModal {
|
||||||
/** Callback when the modal is cancelled. */
|
/** Callback when the modal is cancelled. */
|
||||||
cancelAction?: () => void,
|
cancelAction?: () => void,
|
||||||
/** Cancel button text. */
|
/** Cancel button text. */
|
||||||
cancelText?: string,
|
cancelText?: React.ReactNode,
|
||||||
/** URL to an SVG icon for the close button. */
|
/** URL to an SVG icon for the close button. */
|
||||||
closeIcon?: string,
|
closeIcon?: string,
|
||||||
/** Position of the close button. */
|
/** Position of the close button. */
|
||||||
|
|
|
@ -9,13 +9,13 @@ import type { Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IReplyIndicator {
|
interface IReplyIndicator {
|
||||||
status?: Status,
|
status?: Status,
|
||||||
onCancel: () => void,
|
onCancel?: () => void,
|
||||||
hideActions: boolean,
|
hideActions: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCancel }) => {
|
const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCancel }) => {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
onCancel();
|
onCancel!();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
|
@ -23,7 +23,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCanc
|
||||||
}
|
}
|
||||||
|
|
||||||
let actions = {};
|
let actions = {};
|
||||||
if (!hideActions) {
|
if (!hideActions && onCancel) {
|
||||||
actions = {
|
actions = {
|
||||||
onActionClick: handleClick,
|
onActionClick: handleClick,
|
||||||
actionIcon: require('@tabler/icons/icons/x.svg'),
|
actionIcon: require('@tabler/icons/icons/x.svg'),
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { getTitle } from '../utils/coin_db';
|
||||||
|
|
||||||
import CryptoIcon from './crypto_icon';
|
import CryptoIcon from './crypto_icon';
|
||||||
|
|
||||||
interface ICryptoAddress {
|
export interface ICryptoAddress {
|
||||||
address: string,
|
address: string,
|
||||||
ticker: string,
|
ticker: string,
|
||||||
note?: string,
|
note?: string,
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
// The output of this module is designed to mimic emoji-mart's
|
// The output of this module is designed to mimic emoji-mart's
|
||||||
// "data" object, such that we can use it for a light version of emoji-mart's
|
// "data" object, such that we can use it for a light version of emoji-mart's
|
||||||
// emojiIndex.search functionality.
|
// emojiIndex.search functionality.
|
||||||
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
|
import emojiCompressed from './emoji_compressed';
|
||||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
|
|
||||||
const emojis = {};
|
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
|
||||||
|
|
||||||
|
const emojis: Record<string, any> = {};
|
||||||
|
|
||||||
// decompress
|
// decompress
|
||||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||||
const [
|
const [
|
||||||
filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
|
_filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
searchData,
|
searchData,
|
||||||
] = shortCodesToEmojiData[shortCode];
|
] = shortCodesToEmojiData[shortCode];
|
||||||
const [
|
const [
|
||||||
|
@ -27,7 +29,14 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
|
emojis,
|
||||||
|
skins,
|
||||||
|
categories,
|
||||||
|
short_names,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
emojis,
|
emojis,
|
||||||
skins,
|
skins,
|
||||||
categories,
|
categories,
|
|
@ -1,257 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filters';
|
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
|
||||||
import { Button, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
|
||||||
import {
|
|
||||||
FieldsGroup,
|
|
||||||
Checkbox,
|
|
||||||
} from 'soapbox/features/forms';
|
|
||||||
|
|
||||||
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 {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
params: PropTypes.object.isRequired,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
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;
|
|
||||||
const 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(intl, phrase, expires_at, context, whole_word, irreversible)).then(response => {
|
|
||||||
return dispatch(fetchFilters());
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(snackbar.error(intl.formatMessage(messages.create_error)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFilterDelete = e => {
|
|
||||||
const { intl, dispatch } = this.props;
|
|
||||||
dispatch(deleteFilter(intl, e.currentTarget.dataset.value)).then(response => {
|
|
||||||
return dispatch(fetchFilters());
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(snackbar.error(intl.formatMessage(messages.delete_error)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { intl, filters } = this.props;
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column className='filter-settings-panel' icon='filter' label={intl.formatMessage(messages.heading)}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle title={intl.formatMessage(messages.subheading_add_new)} />
|
|
||||||
</CardHeader>
|
|
||||||
<Form onSubmit={this.handleAddNew}>
|
|
||||||
<FormGroup labelText={intl.formatMessage(messages.keyword)}>
|
|
||||||
<Input
|
|
||||||
required
|
|
||||||
type='text'
|
|
||||||
name='phrase'
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
|
|
||||||
<SelectDropdown
|
|
||||||
items={expirations}
|
|
||||||
defaultValue={expirations.never}
|
|
||||||
onChange={this.handleSelectChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> */}
|
|
||||||
|
|
||||||
<FieldsGroup>
|
|
||||||
<Text tag='label'>
|
|
||||||
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
|
||||||
</Text>
|
|
||||||
<Text theme='muted' size='xs'>
|
|
||||||
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
|
||||||
</Text>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<FormActions>
|
|
||||||
<Button type='submit' theme='primary'>{intl.formatMessage(messages.add_new)}</Button>
|
|
||||||
</FormActions>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle title={intl.formatMessage(messages.subheading_filters)} />
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,229 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filters';
|
||||||
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
|
import { Button, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||||
|
import {
|
||||||
|
FieldsGroup,
|
||||||
|
Checkbox,
|
||||||
|
} from 'soapbox/features/forms';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
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 Filters = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const filters = useAppSelector((state) => state.filters);
|
||||||
|
|
||||||
|
const [phrase, setPhrase] = useState('');
|
||||||
|
const [expiresAt] = useState('');
|
||||||
|
const [homeTimeline, setHomeTimeline] = useState(true);
|
||||||
|
const [publicTimeline, setPublicTimeline] = useState(false);
|
||||||
|
const [notifications, setNotifications] = useState(false);
|
||||||
|
const [conversations, setConversations] = useState(false);
|
||||||
|
const [irreversible, setIrreversible] = useState(false);
|
||||||
|
const [wholeWord, setWholeWord] = useState(true);
|
||||||
|
|
||||||
|
// const handleSelectChange = e => {
|
||||||
|
// this.setState({ [e.target.name]: e.target.value });
|
||||||
|
// };
|
||||||
|
|
||||||
|
const handleAddNew: React.FormEventHandler = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const context = [];
|
||||||
|
|
||||||
|
if (homeTimeline) {
|
||||||
|
context.push('home');
|
||||||
|
}
|
||||||
|
if (publicTimeline) {
|
||||||
|
context.push('public');
|
||||||
|
}
|
||||||
|
if (notifications) {
|
||||||
|
context.push('notifications');
|
||||||
|
}
|
||||||
|
if (conversations) {
|
||||||
|
context.push('thread');
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(createFilter(intl, phrase, expiresAt, context, wholeWord, irreversible)).then(() => {
|
||||||
|
return dispatch(fetchFilters());
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(snackbar.error(intl.formatMessage(messages.create_error)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterDelete: React.MouseEventHandler<HTMLDivElement> = e => {
|
||||||
|
dispatch(deleteFilter(intl, e.currentTarget.dataset.value)).then(() => {
|
||||||
|
return dispatch(fetchFilters());
|
||||||
|
}).catch(() => {
|
||||||
|
dispatch(snackbar.error(intl.formatMessage(messages.delete_error)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchFilters());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column className='filter-settings-panel' label={intl.formatMessage(messages.heading)}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle title={intl.formatMessage(messages.subheading_add_new)} />
|
||||||
|
</CardHeader>
|
||||||
|
<Form onSubmit={handleAddNew}>
|
||||||
|
<FormGroup labelText={intl.formatMessage(messages.keyword)}>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
type='text'
|
||||||
|
name='phrase'
|
||||||
|
onChange={({ target }) => setPhrase(target.value)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
|
||||||
|
<SelectDropdown
|
||||||
|
items={expirations}
|
||||||
|
defaultValue={expirations.never}
|
||||||
|
onChange={this.handleSelectChange}
|
||||||
|
/>
|
||||||
|
</FormGroup> */}
|
||||||
|
|
||||||
|
<FieldsGroup>
|
||||||
|
<Text tag='label'>
|
||||||
|
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
||||||
|
</Text>
|
||||||
|
<Text theme='muted' size='xs'>
|
||||||
|
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
||||||
|
</Text>
|
||||||
|
<div className='two-col'>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.home_timeline)}
|
||||||
|
name='home_timeline'
|
||||||
|
checked={homeTimeline}
|
||||||
|
onChange={({ target }) => setHomeTimeline(target.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.public_timeline)}
|
||||||
|
name='public_timeline'
|
||||||
|
checked={publicTimeline}
|
||||||
|
onChange={({ target }) => setPublicTimeline(target.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.notifications)}
|
||||||
|
name='notifications'
|
||||||
|
checked={notifications}
|
||||||
|
onChange={({ target }) => setNotifications(target.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.conversations)}
|
||||||
|
name='conversations'
|
||||||
|
checked={conversations}
|
||||||
|
onChange={({ target }) => setConversations(target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</FieldsGroup>
|
||||||
|
|
||||||
|
<FieldsGroup>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.drop_header)}
|
||||||
|
hint={intl.formatMessage(messages.drop_hint)}
|
||||||
|
name='irreversible'
|
||||||
|
checked={irreversible}
|
||||||
|
onChange={({ target }) => setIrreversible(target.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.whole_word_header)}
|
||||||
|
hint={intl.formatMessage(messages.whole_word_hint)}
|
||||||
|
name='whole_word'
|
||||||
|
checked={wholeWord}
|
||||||
|
onChange={({ target }) => setWholeWord(target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldsGroup>
|
||||||
|
|
||||||
|
<FormActions>
|
||||||
|
<Button type='submit' theme='primary'>{intl.formatMessage(messages.add_new)}</Button>
|
||||||
|
</FormActions>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle title={intl.formatMessage(messages.subheading_filters)} />
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<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.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.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.irreversible ?
|
||||||
|
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
|
||||||
|
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
|
||||||
|
}
|
||||||
|
{filter.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={handleFilterDelete} data-value={filter.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>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Filters;
|
|
@ -13,7 +13,7 @@ import { buildStatus } from '../builder';
|
||||||
|
|
||||||
import ScheduledStatusActionBar from './scheduled_status_action_bar';
|
import ScheduledStatusActionBar from './scheduled_status_action_bar';
|
||||||
|
|
||||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity, Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IScheduledStatus {
|
interface IScheduledStatus {
|
||||||
statusId: string,
|
statusId: string,
|
||||||
|
@ -55,7 +55,7 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status.poll && <PollPreview poll={status.poll} />}
|
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
|
||||||
|
|
||||||
<ScheduledStatusActionBar status={status} {...other} />
|
<ScheduledStatusActionBar status={status} {...other} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import { Modal, Stack, Text } from 'soapbox/components/ui';
|
|
||||||
import ReplyIndicator from 'soapbox/features/compose/components/reply_indicator';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @injectIntl @withRouter
|
|
||||||
class BoostModal extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
status: ImmutablePropTypes.record.isRequired,
|
|
||||||
onReblog: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleReblog = () => {
|
|
||||||
this.props.onReblog(this.props.status);
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onClose();
|
|
||||||
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStatusClick = (e) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onClose();
|
|
||||||
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.get('url')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { status, intl } = this.props;
|
|
||||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title='Repost?'
|
|
||||||
confirmationAction={this.handleReblog}
|
|
||||||
confirmationText={intl.formatMessage(buttonText)}
|
|
||||||
>
|
|
||||||
<Stack space={4}>
|
|
||||||
<ReplyIndicator status={status} hideActions />
|
|
||||||
|
|
||||||
<Text>
|
|
||||||
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import { Modal, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import ReplyIndicator from 'soapbox/features/compose/components/reply_indicator';
|
||||||
|
|
||||||
|
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||||
|
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IBoostModal {
|
||||||
|
status: StatusEntity,
|
||||||
|
onReblog: (status: StatusEntity) => void,
|
||||||
|
onClose: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BoostModal: React.FC<IBoostModal> = ({ status, onReblog, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleReblog = () => {
|
||||||
|
onReblog(status);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonText = status.reblogged ? messages.cancel_reblog : messages.reblog;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title='Repost?'
|
||||||
|
confirmationAction={handleReblog}
|
||||||
|
confirmationText={intl.formatMessage(buttonText)}
|
||||||
|
>
|
||||||
|
<Stack space={4}>
|
||||||
|
<ReplyIndicator status={status} hideActions />
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoostModal;
|
|
@ -1,84 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Modal } from 'soapbox/components/ui';
|
|
||||||
import { SimpleForm, FieldsGroup, Checkbox } from 'soapbox/features/forms';
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
class ConfirmationModal extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
heading: PropTypes.node,
|
|
||||||
icon: PropTypes.node,
|
|
||||||
message: PropTypes.node.isRequired,
|
|
||||||
confirm: PropTypes.node.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onConfirm: PropTypes.func.isRequired,
|
|
||||||
secondary: PropTypes.string,
|
|
||||||
onSecondary: PropTypes.func,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onCancel: PropTypes.func,
|
|
||||||
checkbox: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
checked: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
this.props.onClose('CONFIRM');
|
|
||||||
this.props.onConfirm();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSecondary = () => {
|
|
||||||
this.props.onClose('CONFIRM');
|
|
||||||
this.props.onSecondary();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel = () => {
|
|
||||||
const { onClose, onCancel } = this.props;
|
|
||||||
onClose('CONFIRM');
|
|
||||||
if (onCancel) onCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCheckboxChange = e => {
|
|
||||||
this.setState({ checked: e.target.checked });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { heading, message, confirm, secondary, checkbox } = this.props;
|
|
||||||
const { checked } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={heading}
|
|
||||||
confirmationAction={this.handleClick}
|
|
||||||
confirmationText={confirm}
|
|
||||||
confirmationDisabled={checkbox && !checked}
|
|
||||||
confirmationTheme='danger'
|
|
||||||
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
|
|
||||||
cancelAction={this.handleCancel}
|
|
||||||
secondaryText={secondary}
|
|
||||||
secondaryAction={this.props.onSecondary && this.handleSecondary}
|
|
||||||
>
|
|
||||||
<p className='text-gray-600 dark:text-gray-300'>{message}</p>
|
|
||||||
|
|
||||||
<div className='mt-2'>
|
|
||||||
{checkbox && <div className='confirmation-modal__checkbox'>
|
|
||||||
<SimpleForm>
|
|
||||||
<FieldsGroup>
|
|
||||||
<Checkbox
|
|
||||||
onChange={this.handleCheckboxChange}
|
|
||||||
label={checkbox}
|
|
||||||
checked={checked}
|
|
||||||
/>
|
|
||||||
</FieldsGroup>
|
|
||||||
</SimpleForm>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Modal } from 'soapbox/components/ui';
|
||||||
|
import { SimpleForm, FieldsGroup, Checkbox } from 'soapbox/features/forms';
|
||||||
|
|
||||||
|
interface IConfirmationModal {
|
||||||
|
heading: React.ReactNode,
|
||||||
|
message: React.ReactNode,
|
||||||
|
confirm: React.ReactNode,
|
||||||
|
onClose: (type: string) => void,
|
||||||
|
onConfirm: () => void,
|
||||||
|
secondary: React.ReactNode,
|
||||||
|
onSecondary?: () => void,
|
||||||
|
onCancel: () => void,
|
||||||
|
checkbox?: JSX.Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmationModal: React.FC<IConfirmationModal> = ({
|
||||||
|
heading,
|
||||||
|
message,
|
||||||
|
confirm,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
secondary,
|
||||||
|
onSecondary,
|
||||||
|
onCancel,
|
||||||
|
checkbox,
|
||||||
|
}) => {
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onClose('CONFIRM');
|
||||||
|
onConfirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondary = () => {
|
||||||
|
onClose('CONFIRM');
|
||||||
|
onSecondary!();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onClose('CONFIRM');
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||||
|
setChecked(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={heading}
|
||||||
|
confirmationAction={handleClick}
|
||||||
|
confirmationText={confirm}
|
||||||
|
confirmationDisabled={checkbox && !checked}
|
||||||
|
confirmationTheme='danger'
|
||||||
|
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
|
||||||
|
cancelAction={handleCancel}
|
||||||
|
secondaryText={secondary}
|
||||||
|
secondaryAction={onSecondary && handleSecondary}
|
||||||
|
>
|
||||||
|
<p className='text-gray-600 dark:text-gray-300'>{message}</p>
|
||||||
|
|
||||||
|
<div className='mt-2'>
|
||||||
|
{checkbox && <div className='confirmation-modal__checkbox'>
|
||||||
|
<SimpleForm>
|
||||||
|
<FieldsGroup>
|
||||||
|
<Checkbox
|
||||||
|
onChange={handleCheckboxChange}
|
||||||
|
label={checkbox}
|
||||||
|
checked={checked}
|
||||||
|
/>
|
||||||
|
</FieldsGroup>
|
||||||
|
</SimpleForm>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmationModal;
|
|
@ -1,22 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import DetailedCryptoAddress from 'soapbox/features/crypto_donate/components/detailed_crypto_address';
|
|
||||||
|
|
||||||
export default class CryptoDonateModal extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
address: PropTypes.string.isRequired,
|
|
||||||
ticker: PropTypes.string.isRequired,
|
|
||||||
note: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal crypto-donate-modal'>
|
|
||||||
<DetailedCryptoAddress {...this.props} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import DetailedCryptoAddress from 'soapbox/features/crypto_donate/components/detailed_crypto_address';
|
||||||
|
|
||||||
|
import type { ICryptoAddress } from '../../crypto_donate/components/crypto_address';
|
||||||
|
|
||||||
|
const CryptoDonateModal: React.FC<ICryptoAddress> = (props) => {
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal crypto-donate-modal'>
|
||||||
|
<DetailedCryptoAddress {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CryptoDonateModal;
|
|
@ -1,56 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import { fetchLists } from 'soapbox/actions/lists';
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
|
|
||||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
|
||||||
if (!lists) {
|
|
||||||
return lists;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
lists: getOrderedLists(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default @withRouter
|
|
||||||
@connect(mapStateToProps)
|
|
||||||
class ListPanel extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
lists: ImmutablePropTypes.list,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(fetchLists());
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { lists } = this.props;
|
|
||||||
|
|
||||||
if (!lists || lists.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
{lists.map(list => (
|
|
||||||
<NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { fetchLists } from 'soapbox/actions/lists';
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
import type { RootState } from 'soapbox/store';
|
||||||
|
import type { List as ListEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const getOrderedLists = createSelector([(state: RootState) => state.lists], lists => {
|
||||||
|
if (!lists) {
|
||||||
|
return lists;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lists.toList().filter(item => !!item).sort((a, b) => (a as ListEntity).title.localeCompare((b as ListEntity).title)).take(4) as ImmutableList<ListEntity>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListPanel = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchLists());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!lists || lists.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{lists.map(list => (
|
||||||
|
<NavLink key={list.id} className='column-link column-link--transparent' strict to={`/list/${list.id}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.title}</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListPanel;
|
|
@ -1,108 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
|
|
||||||
import { muteAccount } from 'soapbox/actions/accounts';
|
|
||||||
import { closeModal } from 'soapbox/actions/modals';
|
|
||||||
import { toggleHideNotifications } from 'soapbox/actions/mutes';
|
|
||||||
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
isSubmitting: state.reports.new.isSubmitting,
|
|
||||||
account: state.getIn(['mutes', 'new', 'account']),
|
|
||||||
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
onConfirm(account, notifications) {
|
|
||||||
dispatch(muteAccount(account.get('id'), notifications));
|
|
||||||
},
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
dispatch(closeModal());
|
|
||||||
},
|
|
||||||
|
|
||||||
onToggleNotifications() {
|
|
||||||
dispatch(toggleHideNotifications());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
|
||||||
@injectIntl
|
|
||||||
class MuteModal extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
isSubmitting: PropTypes.bool.isRequired,
|
|
||||||
account: PropTypes.object.isRequired,
|
|
||||||
notifications: PropTypes.bool.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onConfirm: PropTypes.func.isRequired,
|
|
||||||
onToggleNotifications: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
this.props.onClose();
|
|
||||||
this.props.onConfirm(this.props.account, this.props.notifications);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel = () => {
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleNotifications = () => {
|
|
||||||
this.props.onToggleNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { account, notifications } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={
|
|
||||||
<FormattedMessage
|
|
||||||
id='confirmations.mute.heading'
|
|
||||||
defaultMessage='Mute @{name}'
|
|
||||||
values={{ name: account.get('acct') }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClose={this.handleCancel}
|
|
||||||
confirmationAction={this.handleClick}
|
|
||||||
confirmationText={<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />}
|
|
||||||
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
|
|
||||||
cancelAction={this.handleCancel}
|
|
||||||
>
|
|
||||||
<Stack space={4}>
|
|
||||||
<Text>
|
|
||||||
<FormattedMessage
|
|
||||||
id='confirmations.mute.message'
|
|
||||||
defaultMessage='Are you sure you want to mute {name}?'
|
|
||||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<HStack alignItems='center' space={2}>
|
|
||||||
<Text tag='span'>
|
|
||||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Toggle
|
|
||||||
checked={notifications}
|
|
||||||
onChange={this.toggleNotifications}
|
|
||||||
icons={false}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</label>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
import { muteAccount } from 'soapbox/actions/accounts';
|
||||||
|
import { closeModal } from 'soapbox/actions/modals';
|
||||||
|
import { toggleHideNotifications } from 'soapbox/actions/mutes';
|
||||||
|
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const MuteModal = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!));
|
||||||
|
const notifications = useAppSelector((state) => state.mutes.new.notifications);
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
dispatch(closeModal());
|
||||||
|
dispatch(muteAccount(account.id, notifications));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
dispatch(closeModal());
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNotifications = () => {
|
||||||
|
dispatch(toggleHideNotifications());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.mute.heading'
|
||||||
|
defaultMessage='Mute @{name}'
|
||||||
|
values={{ name: account.acct }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClose={handleCancel}
|
||||||
|
confirmationAction={handleClick}
|
||||||
|
confirmationText={<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />}
|
||||||
|
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
|
||||||
|
cancelAction={handleCancel}
|
||||||
|
>
|
||||||
|
<Stack space={4}>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.mute.message'
|
||||||
|
defaultMessage='Are you sure you want to mute {name}?'
|
||||||
|
values={{ name: <strong>@{account.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<HStack alignItems='center' space={2}>
|
||||||
|
<Text tag='span'>
|
||||||
|
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
checked={notifications}
|
||||||
|
onChange={toggleNotifications}
|
||||||
|
icons={false}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</label>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MuteModal;
|
|
@ -14,7 +14,7 @@ import { buildStatus } from '../util/pending_status_builder';
|
||||||
|
|
||||||
import PollPreview from './poll_preview';
|
import PollPreview from './poll_preview';
|
||||||
|
|
||||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity, Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const shouldHaveCard = (pendingStatus: StatusEntity) => {
|
const shouldHaveCard = (pendingStatus: StatusEntity) => {
|
||||||
return Boolean(pendingStatus.content.match(/https?:\/\/\S*/));
|
return Boolean(pendingStatus.content.match(/https?:\/\/\S*/));
|
||||||
|
@ -81,7 +81,7 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
||||||
|
|
||||||
<PendingStatusMedia status={status} />
|
<PendingStatusMedia status={status} />
|
||||||
|
|
||||||
{status.poll && <PollPreview poll={status.poll} />}
|
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
|
||||||
|
|
||||||
{status.quote && <QuotedStatus statusId={status.quote as string} />}
|
{status.quote && <QuotedStatus statusId={status.quote as string} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
export default class PollPreview extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
poll: ImmutablePropTypes.map,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderOption(option) {
|
|
||||||
const { poll } = this.props;
|
|
||||||
const showResults = poll.get('voted') || poll.get('expired');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={option}>
|
|
||||||
<label className={classNames('poll__text', { selectable: !showResults })}>
|
|
||||||
<input
|
|
||||||
name='vote-options'
|
|
||||||
type={poll.get('multiple') ? 'checkbox' : 'radio'}
|
|
||||||
onChange={this.handleOptionChange}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={classNames('poll__input', { checkbox: poll.get('multiple') })} />
|
|
||||||
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: option }} />
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { poll } = this.props;
|
|
||||||
|
|
||||||
if (!poll) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='poll'>
|
|
||||||
<ul>
|
|
||||||
{poll.get('options').map((option, i) => this.renderOption(option, i))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Poll as PollEntity, PollOption as PollOptionEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IPollPreview {
|
||||||
|
poll: PollEntity,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PollPreview: React.FC<IPollPreview> = ({ poll }) => {
|
||||||
|
const renderOption = (option: PollOptionEntity, index: number) => {
|
||||||
|
const showResults = poll.voted || poll.expired;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
<label className={classNames('poll__text', { selectable: !showResults })}>
|
||||||
|
<input
|
||||||
|
name='vote-options'
|
||||||
|
type={poll.multiple ? 'checkbox' : 'radio'}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={classNames('poll__input', { checkbox: poll.multiple })} />
|
||||||
|
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!poll) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='poll'>
|
||||||
|
<ul>
|
||||||
|
{poll.options.map((option, i) => renderOption(option, i))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PollPreview;
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Filter normalizer:
|
||||||
|
* Converts API filters into our internal format.
|
||||||
|
* @see {@link https://docs.joinmastodon.org/entities/filter/}
|
||||||
|
*/
|
||||||
|
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
// https://docs.joinmastodon.org/entities/filter/
|
||||||
|
export const FilterRecord = ImmutableRecord({
|
||||||
|
id: '',
|
||||||
|
phrase: '',
|
||||||
|
context: ImmutableList<string>(),
|
||||||
|
whole_word: false,
|
||||||
|
expires_at: '',
|
||||||
|
irreversible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const normalizeFilter = (filter: Record<string, any>) => {
|
||||||
|
return FilterRecord(
|
||||||
|
ImmutableMap(fromJS(filter)),
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ export { CardRecord, normalizeCard } from './card';
|
||||||
export { ChatRecord, normalizeChat } from './chat';
|
export { ChatRecord, normalizeChat } from './chat';
|
||||||
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
|
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
|
||||||
export { EmojiRecord, normalizeEmoji } from './emoji';
|
export { EmojiRecord, normalizeEmoji } from './emoji';
|
||||||
|
export { FilterRecord, normalizeFilter } from './filter';
|
||||||
export { HistoryRecord, normalizeHistory } from './history';
|
export { HistoryRecord, normalizeHistory } from './history';
|
||||||
export { InstanceRecord, normalizeInstance } from './instance';
|
export { InstanceRecord, normalizeInstance } from './instance';
|
||||||
export { ListRecord, normalizeList } from './list';
|
export { ListRecord, normalizeList } from './list';
|
||||||
|
|
|
@ -47,8 +47,18 @@ const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizePollOption = (option: ImmutableMap<string, any>, emojis: ImmutableList<ImmutableMap<string, string>> = ImmutableList()) => {
|
const normalizePollOption = (option: ImmutableMap<string, any> | string, emojis: ImmutableList<ImmutableMap<string, string>> = ImmutableList()) => {
|
||||||
const emojiMap = makeEmojiMap(emojis);
|
const emojiMap = makeEmojiMap(emojis);
|
||||||
|
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
const titleEmojified = emojify(escapeTextContentForBrowser(option), emojiMap);
|
||||||
|
|
||||||
|
return PollOptionRecord({
|
||||||
|
title: option,
|
||||||
|
title_emojified: titleEmojified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
const titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
||||||
|
|
||||||
return PollOptionRecord(
|
return PollOptionRecord(
|
||||||
|
|
|
@ -6,7 +6,7 @@ import reducer from '../accounts';
|
||||||
|
|
||||||
describe('accounts reducer', () => {
|
describe('accounts reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ACCOUNT_IMPORT', () => {
|
describe('ACCOUNT_IMPORT', () => {
|
|
@ -9,7 +9,7 @@ import reducer from '../accounts_counters';
|
||||||
|
|
||||||
describe('accounts_counters reducer', () => {
|
describe('accounts_counters reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||||
});
|
});
|
||||||
|
|
||||||
// it('should handle ACCOUNT_FOLLOW_SUCCESS', () => {
|
// it('should handle ACCOUNT_FOLLOW_SUCCESS', () => {
|
|
@ -4,7 +4,7 @@ import reducer from '../admin';
|
||||||
|
|
||||||
describe('admin reducer', () => {
|
describe('admin reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
const result = reducer(undefined, {});
|
const result = reducer(undefined, {} as any);
|
||||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
||||||
expect(result.needsReboot).toBe(false);
|
expect(result.needsReboot).toBe(false);
|
||||||
});
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
import * as actions from 'soapbox/actions/compose';
|
import * as actions from 'soapbox/actions/compose';
|
||||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
||||||
|
@ -220,7 +220,7 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => {
|
it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => {
|
||||||
const state = ImmutableMap({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: { } });
|
const state = ImmutableMap({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() });
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_SPOILERNESS_CHANGE,
|
type: actions.COMPOSE_SPOILERNESS_CHANGE,
|
||||||
};
|
};
|
||||||
|
@ -337,7 +337,7 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_UPLOAD_SUCCESS', () => {
|
it('should handle COMPOSE_UPLOAD_SUCCESS', () => {
|
||||||
const state = ImmutableMap({ media_attachments: [] });
|
const state = ImmutableMap({ media_attachments: ImmutableList() });
|
||||||
const media = [
|
const media = [
|
||||||
{
|
{
|
||||||
description: null,
|
description: null,
|
||||||
|
@ -385,19 +385,18 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => {
|
it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => {
|
||||||
const state = ImmutableMap({ });
|
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_SUGGESTIONS_CLEAR,
|
type: actions.COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
suggestion_token: 'aiekdns3',
|
suggestion_token: 'aiekdns3',
|
||||||
};
|
};
|
||||||
expect(reducer(state, action).toJS()).toMatchObject({
|
expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||||
suggestion_token: null,
|
suggestion_token: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => {
|
it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => {
|
||||||
const state = ImmutableMap({ tagHistory: [ 'hashtag' ] });
|
const state = ImmutableMap({ tagHistory: ImmutableList([ 'hashtag' ]) });
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE,
|
type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||||
token: 'aaadken3',
|
token: 'aaadken3',
|
||||||
|
@ -410,12 +409,11 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => {
|
it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => {
|
||||||
const state = ImmutableMap({ });
|
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_TAG_HISTORY_UPDATE,
|
type: actions.COMPOSE_TAG_HISTORY_UPDATE,
|
||||||
tags: [ 'hashtag', 'hashtag2'],
|
tags: [ 'hashtag', 'hashtag2'],
|
||||||
};
|
};
|
||||||
expect(reducer(state, action).toJS()).toMatchObject({
|
expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||||
tagHistory: [ 'hashtag', 'hashtag2' ],
|
tagHistory: [ 'hashtag', 'hashtag2' ],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -450,11 +448,10 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_POLL_REMOVE', () => {
|
it('should handle COMPOSE_POLL_REMOVE', () => {
|
||||||
const state = ImmutableMap({ });
|
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_POLL_REMOVE,
|
type: actions.COMPOSE_POLL_REMOVE,
|
||||||
};
|
};
|
||||||
expect(reducer(state, action).toJS()).toMatchObject({
|
expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||||
poll: null,
|
poll: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -13,7 +13,7 @@ import reducer, { ReducerRecord } from '../contexts';
|
||||||
|
|
||||||
describe('contexts reducer', () => {
|
describe('contexts reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ReducerRecord({
|
expect(reducer(undefined, {} as any)).toEqual(ReducerRecord({
|
||||||
inReplyTos: ImmutableMap(),
|
inReplyTos: ImmutableMap(),
|
||||||
replies: ImmutableMap(),
|
replies: ImmutableMap(),
|
||||||
}));
|
}));
|
||||||
|
@ -97,18 +97,18 @@ describe('contexts reducer', () => {
|
||||||
inReplyTos: fromJS({
|
inReplyTos: fromJS({
|
||||||
B: 'A',
|
B: 'A',
|
||||||
C: 'B',
|
C: 'B',
|
||||||
}),
|
}) as ImmutableMap<string, string>,
|
||||||
replies: fromJS({
|
replies: fromJS({
|
||||||
A: ImmutableOrderedSet(['B']),
|
A: ImmutableOrderedSet(['B']),
|
||||||
B: ImmutableOrderedSet(['C']),
|
B: ImmutableOrderedSet(['C']),
|
||||||
}),
|
}) as ImmutableMap<string, ImmutableOrderedSet<string>>,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = ReducerRecord({
|
const expected = ReducerRecord({
|
||||||
inReplyTos: fromJS({}),
|
inReplyTos: fromJS({}) as ImmutableMap<string, string>,
|
||||||
replies: fromJS({
|
replies: fromJS({
|
||||||
A: ImmutableOrderedSet(),
|
A: ImmutableOrderedSet(),
|
||||||
}),
|
}) as ImmutableMap<string, ImmutableOrderedSet<string>>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(reducer(state, action)).toEqual(expected);
|
expect(reducer(state, action)).toEqual(expected);
|
|
@ -4,6 +4,6 @@ import reducer from '../filters';
|
||||||
|
|
||||||
describe('filters reducer', () => {
|
describe('filters reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableList());
|
expect(reducer(undefined, {} as any)).toEqual(ImmutableList());
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -7,7 +7,7 @@ import reducer from '../instance';
|
||||||
|
|
||||||
describe('instance reducer', () => {
|
describe('instance reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
const result = reducer(undefined, {});
|
const result = reducer(undefined, {} as any);
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
description_limit: 1500,
|
description_limit: 1500,
|
||||||
|
@ -128,7 +128,7 @@ describe('instance reducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// The normalizer has `registrations: closed` by default
|
// The normalizer has `registrations: closed` by default
|
||||||
const state = reducer(undefined, {});
|
const state = reducer(undefined, {} as any);
|
||||||
expect(state.registrations).toBe(false);
|
expect(state.registrations).toBe(false);
|
||||||
|
|
||||||
// After importing the configs, registration will be open
|
// After importing the configs, registration will be open
|
|
@ -6,7 +6,7 @@ import reducer from '../list_adder';
|
||||||
|
|
||||||
describe('list_adder reducer', () => {
|
describe('list_adder reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toMatchObject({
|
expect(reducer(undefined, {} as any)).toMatchObject({
|
||||||
accountId: null,
|
accountId: null,
|
||||||
|
|
||||||
lists: {
|
lists: {
|
||||||
|
@ -22,7 +22,7 @@ describe('list_adder reducer', () => {
|
||||||
accountId: null,
|
accountId: null,
|
||||||
|
|
||||||
lists: ImmutableRecord({
|
lists: ImmutableRecord({
|
||||||
items: ImmutableList(),
|
items: ImmutableList<string>(),
|
||||||
loaded: false,
|
loaded: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})(),
|
})(),
|
||||||
|
@ -46,7 +46,7 @@ describe('list_adder reducer', () => {
|
||||||
accountId: null,
|
accountId: null,
|
||||||
|
|
||||||
lists: ImmutableRecord({
|
lists: ImmutableRecord({
|
||||||
items: ImmutableList(),
|
items: ImmutableList<string>(),
|
||||||
loaded: false,
|
loaded: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})(),
|
})(),
|
||||||
|
@ -70,7 +70,7 @@ describe('list_adder reducer', () => {
|
||||||
accountId: null,
|
accountId: null,
|
||||||
|
|
||||||
lists: ImmutableRecord({
|
lists: ImmutableRecord({
|
||||||
items: ImmutableList(),
|
items: ImmutableList<string>(),
|
||||||
loaded: false,
|
loaded: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})(),
|
})(),
|
|
@ -4,6 +4,6 @@ import reducer from '../lists';
|
||||||
|
|
||||||
describe('lists reducer', () => {
|
describe('lists reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -2,6 +2,6 @@ import reducer from '../me';
|
||||||
|
|
||||||
describe('me reducer', () => {
|
describe('me reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(null);
|
expect(reducer(undefined, {} as any)).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import { MODAL_OPEN, MODAL_CLOSE } from 'soapbox/actions/modals';
|
import { MODAL_OPEN, MODAL_CLOSE } from 'soapbox/actions/modals';
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@ import reducer from '../modals';
|
||||||
|
|
||||||
describe('modal reducer', () => {
|
describe('modal reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableList());
|
expect(reducer(undefined, {} as any)).toEqual(ImmutableList());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MODAL_OPEN', () => {
|
it('should handle MODAL_OPEN', () => {
|
||||||
const state = ImmutableList();
|
const state = ImmutableList<any>();
|
||||||
const action = {
|
const action = {
|
||||||
type: MODAL_OPEN,
|
type: MODAL_OPEN,
|
||||||
modalType: 'type1',
|
modalType: 'type1',
|
||||||
|
@ -23,35 +23,43 @@ describe('modal reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MODAL_CLOSE', () => {
|
it('should handle MODAL_CLOSE', () => {
|
||||||
const state = ImmutableList([{
|
const state = ImmutableList([
|
||||||
modalType: 'type1',
|
ImmutableRecord({
|
||||||
modalProps: { props1: '1' },
|
modalType: 'type1',
|
||||||
}]);
|
modalProps: { props1: '1' },
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
const action = {
|
const action = {
|
||||||
type: MODAL_CLOSE,
|
type: MODAL_CLOSE,
|
||||||
};
|
};
|
||||||
expect(reducer(state, action)).toMatchObject(ImmutableList());
|
expect(reducer(state, action).toJS()).toMatchObject([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MODAL_CLOSE with specified modalType', () => {
|
it('should handle MODAL_CLOSE with specified modalType', () => {
|
||||||
const state = ImmutableList([
|
const state = ImmutableList([
|
||||||
{
|
ImmutableRecord({
|
||||||
modalType: 'type1',
|
modalType: 'type1',
|
||||||
},
|
modalProps: null,
|
||||||
{
|
})(),
|
||||||
|
ImmutableRecord({
|
||||||
modalType: 'type2',
|
modalType: 'type2',
|
||||||
},
|
modalProps: null,
|
||||||
{
|
})(),
|
||||||
|
ImmutableRecord({
|
||||||
modalType: 'type1',
|
modalType: 'type1',
|
||||||
},
|
modalProps: null,
|
||||||
|
})(),
|
||||||
]);
|
]);
|
||||||
const action = {
|
const action = {
|
||||||
type: MODAL_CLOSE,
|
type: MODAL_CLOSE,
|
||||||
modalType: 'type2',
|
modalType: 'type2',
|
||||||
};
|
};
|
||||||
expect(reducer(state, action)).toMatchObject(ImmutableList([{
|
expect(reducer(state, action).toJS()).toEqual([
|
||||||
modalType: 'type1',
|
{
|
||||||
}]));
|
modalType: 'type1',
|
||||||
|
modalProps: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MUTES_INIT_MODAL,
|
MUTES_INIT_MODAL,
|
||||||
|
@ -9,50 +9,54 @@ import reducer from '../mutes';
|
||||||
|
|
||||||
describe('mutes reducer', () => {
|
describe('mutes reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
expect(reducer(undefined, {}).toJS()).toEqual({
|
||||||
new: ImmutableMap({
|
new: {
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
account: null,
|
accountId: null,
|
||||||
notifications: true,
|
notifications: true,
|
||||||
}),
|
},
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MUTES_INIT_MODAL', () => {
|
it('should handle MUTES_INIT_MODAL', () => {
|
||||||
const state = ImmutableMap({
|
const state = ImmutableRecord({
|
||||||
new: ImmutableMap({
|
new: ImmutableRecord({
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
account: null,
|
accountId: null,
|
||||||
notifications: true,
|
notifications: true,
|
||||||
}),
|
})(),
|
||||||
});
|
})();
|
||||||
const action = {
|
const action = {
|
||||||
type: MUTES_INIT_MODAL,
|
type: MUTES_INIT_MODAL,
|
||||||
account: 'account1',
|
account: { id: 'account1' },
|
||||||
};
|
};
|
||||||
expect(reducer(state, action)).toEqual(ImmutableMap({
|
expect(reducer(state, action).toJS()).toEqual({
|
||||||
new: ImmutableMap({
|
new: {
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
account: 'account1',
|
accountId: 'account1',
|
||||||
notifications: true,
|
notifications: true,
|
||||||
}),
|
},
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MUTES_TOGGLE_HIDE_NOTIFICATIONS', () => {
|
it('should handle MUTES_TOGGLE_HIDE_NOTIFICATIONS', () => {
|
||||||
const state = ImmutableMap({
|
const state = ImmutableRecord({
|
||||||
new: ImmutableMap({
|
new: ImmutableRecord({
|
||||||
|
isSubmitting: false,
|
||||||
|
accountId: null,
|
||||||
notifications: true,
|
notifications: true,
|
||||||
}),
|
})(),
|
||||||
});
|
})();
|
||||||
const action = {
|
const action = {
|
||||||
type: MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
type: MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||||
};
|
};
|
||||||
expect(reducer(state, action)).toEqual(ImmutableMap({
|
expect(reducer(state, action).toJS()).toEqual({
|
||||||
new: ImmutableMap({
|
new: {
|
||||||
|
isSubmitting: false,
|
||||||
|
accountId: null,
|
||||||
notifications: false,
|
notifications: false,
|
||||||
}),
|
},
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
|
@ -5,7 +5,7 @@ import reducer from '../patron';
|
||||||
|
|
||||||
describe('patron reducer', () => {
|
describe('patron reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
const result = reducer(undefined, {});
|
const result = reducer(undefined, {} as any);
|
||||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
||||||
expect(result.instance.url).toBe('');
|
expect(result.instance.url).toBe('');
|
||||||
});
|
});
|
|
@ -6,7 +6,7 @@ import reducer from '../polls';
|
||||||
|
|
||||||
describe('polls reducer', () => {
|
describe('polls reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POLLS_IMPORT', () => {
|
describe('POLLS_IMPORT', () => {
|
|
@ -6,7 +6,7 @@ describe('push_notifications reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
||||||
subscription: null,
|
subscription: null,
|
||||||
alerts: new ImmutableMap({
|
alerts: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
follow_request: true,
|
follow_request: true,
|
||||||
favourite: true,
|
favourite: true,
|
|
@ -2,7 +2,7 @@ import reducer from '../reports';
|
||||||
|
|
||||||
describe('reports reducer', () => {
|
describe('reports reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {}).toJS()).toEqual({
|
expect(reducer(undefined, {} as any).toJS()).toEqual({
|
||||||
new: {
|
new: {
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
account_id: null,
|
account_id: null,
|
|
@ -2,6 +2,6 @@ import reducer from '../sidebar';
|
||||||
|
|
||||||
describe('sidebar reducer', () => {
|
describe('sidebar reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual({ sidebarOpen: false });
|
expect(reducer(undefined, {} as any)).toEqual({ sidebarOpen: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { List as ImmutableList, fromJS } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { emojis as emojiData } from 'soapbox/features/emoji/emoji_mart_data_light';
|
import { emojis as emojiData } from 'soapbox/features/emoji/emoji_mart_data_light';
|
||||||
import { addCustomToPool } from 'soapbox/features/emoji/emoji_mart_search_light';
|
import { addCustomToPool } from 'soapbox/features/emoji/emoji_mart_search_light';
|
||||||
|
@ -6,15 +6,18 @@ import { addCustomToPool } from 'soapbox/features/emoji/emoji_mart_search_light'
|
||||||
import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
|
import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
|
||||||
import { buildCustomEmojis } from '../features/emoji/emoji';
|
import { buildCustomEmojis } from '../features/emoji/emoji';
|
||||||
|
|
||||||
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const initialState = ImmutableList();
|
const initialState = ImmutableList();
|
||||||
|
|
||||||
// Populate custom emojis for composer autosuggest
|
// Populate custom emojis for composer autosuggest
|
||||||
const autosuggestPopulate = emojis => {
|
const autosuggestPopulate = (emojis: ImmutableList<ImmutableMap<string, string>>) => {
|
||||||
addCustomToPool(buildCustomEmojis(emojis));
|
addCustomToPool(buildCustomEmojis(emojis));
|
||||||
};
|
};
|
||||||
|
|
||||||
const importEmojis = (state, customEmojis) => {
|
const importEmojis = (customEmojis: APIEntity[]) => {
|
||||||
const emojis = fromJS(customEmojis).filter(emoji => {
|
const emojis = (fromJS(customEmojis) as ImmutableList<ImmutableMap<string, string>>).filter((emoji) => {
|
||||||
// If a custom emoji has the shortcode of a Unicode emoji, skip it.
|
// If a custom emoji has the shortcode of a Unicode emoji, skip it.
|
||||||
// Otherwise it breaks EmojiMart.
|
// Otherwise it breaks EmojiMart.
|
||||||
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/610
|
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/610
|
||||||
|
@ -26,9 +29,9 @@ const importEmojis = (state, customEmojis) => {
|
||||||
return emojis;
|
return emojis;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function custom_emojis(state = initialState, action) {
|
export default function custom_emojis(state = initialState, action: AnyAction) {
|
||||||
if (action.type === CUSTOM_EMOJIS_FETCH_SUCCESS) {
|
if (action.type === CUSTOM_EMOJIS_FETCH_SUCCESS) {
|
||||||
return importEmojis(state, action.custom_emojis);
|
return importEmojis(action.custom_emojis);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import { normalizeFilter } from 'soapbox/normalizers';
|
||||||
|
|
||||||
|
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
|
||||||
|
|
||||||
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { APIEntity, Filter as FilterEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
type State = ImmutableList<FilterEntity>;
|
||||||
|
|
||||||
|
const importFilters = (_state: State, filters: APIEntity[]): State => {
|
||||||
|
return ImmutableList(filters.map((filter) => normalizeFilter(filter)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function filters(state: State = ImmutableList<FilterEntity>(), action: AnyAction): State {
|
||||||
|
switch (action.type) {
|
||||||
|
case FILTERS_FETCH_SUCCESS:
|
||||||
|
return importFilters(state, action.filters);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
List as ImmutableList,
|
|
||||||
fromJS,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
|
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
type Filter = ImmutableMap<string, any>;
|
|
||||||
type State = ImmutableList<Filter>;
|
|
||||||
|
|
||||||
const importFilters = (_state: State, filters: unknown): State => {
|
|
||||||
return ImmutableList(fromJS(filters)).map(filter => ImmutableMap(fromJS(filter)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function filters(state: State = ImmutableList<Filter>(), action: AnyAction): State {
|
|
||||||
switch (action.type) {
|
|
||||||
case FILTERS_FETCH_SUCCESS:
|
|
||||||
return importFilters(state, action.filters);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +1,30 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MUTES_INIT_MODAL,
|
MUTES_INIT_MODAL,
|
||||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||||
} from '../actions/mutes';
|
} from '../actions/mutes';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
import type { AnyAction } from 'redux';
|
||||||
new: ImmutableMap({
|
|
||||||
isSubmitting: false,
|
const NewMuteRecord = ImmutableRecord({
|
||||||
account: null,
|
isSubmitting: false,
|
||||||
notifications: true,
|
accountId: null,
|
||||||
}),
|
notifications: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function mutes(state = initialState, action) {
|
const ReducerRecord = ImmutableRecord({
|
||||||
|
new: NewMuteRecord(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type State = ReturnType<typeof ReducerRecord>;
|
||||||
|
|
||||||
|
export default function mutes(state: State = ReducerRecord(), action: AnyAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case MUTES_INIT_MODAL:
|
case MUTES_INIT_MODAL:
|
||||||
return state.withMutations((state) => {
|
return state.withMutations((state) => {
|
||||||
state.setIn(['new', 'isSubmitting'], false);
|
state.setIn(['new', 'isSubmitting'], false);
|
||||||
state.setIn(['new', 'account'], action.account);
|
state.setIn(['new', 'accountId'], action.account.id);
|
||||||
state.setIn(['new', 'notifications'], true);
|
state.setIn(['new', 'notifications'], true);
|
||||||
});
|
});
|
||||||
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
|
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
|
|
@ -1,28 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
PROFILE_HOVER_CARD_OPEN,
|
|
||||||
PROFILE_HOVER_CARD_CLOSE,
|
|
||||||
PROFILE_HOVER_CARD_UPDATE,
|
|
||||||
} from 'soapbox/actions/profile_hover_card';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
export default function profileHoverCard(state = initialState, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case PROFILE_HOVER_CARD_OPEN:
|
|
||||||
return ImmutableMap({
|
|
||||||
ref: action.ref,
|
|
||||||
accountId: action.accountId,
|
|
||||||
});
|
|
||||||
case PROFILE_HOVER_CARD_UPDATE:
|
|
||||||
return state.set('hovered', true);
|
|
||||||
case PROFILE_HOVER_CARD_CLOSE:
|
|
||||||
if (state.get('hovered') === true && !action.force)
|
|
||||||
return state;
|
|
||||||
else
|
|
||||||
return ImmutableMap();
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PROFILE_HOVER_CARD_OPEN,
|
||||||
|
PROFILE_HOVER_CARD_CLOSE,
|
||||||
|
PROFILE_HOVER_CARD_UPDATE,
|
||||||
|
} from 'soapbox/actions/profile_hover_card';
|
||||||
|
|
||||||
|
import type { AnyAction } from 'redux';
|
||||||
|
|
||||||
|
const ReducerRecord = ImmutableRecord({
|
||||||
|
ref: null as React.MutableRefObject<HTMLDivElement> | null,
|
||||||
|
accountId: '',
|
||||||
|
hovered: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
type State = ReturnType<typeof ReducerRecord>;
|
||||||
|
|
||||||
|
export default function profileHoverCard(state: State = ReducerRecord(), action: AnyAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case PROFILE_HOVER_CARD_OPEN:
|
||||||
|
return state.withMutations((state) => {
|
||||||
|
state.set('ref', action.ref);
|
||||||
|
state.set('accountId', action.accountId);
|
||||||
|
});
|
||||||
|
case PROFILE_HOVER_CARD_UPDATE:
|
||||||
|
return state.set('hovered', true);
|
||||||
|
case PROFILE_HOVER_CARD_CLOSE:
|
||||||
|
if (state.get('hovered') === true && !action.force)
|
||||||
|
return state;
|
||||||
|
else
|
||||||
|
return ReducerRecord();
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import { shouldFilter } from 'soapbox/utils/timelines';
|
||||||
|
|
||||||
import type { ReducerChat } from 'soapbox/reducers/chats';
|
import type { ReducerChat } from 'soapbox/reducers/chats';
|
||||||
import type { RootState } from 'soapbox/store';
|
import type { RootState } from 'soapbox/store';
|
||||||
import type { Notification } from 'soapbox/types/entities';
|
import type { Filter as FilterEntity, Notification } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const normalizeId = (id: any): string => typeof id === 'string' ? id : '';
|
const normalizeId = (id: any): string => typeof id === 'string' ? id : '';
|
||||||
|
|
||||||
|
@ -104,18 +104,18 @@ const toServerSideType = (columnType: string): string => {
|
||||||
type FilterContext = { contextType?: string };
|
type FilterContext = { contextType?: string };
|
||||||
|
|
||||||
export const getFilters = (state: RootState, query: FilterContext) => {
|
export const getFilters = (state: RootState, query: FilterContext) => {
|
||||||
return state.filters.filter((filter): boolean => {
|
return state.filters.filter((filter) => {
|
||||||
return query?.contextType
|
return query?.contextType
|
||||||
&& filter.get('context').includes(toServerSideType(query.contextType))
|
&& filter.context.includes(toServerSideType(query.contextType))
|
||||||
&& (filter.get('expires_at') === null
|
&& (filter.expires_at === null
|
||||||
|| Date.parse(filter.get('expires_at')) > new Date().getTime());
|
|| Date.parse(filter.expires_at) > new Date().getTime());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const escapeRegExp = (string: string) =>
|
const escapeRegExp = (string: string) =>
|
||||||
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
|
|
||||||
export const regexFromFilters = (filters: ImmutableList<ImmutableMap<string, any>>) => {
|
export const regexFromFilters = (filters: ImmutableList<FilterEntity>) => {
|
||||||
if (filters.size === 0) return null;
|
if (filters.size === 0) return null;
|
||||||
|
|
||||||
return new RegExp(filters.map(filter => {
|
return new RegExp(filters.map(filter => {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
ChatMessageRecord,
|
ChatMessageRecord,
|
||||||
EmojiRecord,
|
EmojiRecord,
|
||||||
FieldRecord,
|
FieldRecord,
|
||||||
|
FilterRecord,
|
||||||
HistoryRecord,
|
HistoryRecord,
|
||||||
InstanceRecord,
|
InstanceRecord,
|
||||||
ListRecord,
|
ListRecord,
|
||||||
|
@ -31,6 +32,7 @@ type Chat = ReturnType<typeof ChatRecord>;
|
||||||
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
||||||
type Emoji = ReturnType<typeof EmojiRecord>;
|
type Emoji = ReturnType<typeof EmojiRecord>;
|
||||||
type Field = ReturnType<typeof FieldRecord>;
|
type Field = ReturnType<typeof FieldRecord>;
|
||||||
|
type Filter = ReturnType<typeof FilterRecord>;
|
||||||
type History = ReturnType<typeof HistoryRecord>;
|
type History = ReturnType<typeof HistoryRecord>;
|
||||||
type Instance = ReturnType<typeof InstanceRecord>;
|
type Instance = ReturnType<typeof InstanceRecord>;
|
||||||
type List = ReturnType<typeof ListRecord>;
|
type List = ReturnType<typeof ListRecord>;
|
||||||
|
@ -68,6 +70,7 @@ export {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
Emoji,
|
Emoji,
|
||||||
Field,
|
Field,
|
||||||
|
Filter,
|
||||||
History,
|
History,
|
||||||
Instance,
|
Instance,
|
||||||
List,
|
List,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { fromJS } from 'immutable';
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
import config_db from 'soapbox/__fixtures__/config_db.json';
|
import config_db from 'soapbox/__fixtures__/config_db.json';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { ConfigDB } from '../config_db';
|
||||||
|
|
||||||
test('find', () => {
|
test('find', () => {
|
||||||
const configs = fromJS(config_db).get('configs');
|
const configs = fromJS(config_db).get('configs');
|
||||||
expect(ConfigDB.find(configs, ':phoenix', ':json_library')).toEqual(fromJS({
|
expect(ConfigDB.find(configs as ImmutableList<any>, ':phoenix', ':json_library')).toEqual(fromJS({
|
||||||
group: ':phoenix',
|
group: ':phoenix',
|
||||||
key: ':json_library',
|
key: ':json_library',
|
||||||
value: 'Jason',
|
value: 'Jason',
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue