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 {
{text && {text}}
@@ -174,7 +176,7 @@ export default class IconButton extends React.PureComponent {
{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,
})}
>
-
+
);
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;
}