From beb7b8e713c4e72d8f57d824ba3dbb68e4c17d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 30 May 2022 18:18:31 +0200 Subject: [PATCH] TypeScript, FC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/radio_button.tsx | 2 +- .../components/latest_accounts_panel.tsx | 3 +- .../conversations/components/conversation.js | 64 ---------- .../conversations/components/conversation.tsx | 63 ++++++++++ .../components/conversations_list.js | 79 ------------ .../components/conversations_list.tsx | 74 +++++++++++ .../containers/conversation_container.js | 20 --- .../conversations_list_container.js | 16 --- app/soapbox/features/conversations/index.js | 75 ----------- app/soapbox/features/conversations/index.tsx | 50 ++++++++ .../developers/developers_challenge.tsx | 3 +- app/soapbox/features/direct_timeline/index.js | 84 ------------- .../features/direct_timeline/index.tsx | 66 ++++++++++ .../directory/components/account_card.js | 85 ------------- .../directory/components/account_card.tsx | 82 +++++++++++++ app/soapbox/features/directory/index.js | 116 ------------------ app/soapbox/features/directory/index.tsx | 80 ++++++++++++ .../edit_password/{index.js => index.tsx} | 7 +- .../export_data/components/csv_exporter.tsx | 3 +- .../components/restricted_instance.js | 61 --------- .../components/restricted_instance.tsx | 47 +++++++ .../features/federation_restrictions/index.js | 76 ------------ .../federation_restrictions/index.tsx | 62 ++++++++++ .../import_data/components/csv_importer.tsx | 3 +- app/soapbox/features/new_status/index.tsx | 3 +- .../features/reply_mentions/account.tsx | 3 +- .../ui/components/reactions_modal.tsx | 3 +- app/soapbox/features/ui/index.tsx | 2 +- 28 files changed, 536 insertions(+), 696 deletions(-) delete mode 100644 app/soapbox/features/conversations/components/conversation.js create mode 100644 app/soapbox/features/conversations/components/conversation.tsx delete mode 100644 app/soapbox/features/conversations/components/conversations_list.js create mode 100644 app/soapbox/features/conversations/components/conversations_list.tsx delete mode 100644 app/soapbox/features/conversations/containers/conversation_container.js delete mode 100644 app/soapbox/features/conversations/containers/conversations_list_container.js delete mode 100644 app/soapbox/features/conversations/index.js create mode 100644 app/soapbox/features/conversations/index.tsx delete mode 100644 app/soapbox/features/direct_timeline/index.js create mode 100644 app/soapbox/features/direct_timeline/index.tsx delete mode 100644 app/soapbox/features/directory/components/account_card.js create mode 100644 app/soapbox/features/directory/components/account_card.tsx delete mode 100644 app/soapbox/features/directory/index.js create mode 100644 app/soapbox/features/directory/index.tsx rename app/soapbox/features/edit_password/{index.js => index.tsx} (96%) delete mode 100644 app/soapbox/features/federation_restrictions/components/restricted_instance.js create mode 100644 app/soapbox/features/federation_restrictions/components/restricted_instance.tsx delete mode 100644 app/soapbox/features/federation_restrictions/index.js create mode 100644 app/soapbox/features/federation_restrictions/index.tsx diff --git a/app/soapbox/components/radio_button.tsx b/app/soapbox/components/radio_button.tsx index c3f87ce02..b317e4259 100644 --- a/app/soapbox/components/radio_button.tsx +++ b/app/soapbox/components/radio_button.tsx @@ -6,7 +6,7 @@ interface IRadioButton { checked?: boolean, name: string, onChange: React.ChangeEventHandler, - label: JSX.Element, + label: React.ReactNode, } const RadioButton: React.FC = ({ name, value, checked, onChange, label }) => ( diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.tsx b/app/soapbox/features/admin/components/latest_accounts_panel.tsx index 7998c18d2..804744d5e 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.tsx +++ b/app/soapbox/features/admin/components/latest_accounts_panel.tsx @@ -1,6 +1,5 @@ import { OrderedSet as ImmutableOrderedSet, is } from 'immutable'; -import React, { useState } from 'react'; -import { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; diff --git a/app/soapbox/features/conversations/components/conversation.js b/app/soapbox/features/conversations/components/conversation.js deleted file mode 100644 index 717949968..000000000 --- a/app/soapbox/features/conversations/components/conversation.js +++ /dev/null @@ -1,64 +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 { withRouter } from 'react-router-dom'; - -import StatusContainer from '../../../containers/status_container'; - -export default @withRouter -class Conversation extends ImmutablePureComponent { - - static propTypes = { - conversationId: PropTypes.string.isRequired, - accounts: ImmutablePropTypes.list.isRequired, - lastStatusId: PropTypes.string, - unread: PropTypes.bool.isRequired, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - markRead: PropTypes.func.isRequired, - history: PropTypes.object, - }; - - handleClick = () => { - if (!this.props.history) { - return; - } - - const { lastStatusId, unread, markRead } = this.props; - - if (unread) { - markRead(); - } - - this.props.history.push(`/statuses/${lastStatusId}`); - } - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.conversationId); - } - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.conversationId); - } - - render() { - const { accounts, lastStatusId, unread } = this.props; - - if (lastStatusId === null) { - return null; - } - - return ( - - ); - } - -} diff --git a/app/soapbox/features/conversations/components/conversation.tsx b/app/soapbox/features/conversations/components/conversation.tsx new file mode 100644 index 000000000..995af88b3 --- /dev/null +++ b/app/soapbox/features/conversations/components/conversation.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; + +import { markConversationRead } from 'soapbox/actions/conversations'; +import StatusContainer from 'soapbox/containers/status_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import type { Map as ImmutableMap } from 'immutable'; + +interface IConversation { + conversationId: string, + onMoveUp: (id: string) => void, + onMoveDown: (id: string) => void, +} + +const Conversation: React.FC = ({ conversationId, onMoveUp, onMoveDown }) => { + const dispatch = useAppDispatch(); + const history = useHistory(); + + const { accounts, unread, lastStatusId } = useAppSelector((state) => { + const conversation = state.conversations.get('items').find((x: ImmutableMap) => x.get('id') === conversationId); + + return { + accounts: conversation.get('accounts').map((accountId: string) => state.accounts.get(accountId, null)), + unread: conversation.get('unread'), + lastStatusId: conversation.get('last_status', null), + }; + }); + + const handleClick = () => { + if (unread) { + dispatch(markConversationRead(conversationId)); + } + + history.push(`/statuses/${lastStatusId}`); + }; + + const handleHotkeyMoveUp = () => { + onMoveUp(conversationId); + }; + + const handleHotkeyMoveDown = () => { + onMoveDown(conversationId); + }; + + if (lastStatusId === null) { + return null; + } + + return ( + + ); +}; + +export default Conversation; diff --git a/app/soapbox/features/conversations/components/conversations_list.js b/app/soapbox/features/conversations/components/conversations_list.js deleted file mode 100644 index 6e2f751a9..000000000 --- a/app/soapbox/features/conversations/components/conversations_list.js +++ /dev/null @@ -1,79 +0,0 @@ -import { debounce } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import ScrollableList from '../../../components/scrollable_list'; -import ConversationContainer from '../containers/conversation_container'; - -export default class ConversationsList extends ImmutablePureComponent { - - static propTypes = { - conversations: ImmutablePropTypes.list.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - onLoadMore: PropTypes.func, - }; - - getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id) - - handleMoveUp = id => { - const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex); - } - - handleMoveDown = id => { - const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex); - } - - _selectChild(index) { - this.node.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - const element = document.querySelector(`#direct-list [data-index="${index}"] .focusable`); - - if (element) { - element.focus(); - } - }, - }); - } - - setRef = c => { - this.node = c; - } - - handleLoadOlder = debounce(() => { - const maxId = this.props.conversations.getIn([-1, 'id']); - if (maxId) this.props.onLoadMore(maxId); - }, 300, { leading: true }) - - render() { - const { conversations, isLoading, onLoadMore, ...other } = this.props; - - return ( - - {conversations.map(item => ( - - ))} - - ); - } - -} diff --git a/app/soapbox/features/conversations/components/conversations_list.tsx b/app/soapbox/features/conversations/components/conversations_list.tsx new file mode 100644 index 000000000..e7bc79078 --- /dev/null +++ b/app/soapbox/features/conversations/components/conversations_list.tsx @@ -0,0 +1,74 @@ +import { debounce } from 'lodash'; +import React from 'react'; +import { useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { expandConversations } from 'soapbox/actions/conversations'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import Conversation from '../components/conversation'; + +import type { VirtuosoHandle } from 'react-virtuoso'; + +const ConversationsList: React.FC = () => { + const dispatch = useAppDispatch(); + const ref = useRef(null); + + const conversations = useAppSelector((state) => state.conversations.get('items')); + const isLoading = useAppSelector((state) => state.conversations.get('isLoading', true)); + + const getCurrentIndex = (id: string) => conversations.findIndex((x: any) => x.get('id') === id); + + const handleMoveUp = (id: string) => { + const elementIndex = getCurrentIndex(id) - 1; + selectChild(elementIndex); + }; + + const handleMoveDown = (id: string) => { + const elementIndex = getCurrentIndex(id) + 1; + selectChild(elementIndex); + }; + + const selectChild = (index: number) => { + ref.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element = document.querySelector(`#direct-list [data-index="${index}"] .focusable`); + + if (element) { + element.focus(); + } + }, + }); + }; + + const handleLoadOlder = debounce(() => { + const maxId = conversations.getIn([-1, 'id']); + if (maxId) dispatch(expandConversations({ maxId })); + }, 300, { leading: true }); + + return ( + } + > + {conversations.map((item: any) => ( + + ))} + + ); +}; + +export default ConversationsList; diff --git a/app/soapbox/features/conversations/containers/conversation_container.js b/app/soapbox/features/conversations/containers/conversation_container.js deleted file mode 100644 index 885fada9c..000000000 --- a/app/soapbox/features/conversations/containers/conversation_container.js +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; - -import { markConversationRead } from '../../../actions/conversations'; -import Conversation from '../components/conversation'; - -const mapStateToProps = (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatusId: conversation.get('last_status', null), - }; -}; - -const mapDispatchToProps = (dispatch, { conversationId }) => ({ - markRead: () => dispatch(markConversationRead(conversationId)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Conversation); diff --git a/app/soapbox/features/conversations/containers/conversations_list_container.js b/app/soapbox/features/conversations/containers/conversations_list_container.js deleted file mode 100644 index 1dcd3ec1b..000000000 --- a/app/soapbox/features/conversations/containers/conversations_list_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; - -import { expandConversations } from '../../../actions/conversations'; -import ConversationsList from '../components/conversations_list'; - -const mapStateToProps = state => ({ - conversations: state.getIn(['conversations', 'items']), - isLoading: state.getIn(['conversations', 'isLoading'], true), - hasMore: state.getIn(['conversations', 'hasMore'], false), -}); - -const mapDispatchToProps = dispatch => ({ - onLoadMore: maxId => dispatch(expandConversations({ maxId })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/soapbox/features/conversations/index.js b/app/soapbox/features/conversations/index.js deleted file mode 100644 index 6d4b2ec22..000000000 --- a/app/soapbox/features/conversations/index.js +++ /dev/null @@ -1,75 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { directComposeById } from 'soapbox/actions/compose'; -import AccountSearch from 'soapbox/components/account_search'; - -import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations'; -import { connectDirectStream } from '../../actions/streaming'; -import { Column } from '../../components/ui'; - -import ConversationsListContainer from './containers/conversations_list_container'; - -const messages = defineMessages({ - title: { id: 'column.direct', defaultMessage: 'Direct messages' }, - searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, -}); - -export default @connect() -@injectIntl -class ConversationsTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - }; - - componentDidMount() { - const { dispatch } = this.props; - - dispatch(mountConversations()); - dispatch(expandConversations()); - this.disconnect = dispatch(connectDirectStream()); - } - - componentWillUnmount() { - this.props.dispatch(unmountConversations()); - - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - handleSuggestion = accountId => { - this.props.dispatch(directComposeById(accountId)); - } - - handleLoadMore = maxId => { - this.props.dispatch(expandConversations({ maxId })); - } - - render() { - const { intl } = this.props; - - return ( - - - - } - /> - - ); - } - -} diff --git a/app/soapbox/features/conversations/index.tsx b/app/soapbox/features/conversations/index.tsx new file mode 100644 index 000000000..8e81ac1c5 --- /dev/null +++ b/app/soapbox/features/conversations/index.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { directComposeById } from 'soapbox/actions/compose'; +import { mountConversations, unmountConversations, expandConversations } from 'soapbox/actions/conversations'; +import { connectDirectStream } from 'soapbox/actions/streaming'; +import AccountSearch from 'soapbox/components/account_search'; +import { Column } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +import ConversationsList from './components/conversations_list'; + +const messages = defineMessages({ + title: { id: 'column.direct', defaultMessage: 'Direct messages' }, + searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, +}); + +const ConversationsTimeline = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(mountConversations()); + dispatch(expandConversations()); + + const disconnect = dispatch(connectDirectStream()); + + return () => { + dispatch(unmountConversations()); + disconnect(); + }; + }, []); + + const handleSuggestion = (accountId: string) => { + dispatch(directComposeById(accountId)); + }; + + return ( + + + + + + ); +}; + +export default ConversationsTimeline; diff --git a/app/soapbox/features/developers/developers_challenge.tsx b/app/soapbox/features/developers/developers_challenge.tsx index d27b46429..50d107384 100644 --- a/app/soapbox/features/developers/developers_challenge.tsx +++ b/app/soapbox/features/developers/developers_challenge.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; diff --git a/app/soapbox/features/direct_timeline/index.js b/app/soapbox/features/direct_timeline/index.js deleted file mode 100644 index b09a58158..000000000 --- a/app/soapbox/features/direct_timeline/index.js +++ /dev/null @@ -1,84 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { directComposeById } from 'soapbox/actions/compose'; -import AccountSearch from 'soapbox/components/account_search'; - -import { connectDirectStream } from '../../actions/streaming'; -import { expandDirectTimeline } from '../../actions/timelines'; -import ColumnHeader from '../../components/column_header'; -import { Column } from '../../components/ui'; -import StatusListContainer from '../ui/containers/status_list_container'; - -const messages = defineMessages({ - title: { id: 'column.direct', defaultMessage: 'Direct messages' }, - searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, -}); - -export default @connect(mapStateToProps) -@injectIntl -class DirectTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - }; - - componentDidMount() { - const { dispatch } = this.props; - - dispatch(expandDirectTimeline()); - this.disconnect = dispatch(connectDirectStream()); - } - - componentWillUnmount() { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - handleSuggestion = accountId => { - this.props.dispatch(directComposeById(accountId)); - } - - handleLoadMore = maxId => { - this.props.dispatch(expandDirectTimeline({ maxId })); - } - - render() { - const { intl, hasUnread } = this.props; - - return ( - - - - - - } - divideType='space' - /> - - ); - } - -} diff --git a/app/soapbox/features/direct_timeline/index.tsx b/app/soapbox/features/direct_timeline/index.tsx new file mode 100644 index 000000000..5244533a8 --- /dev/null +++ b/app/soapbox/features/direct_timeline/index.tsx @@ -0,0 +1,66 @@ +import React, { useEffect } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { directComposeById } from 'soapbox/actions/compose'; +import { connectDirectStream } from 'soapbox/actions/streaming'; +import { expandDirectTimeline } from 'soapbox/actions/timelines'; +import AccountSearch from 'soapbox/components/account_search'; +import ColumnHeader from 'soapbox/components/column_header'; +import { Column } from 'soapbox/components/ui'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import StatusListContainer from '../ui/containers/status_list_container'; + +const messages = defineMessages({ + title: { id: 'column.direct', defaultMessage: 'Direct messages' }, + searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, +}); + +const DirectTimeline = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const hasUnread = useAppSelector((state) => state.timelines.getIn(['direct', 'unread']) > 0); + + useEffect(() => { + dispatch(expandDirectTimeline()); + const disconnect = dispatch(connectDirectStream()); + + return (() => { + disconnect(); + }); + }, []); + + const handleSuggestion = (accountId: string) => { + dispatch(directComposeById(accountId)); + }; + + const handleLoadMore = (maxId: string) => { + dispatch(expandDirectTimeline({ maxId })); + }; + + return ( + + + + + + } + divideType='space' + /> + + ); +}; + +export default DirectTimeline; diff --git a/app/soapbox/features/directory/components/account_card.js b/app/soapbox/features/directory/components/account_card.js deleted file mode 100644 index 542a05344..000000000 --- a/app/soapbox/features/directory/components/account_card.js +++ /dev/null @@ -1,85 +0,0 @@ -import classNames from 'classnames'; -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 { getSettings } from 'soapbox/actions/settings'; -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import Permalink from 'soapbox/components/permalink'; -import RelativeTimestamp from 'soapbox/components/relative_timestamp'; -import { Text } from 'soapbox/components/ui'; -import ActionButton from 'soapbox/features/ui/components/action-button'; -import { makeGetAccount } from 'soapbox/selectors'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; -import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { id }) => ({ - me: state.get('me'), - account: getAccount(state, id), - autoPlayGif: getSettings(state).get('autoPlayGif'), - }); - - return mapStateToProps; -}; - -export default @injectIntl -@connect(makeMapStateToProps) -class AccountCard extends ImmutablePureComponent { - - static propTypes = { - me: SoapboxPropTypes.me, - account: ImmutablePropTypes.record.isRequired, - autoPlayGif: PropTypes.bool, - }; - - render() { - const { account, autoPlayGif, me } = this.props; - - const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']); - - return ( -
- {followedBy && -
- - - -
} -
- -
-
- -
- -
- - - - -
- -
-

') && 'empty')} - dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} - /> -
- -
-
{shortNumberFormat(account.get('statuses_count'))}
-
{shortNumberFormat(account.get('followers_count'))}
-
{account.get('last_status_at') === null ? : }
-
-
- ); - } - -} diff --git a/app/soapbox/features/directory/components/account_card.tsx b/app/soapbox/features/directory/components/account_card.tsx new file mode 100644 index 000000000..0b5a74b8b --- /dev/null +++ b/app/soapbox/features/directory/components/account_card.tsx @@ -0,0 +1,82 @@ +import classNames from 'classnames'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { getSettings } from 'soapbox/actions/settings'; +import Avatar from 'soapbox/components/avatar'; +import DisplayName from 'soapbox/components/display-name'; +import Permalink from 'soapbox/components/permalink'; +import RelativeTimestamp from 'soapbox/components/relative_timestamp'; +import { Text } from 'soapbox/components/ui'; +import ActionButton from 'soapbox/features/ui/components/action-button'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; + +const getAccount = makeGetAccount(); + +interface IAccountCard { + id: string, +} + +const AccountCard: React.FC = ({ id }) => { + const me = useAppSelector((state) => state.me); + const account = useAppSelector((state) => getAccount(state, id)); + const autoPlayGif = useAppSelector((state) => getSettings(state).get('autoPlayGif')); + + if (!account) return null; + + const followedBy = me !== account.id && account.relationship.get('followed_by'); + + return ( +
+ {followedBy && +
+ + + +
} +
+ +
+
+ +
+ +
+ + + + +
+ +
+

') && 'empty')} + dangerouslySetInnerHTML={{ __html: account.note_emojified }} + /> +
+ +
+
+ + {shortNumberFormat(account.statuses_count)} + +
+
+ + {shortNumberFormat(account.followers_count)} + + +
+
+ {account.last_status_at === null + ? + : } +
+
+
+ ); +}; + +export default AccountCard; diff --git a/app/soapbox/features/directory/index.js b/app/soapbox/features/directory/index.js deleted file mode 100644 index 37795a4e9..000000000 --- a/app/soapbox/features/directory/index.js +++ /dev/null @@ -1,116 +0,0 @@ -import classNames from 'classnames'; -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory'; -import LoadMore from 'soapbox/components/load_more'; -import RadioButton from 'soapbox/components/radio_button'; -import Column from 'soapbox/features/ui/components/column'; -import { getFeatures } from 'soapbox/utils/features'; - -import AccountCard from './components/account_card'; - -const messages = defineMessages({ - title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, - recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, - newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, - local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, - federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()), - isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true), - title: state.getIn(['instance', 'title']), - features: getFeatures(state.get('instance')), -}); - -export default @connect(mapStateToProps) -@injectIntl -class Directory extends React.PureComponent { - - static propTypes = { - isLoading: PropTypes.bool, - accountIds: ImmutablePropTypes.list.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - title: PropTypes.string.isRequired, - params: PropTypes.shape({ - order: PropTypes.string, - local: PropTypes.bool, - }), - features: PropTypes.object.isRequired, - }; - - state = { - order: null, - local: null, - }; - - getParams = (props, state) => ({ - order: state.order === null ? (props.params.order || 'active') : state.order, - local: state.local === null ? (props.params.local || false) : state.local, - }); - - componentDidMount() { - const { dispatch } = this.props; - dispatch(fetchDirectory(this.getParams(this.props, this.state))); - } - - componentDidUpdate(prevProps, prevState) { - const { dispatch } = this.props; - const paramsOld = this.getParams(prevProps, prevState); - const paramsNew = this.getParams(this.props, this.state); - - if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) { - dispatch(fetchDirectory(paramsNew)); - } - } - - handleChangeOrder = e => { - this.setState({ order: e.target.value }); - } - - handleChangeLocal = e => { - this.setState({ local: e.target.value === '1' }); - } - - handleLoadMore = () => { - const { dispatch } = this.props; - dispatch(expandDirectory(this.getParams(this.props, this.state))); - } - - render() { - const { isLoading, accountIds, intl, title, features } = this.props; - const { order, local } = this.getParams(this.props, this.state); - - return ( - -
-
- - -
- - {features.federating && ( -
- - -
- )} -
- -
- {accountIds.map(accountId => )} -
- - -
- ); - } - -} \ No newline at end of file diff --git a/app/soapbox/features/directory/index.tsx b/app/soapbox/features/directory/index.tsx new file mode 100644 index 000000000..c4bb5b4d8 --- /dev/null +++ b/app/soapbox/features/directory/index.tsx @@ -0,0 +1,80 @@ +import classNames from 'classnames'; +import { List as ImmutableList } from 'immutable'; +import React, { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; + +import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory'; +import LoadMore from 'soapbox/components/load_more'; +import RadioButton from 'soapbox/components/radio_button'; +import Column from 'soapbox/features/ui/components/column'; +import { useAppSelector } from 'soapbox/hooks'; +import { getFeatures } from 'soapbox/utils/features'; + +import AccountCard from './components/account_card'; + +const messages = defineMessages({ + title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, + recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, + newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, + local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, + federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, +}); + +const Directory = () => { + const intl = useIntl(); + const dispatch = useDispatch(); + const { search } = useLocation(); + const params = new URLSearchParams(search); + + const accountIds = useAppSelector((state) => state.user_lists.getIn(['directory', 'items'], ImmutableList())); + const isLoading = useAppSelector((state) => state.user_lists.getIn(['directory', 'isLoading'], true)); + const title = useAppSelector((state) => state.instance.get('title')); + const features = useAppSelector((state) => getFeatures(state.instance)); + + const [order, setOrder] = useState(params.get('order') || 'active'); + const [local, setLocal] = useState(!!params.get('local')); + + useEffect(() => { + dispatch(fetchDirectory({ order: order || 'active', local: local || false })); + }, [order, local]); + + const handleChangeOrder: React.ChangeEventHandler = e => { + setOrder(e.target.value); + }; + + const handleChangeLocal: React.ChangeEventHandler = e => { + setLocal(e.target.value === '1'); + }; + + const handleLoadMore = () => { + dispatch(expandDirectory({ order: order || 'active', local: local || false })); + }; + + return ( + +
+
+ + +
+ + {features.federating && ( +
+ + +
+ )} +
+ +
+ {accountIds.map((accountId: string) => )} +
+ + +
+ ); +}; + +export default Directory; diff --git a/app/soapbox/features/edit_password/index.js b/app/soapbox/features/edit_password/index.tsx similarity index 96% rename from app/soapbox/features/edit_password/index.js rename to app/soapbox/features/edit_password/index.tsx index a007b24bc..e95e6dec4 100644 --- a/app/soapbox/features/edit_password/index.js +++ b/app/soapbox/features/edit_password/index.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { changePassword } from 'soapbox/actions/security'; import snackbar from 'soapbox/actions/snackbar'; - -import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from '../../components/ui'; +import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; const messages = defineMessages({ updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' }, @@ -22,7 +21,7 @@ const initialState = { currentPassword: '', newPassword: '', newPasswordConfirma const EditPassword = () => { const intl = useIntl(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const [state, setState] = React.useState(initialState); const [isLoading, setLoading] = React.useState(false); diff --git a/app/soapbox/features/export_data/components/csv_exporter.tsx b/app/soapbox/features/export_data/components/csv_exporter.tsx index f44aaa424..de524bcce 100644 --- a/app/soapbox/features/export_data/components/csv_exporter.tsx +++ b/app/soapbox/features/export_data/components/csv_exporter.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { MessageDescriptor, useIntl } from 'react-intl'; import { Button, Form, FormActions, Text } from 'soapbox/components/ui'; diff --git a/app/soapbox/features/federation_restrictions/components/restricted_instance.js b/app/soapbox/features/federation_restrictions/components/restricted_instance.js deleted file mode 100644 index 38ca1b602..000000000 --- a/app/soapbox/features/federation_restrictions/components/restricted_instance.js +++ /dev/null @@ -1,61 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import Icon from 'soapbox/components/icon'; -import { makeGetRemoteInstance } from 'soapbox/selectors'; - -import InstanceRestrictions from './instance_restrictions'; - -const getRemoteInstance = makeGetRemoteInstance(); - -const mapStateToProps = (state, ownProps) => { - return { - remoteInstance: getRemoteInstance(state, ownProps.host), - }; -}; - -export default @connect(mapStateToProps) -class RestrictedInstance extends ImmutablePureComponent { - - static propTypes = { - host: PropTypes.string.isRequired, - } - - state = { - expanded: false, - } - - toggleExpanded = e => { - this.setState({ expanded: !this.state.expanded }); - e.preventDefault(); - } - - render() { - const { remoteInstance } = this.props; - const { expanded } = this.state; - - return ( - - ); - } - -} diff --git a/app/soapbox/features/federation_restrictions/components/restricted_instance.tsx b/app/soapbox/features/federation_restrictions/components/restricted_instance.tsx new file mode 100644 index 000000000..789cb215f --- /dev/null +++ b/app/soapbox/features/federation_restrictions/components/restricted_instance.tsx @@ -0,0 +1,47 @@ +import classNames from 'classnames'; +import React, { useState } from 'react'; + +import Icon from 'soapbox/components/icon'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetRemoteInstance } from 'soapbox/selectors'; + +import InstanceRestrictions from './instance_restrictions'; + +const getRemoteInstance = makeGetRemoteInstance(); + +interface IRestrictedInstance { + host: string, +} + +const RestrictedInstance: React.FC = ({ host }) => { + const remoteInstance: any = useAppSelector((state) => getRemoteInstance(state, host)); + + const [expanded, setExpanded] = useState(false); + + const toggleExpanded: React.MouseEventHandler = e => { + setExpanded((value) => !value); + e.preventDefault(); + }; + + return ( + + ); +}; + +export default RestrictedInstance; diff --git a/app/soapbox/features/federation_restrictions/index.js b/app/soapbox/features/federation_restrictions/index.js deleted file mode 100644 index bb5ccdab2..000000000 --- a/app/soapbox/features/federation_restrictions/index.js +++ /dev/null @@ -1,76 +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 ScrollableList from 'soapbox/components/scrollable_list'; -import Accordion from 'soapbox/features/ui/components/accordion'; -import { makeGetHosts } from 'soapbox/selectors'; -import { federationRestrictionsDisclosed } from 'soapbox/utils/state'; - -import Column from '../ui/components/column'; - -import RestrictedInstance from './components/restricted_instance'; - -const messages = defineMessages({ - heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' }, - boxTitle: { id: 'federation_restrictions.explanation_box.title', defaultMessage: 'Instance-specific policies' }, - boxMessage: { id: 'federation_restrictions.explanation_box.message', defaultMessage: 'Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.' }, - emptyMessage: { id: 'federation_restrictions.empty_message', defaultMessage: '{siteTitle} has not restricted any instances.' }, - notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' }, -}); - -const getHosts = makeGetHosts(); - -const mapStateToProps = state => ({ - siteTitle: state.getIn(['instance', 'title']), - hosts: getHosts(state), - disclosed: federationRestrictionsDisclosed(state), -}); - -export default @connect(mapStateToProps) -@injectIntl -class FederationRestrictions extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - disclosed: PropTypes.bool, - }; - - state = { - explanationBoxExpanded: true, - } - - toggleExplanationBox = setting => { - this.setState({ explanationBoxExpanded: setting }); - } - - render() { - const { intl, hosts, siteTitle, disclosed } = this.props; - const { explanationBoxExpanded } = this.state; - - const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed; - - return ( - -
- - {intl.formatMessage(messages.boxMessage, { siteTitle })} - -
- -
- - {hosts.map(host => )} - -
-
- ); - } - -} diff --git a/app/soapbox/features/federation_restrictions/index.tsx b/app/soapbox/features/federation_restrictions/index.tsx new file mode 100644 index 000000000..396f527a0 --- /dev/null +++ b/app/soapbox/features/federation_restrictions/index.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import ScrollableList from 'soapbox/components/scrollable_list'; +import Accordion from 'soapbox/features/ui/components/accordion'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetHosts } from 'soapbox/selectors'; +import { federationRestrictionsDisclosed } from 'soapbox/utils/state'; + +import Column from '../ui/components/column'; + +import RestrictedInstance from './components/restricted_instance'; + +import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; + +const messages = defineMessages({ + heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' }, + boxTitle: { id: 'federation_restrictions.explanation_box.title', defaultMessage: 'Instance-specific policies' }, + boxMessage: { id: 'federation_restrictions.explanation_box.message', defaultMessage: 'Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.' }, + emptyMessage: { id: 'federation_restrictions.empty_message', defaultMessage: '{siteTitle} has not restricted any instances.' }, + notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' }, +}); + +const getHosts = makeGetHosts(); + +const FederationRestrictions = () => { + const intl = useIntl(); + + const siteTitle = useAppSelector((state) => state.instance.get('title')); + const hosts = useAppSelector((state) => getHosts(state)) as ImmutableOrderedSet; + const disclosed = useAppSelector((state) => federationRestrictionsDisclosed(state)); + + const [explanationBoxExpanded, setExplanationBoxExpanded] = useState(true); + + const toggleExplanationBox = (setting: boolean) => { + setExplanationBoxExpanded(setting); + }; + + const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed; + + return ( + +
+ + {intl.formatMessage(messages.boxMessage, { siteTitle })} + +
+ +
+ + {hosts.map((host) => )} + +
+
+ ); +}; + +export default FederationRestrictions; diff --git a/app/soapbox/features/import_data/components/csv_importer.tsx b/app/soapbox/features/import_data/components/csv_importer.tsx index aa5937af9..fafbe813d 100644 --- a/app/soapbox/features/import_data/components/csv_importer.tsx +++ b/app/soapbox/features/import_data/components/csv_importer.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { MessageDescriptor, useIntl } from 'react-intl'; import { Button, FileInput, Form, FormActions, FormGroup, Text } from 'soapbox/components/ui'; diff --git a/app/soapbox/features/new_status/index.tsx b/app/soapbox/features/new_status/index.tsx index 322976afc..e5b4aacd2 100644 --- a/app/soapbox/features/new_status/index.tsx +++ b/app/soapbox/features/new_status/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Redirect } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; diff --git a/app/soapbox/features/reply_mentions/account.tsx b/app/soapbox/features/reply_mentions/account.tsx index c4817e8cf..b96d6cf5b 100644 --- a/app/soapbox/features/reply_mentions/account.tsx +++ b/app/soapbox/features/reply_mentions/account.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { fetchAccount } from 'soapbox/actions/accounts'; diff --git a/app/soapbox/features/ui/components/reactions_modal.tsx b/app/soapbox/features/ui/components/reactions_modal.tsx index 4fb4df72a..4832f943d 100644 --- a/app/soapbox/features/ui/components/reactions_modal.tsx +++ b/app/soapbox/features/ui/components/reactions_modal.tsx @@ -1,6 +1,5 @@ import { List as ImmutableList } from 'immutable'; -import React, { useEffect } from 'react'; -import { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions'; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index dfdcfeb9f..1ebfc30a0 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -282,7 +282,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { - + {features.scheduledStatuses && }