diff --git a/app/soapbox/actions/remote_timeline.js b/app/soapbox/actions/remote_timeline.js index 38249c009..8238c3326 100644 --- a/app/soapbox/actions/remote_timeline.js +++ b/app/soapbox/actions/remote_timeline.js @@ -10,7 +10,7 @@ export function pinHost(host) { const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.push(host))); }; } @@ -19,6 +19,6 @@ export function unpinHost(host) { const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.filter((value) => value !== host))); }; } diff --git a/app/soapbox/components/avatar_overlay.js b/app/soapbox/components/avatar_overlay.js deleted file mode 100644 index ce9784c95..000000000 --- a/app/soapbox/components/avatar_overlay.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import StillImage from 'soapbox/components/still_image'; - -export default class AvatarOverlay extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - friend: ImmutablePropTypes.map.isRequired, - }; - - render() { - const { account, friend } = this.props; - - return ( -
- - -
- ); - } - -} diff --git a/app/soapbox/components/avatar_overlay.tsx b/app/soapbox/components/avatar_overlay.tsx new file mode 100644 index 000000000..ae38b5e4c --- /dev/null +++ b/app/soapbox/components/avatar_overlay.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import StillImage from 'soapbox/components/still_image'; + +import type { Account as AccountEntity } from 'soapbox/types/entities'; + +interface IAvatarOverlay { + account: AccountEntity, + friend: AccountEntity, +} + +const AvatarOverlay: React.FC = ({ account, friend }) => ( +
+ + +
+); + +export default AvatarOverlay; diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js index fa5ec1782..b6ed8b3d4 100644 --- a/app/soapbox/components/icon_button.js +++ b/app/soapbox/components/icon_button.js @@ -12,6 +12,7 @@ export default class IconButton extends React.PureComponent { static propTypes = { className: PropTypes.string, + iconClassName: PropTypes.string, title: PropTypes.string.isRequired, icon: PropTypes.string, src: PropTypes.string, @@ -99,6 +100,7 @@ export default class IconButton extends React.PureComponent { active, animate, className, + iconClassName, disabled, expanded, icon, @@ -144,7 +146,7 @@ export default class IconButton extends React.PureComponent {
{emoji ? {text && {text}} @@ -174,7 +176,7 @@ export default class IconButton extends React.PureComponent {
{emoji ? {text && {text}} diff --git a/app/soapbox/components/load_gap.js b/app/soapbox/components/load_gap.js deleted file mode 100644 index 84eeec000..000000000 --- a/app/soapbox/components/load_gap.js +++ /dev/null @@ -1,35 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; - -import Icon from 'soapbox/components/icon'; - -const messages = defineMessages({ - load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, -}); - -export default @injectIntl -class LoadGap extends React.PureComponent { - - static propTypes = { - disabled: PropTypes.bool, - maxId: PropTypes.string, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleClick = () => { - this.props.onClick(this.props.maxId); - } - - render() { - const { disabled, intl } = this.props; - - return ( - - ); - } - -} diff --git a/app/soapbox/components/load_gap.tsx b/app/soapbox/components/load_gap.tsx new file mode 100644 index 000000000..b784c871d --- /dev/null +++ b/app/soapbox/components/load_gap.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Icon from 'soapbox/components/icon'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +interface ILoadGap { + disabled?: boolean, + maxId: string, + onClick: (id: string) => void, +} + +const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + + const handleClick = () => onClick(maxId); + + return ( + + ); +}; + +export default LoadGap; diff --git a/app/soapbox/components/pull-to-refresh.tsx b/app/soapbox/components/pull-to-refresh.tsx index 6ef199f3c..7596fabea 100644 --- a/app/soapbox/components/pull-to-refresh.tsx +++ b/app/soapbox/components/pull-to-refresh.tsx @@ -4,7 +4,9 @@ import PTRComponent from 'react-simple-pull-to-refresh'; import { Spinner } from 'soapbox/components/ui'; interface IPullToRefresh { - onRefresh?: () => Promise + onRefresh?: () => Promise; + refreshingContent?: JSX.Element | string; + pullingContent?: JSX.Element | string; } /** diff --git a/app/soapbox/components/pullable.js b/app/soapbox/components/pullable.js deleted file mode 100644 index 0f7546a9c..000000000 --- a/app/soapbox/components/pullable.js +++ /dev/null @@ -1,30 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import PullToRefresh from './pull-to-refresh'; - -/** - * Pullable: - * Basic "pull to refresh" without the refresh. - * Just visual feedback. - */ -export default class Pullable extends React.Component { - - static propTypes = { - children: PropTypes.node.isRequired, - } - - render() { - const { children } = this.props; - - return ( - - {children} - - ); - } - -} diff --git a/app/soapbox/components/pullable.tsx b/app/soapbox/components/pullable.tsx new file mode 100644 index 000000000..0304a1d46 --- /dev/null +++ b/app/soapbox/components/pullable.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import PullToRefresh from './pull-to-refresh'; + +interface IPullable { + children: JSX.Element, +} + +/** + * Pullable: + * Basic "pull to refresh" without the refresh. + * Just visual feedback. + */ +const Pullable: React.FC = ({ children }) =>( + + {children} + +); + +export default Pullable; diff --git a/app/soapbox/components/radio_button.js b/app/soapbox/components/radio_button.js deleted file mode 100644 index 0f82af95f..000000000 --- a/app/soapbox/components/radio_button.js +++ /dev/null @@ -1,35 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -export default class RadioButton extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - checked: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - label: PropTypes.node.isRequired, - }; - - render() { - const { name, value, checked, onChange, label } = this.props; - - return ( - - ); - } - -} \ No newline at end of file diff --git a/app/soapbox/components/radio_button.tsx b/app/soapbox/components/radio_button.tsx new file mode 100644 index 000000000..c3f87ce02 --- /dev/null +++ b/app/soapbox/components/radio_button.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import React from 'react'; + +interface IRadioButton { + value: string, + checked?: boolean, + name: string, + onChange: React.ChangeEventHandler, + label: JSX.Element, +} + +const RadioButton: React.FC = ({ name, value, checked, onChange, label }) => ( + +); + +export default RadioButton; diff --git a/app/soapbox/components/site-logo.tsx b/app/soapbox/components/site-logo.tsx index 81f9ed417..01bfc54ed 100644 --- a/app/soapbox/components/site-logo.tsx +++ b/app/soapbox/components/site-logo.tsx @@ -6,16 +6,20 @@ import { useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks'; interface ISiteLogo extends React.ComponentProps<'img'> { /** Extra class names for the element. */ className?: string, + /** Override theme setting for */ + theme?: 'dark' | 'light', } /** Display the most appropriate site logo based on the theme and configuration. */ -const SiteLogo: React.FC = ({ className, ...rest }) => { +const SiteLogo: React.FC = ({ className, theme, ...rest }) => { const { logo, logoDarkMode } = useSoapboxConfig(); const settings = useSettings(); const systemTheme = useSystemTheme(); const userTheme = settings.get('themeMode'); - const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark'); + const darkMode = theme + ? theme === 'dark' + : (userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark')); /** Soapbox logo. */ const soapboxLogo = darkMode diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx new file mode 100644 index 000000000..955924acc --- /dev/null +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -0,0 +1,75 @@ +import { List as ImmutableList } from 'immutable'; +import React from 'react'; +import { FormattedList, FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { openModal } from 'soapbox/actions/modals'; +import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import { useAppDispatch } from 'soapbox/hooks'; + +import type { Status } from 'soapbox/types/entities'; + +interface IStatusReplyMentions { + status: Status, +} + +const StatusReplyMentions: React.FC = ({ status }) => { + const dispatch = useAppDispatch(); + + const handleOpenMentionsModal: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + dispatch(openModal('MENTIONS', { + username: status.getIn(['account', 'acct']), + statusId: status.get('id'), + })); + }; + + if (!status.get('in_reply_to_id')) { + return null; + } + + const to = status.get('mentions', ImmutableList()); + + // The post is a reply, but it has no mentions. + // Rare, but it can happen. + if (to.size === 0) { + return ( +
+ +
+ ); + } + + // The typical case with a reply-to and a list of mentions. + const accounts = to.slice(0, 2).map(account => ( + + @{account.get('username')} + + )).toArray(); + + if (to.size > 2) { + accounts.push( + + + , + ); + } + + return ( +
+ , + }} + /> +
+ ); +}; + +export default StatusReplyMentions; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 141394420..3cea7c280 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -6,18 +6,17 @@ import { injectIntl, FormattedMessage, IntlShape, defineMessages } from 'react-i import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; +import AccountContainer from 'soapbox/containers/account_container'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; +import Card from 'soapbox/features/status/components/card'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; - -import AccountContainer from '../containers/account_container'; -import Card from '../features/status/components/card'; -import Bundle from '../features/ui/components/bundle'; -import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; +import Bundle from 'soapbox/features/ui/components/bundle'; +import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components'; import AttachmentThumbs from './attachment-thumbs'; +import StatusReplyMentions from './status-reply-mentions'; import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; -import StatusReplyMentions from './status_reply_mentions'; import { HStack, Text } from './ui'; import type { History } from 'history'; diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js deleted file mode 100644 index 0809d6085..000000000 --- a/app/soapbox/components/status_reply_mentions.js +++ /dev/null @@ -1,89 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedList, FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import { openModal } from 'soapbox/actions/modals'; -import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; - -const mapDispatchToProps = (dispatch) => ({ - onOpenMentionsModal(username, statusId) { - dispatch(openModal('MENTIONS', { - username, - statusId, - })); - }, -}); - -export default @connect(null, mapDispatchToProps) -@injectIntl -class StatusReplyMentions extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.record.isRequired, - onOpenMentionsModal: PropTypes.func, - } - - handleOpenMentionsModal = (e) => { - const { status, onOpenMentionsModal } = this.props; - - e.stopPropagation(); - - onOpenMentionsModal(status.getIn(['account', 'acct']), status.get('id')); - } - - render() { - const { status } = this.props; - - if (!status.get('in_reply_to_id')) { - return null; - } - - const to = status.get('mentions', ImmutableList()); - - // The post is a reply, but it has no mentions. - // Rare, but it can happen. - if (to.size === 0) { - return ( -
- -
- ); - } - - // The typical case with a reply-to and a list of mentions. - const accounts = to.slice(0, 2).map(account => ( - - @{account.get('username')} - - )).toArray(); - - if (to.size > 2) { - accounts.push( - - - , - ); - } - - return ( -
- , - }} - /> -
- ); - } - -} diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx index 5a3b887df..dee12198d 100644 --- a/app/soapbox/components/ui/widget/widget.tsx +++ b/app/soapbox/components/ui/widget/widget.tsx @@ -28,6 +28,7 @@ interface IWidget { actionIcon?: string, /** Text for the action. */ actionTitle?: string, + action?: JSX.Element, } /** Sidebar widget. */ @@ -37,19 +38,20 @@ const Widget: React.FC = ({ onActionClick, actionIcon = require('@tabler/icons/icons/arrow-right.svg'), actionTitle, + action, }): JSX.Element => { return ( - {onActionClick && ( + {action || (onActionClick && ( - )} + ))} {children} diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.js b/app/soapbox/features/auth_login/components/otp_auth_form.js deleted file mode 100644 index 0446a7afe..000000000 --- a/app/soapbox/features/auth_login/components/otp_auth_form.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; -import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; - -const messages = defineMessages({ - otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' }, - otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' }, - otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' }, -}); - -export default @connect() -@injectIntl -class OtpAuthForm extends ImmutablePureComponent { - - state = { - isLoading: false, - code_error: '', - shouldRedirect: false, - } - - static propTypes = { - intl: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - mfa_token: PropTypes.string, - }; - - getFormData = (form) => { - return Object.fromEntries( - Array.from(form).map(i => [i.name, i.value]), - ); - } - - handleSubmit = (event) => { - const { dispatch, mfa_token } = this.props; - const { code } = this.getFormData(event.target); - dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => { - this.setState({ code_error: false }); - return dispatch(verifyCredentials(access_token)); - }).then(account => { - this.setState({ shouldRedirect: true }); - return dispatch(switchAccount(account.id)); - }).catch(error => { - this.setState({ isLoading: false, code_error: true }); - }); - this.setState({ isLoading: true }); - event.preventDefault(); - } - - render() { - const { intl } = this.props; - const { code_error, shouldRedirect } = this.state; - - if (shouldRedirect) return ; - - return ( -
-
-

- -

-
- -
-
- - - - - - - -
-
-
- ); - } - -} diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.tsx b/app/soapbox/features/auth_login/components/otp_auth_form.tsx new file mode 100644 index 000000000..eaeade08b --- /dev/null +++ b/app/soapbox/features/auth_login/components/otp_auth_form.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { Redirect } from 'react-router-dom'; + +import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; +import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +const messages = defineMessages({ + otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' }, + otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' }, + otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' }, +}); + +interface IOtpAuthForm { + mfa_token: string, +} + +const OtpAuthForm: React.FC = ({ mfa_token }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const [isLoading, setIsLoading] = useState(false); + const [shouldRedirect, setShouldRedirect] = useState(false); + const [codeError, setCodeError] = useState(''); + + const getFormData = (form: any) => Object.fromEntries( + Array.from(form).map((i: any) => [i.name, i.value]), + ); + + const handleSubmit = (event: React.FormEvent) => { + const { code } = getFormData(event.target); + dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => { + setCodeError(false); + return dispatch(verifyCredentials(access_token)); + }).then(account => { + setShouldRedirect(true); + return dispatch(switchAccount(account.id)); + }).catch(() => { + setIsLoading(false); + setCodeError(true); + }); + setIsLoading(true); + event.preventDefault(); + }; + + if (shouldRedirect) return ; + + return ( +
+
+

+ +

+
+ +
+
+ + + + + + + +
+
+
+ ); +}; + +export default OtpAuthForm; diff --git a/app/soapbox/features/auth_login/components/password_reset.js b/app/soapbox/features/auth_login/components/password_reset.js deleted file mode 100644 index f047207be..000000000 --- a/app/soapbox/features/auth_login/components/password_reset.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { resetPassword } from 'soapbox/actions/security'; -import snackbar from 'soapbox/actions/snackbar'; - -import { Button, Form, FormActions, FormGroup, Input } from '../../../components/ui'; - -const messages = defineMessages({ - nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' }, - confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' }, -}); - -export default @connect() -@injectIntl -class PasswordReset extends ImmutablePureComponent { - - state = { - isLoading: false, - success: false, - } - - handleSubmit = e => { - const { dispatch, intl } = this.props; - const nicknameOrEmail = e.target.nickname_or_email.value; - this.setState({ isLoading: true }); - dispatch(resetPassword(nicknameOrEmail)).then(() => { - this.setState({ isLoading: false, success: true }); - dispatch(snackbar.info(intl.formatMessage(messages.confirmation))); - }).catch(error => { - this.setState({ isLoading: false }); - }); - } - - render() { - const { intl } = this.props; - - if (this.state.success) return ; - - return ( -
-
-

- -

-
- -
-
- - - - - - - -
-
-
- ); - } - -} diff --git a/app/soapbox/features/auth_login/components/password_reset.tsx b/app/soapbox/features/auth_login/components/password_reset.tsx new file mode 100644 index 000000000..bcd7976c6 --- /dev/null +++ b/app/soapbox/features/auth_login/components/password_reset.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { Redirect } from 'react-router-dom'; + +import { resetPassword } from 'soapbox/actions/security'; +import snackbar from 'soapbox/actions/snackbar'; +import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +const messages = defineMessages({ + nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' }, + confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' }, +}); + +const PasswordReset = () => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + const nicknameOrEmail = (e.target as any).nickname_or_email.value; + setIsLoading(true); + dispatch(resetPassword(nicknameOrEmail)).then(() => { + setIsLoading(false); + setSuccess(true); + dispatch(snackbar.info(intl.formatMessage(messages.confirmation))); + }).catch(() => { + setIsLoading(false); + }); + }; + + if (success) return ; + + return ( +
+
+

+ +

+
+ +
+
+ + + + + + + +
+
+
+ ); +}; + +export default PasswordReset; diff --git a/app/soapbox/features/auth_token_list/index.tsx b/app/soapbox/features/auth_token_list/index.tsx index 4563e4b34..458d8448b 100644 --- a/app/soapbox/features/auth_token_list/index.tsx +++ b/app/soapbox/features/auth_token_list/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, FormattedDate, useIntl } from 'react-intl'; import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security'; diff --git a/app/soapbox/features/compose/components/poll_form.js b/app/soapbox/features/compose/components/poll_form.js index d566b3055..41b51ca5e 100644 --- a/app/soapbox/features/compose/components/poll_form.js +++ b/app/soapbox/features/compose/components/poll_form.js @@ -11,6 +11,7 @@ import { connect } from 'react-redux'; import AutosuggestInput from 'soapbox/components/autosuggest_input'; import Icon from 'soapbox/components/icon'; import IconButton from 'soapbox/components/icon_button'; +import { HStack } from 'soapbox/components/ui'; const messages = defineMessages({ option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, @@ -177,7 +178,7 @@ class PollForm extends ImmutablePureComponent { ))} -
+ {options.size < maxOptions && ( )} @@ -191,7 +192,7 @@ class PollForm extends ImmutablePureComponent { -
+
); } diff --git a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js b/app/soapbox/features/federation_restrictions/components/instance_restrictions.js index f4f1145f4..5539b845b 100644 --- a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js +++ b/app/soapbox/features/federation_restrictions/components/instance_restrictions.js @@ -8,6 +8,7 @@ import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import Icon from 'soapbox/components/icon'; +import { Text } from 'soapbox/components/ui'; const hasRestrictions = remoteInstance => { return remoteInstance @@ -49,77 +50,57 @@ class InstanceRestrictions extends ImmutablePureComponent { if (followers_only) { items.push(( -
-
- -
-
- -
-
+ + + + )); } else if (federated_timeline_removal) { items.push(( -
-
- -
-
- -
-
+ + + + )); } if (fullMediaRemoval) { items.push(( -
-
- -
-
- -
-
+ + + + )); } else if (partialMediaRemoval) { items.push(( -
-
- -
-
- -
-
+ + + + )); } if (!fullMediaRemoval && media_nsfw) { items.push(( -
-
- -
-
- -
-
+ + + + )); } @@ -135,38 +116,38 @@ class InstanceRestrictions extends ImmutablePureComponent { if (remoteInstance.getIn(['federation', 'reject']) === true) { return ( -
- + + -
+ ); } else if (hasRestrictions(remoteInstance)) { return [ ( -
+ -
+ ), this.renderRestrictions(), ]; } else { return ( -
- + + -
+ ); } } diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx index fc5ab38d0..841f5e7d0 100644 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx +++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; diff --git a/app/soapbox/features/list_timeline/index.tsx b/app/soapbox/features/list_timeline/index.tsx index 2697c201c..85441c98f 100644 --- a/app/soapbox/features/list_timeline/index.tsx +++ b/app/soapbox/features/list_timeline/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; diff --git a/app/soapbox/features/lists/index.tsx b/app/soapbox/features/lists/index.tsx index a25534910..82f4b48a9 100644 --- a/app/soapbox/features/lists/index.tsx +++ b/app/soapbox/features/lists/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; diff --git a/app/soapbox/features/new_status/index.js b/app/soapbox/features/new_status/index.js deleted file mode 100644 index ef6092b11..000000000 --- a/app/soapbox/features/new_status/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { openModal } from '../../actions/modals'; - -const mapDispatchToProps = dispatch => ({ - - onLoad: (text) => { - dispatch(openModal('COMPOSE')); - }, - -}); - -export default @connect(null, mapDispatchToProps) -class NewStatus extends React.Component { - - static propTypes = { - onLoad: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.props.onLoad(); - } - - render() { - return ( - - ); - } - -} diff --git a/app/soapbox/features/new_status/index.tsx b/app/soapbox/features/new_status/index.tsx new file mode 100644 index 000000000..322976afc --- /dev/null +++ b/app/soapbox/features/new_status/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useEffect } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { openModal } from 'soapbox/actions/modals'; +import { useAppDispatch } from 'soapbox/hooks'; + +const NewStatus = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(openModal('COMPOSE')); + }, []); + + return ( + + ); +}; + +export default NewStatus; diff --git a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx index f1c1ac425..3f08e94d6 100644 --- a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx +++ b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx @@ -1,9 +1,8 @@ 'use strict'; -import classNames from 'classnames'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Button, HStack } from 'soapbox/components/ui'; import { useSettings } from 'soapbox/hooks'; interface IPinnedHostsPicker { @@ -18,13 +17,18 @@ const PinnedHostsPicker: React.FC = ({ host: activeHost }) = if (!pinnedHosts || pinnedHosts.isEmpty()) return null; return ( -
+ {pinnedHosts.map((host: any) => ( -
- {host} -
+ ))} -
+ ); }; diff --git a/app/soapbox/features/remote_timeline/index.tsx b/app/soapbox/features/remote_timeline/index.tsx index c429549f5..6f8fe9477 100644 --- a/app/soapbox/features/remote_timeline/index.tsx +++ b/app/soapbox/features/remote_timeline/index.tsx @@ -3,6 +3,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; import IconButton from 'soapbox/components/icon_button'; +import { HStack, Text } from 'soapbox/components/ui'; import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useSettings } from 'soapbox/hooks'; @@ -66,14 +67,16 @@ const RemoteTimeline: React.FC = ({ params }) => { return ( {instance && } - {!pinned &&
- - -
} + {!pinned && + + + + + } { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => { - const account = getAccount(state, accountId); - - return { - added: !!account && state.getIn(['compose', 'to']).includes(account.get('acct')), - account, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { accountId }) => ({ - onRemove: () => dispatch(removeFromMentions(accountId)), - onAdd: () => dispatch(addToMentions(accountId)), - fetchAccount: () => dispatch(fetchAccount(accountId)), -}); - -export default @connect(makeMapStateToProps, mapDispatchToProps) -@injectIntl -class Account extends ImmutablePureComponent { - - static propTypes = { - accountId: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired, - onRemove: PropTypes.func.isRequired, - onAdd: PropTypes.func.isRequired, - added: PropTypes.bool, - author: PropTypes.bool, - }; - - static defaultProps = { - added: false, - }; - - componentDidMount() { - const { account, accountId } = this.props; - - if (accountId && !account) { - this.props.fetchAccount(accountId); - } - } - - render() { - const { account, intl, onRemove, onAdd, added, author } = this.props; - - if (!account) return null; - - let button; - - if (added) { - button = ; - } else { - button = ; - } - - return ( -
-
-
-
- -
- -
- {!author && button} -
-
-
- ); - } - -} diff --git a/app/soapbox/features/reply_mentions/account.tsx b/app/soapbox/features/reply_mentions/account.tsx new file mode 100644 index 000000000..c4817e8cf --- /dev/null +++ b/app/soapbox/features/reply_mentions/account.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { useEffect } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { fetchAccount } from 'soapbox/actions/accounts'; +import { addToMentions, removeFromMentions } from 'soapbox/actions/compose'; +import Avatar from 'soapbox/components/avatar'; +import DisplayName from 'soapbox/components/display-name'; +import IconButton from 'soapbox/components/icon_button'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; + +const messages = defineMessages({ + remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' }, + add: { id: 'reply_mentions.account.add', defaultMessage: 'Add to mentions' }, +}); + +const getAccount = makeGetAccount(); + +interface IAccount { + accountId: string, + author: boolean, +} + +const Account: React.FC = ({ accountId, author }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const account = useAppSelector((state) => getAccount(state, accountId)); + const added = useAppSelector((state) => !!account && state.compose.get('to').includes(account.acct)); + + const onRemove = () => dispatch(removeFromMentions(accountId)); + const onAdd = () => dispatch(addToMentions(accountId)); + + useEffect(() => { + if (accountId && !account) { + dispatch(fetchAccount(accountId)); + } + }, []); + + if (!account) return null; + + let button; + + if (added) { + button = ; + } else { + button = ; + } + + return ( +
+
+
+
+ +
+ +
+ {!author && button} +
+
+
+ ); +}; + +export default Account; diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx index 0d8b14cd3..451529a32 100644 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx @@ -2,8 +2,8 @@ import classNames from 'classnames'; import React from 'react'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; import StatusContent from 'soapbox/components/status_content'; -import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; import { HStack } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import PollPreview from 'soapbox/features/ui/components/poll_preview'; diff --git a/app/soapbox/features/scheduled_statuses/index.tsx b/app/soapbox/features/scheduled_statuses/index.tsx index 7d387022d..006e8894c 100644 --- a/app/soapbox/features/scheduled_statuses/index.tsx +++ b/app/soapbox/features/scheduled_statuses/index.tsx @@ -1,6 +1,5 @@ import { debounce } from 'lodash'; -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { fetchScheduledStatuses, expandScheduledStatuses } from 'soapbox/actions/scheduled_statuses'; diff --git a/app/soapbox/features/server_info/index.js b/app/soapbox/features/server_info/index.js deleted file mode 100644 index a7d19c72a..000000000 --- a/app/soapbox/features/server_info/index.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import Column from '../ui/components/column'; -import LinkFooter from '../ui/components/link_footer'; -import PromoPanel from '../ui/components/promo_panel'; - -const messages = defineMessages({ - heading: { id: 'column.info', defaultMessage: 'Server information' }, -}); - -const mapStateToProps = (state, props) => ({ - instance: state.get('instance'), -}); - -export default @connect(mapStateToProps) -@injectIntl -class ServerInfo extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - }; - - render() { - const { intl, instance } = this.props; - - return ( - -
-
-
-

{instance.get('title')}

-
-
- {instance.get('description')} -
-
- - -
-
- ); - } - -} diff --git a/app/soapbox/features/server_info/index.tsx b/app/soapbox/features/server_info/index.tsx new file mode 100644 index 000000000..0e12538d4 --- /dev/null +++ b/app/soapbox/features/server_info/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { useAppSelector } from 'soapbox/hooks'; + +import Column from '../ui/components/column'; +import LinkFooter from '../ui/components/link_footer'; +import PromoPanel from '../ui/components/promo_panel'; + +const messages = defineMessages({ + heading: { id: 'column.info', defaultMessage: 'Server information' }, +}); + +const ServerInfo = () => { + const intl = useIntl(); + const instance = useAppSelector((state) => state.instance); + + return ( + +
+
+
+

{instance.title}

+
+
+ {instance.description} +
+
+ + +
+
+ ); +}; + +export default ServerInfo; diff --git a/app/soapbox/features/settings/media_display.js b/app/soapbox/features/settings/media_display.tsx similarity index 78% rename from app/soapbox/features/settings/media_display.js rename to app/soapbox/features/settings/media_display.tsx index 0ce0eb51a..ee522347f 100644 --- a/app/soapbox/features/settings/media_display.js +++ b/app/soapbox/features/settings/media_display.tsx @@ -1,16 +1,11 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { getSettings, changeSettingImmediate } from 'soapbox/actions/settings'; -import { - SimpleForm, - SelectDropdown, -} from 'soapbox/features/forms'; -import { useAppSelector } from 'soapbox/hooks'; - -import List, { ListItem } from '../../components/list'; -import { Card, CardBody, CardHeader, CardTitle } from '../../components/ui'; +import List, { ListItem } from 'soapbox/components/list'; +import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui'; +import { SimpleForm, SelectDropdown } from 'soapbox/features/forms'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ mediaDisplay: { id: 'preferences.fields.media_display_label', defaultMessage: 'Media display' }, @@ -20,7 +15,7 @@ const messages = defineMessages({ }); const MediaDisplay = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const intl = useIntl(); const settings = useAppSelector((state) => getSettings(state)); @@ -31,7 +26,7 @@ const MediaDisplay = () => { show_all: intl.formatMessage(messages.display_media_show_all), }; - const onSelectChange = path => { + const onSelectChange: (path: string[]) => React.ChangeEventHandler = path => { return e => { dispatch(changeSettingImmediate(path, e.target.value)); }; @@ -49,7 +44,7 @@ const MediaDisplay = () => { diff --git a/app/soapbox/features/share/index.js b/app/soapbox/features/share/index.js deleted file mode 100644 index ff6dd18f3..000000000 --- a/app/soapbox/features/share/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { openComposeWithText } from '../../actions/compose'; - -const mapDispatchToProps = dispatch => ({ - - onShare: (text) => { - dispatch(openComposeWithText(text)); - }, - -}); - -export default @connect(null, mapDispatchToProps) -class Share extends React.Component { - - static propTypes = { - onShare: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - const params = new URLSearchParams(window.location.search); - - const text = [ - params.get('title'), - params.get('text'), - params.get('url'), - ] - .filter(v => v) - .join('\n\n'); - - if (text) { - this.props.onShare(text); - } - } - - render() { - return ( - - ); - } - -} diff --git a/app/soapbox/features/share/index.tsx b/app/soapbox/features/share/index.tsx new file mode 100644 index 000000000..562f23689 --- /dev/null +++ b/app/soapbox/features/share/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Redirect, useLocation } from 'react-router-dom'; + +import { openComposeWithText } from 'soapbox/actions/compose'; +import { useAppDispatch } from 'soapbox/hooks'; + +const Share = () => { + const dispatch = useAppDispatch(); + + const { search } = useLocation(); + const params = new URLSearchParams(search); + + const text = [ + params.get('title'), + params.get('text'), + params.get('url'), + ] + .filter(v => v) + .join('\n\n'); + + if (text) { + dispatch(openComposeWithText(text)); + } + + return ( + + ); +}; + +export default Share; \ No newline at end of file diff --git a/app/soapbox/features/soapbox_config/components/site-preview.tsx b/app/soapbox/features/soapbox_config/components/site-preview.tsx index 82b35974a..1c4efc92f 100644 --- a/app/soapbox/features/soapbox_config/components/site-preview.tsx +++ b/app/soapbox/features/soapbox_config/components/site-preview.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import { defaultSettings } from 'soapbox/actions/settings'; +import SiteLogo from 'soapbox/components/site-logo'; import BackgroundShapes from 'soapbox/features/ui/components/background_shapes'; import { useSystemTheme } from 'soapbox/hooks'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; @@ -47,7 +48,7 @@ const SitePreview: React.FC = ({ soapbox }) => { 'bg-slate-800': dark, })} > - Logo +
); diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 5397c2e4c..093e0a896 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -5,13 +5,13 @@ import { FormattedMessage, injectIntl, WrappedComponentProps as IntlProps } from import { FormattedDate } from 'react-intl'; import Icon from 'soapbox/components/icon'; +import MediaGallery from 'soapbox/components/media_gallery'; +import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; +import StatusContent from 'soapbox/components/status_content'; +import { HStack, Text } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; -import MediaGallery from '../../../components/media_gallery'; -import StatusContent from '../../../components/status_content'; -import StatusReplyMentions from '../../../components/status_reply_mentions'; -import { HStack, Text } from '../../../components/ui'; -import AccountContainer from '../../../containers/account_container'; import Audio from '../../audio'; import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import Video from '../../video'; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 3f762c31b..804005fa5 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -582,7 +582,7 @@ class Status extends ImmutablePureComponent { } renderPendingStatus(id: string) { - const { status } = this.props; + // const { status } = this.props; const idempotencyKey = id.replace(/^末pending-/, ''); return ( @@ -590,10 +590,10 @@ class Status extends ImmutablePureComponent { className='thread__status' key={id} idempotencyKey={idempotencyKey} - focusedStatusId={status.id} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - contextType='thread' + // focusedStatusId={status.id} + // onMoveUp={this.handleMoveUp} + // onMoveDown={this.handleMoveDown} + // contextType='thread' /> ); } diff --git a/app/soapbox/features/ui/components/component_modal.js b/app/soapbox/features/ui/components/component_modal.js deleted file mode 100644 index 51c23a8e1..000000000 --- a/app/soapbox/features/ui/components/component_modal.js +++ /dev/null @@ -1,26 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -export default class ComponentModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - component: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - componentProps: PropTypes.object, - } - - static defaultProps = { - componentProps: {}, - } - - render() { - const { onClose, component: Component, componentProps } = this.props; - - return ( -
- -
- ); - } - -} diff --git a/app/soapbox/features/ui/components/component_modal.tsx b/app/soapbox/features/ui/components/component_modal.tsx new file mode 100644 index 000000000..b4daa41e7 --- /dev/null +++ b/app/soapbox/features/ui/components/component_modal.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { Modal } from 'soapbox/components/ui'; + +interface IComponentModal { + onClose: (type?: string) => void, + component: React.ComponentType<{ + onClose: (type?: string) => void, + }>, + componentProps: Record, +} + +const ComponentModal: React.FC = ({ onClose, component: Component, componentProps = {} }) => ( + + + +); + +export default ComponentModal; diff --git a/app/soapbox/features/ui/components/edit_federation_modal.tsx b/app/soapbox/features/ui/components/edit_federation_modal.tsx index 3d4456a75..9dbde1f79 100644 --- a/app/soapbox/features/ui/components/edit_federation_modal.tsx +++ b/app/soapbox/features/ui/components/edit_federation_modal.tsx @@ -1,17 +1,18 @@ import { Map as ImmutableMap } from 'immutable'; import React, { useState, useEffect } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import Toggle from 'react-toggle'; import { updateMrf } from 'soapbox/actions/mrf'; import snackbar from 'soapbox/actions/snackbar'; -import { SimpleForm, Checkbox } from 'soapbox/features/forms'; +import { HStack, Modal, Stack, Text } from 'soapbox/components/ui'; +import { SimpleForm } from 'soapbox/features/forms'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetRemoteInstance } from 'soapbox/selectors'; const getRemoteInstance = makeGetRemoteInstance(); const messages = defineMessages({ - reject: { id: 'edit_federation.reject', defaultMessage: 'Reject all activities' }, mediaRemoval: { id: 'edit_federation.media_removal', defaultMessage: 'Strip media' }, forceNsfw: { id: 'edit_federation.force_nsfw', defaultMessage: 'Force attachments to be marked sensitive' }, unlisted: { id: 'edit_federation.unlisted', defaultMessage: 'Force posts unlisted' }, @@ -54,7 +55,7 @@ const EditFederationModal: React.FC = ({ host, onClose }) setData(newData); }; - const handleSubmit: React.FormEventHandler = () => { + const handleSubmit = () => { dispatch(updateMrf(host, data)) .then(() => dispatch(snackbar.success(intl.formatMessage(messages.success, { host })))) .catch(() => {}); @@ -75,47 +76,81 @@ const EditFederationModal: React.FC = ({ host, onClose }) const fullMediaRemoval = avatar_removal && banner_removal && media_removal; return ( -
-
-
- {host} -
- - - - - - - - -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/app/soapbox/features/ui/components/favourites_modal.js b/app/soapbox/features/ui/components/favourites_modal.js deleted file mode 100644 index 34e6ceb34..000000000 --- a/app/soapbox/features/ui/components/favourites_modal.js +++ /dev/null @@ -1,76 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchFavourites } from 'soapbox/actions/interactions'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import { Modal, Spinner } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; - -const mapStateToProps = (state, props) => { - return { - accountIds: state.getIn(['user_lists', 'favourited_by', props.statusId]), - }; -}; - -export default @connect(mapStateToProps) -class FavouritesModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - statusId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - }; - - fetchData = () => { - const { dispatch, statusId } = this.props; - - dispatch(fetchFavourites(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - onClickClose = () => { - this.props.onClose('FAVOURITES'); - }; - - render() { - const { accountIds } = this.props; - - let body; - - if (!accountIds) { - body = ; - } else { - const emptyMessage = ; - - body = ( - - {accountIds.map(id => - , - )} - - ); - } - - return ( - } - onClose={this.onClickClose} - > - {body} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/favourites_modal.tsx b/app/soapbox/features/ui/components/favourites_modal.tsx new file mode 100644 index 000000000..f6089acf6 --- /dev/null +++ b/app/soapbox/features/ui/components/favourites_modal.tsx @@ -0,0 +1,62 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchFavourites } from 'soapbox/actions/interactions'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +interface IFavouritesModal { + onClose: (type: string) => void, + statusId: string, +} + +const FavouritesModal: React.FC = ({ onClose, statusId }) => { + const dispatch = useAppDispatch(); + + const accountIds = useAppSelector((state) => state.user_lists.getIn(['favourited_by', statusId])); + + const fetchData = () => { + dispatch(fetchFavourites(statusId)); + }; + + useEffect(() => { + fetchData(); + }, []); + + const onClickClose = () => { + onClose('FAVOURITES'); + }; + + let body; + + if (!accountIds) { + body = ; + } else { + const emptyMessage = ; + + body = ( + + {accountIds.map((id: string) => + , + )} + + ); + } + + return ( + } + onClose={onClickClose} + > + {body} + + ); +}; + +export default FavouritesModal; diff --git a/app/soapbox/features/ui/components/instance_info_panel.tsx b/app/soapbox/features/ui/components/instance_info_panel.tsx index a100abc2c..4c2462e10 100644 --- a/app/soapbox/features/ui/components/instance_info_panel.tsx +++ b/app/soapbox/features/ui/components/instance_info_panel.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { useIntl, defineMessages } from 'react-intl'; import { pinHost, unpinHost } from 'soapbox/actions/remote_timeline'; -import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import { Widget } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; import { makeGetRemoteInstance } from 'soapbox/selectors'; @@ -29,7 +29,7 @@ const InstanceInfoPanel: React.FC = ({ host }) => { const remoteInstance: any = useAppSelector(state => getRemoteInstance(state, host)); const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(host); - const handlePinHost: React.MouseEventHandler = () => { + const handlePinHost = () => { if (!pinned) { dispatch(pinHost(host)); } else { @@ -37,31 +37,15 @@ const InstanceInfoPanel: React.FC = ({ host }) => { } }; - const makeMenu = () => { - return [{ - text: intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host }), - action: handlePinHost, - icon: require(pinned ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'), - }]; - }; - - const menu = makeMenu(); - const icon = pinned ? 'thumbtack' : 'globe-w'; - if (!remoteInstance) return null; return ( -
-
- - - {remoteInstance.get('host')} - -
- -
-
-
+ ); }; diff --git a/app/soapbox/features/ui/components/instance_moderation_panel.tsx b/app/soapbox/features/ui/components/instance_moderation_panel.tsx index 1a494df7e..ed97495f1 100644 --- a/app/soapbox/features/ui/components/instance_moderation_panel.tsx +++ b/app/soapbox/features/ui/components/instance_moderation_panel.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { openModal } from 'soapbox/actions/modals'; +import { Widget } from 'soapbox/components/ui'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions'; import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks'; @@ -43,22 +44,14 @@ const InstanceModerationPanel: React.FC = ({ host }) = const menu = makeMenu(); return ( -
-
- - - - - {account?.admin && ( -
- -
- )} -
-
- -
-
+ } + action={account?.admin ? ( + + ) : undefined} + > + + ); }; diff --git a/app/soapbox/features/ui/components/mentions_modal.js b/app/soapbox/features/ui/components/mentions_modal.js deleted file mode 100644 index 0f4c4626b..000000000 --- a/app/soapbox/features/ui/components/mentions_modal.js +++ /dev/null @@ -1,83 +0,0 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchStatusWithContext } from 'soapbox/actions/statuses'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import { Modal, Spinner } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; -import { makeGetStatus } from 'soapbox/selectors'; - -const mapStateToProps = (state, props) => { - const getStatus = makeGetStatus(); - const status = getStatus(state, { - id: props.statusId, - username: props.username, - }); - - return { - accountIds: status ? ImmutableOrderedSet(status.get('mentions').map(m => m.get('id'))) : null, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class MentionsModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - statusId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - }; - - fetchData = () => { - const { dispatch, statusId } = this.props; - - dispatch(fetchStatusWithContext(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - onClickClose = () => { - this.props.onClose('MENTIONS'); - }; - - render() { - const { accountIds } = this.props; - - let body; - - if (!accountIds) { - body = ; - } else { - body = ( - - {accountIds.map(id => - , - )} - - ); - } - - return ( - } - onClose={this.onClickClose} - > - {body} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/mentions_modal.tsx b/app/soapbox/features/ui/components/mentions_modal.tsx new file mode 100644 index 000000000..445858843 --- /dev/null +++ b/app/soapbox/features/ui/components/mentions_modal.tsx @@ -0,0 +1,64 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchStatusWithContext } from 'soapbox/actions/statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { makeGetStatus } from 'soapbox/selectors'; + +const getStatus = makeGetStatus(); + +interface IMentionsModal { + onClose: (type: string) => void, + statusId: string, +} + +const MentionsModal: React.FC = ({ onClose, statusId }) => { + const dispatch = useAppDispatch(); + + const status = useAppSelector((state) => getStatus(state, { id: statusId })); + const accountIds = status ? ImmutableOrderedSet(status.mentions.map(m => m.get('id'))) : null; + + const fetchData = () => { + dispatch(fetchStatusWithContext(statusId)); + }; + + const onClickClose = () => { + onClose('MENTIONS'); + }; + + useEffect(() => { + fetchData(); + }, []); + + let body; + + if (!accountIds) { + body = ; + } else { + body = ( + + {accountIds.map(id => + , + )} + + ); + } + + return ( + } + onClose={onClickClose} + > + {body} + + ); +}; + +export default MentionsModal; diff --git a/app/soapbox/features/ui/components/modal_loading.js b/app/soapbox/features/ui/components/modal_loading.tsx similarity index 100% rename from app/soapbox/features/ui/components/modal_loading.js rename to app/soapbox/features/ui/components/modal_loading.tsx diff --git a/app/soapbox/features/ui/components/pending_status.js b/app/soapbox/features/ui/components/pending_status.js deleted file mode 100644 index e8da15a73..000000000 --- a/app/soapbox/features/ui/components/pending_status.js +++ /dev/null @@ -1,95 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import StatusContent from 'soapbox/components/status_content'; -import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; -import { HStack } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; -import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; -import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery'; -import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; - -import { buildStatus } from '../util/pending_status_builder'; - -import PollPreview from './poll_preview'; - -const shouldHaveCard = pendingStatus => { - return Boolean(pendingStatus.get('content').match(/https?:\/\/\S*/)); -}; - -const mapStateToProps = (state, props) => { - const { idempotencyKey } = props; - const pendingStatus = state.getIn(['pending_statuses', idempotencyKey]); - return { - status: pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class PendingStatus extends ImmutablePureComponent { - - renderMedia = () => { - const { status } = this.props; - - if (status.get('media_attachments') && !status.get('media_attachments').isEmpty()) { - return ( - - ); - } else if (!status.get('quote') && shouldHaveCard(status)) { - return ; - } else { - return null; - } - } - - render() { - const { status, className } = this.props; - if (!status) return null; - if (!status.get('account')) return null; - - return ( -
-
-
-
- - - -
- -
- - - - - {this.renderMedia()} - {status.get('poll') && } - - {status.get('quote') && } -
- - {/* TODO */} - {/* */} -
-
-
- ); - } - -} diff --git a/app/soapbox/features/ui/components/pending_status.tsx b/app/soapbox/features/ui/components/pending_status.tsx new file mode 100644 index 000000000..f4b990fc6 --- /dev/null +++ b/app/soapbox/features/ui/components/pending_status.tsx @@ -0,0 +1,97 @@ +import classNames from 'classnames'; +import React from 'react'; + +import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; +import StatusContent from 'soapbox/components/status_content'; +import { HStack } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; +import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery'; +import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; +import { useAppSelector } from 'soapbox/hooks'; + +import { buildStatus } from '../util/pending_status_builder'; + +import PollPreview from './poll_preview'; + +import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; + +const shouldHaveCard = (pendingStatus: StatusEntity) => { + return Boolean(pendingStatus.content.match(/https?:\/\/\S*/)); +}; + +interface IPendingStatus { + className?: string, + idempotencyKey: string, + muted?: boolean, +} + +interface IPendingStatusMedia { + status: StatusEntity, +} + +const PendingStatusMedia: React.FC = ({ status }) => { + if (status.media_attachments && !status.media_attachments.isEmpty()) { + return ( + + ); + } else if (!status.quote && shouldHaveCard(status)) { + return ; + } else { + return null; + } +}; + +const PendingStatus: React.FC = ({ idempotencyKey, className, muted }) => { + const status = useAppSelector((state) => { + const pendingStatus = state.pending_statuses.get(idempotencyKey); + return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null; + }) as StatusEntity | null; + + if (!status) return null; + if (!status.account) return null; + + const account = status.account as AccountEntity; + + return ( +
+
+
+
+ + + +
+ +
+ + + + + + + {status.poll && } + + {status.quote && } +
+ + {/* TODO */} + {/* */} +
+
+
+ ); +}; + +export default PendingStatus; diff --git a/app/soapbox/features/ui/components/pinned_accounts_panel.js b/app/soapbox/features/ui/components/pinned_accounts_panel.js deleted file mode 100644 index e0cfb1f93..000000000 --- a/app/soapbox/features/ui/components/pinned_accounts_panel.js +++ /dev/null @@ -1,79 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import Icon from 'soapbox/components/icon'; - -import { fetchPinnedAccounts } from '../../../actions/accounts'; -import AccountContainer from '../../../containers/account_container'; - -class PinnedAccountsPanel extends ImmutablePureComponent { - - static propTypes = { - pinned: ImmutablePropTypes.list.isRequired, - fetchPinned: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.props.fetchPinned(); - } - - render() { - const { account } = this.props; - const pinned = this.props.pinned.slice(0, this.props.limit); - - if (pinned.isEmpty()) { - return null; - } - - return ( -
-
- - - , - }} - /> - -
-
-
- {pinned && pinned.map(suggestion => ( - - ))} -
-
-
- ); - } - -} - -const mapStateToProps = (state, { account }) => ({ - pinned: state.getIn(['user_lists', 'pinned', account.get('id'), 'items'], ImmutableList()), -}); - -const mapDispatchToProps = (dispatch, { account }) => { - return { - fetchPinned: () => dispatch(fetchPinnedAccounts(account.get('id'))), - }; -}; - -export default injectIntl( - connect(mapStateToProps, mapDispatchToProps, null, { - forwardRef: true, - }, - )(PinnedAccountsPanel)); diff --git a/app/soapbox/features/ui/components/pinned_accounts_panel.tsx b/app/soapbox/features/ui/components/pinned_accounts_panel.tsx new file mode 100644 index 000000000..d364d61e8 --- /dev/null +++ b/app/soapbox/features/ui/components/pinned_accounts_panel.tsx @@ -0,0 +1,50 @@ +import { List as ImmutableList } from 'immutable'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchPinnedAccounts } from 'soapbox/actions/accounts'; +import { Widget } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import type { Account } from 'soapbox/types/entities'; + +interface IPinnedAccountsPanel { + account: Account, + limit: number, +} + +const PinnedAccountsPanel: React.FC = ({ account, limit }) => { + const dispatch = useAppDispatch(); + const pinned = useAppSelector((state) => state.user_lists.getIn(['pinned', account.id, 'items'], ImmutableList())).slice(0, limit); + + useEffect(() => { + dispatch(fetchPinnedAccounts(account.id)); + }, []); + + if (pinned.isEmpty()) { + return null; + } + + return ( + , + }} + />} + > + {pinned && pinned.map((suggestion: string) => ( + + ))} + + ); +}; + +export default PinnedAccountsPanel; diff --git a/app/soapbox/features/ui/components/profile_familiar_followers.tsx b/app/soapbox/features/ui/components/profile_familiar_followers.tsx index f2e0c9b61..0b7c5b144 100644 --- a/app/soapbox/features/ui/components/profile_familiar_followers.tsx +++ b/app/soapbox/features/ui/components/profile_familiar_followers.tsx @@ -1,6 +1,5 @@ import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedList, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; diff --git a/app/soapbox/features/ui/components/reactions_modal.tsx b/app/soapbox/features/ui/components/reactions_modal.tsx index 31422ab05..4fb4df72a 100644 --- a/app/soapbox/features/ui/components/reactions_modal.tsx +++ b/app/soapbox/features/ui/components/reactions_modal.tsx @@ -1,16 +1,14 @@ import { List as ImmutableList } from 'immutable'; -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions'; import FilterBar from 'soapbox/components/filter_bar'; import ScrollableList from 'soapbox/components/scrollable_list'; import { Modal, Spinner } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -20,14 +18,13 @@ const messages = defineMessages({ interface IReactionsModal { onClose: (string: string) => void, statusId: string, - username: string, reaction?: string, } -const ReactionsModal: React.FC = ({ onClose, statusId, ...props }) => { - const dispatch = useDispatch(); +const ReactionsModal: React.FC = ({ onClose, statusId, reaction: initialReaction }) => { + const dispatch = useAppDispatch(); const intl = useIntl(); - const [reaction, setReaction] = useState(props.reaction); + const [reaction, setReaction] = useState(initialReaction); const reactions = useAppSelector, count: number, diff --git a/app/soapbox/features/ui/components/reblogs_modal.js b/app/soapbox/features/ui/components/reblogs_modal.js deleted file mode 100644 index a5945c3a1..000000000 --- a/app/soapbox/features/ui/components/reblogs_modal.js +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; - -import { fetchReblogs } from 'soapbox/actions/interactions'; -import { fetchStatus } from 'soapbox/actions/statuses'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import { Modal, Spinner } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; - -const mapStateToProps = (state, props) => { - return { - accountIds: state.getIn(['user_lists', 'reblogged_by', props.statusId]), - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -@withRouter -class ReblogsModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - statusId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - history: PropTypes.object, - }; - - fetchData = () => { - const { dispatch, statusId } = this.props; - - dispatch(fetchReblogs(statusId)); - dispatch(fetchStatus(statusId)); - } - - componentDidMount() { - this.fetchData(); - this.unlistenHistory = this.props.history.listen((_, action) => { - if (action === 'PUSH') { - this.onClickClose(null, true); - } - }); - } - - componentWillUnmount() { - if (this.unlistenHistory) { - this.unlistenHistory(); - } - } - - onClickClose = () => { - this.props.onClose('REBLOGS'); - }; - - render() { - const { accountIds } = this.props; - - let body; - - if (!accountIds) { - body = ; - } else { - const emptyMessage = ; - - body = ( - - {accountIds.map(id => - , - )} - - ); - } - - - return ( - } - onClose={this.onClickClose} - > - {body} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/reblogs_modal.tsx b/app/soapbox/features/ui/components/reblogs_modal.tsx new file mode 100644 index 000000000..cb9906bad --- /dev/null +++ b/app/soapbox/features/ui/components/reblogs_modal.tsx @@ -0,0 +1,64 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchReblogs } from 'soapbox/actions/interactions'; +import { fetchStatus } from 'soapbox/actions/statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +interface IReblogsModal { + onClose: (string: string) => void, + statusId: string, +} + +const ReblogsModal: React.FC = ({ onClose, statusId }) => { + const dispatch = useAppDispatch(); + const accountIds = useAppSelector((state) => state.user_lists.getIn(['reblogged_by', statusId])); + + const fetchData = () => { + dispatch(fetchReblogs(statusId)); + dispatch(fetchStatus(statusId)); + }; + + useEffect(() => { + fetchData(); + }, []); + + const onClickClose = () => { + onClose('REBLOGS'); + }; + + let body; + + if (!accountIds) { + body = ; + } else { + const emptyMessage = ; + + body = ( + + {accountIds.map((id: string) => + , + )} + + ); + } + + + return ( + } + onClose={onClickClose} + > + {body} + + ); +}; + +export default ReblogsModal; diff --git a/app/soapbox/features/ui/components/reply_mentions_modal.tsx b/app/soapbox/features/ui/components/reply_mentions_modal.tsx index c1d3422fb..b1a959afb 100644 --- a/app/soapbox/features/ui/components/reply_mentions_modal.tsx +++ b/app/soapbox/features/ui/components/reply_mentions_modal.tsx @@ -33,7 +33,7 @@ const ReplyMentionsModal: React.FC = ({ onClose }) => { closePosition='left' >
- {mentions.map(accountId => )} + {mentions.map(accountId => )}
); diff --git a/app/soapbox/features/ui/components/user_panel.js b/app/soapbox/features/ui/components/user_panel.js deleted file mode 100644 index 747717add..000000000 --- a/app/soapbox/features/ui/components/user_panel.js +++ /dev/null @@ -1,136 +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 { injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import Avatar from 'soapbox/components/avatar'; -import StillImage from 'soapbox/components/still_image'; -import VerificationBadge from 'soapbox/components/verification_badge'; -import { getAcct } from 'soapbox/utils/accounts'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; -import { displayFqn } from 'soapbox/utils/state'; - -import { HStack, Stack, Text } from '../../../components/ui'; -import { makeGetAccount } from '../../../selectors'; - -class UserPanel extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record, - displayFqn: PropTypes.bool, - intl: PropTypes.object.isRequired, - domain: PropTypes.string, - } - - render() { - const { account, action, badges, displayFqn, intl, domain } = this.props; - if (!account) return null; - const displayNameHtml = { __html: account.get('display_name_html') }; - const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); - const header = account.get('header'); - const verified = account.get('verified'); - - return ( -
- - -
- {header && ( - - )} -
- - - - - - - {action && ( -
{action}
- )} -
-
- - - - - - - {verified && } - - {badges.length > 0 && ( - - {badges} - - )} - - - - - @{getAcct(account, displayFqn)} - - - - - {account.get('followers_count') >= 0 && ( - - - - {shortNumberFormat(account.get('followers_count'))} - - - - - - - )} - - {account.get('following_count') >= 0 && ( - - - - {shortNumberFormat(account.get('following_count'))} - - - - - - - )} - -
-
- ); - } - -} - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => ({ - account: getAccount(state, accountId), - displayFqn: displayFqn(state), - }); - - return mapStateToProps; -}; - -export default injectIntl( - connect(makeMapStateToProps, null, null, { - forwardRef: true, - })(UserPanel)); diff --git a/app/soapbox/features/ui/components/user_panel.tsx b/app/soapbox/features/ui/components/user_panel.tsx new file mode 100644 index 000000000..6bf77b659 --- /dev/null +++ b/app/soapbox/features/ui/components/user_panel.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import Avatar from 'soapbox/components/avatar'; +import StillImage from 'soapbox/components/still_image'; +import { HStack, Stack, Text } from 'soapbox/components/ui'; +import VerificationBadge from 'soapbox/components/verification_badge'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; +import { getAcct } from 'soapbox/utils/accounts'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; +import { displayFqn } from 'soapbox/utils/state'; + +const getAccount = makeGetAccount(); + +interface IUserPanel { + accountId: string, + action?: JSX.Element, + badges?: JSX.Element[], + domain?: string, +} + +const UserPanel: React.FC = ({ accountId, action, badges, domain }) => { + const intl = useIntl(); + const account = useAppSelector((state) => getAccount(state, accountId)); + const fqn = useAppSelector((state) => displayFqn(state)); + + if (!account) return null; + const displayNameHtml = { __html: account.get('display_name_html') }; + const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const header = account.get('header'); + const verified = account.get('verified'); + + return ( +
+ + +
+ {header && ( + + )} +
+ + + + + + + {action && ( +
{action}
+ )} +
+
+ + + + + + + {verified && } + + {badges && badges.length > 0 && ( + + {badges} + + )} + + + + + @{getAcct(account, fqn)} + + + + + {account.get('followers_count') >= 0 && ( + + + + {shortNumberFormat(account.get('followers_count'))} + + + + + + + )} + + {account.get('following_count') >= 0 && ( + + + + {shortNumberFormat(account.get('following_count'))} + + + + + + + )} + +
+
+ ); +}; + +export default UserPanel; diff --git a/app/soapbox/features/ui/util/pending_status_builder.js b/app/soapbox/features/ui/util/pending_status_builder.ts similarity index 66% rename from app/soapbox/features/ui/util/pending_status_builder.js rename to app/soapbox/features/ui/util/pending_status_builder.ts index 8ea186f56..e74b0a897 100644 --- a/app/soapbox/features/ui/util/pending_status_builder.js +++ b/app/soapbox/features/ui/util/pending_status_builder.ts @@ -4,9 +4,11 @@ import { normalizeStatus } from 'soapbox/normalizers/status'; import { calculateStatus } from 'soapbox/reducers/statuses'; import { makeGetAccount } from 'soapbox/selectors'; +import type { RootState } from 'soapbox/store'; + const getAccount = makeGetAccount(); -const buildMentions = pendingStatus => { +const buildMentions = (pendingStatus: ImmutableMap) => { if (pendingStatus.get('in_reply_to_id')) { return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct })); } else { @@ -14,18 +16,18 @@ const buildMentions = pendingStatus => { } }; -const buildPoll = pendingStatus => { +const buildPoll = (pendingStatus: ImmutableMap) => { if (pendingStatus.hasIn(['poll', 'options'])) { - return pendingStatus.get('poll').update('options', options => { - return options.map(title => ImmutableMap({ title })); + return pendingStatus.get('poll').update('options', (options: ImmutableMap) => { + return options.map((title: string) => ImmutableMap({ title })); }); } else { return null; } }; -export const buildStatus = (state, pendingStatus, idempotencyKey) => { - const me = state.get('me'); +export const buildStatus = (state: RootState, pendingStatus: ImmutableMap, idempotencyKey: string) => { + const me = state.me as string; const account = getAccount(state, me); const inReplyToId = pendingStatus.get('in_reply_to_id'); @@ -33,9 +35,9 @@ export const buildStatus = (state, pendingStatus, idempotencyKey) => { account, content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '
'), /* eslint-disable-line no-control-regex */ id: `末pending-${idempotencyKey}`, - in_reply_to_account_id: state.getIn(['statuses', inReplyToId, 'account'], null), + in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null), in_reply_to_id: inReplyToId, - media_attachments: pendingStatus.get('media_ids', ImmutableList()).map(id => ImmutableMap({ id })), + media_attachments: pendingStatus.get('media_ids', ImmutableList()).map((id: string) => ImmutableMap({ id })), mentions: buildMentions(pendingStatus), poll: buildPoll(pendingStatus), quote: pendingStatus.get('quote_id', null), diff --git a/app/soapbox/features/verification/email_passthru.js b/app/soapbox/features/verification/email_passthru.tsx similarity index 96% rename from app/soapbox/features/verification/email_passthru.js rename to app/soapbox/features/verification/email_passthru.tsx index 52b791c52..d014143aa 100644 --- a/app/soapbox/features/verification/email_passthru.js +++ b/app/soapbox/features/verification/email_passthru.tsx @@ -1,7 +1,8 @@ -import PropTypes from 'prop-types'; +import { AxiosError } from 'axios'; import * as React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; import snackbar from 'soapbox/actions/snackbar'; import { confirmEmailVerification } from 'soapbox/actions/verification'; @@ -91,8 +92,8 @@ const TokenExpired = () => { ); }; -const EmailPassThru = ({ match }) => { - const { token } = match.params; +const EmailPassThru = () => { + const { token } = useParams<{ token: string }>(); const dispatch = useDispatch(); const intl = useIntl(); @@ -106,7 +107,7 @@ const EmailPassThru = ({ match }) => { setStatus(Statuses.SUCCESS); dispatch(snackbar.success(intl.formatMessage({ id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' }))); }) - .catch((error) => { + .catch((error: AxiosError) => { const errorKey = error?.response?.data?.error; let message = intl.formatMessage({ id: 'email_passthru.fail.generic', @@ -155,8 +156,4 @@ const EmailPassThru = ({ match }) => { } }; -EmailPassThru.propTypes = { - match: PropTypes.object, -}; - export default EmailPassThru; diff --git a/app/soapbox/features/verification/waitlist_page.js b/app/soapbox/features/verification/waitlist_page.tsx similarity index 91% rename from app/soapbox/features/verification/waitlist_page.js rename to app/soapbox/features/verification/waitlist_page.tsx index 14047b7b1..0a28f4993 100644 --- a/app/soapbox/features/verification/waitlist_page.js +++ b/app/soapbox/features/verification/waitlist_page.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, { useEffect } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; @@ -12,15 +11,15 @@ import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; import { logOut } from '../../actions/auth'; import { Button, Stack, Text } from '../../components/ui'; -const WaitlistPage = ({ account }) => { +const WaitlistPage = (/* { account } */) => { const dispatch = useDispatch(); const intl = useIntl(); const title = useAppSelector((state) => state.instance.title); const me = useOwnAccount(); - const isSmsVerified = me.getIn(['source', 'sms_verified']); + const isSmsVerified = me?.source.get('sms_verified'); - const onClickLogOut = (event) => { + const onClickLogOut: React.MouseEventHandler = (event) => { event.preventDefault(); dispatch(logOut(intl)); }; @@ -76,8 +75,4 @@ const WaitlistPage = ({ account }) => { ); }; -WaitlistPage.propTypes = { - account: PropTypes.object, -}; - export default WaitlistPage; diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss index 160fef0b4..5c2c4e1ff 100644 --- a/app/styles/components/columns.scss +++ b/app/styles/components/columns.scss @@ -802,23 +802,6 @@ } } -.timeline-filter-message { - display: flex; - align-items: center; - background-color: var(--brand-color--faint); - color: var(--primary-text-color); - padding: 15px 20px; - - .icon-button { - margin: 2px 8px 2px 0; - - .svg-icon { - height: 20px; - width: 20px; - } - } -} - .column--better { .column__top { display: flex; diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 47e2da514..e3a717fbc 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -47,7 +47,7 @@ @apply focus-within:ring-primary-500 focus-within:ring-2; a { - @apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 focus:hover:bg-slate-800 cursor-pointer; + @apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 dark:focus:bg-slate-800 cursor-pointer; > .svg-icon:first-child { @apply h-5 w-5 mr-2.5 transition-none; diff --git a/app/styles/polls.scss b/app/styles/polls.scss index a49315450..13adcd8ff 100644 --- a/app/styles/polls.scss +++ b/app/styles/polls.scss @@ -133,10 +133,6 @@ line-height: 18px; } - &__footer { - @apply pt-1.5 pb-[5px] text-black dark:text-white; - } - &__link { display: inline; background: transparent; @@ -180,18 +176,6 @@ padding: 10px; } - .poll__footer { - border-top: 1px solid var(--foreground-color); - padding: 10px; - margin: -5px 0 0 -5px; - - button, - select { - flex: 1 1 50%; - margin: 5px 0 0 5px; - } - } - .button.button-secondary { @apply h-auto py-1.5 px-2.5 text-primary-600 dark:text-primary-400 border-primary-600; }