diff --git a/app/soapbox/actions/__tests__/compose.test.js b/app/soapbox/actions/__tests__/compose.test.ts similarity index 100% rename from app/soapbox/actions/__tests__/compose.test.js rename to app/soapbox/actions/__tests__/compose.test.ts diff --git a/app/soapbox/actions/__tests__/preload-test.js b/app/soapbox/actions/__tests__/preload.test.ts similarity index 100% rename from app/soapbox/actions/__tests__/preload-test.js rename to app/soapbox/actions/__tests__/preload.test.ts diff --git a/app/soapbox/actions/__tests__/statuses-test.js b/app/soapbox/actions/__tests__/statuses.test.ts similarity index 100% rename from app/soapbox/actions/__tests__/statuses-test.js rename to app/soapbox/actions/__tests__/statuses.test.ts diff --git a/app/soapbox/components/__tests__/autosuggest_emoji.test.js b/app/soapbox/components/__tests__/autosuggest_emoji.test.tsx similarity index 88% rename from app/soapbox/components/__tests__/autosuggest_emoji.test.js rename to app/soapbox/components/__tests__/autosuggest_emoji.test.tsx index 938ca737b..8fab0ef8b 100644 --- a/app/soapbox/components/__tests__/autosuggest_emoji.test.js +++ b/app/soapbox/components/__tests__/autosuggest_emoji.test.tsx @@ -10,7 +10,7 @@ describe('', () => { colons: ':foobar:', }; - render(); + render(); expect(screen.getByTestId('emoji')).toHaveTextContent('foobar'); expect(screen.getByRole('img').getAttribute('src')).not.toBe('http://example.com/emoji.png'); @@ -24,7 +24,7 @@ describe('', () => { colons: ':foobar:', }; - render(); + render(); expect(screen.getByTestId('emoji')).toHaveTextContent('foobar'); expect(screen.getByRole('img').getAttribute('src')).toBe('http://example.com/emoji.png'); diff --git a/app/soapbox/components/__tests__/avatar.test.js b/app/soapbox/components/__tests__/avatar.test.tsx similarity index 100% rename from app/soapbox/components/__tests__/avatar.test.js rename to app/soapbox/components/__tests__/avatar.test.tsx diff --git a/app/soapbox/components/__tests__/avatar_overlay.test.js b/app/soapbox/components/__tests__/avatar_overlay.test.tsx similarity index 100% rename from app/soapbox/components/__tests__/avatar_overlay.test.js rename to app/soapbox/components/__tests__/avatar_overlay.test.tsx diff --git a/app/soapbox/components/__tests__/badge.test.js b/app/soapbox/components/__tests__/badge.test.tsx similarity index 100% rename from app/soapbox/components/__tests__/badge.test.js rename to app/soapbox/components/__tests__/badge.test.tsx diff --git a/app/soapbox/components/__tests__/display_name.test.js b/app/soapbox/components/__tests__/display_name.test.tsx similarity index 100% rename from app/soapbox/components/__tests__/display_name.test.js rename to app/soapbox/components/__tests__/display_name.test.tsx diff --git a/app/soapbox/components/__tests__/emoji_selector.test.js b/app/soapbox/components/__tests__/emoji_selector.test.tsx similarity index 100% rename from app/soapbox/components/__tests__/emoji_selector.test.js rename to app/soapbox/components/__tests__/emoji_selector.test.tsx diff --git a/app/soapbox/components/__tests__/scroll-top-button.test.js b/app/soapbox/components/__tests__/scroll-top-button.test.tsx similarity index 100% rename from app/soapbox/components/__tests__/scroll-top-button.test.js rename to app/soapbox/components/__tests__/scroll-top-button.test.tsx diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index 8e5e281bd..11de952f2 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -68,9 +68,9 @@ export const ProfileHoverCard: React.FC = ({ visible = true } const [popperElement, setPopperElement] = useState(null); const me = useAppSelector(state => state.me); - const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.get('accountId', undefined)); + const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined); 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) : []; useEffect(() => { diff --git a/app/soapbox/components/ui/modal/modal.tsx b/app/soapbox/components/ui/modal/modal.tsx index f0a661229..3b2ba89e9 100644 --- a/app/soapbox/components/ui/modal/modal.tsx +++ b/app/soapbox/components/ui/modal/modal.tsx @@ -27,7 +27,7 @@ interface IModal { /** Callback when the modal is cancelled. */ cancelAction?: () => void, /** Cancel button text. */ - cancelText?: string, + cancelText?: React.ReactNode, /** URL to an SVG icon for the close button. */ closeIcon?: string, /** Position of the close button. */ diff --git a/app/soapbox/features/auth_login/components/__tests__/captcha.test.js b/app/soapbox/features/auth_login/components/__tests__/captcha.test.tsx similarity index 100% rename from app/soapbox/features/auth_login/components/__tests__/captcha.test.js rename to app/soapbox/features/auth_login/components/__tests__/captcha.test.tsx diff --git a/app/soapbox/features/auth_login/components/__tests__/login_form.test.js b/app/soapbox/features/auth_login/components/__tests__/login_form.test.tsx similarity index 100% rename from app/soapbox/features/auth_login/components/__tests__/login_form.test.js rename to app/soapbox/features/auth_login/components/__tests__/login_form.test.tsx diff --git a/app/soapbox/features/auth_login/components/__tests__/login_page.test.js b/app/soapbox/features/auth_login/components/__tests__/login_page.test.tsx similarity index 100% rename from app/soapbox/features/auth_login/components/__tests__/login_page.test.js rename to app/soapbox/features/auth_login/components/__tests__/login_page.test.tsx diff --git a/app/soapbox/features/auth_login/components/__tests__/password_reset_confirm-test.js b/app/soapbox/features/auth_login/components/__tests__/password_reset_confirm.test.tsx similarity index 100% rename from app/soapbox/features/auth_login/components/__tests__/password_reset_confirm-test.js rename to app/soapbox/features/auth_login/components/__tests__/password_reset_confirm.test.tsx diff --git a/app/soapbox/features/compose/components/reply_indicator.tsx b/app/soapbox/features/compose/components/reply_indicator.tsx index d135bfdeb..f47b0494b 100644 --- a/app/soapbox/features/compose/components/reply_indicator.tsx +++ b/app/soapbox/features/compose/components/reply_indicator.tsx @@ -9,13 +9,13 @@ import type { Status } from 'soapbox/types/entities'; interface IReplyIndicator { status?: Status, - onCancel: () => void, + onCancel?: () => void, hideActions: boolean, } const ReplyIndicator: React.FC = ({ status, hideActions, onCancel }) => { const handleClick = () => { - onCancel(); + onCancel!(); }; if (!status) { @@ -23,7 +23,7 @@ const ReplyIndicator: React.FC = ({ status, hideActions, onCanc } let actions = {}; - if (!hideActions) { + if (!hideActions && onCancel) { actions = { onActionClick: handleClick, actionIcon: require('@tabler/icons/icons/x.svg'), diff --git a/app/soapbox/features/crypto_donate/components/crypto_address.tsx b/app/soapbox/features/crypto_donate/components/crypto_address.tsx index 3fb28f6e4..afffd085a 100644 --- a/app/soapbox/features/crypto_donate/components/crypto_address.tsx +++ b/app/soapbox/features/crypto_donate/components/crypto_address.tsx @@ -10,7 +10,7 @@ import { getTitle } from '../utils/coin_db'; import CryptoIcon from './crypto_icon'; -interface ICryptoAddress { +export interface ICryptoAddress { address: string, ticker: string, note?: string, diff --git a/app/soapbox/features/emoji/__tests__/emoji-test.js b/app/soapbox/features/emoji/__tests__/emoji.test.ts similarity index 100% rename from app/soapbox/features/emoji/__tests__/emoji-test.js rename to app/soapbox/features/emoji/__tests__/emoji.test.ts diff --git a/app/soapbox/features/emoji/__tests__/emoji_index-test.js b/app/soapbox/features/emoji/__tests__/emoji_index.test.ts similarity index 100% rename from app/soapbox/features/emoji/__tests__/emoji_index-test.js rename to app/soapbox/features/emoji/__tests__/emoji_index.test.ts diff --git a/app/soapbox/features/emoji/emoji_mart_data_light.js b/app/soapbox/features/emoji/emoji_mart_data_light.ts similarity index 67% rename from app/soapbox/features/emoji/emoji_mart_data_light.js rename to app/soapbox/features/emoji/emoji_mart_data_light.ts index 4756c1a5d..03bdbf765 100644 --- a/app/soapbox/features/emoji/emoji_mart_data_light.js +++ b/app/soapbox/features/emoji/emoji_mart_data_light.ts @@ -1,15 +1,17 @@ // 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 // emojiIndex.search functionality. -const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed'); -const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); +import emojiCompressed from './emoji_compressed'; +import { unicodeToUnifiedName } from './unicode_to_unified_name'; -const emojis = {}; +const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed; + +const emojis: Record = {}; // decompress Object.keys(shortCodesToEmojiData).forEach((shortCode) => { const [ - filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars + _filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars searchData, ] = shortCodesToEmojiData[shortCode]; const [ @@ -27,7 +29,14 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => { }; }); -module.exports = { +export { + emojis, + skins, + categories, + short_names, +}; + +export default { emojis, skins, categories, diff --git a/app/soapbox/features/filters/index.js b/app/soapbox/features/filters/index.js deleted file mode 100644 index 62e12cdf6..000000000 --- a/app/soapbox/features/filters/index.js +++ /dev/null @@ -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 = ; - - return ( - - - - - - - - - {/* - - */} - - - - - - - - - - - - - - - - - - - - - - - - {intl.formatMessage(messages.add_new)} - - - - - - - - - {filters.map((filter, i) => ( - - - - - {filter.get('phrase')} - - - - - {filter.get('context').map((context, i) => ( - {context} - ))} - - - - - - {filter.get('irreversible') ? - : - - } - {filter.get('whole_word') && - - } - - - - - - - - - ))} - - - ); - } - -} diff --git a/app/soapbox/features/filters/index.tsx b/app/soapbox/features/filters/index.tsx new file mode 100644 index 000000000..d71adb96e --- /dev/null +++ b/app/soapbox/features/filters/index.tsx @@ -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 = 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 = ; + + return ( + + + + + + + setPhrase(target.value)} + /> + + {/* + + */} + + + + + + + + + + setHomeTimeline(target.checked)} + /> + setPublicTimeline(target.checked)} + /> + setNotifications(target.checked)} + /> + setConversations(target.checked)} + /> + + + + + + setIrreversible(target.checked)} + /> + setWholeWord(target.checked)} + /> + + + + {intl.formatMessage(messages.add_new)} + + + + + + + + + {filters.map((filter, i) => ( + + + + + {filter.phrase} + + + + + {filter.context.map((context, i) => ( + {context} + ))} + + + + + + {filter.irreversible ? + : + + } + {filter.whole_word && + + } + + + + + + + + + ))} + + + ); +}; + +export default Filters; diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx index 451529a32..15a8e1ed9 100644 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx @@ -13,7 +13,7 @@ import { buildStatus } from '../builder'; 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 { statusId: string, @@ -55,7 +55,7 @@ const ScheduledStatus: React.FC = ({ statusId, ...other }) => /> )} - {status.poll && } + {status.poll && } diff --git a/app/soapbox/features/ui/components/__tests__/compose-button.test.js b/app/soapbox/features/ui/components/__tests__/compose-button.test.tsx similarity index 100% rename from app/soapbox/features/ui/components/__tests__/compose-button.test.js rename to app/soapbox/features/ui/components/__tests__/compose-button.test.tsx diff --git a/app/soapbox/features/ui/components/boost_modal.js b/app/soapbox/features/ui/components/boost_modal.js deleted file mode 100644 index faf648b12..000000000 --- a/app/soapbox/features/ui/components/boost_modal.js +++ /dev/null @@ -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 ( - - - - - - Shift + }} /> - - - - ); - } - -} diff --git a/app/soapbox/features/ui/components/boost_modal.tsx b/app/soapbox/features/ui/components/boost_modal.tsx new file mode 100644 index 000000000..1dcaaf63d --- /dev/null +++ b/app/soapbox/features/ui/components/boost_modal.tsx @@ -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 = ({ status, onReblog, onClose }) => { + const intl = useIntl(); + + const handleReblog = () => { + onReblog(status); + onClose(); + }; + + const buttonText = status.reblogged ? messages.cancel_reblog : messages.reblog; + + return ( + + + + + + Shift + }} /> + + + + ); +}; + +export default BoostModal; diff --git a/app/soapbox/features/ui/components/confirmation_modal.js b/app/soapbox/features/ui/components/confirmation_modal.js deleted file mode 100644 index e205f6ce1..000000000 --- a/app/soapbox/features/ui/components/confirmation_modal.js +++ /dev/null @@ -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 ( - } - cancelAction={this.handleCancel} - secondaryText={secondary} - secondaryAction={this.props.onSecondary && this.handleSecondary} - > - {message} - - - {checkbox && - - - - - - } - - - ); - } - -} diff --git a/app/soapbox/features/ui/components/confirmation_modal.tsx b/app/soapbox/features/ui/components/confirmation_modal.tsx new file mode 100644 index 000000000..8b4d790e6 --- /dev/null +++ b/app/soapbox/features/ui/components/confirmation_modal.tsx @@ -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 = ({ + 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 = e => { + setChecked(e.target.checked); + }; + + return ( + } + cancelAction={handleCancel} + secondaryText={secondary} + secondaryAction={onSecondary && handleSecondary} + > + {message} + + + {checkbox && + + + + + + } + + + ); +}; + +export default ConfirmationModal; diff --git a/app/soapbox/features/ui/components/crypto_donate_modal.js b/app/soapbox/features/ui/components/crypto_donate_modal.js deleted file mode 100644 index 73d8b25db..000000000 --- a/app/soapbox/features/ui/components/crypto_donate_modal.js +++ /dev/null @@ -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 ( - - - - ); - } - -} diff --git a/app/soapbox/features/ui/components/crypto_donate_modal.tsx b/app/soapbox/features/ui/components/crypto_donate_modal.tsx new file mode 100644 index 000000000..805012d3a --- /dev/null +++ b/app/soapbox/features/ui/components/crypto_donate_modal.tsx @@ -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 = (props) => { + return ( + + + + ); + +}; + +export default CryptoDonateModal; diff --git a/app/soapbox/features/ui/components/list_panel.js b/app/soapbox/features/ui/components/list_panel.js deleted file mode 100644 index 4ca1a78ff..000000000 --- a/app/soapbox/features/ui/components/list_panel.js +++ /dev/null @@ -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 ( - - - - {lists.map(list => ( - {list.get('title')} - ))} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/list_panel.tsx b/app/soapbox/features/ui/components/list_panel.tsx new file mode 100644 index 000000000..8160217e9 --- /dev/null +++ b/app/soapbox/features/ui/components/list_panel.tsx @@ -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; +}); + +const ListPanel = () => { + const dispatch = useAppDispatch(); + + const lists = useAppSelector((state) => getOrderedLists(state)); + + useEffect(() => { + dispatch(fetchLists()); + }, []); + + if (!lists || lists.isEmpty()) { + return null; + } + + return ( + + + + {lists.map(list => ( + {list.title} + ))} + + ); +}; + +export default ListPanel; diff --git a/app/soapbox/features/ui/components/mute_modal.js b/app/soapbox/features/ui/components/mute_modal.js deleted file mode 100644 index 0714b5cfb..000000000 --- a/app/soapbox/features/ui/components/mute_modal.js +++ /dev/null @@ -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 ( - - } - onClose={this.handleCancel} - confirmationAction={this.handleClick} - confirmationText={} - cancelText={} - cancelAction={this.handleCancel} - > - - - @{account.get('acct')} }} - /> - - - - - - - - - - - - - - ); - } - -} diff --git a/app/soapbox/features/ui/components/mute_modal.tsx b/app/soapbox/features/ui/components/mute_modal.tsx new file mode 100644 index 000000000..b2b5e25b2 --- /dev/null +++ b/app/soapbox/features/ui/components/mute_modal.tsx @@ -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 ( + + } + onClose={handleCancel} + confirmationAction={handleClick} + confirmationText={} + cancelText={} + cancelAction={handleCancel} + > + + + @{account.acct} }} + /> + + + + + + + + + + + + + + ); +}; + +export default MuteModal; diff --git a/app/soapbox/features/ui/components/pending_status.tsx b/app/soapbox/features/ui/components/pending_status.tsx index 03853008c..9e7bfaef1 100644 --- a/app/soapbox/features/ui/components/pending_status.tsx +++ b/app/soapbox/features/ui/components/pending_status.tsx @@ -14,7 +14,7 @@ import { buildStatus } from '../util/pending_status_builder'; 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) => { return Boolean(pendingStatus.content.match(/https?:\/\/\S*/)); @@ -81,7 +81,7 @@ const PendingStatus: React.FC = ({ idempotencyKey, className, mu - {status.poll && } + {status.poll && } {status.quote && } diff --git a/app/soapbox/features/ui/components/poll_preview.js b/app/soapbox/features/ui/components/poll_preview.js deleted file mode 100644 index 40766abe4..000000000 --- a/app/soapbox/features/ui/components/poll_preview.js +++ /dev/null @@ -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 ( - - - - - - - - - - ); - } - - render() { - const { poll } = this.props; - - if (!poll) { - return null; - } - - return ( - - - {poll.get('options').map((option, i) => this.renderOption(option, i))} - - - ); - } - -} diff --git a/app/soapbox/features/ui/components/poll_preview.tsx b/app/soapbox/features/ui/components/poll_preview.tsx new file mode 100644 index 000000000..3d3b48408 --- /dev/null +++ b/app/soapbox/features/ui/components/poll_preview.tsx @@ -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 = ({ poll }) => { + const renderOption = (option: PollOptionEntity, index: number) => { + const showResults = poll.voted || poll.expired; + + return ( + + + + + + + + + + ); + }; + + if (!poll) { + return null; + } + + return ( + + + {poll.options.map((option, i) => renderOption(option, i))} + + + ); +}; + +export default PollPreview; diff --git a/app/soapbox/features/verification/steps/__tests__/age-verification.test.js b/app/soapbox/features/verification/steps/__tests__/age-verification.test.tsx similarity index 100% rename from app/soapbox/features/verification/steps/__tests__/age-verification.test.js rename to app/soapbox/features/verification/steps/__tests__/age-verification.test.tsx diff --git a/app/soapbox/features/verification/steps/__tests__/email-verification.test.js b/app/soapbox/features/verification/steps/__tests__/email-verification.test.tsx similarity index 100% rename from app/soapbox/features/verification/steps/__tests__/email-verification.test.js rename to app/soapbox/features/verification/steps/__tests__/email-verification.test.tsx diff --git a/app/soapbox/features/verification/steps/__tests__/sms-verification.test.js b/app/soapbox/features/verification/steps/__tests__/sms-verification.test.tsx similarity index 100% rename from app/soapbox/features/verification/steps/__tests__/sms-verification.test.js rename to app/soapbox/features/verification/steps/__tests__/sms-verification.test.tsx diff --git a/app/soapbox/normalizers/__tests__/account-test.js b/app/soapbox/normalizers/__tests__/account.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/account-test.js rename to app/soapbox/normalizers/__tests__/account.test.ts diff --git a/app/soapbox/normalizers/__tests__/attachment-test.js b/app/soapbox/normalizers/__tests__/attachment.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/attachment-test.js rename to app/soapbox/normalizers/__tests__/attachment.test.ts diff --git a/app/soapbox/normalizers/__tests__/card-test.js b/app/soapbox/normalizers/__tests__/card.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/card-test.js rename to app/soapbox/normalizers/__tests__/card.test.ts diff --git a/app/soapbox/normalizers/__tests__/mention-test.js b/app/soapbox/normalizers/__tests__/mention.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/mention-test.js rename to app/soapbox/normalizers/__tests__/mention.test.ts diff --git a/app/soapbox/normalizers/__tests__/notification-test.js b/app/soapbox/normalizers/__tests__/notification.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/notification-test.js rename to app/soapbox/normalizers/__tests__/notification.test.ts diff --git a/app/soapbox/normalizers/__tests__/poll-test.js b/app/soapbox/normalizers/__tests__/poll.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/poll-test.js rename to app/soapbox/normalizers/__tests__/poll.test.ts diff --git a/app/soapbox/normalizers/__tests__/status-test.js b/app/soapbox/normalizers/__tests__/status.test.ts similarity index 100% rename from app/soapbox/normalizers/__tests__/status-test.js rename to app/soapbox/normalizers/__tests__/status.test.ts diff --git a/app/soapbox/normalizers/filter.ts b/app/soapbox/normalizers/filter.ts new file mode 100644 index 000000000..5f2f57960 --- /dev/null +++ b/app/soapbox/normalizers/filter.ts @@ -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(), + whole_word: false, + expires_at: '', + irreversible: false, +}); + +export const normalizeFilter = (filter: Record) => { + return FilterRecord( + ImmutableMap(fromJS(filter)), + ); +}; \ No newline at end of file diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index d660df0f2..b1b41e0d6 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -6,6 +6,7 @@ export { CardRecord, normalizeCard } from './card'; export { ChatRecord, normalizeChat } from './chat'; export { ChatMessageRecord, normalizeChatMessage } from './chat_message'; export { EmojiRecord, normalizeEmoji } from './emoji'; +export { FilterRecord, normalizeFilter } from './filter'; export { HistoryRecord, normalizeHistory } from './history'; export { InstanceRecord, normalizeInstance } from './instance'; export { ListRecord, normalizeList } from './list'; diff --git a/app/soapbox/normalizers/poll.ts b/app/soapbox/normalizers/poll.ts index d2b1bf492..7b98d1354 100644 --- a/app/soapbox/normalizers/poll.ts +++ b/app/soapbox/normalizers/poll.ts @@ -47,8 +47,18 @@ const normalizeEmojis = (entity: ImmutableMap) => { }); }; -const normalizePollOption = (option: ImmutableMap, emojis: ImmutableList> = ImmutableList()) => { +const normalizePollOption = (option: ImmutableMap | string, emojis: ImmutableList> = ImmutableList()) => { 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); return PollOptionRecord( diff --git a/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js b/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.ts similarity index 100% rename from app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js rename to app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.ts diff --git a/app/soapbox/reducers/__tests__/accounts-test.js b/app/soapbox/reducers/__tests__/accounts.test.ts similarity index 93% rename from app/soapbox/reducers/__tests__/accounts-test.js rename to app/soapbox/reducers/__tests__/accounts.test.ts index 4b7915d32..647bb676a 100644 --- a/app/soapbox/reducers/__tests__/accounts-test.js +++ b/app/soapbox/reducers/__tests__/accounts.test.ts @@ -6,7 +6,7 @@ import reducer from '../accounts'; describe('accounts reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap()); + expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); }); describe('ACCOUNT_IMPORT', () => { diff --git a/app/soapbox/reducers/__tests__/accounts_counters-test.js b/app/soapbox/reducers/__tests__/accounts_counters.test.ts similarity index 95% rename from app/soapbox/reducers/__tests__/accounts_counters-test.js rename to app/soapbox/reducers/__tests__/accounts_counters.test.ts index ca1a279e0..114f28b54 100644 --- a/app/soapbox/reducers/__tests__/accounts_counters-test.js +++ b/app/soapbox/reducers/__tests__/accounts_counters.test.ts @@ -9,7 +9,7 @@ import reducer from '../accounts_counters'; describe('accounts_counters reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap()); + expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); }); // it('should handle ACCOUNT_FOLLOW_SUCCESS', () => { diff --git a/app/soapbox/reducers/__tests__/admin-test.js b/app/soapbox/reducers/__tests__/admin.test.ts similarity index 84% rename from app/soapbox/reducers/__tests__/admin-test.js rename to app/soapbox/reducers/__tests__/admin.test.ts index 43018c880..a83e11022 100644 --- a/app/soapbox/reducers/__tests__/admin-test.js +++ b/app/soapbox/reducers/__tests__/admin.test.ts @@ -4,7 +4,7 @@ import reducer from '../admin'; describe('admin reducer', () => { it('should return the initial state', () => { - const result = reducer(undefined, {}); + const result = reducer(undefined, {} as any); expect(ImmutableRecord.isRecord(result)).toBe(true); expect(result.needsReboot).toBe(false); }); diff --git a/app/soapbox/reducers/__tests__/alerts-test.js b/app/soapbox/reducers/__tests__/alerts.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/alerts-test.js rename to app/soapbox/reducers/__tests__/alerts.test.ts diff --git a/app/soapbox/reducers/__tests__/auth-test.js b/app/soapbox/reducers/__tests__/auth.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/auth-test.js rename to app/soapbox/reducers/__tests__/auth.test.ts diff --git a/app/soapbox/reducers/__tests__/compose-test.js b/app/soapbox/reducers/__tests__/compose.test.ts similarity index 96% rename from app/soapbox/reducers/__tests__/compose-test.js rename to app/soapbox/reducers/__tests__/compose.test.ts index 1c4e70948..42c276cf6 100644 --- a/app/soapbox/reducers/__tests__/compose-test.js +++ b/app/soapbox/reducers/__tests__/compose.test.ts @@ -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 { 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', () => { - 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 = { type: actions.COMPOSE_SPOILERNESS_CHANGE, }; @@ -337,7 +337,7 @@ describe('compose reducer', () => { }); it('should handle COMPOSE_UPLOAD_SUCCESS', () => { - const state = ImmutableMap({ media_attachments: [] }); + const state = ImmutableMap({ media_attachments: ImmutableList() }); const media = [ { description: null, @@ -385,19 +385,18 @@ describe('compose reducer', () => { }); it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => { - const state = ImmutableMap({ }); const action = { type: actions.COMPOSE_SUGGESTIONS_CLEAR, suggestions: [], suggestion_token: 'aiekdns3', }; - expect(reducer(state, action).toJS()).toMatchObject({ + expect(reducer(undefined, action).toJS()).toMatchObject({ suggestion_token: null, }); }); it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => { - const state = ImmutableMap({ tagHistory: [ 'hashtag' ] }); + const state = ImmutableMap({ tagHistory: ImmutableList([ 'hashtag' ]) }); const action = { type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE, token: 'aaadken3', @@ -410,12 +409,11 @@ describe('compose reducer', () => { }); it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => { - const state = ImmutableMap({ }); const action = { type: actions.COMPOSE_TAG_HISTORY_UPDATE, tags: [ 'hashtag', 'hashtag2'], }; - expect(reducer(state, action).toJS()).toMatchObject({ + expect(reducer(undefined, action).toJS()).toMatchObject({ tagHistory: [ 'hashtag', 'hashtag2' ], }); }); @@ -450,11 +448,10 @@ describe('compose reducer', () => { }); it('should handle COMPOSE_POLL_REMOVE', () => { - const state = ImmutableMap({ }); const action = { type: actions.COMPOSE_POLL_REMOVE, }; - expect(reducer(state, action).toJS()).toMatchObject({ + expect(reducer(undefined, action).toJS()).toMatchObject({ poll: null, }); }); diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts.test.ts similarity index 91% rename from app/soapbox/reducers/__tests__/contexts-test.js rename to app/soapbox/reducers/__tests__/contexts.test.ts index d5270f0d8..901763091 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts.test.ts @@ -13,7 +13,7 @@ import reducer, { ReducerRecord } from '../contexts'; describe('contexts reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ReducerRecord({ + expect(reducer(undefined, {} as any)).toEqual(ReducerRecord({ inReplyTos: ImmutableMap(), replies: ImmutableMap(), })); @@ -97,18 +97,18 @@ describe('contexts reducer', () => { inReplyTos: fromJS({ B: 'A', C: 'B', - }), + }) as ImmutableMap, replies: fromJS({ A: ImmutableOrderedSet(['B']), B: ImmutableOrderedSet(['C']), - }), + }) as ImmutableMap>, }); const expected = ReducerRecord({ - inReplyTos: fromJS({}), + inReplyTos: fromJS({}) as ImmutableMap, replies: fromJS({ A: ImmutableOrderedSet(), - }), + }) as ImmutableMap>, }); expect(reducer(state, action)).toEqual(expected); diff --git a/app/soapbox/reducers/__tests__/conversations-test.js b/app/soapbox/reducers/__tests__/conversations.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/conversations-test.js rename to app/soapbox/reducers/__tests__/conversations.test.ts diff --git a/app/soapbox/reducers/__tests__/custom_emojis-test.js b/app/soapbox/reducers/__tests__/custom_emojis.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/custom_emojis-test.js rename to app/soapbox/reducers/__tests__/custom_emojis.test.ts diff --git a/app/soapbox/reducers/__tests__/filters-test.js b/app/soapbox/reducers/__tests__/filters.test.ts similarity index 72% rename from app/soapbox/reducers/__tests__/filters-test.js rename to app/soapbox/reducers/__tests__/filters.test.ts index 90bfc1323..5faa0f948 100644 --- a/app/soapbox/reducers/__tests__/filters-test.js +++ b/app/soapbox/reducers/__tests__/filters.test.ts @@ -4,6 +4,6 @@ import reducer from '../filters'; describe('filters reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableList()); + expect(reducer(undefined, {} as any)).toEqual(ImmutableList()); }); }); diff --git a/app/soapbox/reducers/__tests__/group_editor-test.js b/app/soapbox/reducers/__tests__/group_editor.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/group_editor-test.js rename to app/soapbox/reducers/__tests__/group_editor.test.ts diff --git a/app/soapbox/reducers/__tests__/group_lists-test.js b/app/soapbox/reducers/__tests__/group_lists.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/group_lists-test.js rename to app/soapbox/reducers/__tests__/group_lists.test.ts diff --git a/app/soapbox/reducers/__tests__/group_relationships-test.js b/app/soapbox/reducers/__tests__/group_relationships.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/group_relationships-test.js rename to app/soapbox/reducers/__tests__/group_relationships.test.ts diff --git a/app/soapbox/reducers/__tests__/groups-test.js b/app/soapbox/reducers/__tests__/groups.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/groups-test.js rename to app/soapbox/reducers/__tests__/groups.test.ts diff --git a/app/soapbox/reducers/__tests__/index-test.js b/app/soapbox/reducers/__tests__/index.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/index-test.js rename to app/soapbox/reducers/__tests__/index.test.ts diff --git a/app/soapbox/reducers/__tests__/instance-test.js b/app/soapbox/reducers/__tests__/instance.test.ts similarity index 97% rename from app/soapbox/reducers/__tests__/instance-test.js rename to app/soapbox/reducers/__tests__/instance.test.ts index 33a5fab55..8357d6851 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance.test.ts @@ -7,7 +7,7 @@ import reducer from '../instance'; describe('instance reducer', () => { it('should return the initial state', () => { - const result = reducer(undefined, {}); + const result = reducer(undefined, {} as any); const expected = { description_limit: 1500, @@ -128,7 +128,7 @@ describe('instance reducer', () => { }; // The normalizer has `registrations: closed` by default - const state = reducer(undefined, {}); + const state = reducer(undefined, {} as any); expect(state.registrations).toBe(false); // After importing the configs, registration will be open diff --git a/app/soapbox/reducers/__tests__/list_adder-test.js b/app/soapbox/reducers/__tests__/list_adder.test.ts similarity index 93% rename from app/soapbox/reducers/__tests__/list_adder-test.js rename to app/soapbox/reducers/__tests__/list_adder.test.ts index 4806d3d13..3989cf5e7 100644 --- a/app/soapbox/reducers/__tests__/list_adder-test.js +++ b/app/soapbox/reducers/__tests__/list_adder.test.ts @@ -6,7 +6,7 @@ import reducer from '../list_adder'; describe('list_adder reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchObject({ + expect(reducer(undefined, {} as any)).toMatchObject({ accountId: null, lists: { @@ -22,7 +22,7 @@ describe('list_adder reducer', () => { accountId: null, lists: ImmutableRecord({ - items: ImmutableList(), + items: ImmutableList(), loaded: false, isLoading: false, })(), @@ -46,7 +46,7 @@ describe('list_adder reducer', () => { accountId: null, lists: ImmutableRecord({ - items: ImmutableList(), + items: ImmutableList(), loaded: false, isLoading: false, })(), @@ -70,7 +70,7 @@ describe('list_adder reducer', () => { accountId: null, lists: ImmutableRecord({ - items: ImmutableList(), + items: ImmutableList(), loaded: false, isLoading: false, })(), diff --git a/app/soapbox/reducers/__tests__/lists-test.js b/app/soapbox/reducers/__tests__/lists.test.ts similarity index 72% rename from app/soapbox/reducers/__tests__/lists-test.js rename to app/soapbox/reducers/__tests__/lists.test.ts index 56afd7a5c..43881a218 100644 --- a/app/soapbox/reducers/__tests__/lists-test.js +++ b/app/soapbox/reducers/__tests__/lists.test.ts @@ -4,6 +4,6 @@ import reducer from '../lists'; describe('lists reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap()); + expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); }); }); diff --git a/app/soapbox/reducers/__tests__/me-test.js b/app/soapbox/reducers/__tests__/me.test.ts similarity index 67% rename from app/soapbox/reducers/__tests__/me-test.js rename to app/soapbox/reducers/__tests__/me.test.ts index 2c434dca4..28504ba54 100644 --- a/app/soapbox/reducers/__tests__/me-test.js +++ b/app/soapbox/reducers/__tests__/me.test.ts @@ -2,6 +2,6 @@ import reducer from '../me'; describe('me reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(null); + expect(reducer(undefined, {} as any)).toEqual(null); }); }); diff --git a/app/soapbox/reducers/__tests__/meta-test.js b/app/soapbox/reducers/__tests__/meta.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/meta-test.js rename to app/soapbox/reducers/__tests__/meta.test.ts diff --git a/app/soapbox/reducers/__tests__/modals-test.js b/app/soapbox/reducers/__tests__/modals.test.ts similarity index 56% rename from app/soapbox/reducers/__tests__/modals-test.js rename to app/soapbox/reducers/__tests__/modals.test.ts index 36f4fba64..bbb5c4552 100644 --- a/app/soapbox/reducers/__tests__/modals-test.js +++ b/app/soapbox/reducers/__tests__/modals.test.ts @@ -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'; @@ -6,11 +6,11 @@ import reducer from '../modals'; describe('modal reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableList()); + expect(reducer(undefined, {} as any)).toEqual(ImmutableList()); }); it('should handle MODAL_OPEN', () => { - const state = ImmutableList(); + const state = ImmutableList(); const action = { type: MODAL_OPEN, modalType: 'type1', @@ -23,35 +23,43 @@ describe('modal reducer', () => { }); it('should handle MODAL_CLOSE', () => { - const state = ImmutableList([{ - modalType: 'type1', - modalProps: { props1: '1' }, - }]); + const state = ImmutableList([ + ImmutableRecord({ + modalType: 'type1', + modalProps: { props1: '1' }, + })(), + ]); const action = { type: MODAL_CLOSE, }; - expect(reducer(state, action)).toMatchObject(ImmutableList()); + expect(reducer(state, action).toJS()).toMatchObject([]); }); it('should handle MODAL_CLOSE with specified modalType', () => { const state = ImmutableList([ - { + ImmutableRecord({ modalType: 'type1', - }, - { + modalProps: null, + })(), + ImmutableRecord({ modalType: 'type2', - }, - { + modalProps: null, + })(), + ImmutableRecord({ modalType: 'type1', - }, + modalProps: null, + })(), ]); const action = { type: MODAL_CLOSE, modalType: 'type2', }; - expect(reducer(state, action)).toMatchObject(ImmutableList([{ - modalType: 'type1', - }])); + expect(reducer(state, action).toJS()).toEqual([ + { + modalType: 'type1', + modalProps: null, + }, + ]); }); }); diff --git a/app/soapbox/reducers/__tests__/mutes-test.js b/app/soapbox/reducers/__tests__/mutes.test.ts similarity index 51% rename from app/soapbox/reducers/__tests__/mutes-test.js rename to app/soapbox/reducers/__tests__/mutes.test.ts index 175f2ff38..411db10c6 100644 --- a/app/soapbox/reducers/__tests__/mutes-test.js +++ b/app/soapbox/reducers/__tests__/mutes.test.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Record as ImmutableRecord } from 'immutable'; import { MUTES_INIT_MODAL, @@ -9,50 +9,54 @@ import reducer from '../mutes'; describe('mutes reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ - new: ImmutableMap({ + expect(reducer(undefined, {}).toJS()).toEqual({ + new: { isSubmitting: false, - account: null, + accountId: null, notifications: true, - }), - })); + }, + }); }); it('should handle MUTES_INIT_MODAL', () => { - const state = ImmutableMap({ - new: ImmutableMap({ + const state = ImmutableRecord({ + new: ImmutableRecord({ isSubmitting: false, - account: null, + accountId: null, notifications: true, - }), - }); + })(), + })(); const action = { type: MUTES_INIT_MODAL, - account: 'account1', + account: { id: 'account1' }, }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - new: ImmutableMap({ + expect(reducer(state, action).toJS()).toEqual({ + new: { isSubmitting: false, - account: 'account1', + accountId: 'account1', notifications: true, - }), - })); + }, + }); }); it('should handle MUTES_TOGGLE_HIDE_NOTIFICATIONS', () => { - const state = ImmutableMap({ - new: ImmutableMap({ + const state = ImmutableRecord({ + new: ImmutableRecord({ + isSubmitting: false, + accountId: null, notifications: true, - }), - }); + })(), + })(); const action = { type: MUTES_TOGGLE_HIDE_NOTIFICATIONS, }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - new: ImmutableMap({ + expect(reducer(state, action).toJS()).toEqual({ + new: { + isSubmitting: false, + accountId: null, notifications: false, - }), - })); + }, + }); }); }); diff --git a/app/soapbox/reducers/__tests__/patron-test.js b/app/soapbox/reducers/__tests__/patron.test.ts similarity index 94% rename from app/soapbox/reducers/__tests__/patron-test.js rename to app/soapbox/reducers/__tests__/patron.test.ts index 01736704b..4424285cd 100644 --- a/app/soapbox/reducers/__tests__/patron-test.js +++ b/app/soapbox/reducers/__tests__/patron.test.ts @@ -5,7 +5,7 @@ import reducer from '../patron'; describe('patron reducer', () => { it('should return the initial state', () => { - const result = reducer(undefined, {}); + const result = reducer(undefined, {} as any); expect(ImmutableRecord.isRecord(result)).toBe(true); expect(result.instance.url).toBe(''); }); diff --git a/app/soapbox/reducers/__tests__/polls-test.js b/app/soapbox/reducers/__tests__/polls.test.ts similarity index 92% rename from app/soapbox/reducers/__tests__/polls-test.js rename to app/soapbox/reducers/__tests__/polls.test.ts index b19ad858c..b9ceb07f7 100644 --- a/app/soapbox/reducers/__tests__/polls-test.js +++ b/app/soapbox/reducers/__tests__/polls.test.ts @@ -6,7 +6,7 @@ import reducer from '../polls'; describe('polls reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap()); + expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); }); describe('POLLS_IMPORT', () => { diff --git a/app/soapbox/reducers/__tests__/push_notifications-test.js b/app/soapbox/reducers/__tests__/push_notifications.test.ts similarity index 93% rename from app/soapbox/reducers/__tests__/push_notifications-test.js rename to app/soapbox/reducers/__tests__/push_notifications.test.ts index a5db7131d..e7080d42a 100644 --- a/app/soapbox/reducers/__tests__/push_notifications-test.js +++ b/app/soapbox/reducers/__tests__/push_notifications.test.ts @@ -6,7 +6,7 @@ describe('push_notifications reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap({ subscription: null, - alerts: new ImmutableMap({ + alerts: ImmutableMap({ follow: true, follow_request: true, favourite: true, diff --git a/app/soapbox/reducers/__tests__/reports-test.js b/app/soapbox/reducers/__tests__/reports.test.ts similarity index 84% rename from app/soapbox/reducers/__tests__/reports-test.js rename to app/soapbox/reducers/__tests__/reports.test.ts index 975a7ec49..77148e572 100644 --- a/app/soapbox/reducers/__tests__/reports-test.js +++ b/app/soapbox/reducers/__tests__/reports.test.ts @@ -2,7 +2,7 @@ import reducer from '../reports'; describe('reports reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {}).toJS()).toEqual({ + expect(reducer(undefined, {} as any).toJS()).toEqual({ new: { isSubmitting: false, account_id: null, diff --git a/app/soapbox/reducers/__tests__/settings-test.js b/app/soapbox/reducers/__tests__/settings.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/settings-test.js rename to app/soapbox/reducers/__tests__/settings.test.ts diff --git a/app/soapbox/reducers/__tests__/sidebar-test.js b/app/soapbox/reducers/__tests__/sidebar.test.ts similarity index 63% rename from app/soapbox/reducers/__tests__/sidebar-test.js rename to app/soapbox/reducers/__tests__/sidebar.test.ts index 511325488..83212335f 100644 --- a/app/soapbox/reducers/__tests__/sidebar-test.js +++ b/app/soapbox/reducers/__tests__/sidebar.test.ts @@ -2,6 +2,6 @@ import reducer from '../sidebar'; describe('sidebar reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual({ sidebarOpen: false }); + expect(reducer(undefined, {} as any)).toEqual({ sidebarOpen: false }); }); }); diff --git a/app/soapbox/reducers/__tests__/soapbox-test.js b/app/soapbox/reducers/__tests__/soapbox.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/soapbox-test.js rename to app/soapbox/reducers/__tests__/soapbox.test.ts diff --git a/app/soapbox/reducers/__tests__/statuses-test.js b/app/soapbox/reducers/__tests__/statuses.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/statuses-test.js rename to app/soapbox/reducers/__tests__/statuses.test.ts diff --git a/app/soapbox/reducers/__tests__/trends-test.js b/app/soapbox/reducers/__tests__/trends.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/trends-test.js rename to app/soapbox/reducers/__tests__/trends.test.ts diff --git a/app/soapbox/reducers/__tests__/user_lists-test.js b/app/soapbox/reducers/__tests__/user_lists.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/user_lists-test.js rename to app/soapbox/reducers/__tests__/user_lists.test.ts diff --git a/app/soapbox/reducers/__tests__/verification-test.js b/app/soapbox/reducers/__tests__/verification.test.ts similarity index 100% rename from app/soapbox/reducers/__tests__/verification-test.js rename to app/soapbox/reducers/__tests__/verification.test.ts diff --git a/app/soapbox/reducers/custom_emojis.js b/app/soapbox/reducers/custom_emojis.ts similarity index 65% rename from app/soapbox/reducers/custom_emojis.js rename to app/soapbox/reducers/custom_emojis.ts index 7008d5234..477e7cce9 100644 --- a/app/soapbox/reducers/custom_emojis.js +++ b/app/soapbox/reducers/custom_emojis.ts @@ -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 { 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 { buildCustomEmojis } from '../features/emoji/emoji'; +import type { AnyAction } from 'redux'; +import type { APIEntity } from 'soapbox/types/entities'; + const initialState = ImmutableList(); // Populate custom emojis for composer autosuggest -const autosuggestPopulate = emojis => { +const autosuggestPopulate = (emojis: ImmutableList>) => { addCustomToPool(buildCustomEmojis(emojis)); }; -const importEmojis = (state, customEmojis) => { - const emojis = fromJS(customEmojis).filter(emoji => { +const importEmojis = (customEmojis: APIEntity[]) => { + const emojis = (fromJS(customEmojis) as ImmutableList>).filter((emoji) => { // If a custom emoji has the shortcode of a Unicode emoji, skip it. // Otherwise it breaks EmojiMart. // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/610 @@ -26,9 +29,9 @@ const importEmojis = (state, customEmojis) => { 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) { - return importEmojis(state, action.custom_emojis); + return importEmojis(action.custom_emojis); } return state; diff --git a/app/soapbox/reducers/filters.ts b/app/soapbox/reducers/filters.ts new file mode 100644 index 000000000..a31cb3295 --- /dev/null +++ b/app/soapbox/reducers/filters.ts @@ -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; + +const importFilters = (_state: State, filters: APIEntity[]): State => { + return ImmutableList(filters.map((filter) => normalizeFilter(filter))); +}; + +export default function filters(state: State = ImmutableList(), action: AnyAction): State { + switch (action.type) { + case FILTERS_FETCH_SUCCESS: + return importFilters(state, action.filters); + default: + return state; + } +} diff --git a/app/soapbox/reducers/filters.tsx b/app/soapbox/reducers/filters.tsx deleted file mode 100644 index e79dde019..000000000 --- a/app/soapbox/reducers/filters.tsx +++ /dev/null @@ -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; -type State = ImmutableList; - -const importFilters = (_state: State, filters: unknown): State => { - return ImmutableList(fromJS(filters)).map(filter => ImmutableMap(fromJS(filter))); -}; - -export default function filters(state: State = ImmutableList(), action: AnyAction): State { - switch (action.type) { - case FILTERS_FETCH_SUCCESS: - return importFilters(state, action.filters); - default: - return state; - } -} diff --git a/app/soapbox/reducers/mutes.js b/app/soapbox/reducers/mutes.ts similarity index 50% rename from app/soapbox/reducers/mutes.js rename to app/soapbox/reducers/mutes.ts index 8cc6fc4a8..e232d4039 100644 --- a/app/soapbox/reducers/mutes.js +++ b/app/soapbox/reducers/mutes.ts @@ -1,24 +1,30 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Record as ImmutableRecord } from 'immutable'; import { MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, } from '../actions/mutes'; -const initialState = ImmutableMap({ - new: ImmutableMap({ - isSubmitting: false, - account: null, - notifications: true, - }), +import type { AnyAction } from 'redux'; + +const NewMuteRecord = ImmutableRecord({ + isSubmitting: false, + accountId: null, + notifications: true, }); -export default function mutes(state = initialState, action) { +const ReducerRecord = ImmutableRecord({ + new: NewMuteRecord(), +}); + +type State = ReturnType; + +export default function mutes(state: State = ReducerRecord(), action: AnyAction) { switch (action.type) { case MUTES_INIT_MODAL: return state.withMutations((state) => { state.setIn(['new', 'isSubmitting'], false); - state.setIn(['new', 'account'], action.account); + state.setIn(['new', 'accountId'], action.account.id); state.setIn(['new', 'notifications'], true); }); case MUTES_TOGGLE_HIDE_NOTIFICATIONS: diff --git a/app/soapbox/reducers/profile_hover_card.js b/app/soapbox/reducers/profile_hover_card.js deleted file mode 100644 index 8020b965f..000000000 --- a/app/soapbox/reducers/profile_hover_card.js +++ /dev/null @@ -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; - } -} diff --git a/app/soapbox/reducers/profile_hover_card.ts b/app/soapbox/reducers/profile_hover_card.ts new file mode 100644 index 000000000..b07897715 --- /dev/null +++ b/app/soapbox/reducers/profile_hover_card.ts @@ -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 | null, + accountId: '', + hovered: false, +}); + +type State = ReturnType; + +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; + } +} diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 316517a55..c1160acd3 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -14,7 +14,7 @@ import { shouldFilter } from 'soapbox/utils/timelines'; import type { ReducerChat } from 'soapbox/reducers/chats'; 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 : ''; @@ -104,18 +104,18 @@ const toServerSideType = (columnType: string): string => { type FilterContext = { contextType?: string }; export const getFilters = (state: RootState, query: FilterContext) => { - return state.filters.filter((filter): boolean => { + return state.filters.filter((filter) => { return query?.contextType - && filter.get('context').includes(toServerSideType(query.contextType)) - && (filter.get('expires_at') === null - || Date.parse(filter.get('expires_at')) > new Date().getTime()); + && filter.context.includes(toServerSideType(query.contextType)) + && (filter.expires_at === null + || Date.parse(filter.expires_at) > new Date().getTime()); }); }; const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -export const regexFromFilters = (filters: ImmutableList>) => { +export const regexFromFilters = (filters: ImmutableList) => { if (filters.size === 0) return null; return new RegExp(filters.map(filter => { diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index 023139cef..37572ae24 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -8,6 +8,7 @@ import { ChatMessageRecord, EmojiRecord, FieldRecord, + FilterRecord, HistoryRecord, InstanceRecord, ListRecord, @@ -31,6 +32,7 @@ type Chat = ReturnType; type ChatMessage = ReturnType; type Emoji = ReturnType; type Field = ReturnType; +type Filter = ReturnType; type History = ReturnType; type Instance = ReturnType; type List = ReturnType; @@ -68,6 +70,7 @@ export { ChatMessage, Emoji, Field, + Filter, History, Instance, List, diff --git a/app/soapbox/utils/__tests__/accounts-test.js b/app/soapbox/utils/__tests__/accounts.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/accounts-test.js rename to app/soapbox/utils/__tests__/accounts.test.ts diff --git a/app/soapbox/utils/__tests__/base64-test.js b/app/soapbox/utils/__tests__/base64.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/base64-test.js rename to app/soapbox/utils/__tests__/base64.test.ts diff --git a/app/soapbox/utils/__tests__/colors-test.js b/app/soapbox/utils/__tests__/colors.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/colors-test.js rename to app/soapbox/utils/__tests__/colors.test.ts diff --git a/app/soapbox/utils/__tests__/config_db-test.js b/app/soapbox/utils/__tests__/config_db.test.ts similarity index 61% rename from app/soapbox/utils/__tests__/config_db-test.js rename to app/soapbox/utils/__tests__/config_db.test.ts index b8ad15e0f..a83757eb3 100644 --- a/app/soapbox/utils/__tests__/config_db-test.js +++ b/app/soapbox/utils/__tests__/config_db.test.ts @@ -1,4 +1,4 @@ -import { fromJS } from 'immutable'; +import { List as ImmutableList, fromJS } from 'immutable'; import config_db from 'soapbox/__fixtures__/config_db.json'; @@ -6,7 +6,7 @@ import { ConfigDB } from '../config_db'; test('find', () => { const configs = fromJS(config_db).get('configs'); - expect(ConfigDB.find(configs, ':phoenix', ':json_library')).toEqual(fromJS({ + expect(ConfigDB.find(configs as ImmutableList, ':phoenix', ':json_library')).toEqual(fromJS({ group: ':phoenix', key: ':json_library', value: 'Jason', diff --git a/app/soapbox/utils/__tests__/features-test.js b/app/soapbox/utils/__tests__/features.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/features-test.js rename to app/soapbox/utils/__tests__/features.test.ts diff --git a/app/soapbox/utils/__tests__/html-test.js b/app/soapbox/utils/__tests__/html.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/html-test.js rename to app/soapbox/utils/__tests__/html.test.ts diff --git a/app/soapbox/utils/__tests__/numbers-test.tsx b/app/soapbox/utils/__tests__/numbers.test.tsx similarity index 100% rename from app/soapbox/utils/__tests__/numbers-test.tsx rename to app/soapbox/utils/__tests__/numbers.test.tsx diff --git a/app/soapbox/utils/__tests__/phone-test.js b/app/soapbox/utils/__tests__/phone.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/phone-test.js rename to app/soapbox/utils/__tests__/phone.test.ts diff --git a/app/soapbox/utils/__tests__/status-test.js b/app/soapbox/utils/__tests__/status.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/status-test.js rename to app/soapbox/utils/__tests__/status.test.ts diff --git a/app/soapbox/utils/__tests__/tailwind-test.js b/app/soapbox/utils/__tests__/tailwind.test.ts similarity index 100% rename from app/soapbox/utils/__tests__/tailwind-test.js rename to app/soapbox/utils/__tests__/tailwind.test.ts
{message}