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/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/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/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..14d6fc82c --- /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/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: