diff --git a/app/soapbox/features/followers/index.js b/app/soapbox/features/followers/index.js
deleted file mode 100644
index a25fb0d7d..000000000
--- a/app/soapbox/features/followers/index.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import debounce from 'lodash/debounce';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import {
- fetchAccount,
- fetchFollowers,
- expandFollowers,
- fetchAccountByUsername,
-} from 'soapbox/actions/accounts';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import ScrollableList from 'soapbox/components/scrollable_list';
-import { Spinner } from 'soapbox/components/ui';
-import AccountContainer from 'soapbox/containers/account_container';
-import { findAccountByUsername } from 'soapbox/selectors';
-import { getFollowDifference } from 'soapbox/utils/accounts';
-import { getFeatures } from 'soapbox/utils/features';
-
-import Column from '../ui/components/column';
-
-const messages = defineMessages({
- heading: { id: 'column.followers', defaultMessage: 'Followers' },
-});
-
-const mapStateToProps = (state, { params, withReplies = false }) => {
- const username = params.username || '';
- const me = state.get('me');
- const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
- const features = getFeatures(state.get('instance'));
-
- let accountId = -1;
- if (accountFetchError) {
- accountId = null;
- } else {
- const account = findAccountByUsername(state, username);
- accountId = account ? account.getIn(['id'], null) : -1;
- }
-
- const diffCount = getFollowDifference(state, accountId, 'followers');
- const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
-
- return {
- accountId,
- unavailable,
- isAccount: !!state.getIn(['accounts', accountId]),
- accountIds: state.user_lists.followers.get(accountId)?.items,
- hasMore: !!state.user_lists.followers.get(accountId)?.next,
- diffCount,
- };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Followers extends ImmutablePureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- accountIds: ImmutablePropTypes.orderedSet,
- hasMore: PropTypes.bool,
- diffCount: PropTypes.number,
- isAccount: PropTypes.bool,
- unavailable: PropTypes.bool,
- };
-
- componentDidMount() {
- const { params: { username }, accountId } = this.props;
-
- if (accountId && accountId !== -1) {
- this.props.dispatch(fetchAccount(accountId));
- this.props.dispatch(fetchFollowers(accountId));
- } else {
- this.props.dispatch(fetchAccountByUsername(username));
- }
- }
-
- componentDidUpdate(prevProps) {
- const { accountId, dispatch } = this.props;
- if (accountId && accountId !== -1 && (accountId !== prevProps.accountId && accountId)) {
- dispatch(fetchAccount(accountId));
- dispatch(fetchFollowers(accountId));
- }
- }
-
- handleLoadMore = debounce(() => {
- if (this.props.accountId && this.props.accountId !== -1) {
- this.props.dispatch(expandFollowers(this.props.accountId));
- }
- }, 300, { leading: true });
-
- render() {
- const { intl, accountIds, hasMore, diffCount, isAccount, accountId, unavailable } = this.props;
-
- if (!isAccount && accountId !== -1) {
- return (
-
- );
- }
-
- if (accountId === -1 || (!accountIds)) {
- return (
-
- );
- }
-
- if (unavailable) {
- return (
-
-
-
- );
- }
-
- return (
-
- }
- itemClassName='pb-4'
- >
- {accountIds.map(id =>
- ,
- )}
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/followers/index.tsx b/app/soapbox/features/followers/index.tsx
new file mode 100644
index 000000000..8625f2142
--- /dev/null
+++ b/app/soapbox/features/followers/index.tsx
@@ -0,0 +1,115 @@
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import debounce from 'lodash/debounce';
+import React, { useCallback, useEffect, useState } from 'react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import {
+ fetchAccount,
+ fetchFollowers,
+ expandFollowers,
+ fetchAccountByUsername,
+} from 'soapbox/actions/accounts';
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import { Spinner } from 'soapbox/components/ui';
+import AccountContainer from 'soapbox/containers/account_container';
+import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
+import { findAccountByUsername } from 'soapbox/selectors';
+
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.followers', defaultMessage: 'Followers' },
+});
+
+interface IFollowers {
+ params?: {
+ username?: string,
+ }
+}
+
+/** Displays a list of accounts who follow the given account. */
+const Followers: React.FC = (props) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const features = useFeatures();
+ const ownAccount = useOwnAccount();
+
+ const [loading, setLoading] = useState(true);
+
+ const username = props.params?.username || '';
+ const account = useAppSelector(state => findAccountByUsername(state, username));
+ const isOwnAccount = username.toLowerCase() === ownAccount?.username?.toLowerCase();
+
+ const accountIds = useAppSelector(state => state.user_lists.followers.get(account!?.id)?.items || ImmutableOrderedSet());
+ const hasMore = useAppSelector(state => !!state.user_lists.followers.get(account!?.id)?.next);
+
+ const isUnavailable = useAppSelector(state => {
+ const blockedBy = state.relationships.getIn([account?.id, 'blocked_by']) === true;
+ return isOwnAccount ? false : (blockedBy && !features.blockersVisible);
+ });
+
+ const handleLoadMore = useCallback(debounce(() => {
+ if (account) {
+ dispatch(expandFollowers(account.id));
+ }
+ }, 300, { leading: true }), [account?.id]);
+
+ useEffect(() => {
+ let promises = [];
+
+ if (account) {
+ promises = [
+ dispatch(fetchAccount(account.id)),
+ dispatch(fetchFollowers(account.id)),
+ ];
+ } else {
+ promises = [
+ dispatch(fetchAccountByUsername(username)),
+ ];
+ }
+
+ Promise.all(promises)
+ .then(() => setLoading(false))
+ .catch(() => setLoading(false));
+
+ }, [account?.id, username]);
+
+ if (loading && accountIds.isEmpty()) {
+ return (
+
+ );
+ }
+
+ if (!account) {
+ return (
+
+ );
+ }
+
+ if (isUnavailable) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ }
+ itemClassName='pb-4'
+ >
+ {accountIds.map(id =>
+ ,
+ )}
+
+
+ );
+};
+
+export default Followers;
\ No newline at end of file
diff --git a/app/soapbox/features/following/index.js b/app/soapbox/features/following/index.js
deleted file mode 100644
index 682a26411..000000000
--- a/app/soapbox/features/following/index.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import debounce from 'lodash/debounce';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import {
- fetchAccount,
- fetchFollowing,
- expandFollowing,
- fetchAccountByUsername,
-} from 'soapbox/actions/accounts';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import ScrollableList from 'soapbox/components/scrollable_list';
-import { Spinner } from 'soapbox/components/ui';
-import AccountContainer from 'soapbox/containers/account_container';
-import { findAccountByUsername } from 'soapbox/selectors';
-import { getFollowDifference } from 'soapbox/utils/accounts';
-import { getFeatures } from 'soapbox/utils/features';
-
-import Column from '../ui/components/column';
-
-const messages = defineMessages({
- heading: { id: 'column.following', defaultMessage: 'Following' },
-});
-
-const mapStateToProps = (state, { params, withReplies = false }) => {
- const username = params.username || '';
- const me = state.get('me');
- const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
- const features = getFeatures(state.get('instance'));
-
- let accountId = -1;
- if (accountFetchError) {
- accountId = null;
- } else {
- const account = findAccountByUsername(state, username);
- accountId = account ? account.getIn(['id'], null) : -1;
- }
-
- const diffCount = getFollowDifference(state, accountId, 'following');
- const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
-
- return {
- accountId,
- unavailable,
- isAccount: !!state.getIn(['accounts', accountId]),
- accountIds: state.user_lists.following.get(accountId)?.items,
- hasMore: !!state.user_lists.following.get(accountId)?.next,
- diffCount,
- };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Following extends ImmutablePureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- accountIds: ImmutablePropTypes.orderedSet,
- hasMore: PropTypes.bool,
- isAccount: PropTypes.bool,
- unavailable: PropTypes.bool,
- diffCount: PropTypes.number,
- };
-
- componentDidMount() {
- const { params: { username }, accountId } = this.props;
-
- if (accountId && accountId !== -1) {
- this.props.dispatch(fetchAccount(accountId));
- this.props.dispatch(fetchFollowing(accountId));
- } else {
- this.props.dispatch(fetchAccountByUsername(username));
- }
- }
-
- componentDidUpdate(prevProps) {
- const { accountId, dispatch } = this.props;
- if (accountId && accountId !== -1 && (accountId !== prevProps.accountId && accountId)) {
- dispatch(fetchAccount(accountId));
- dispatch(fetchFollowing(accountId));
- }
- }
-
- handleLoadMore = debounce(() => {
- if (this.props.accountId && this.props.accountId !== -1) {
- this.props.dispatch(expandFollowing(this.props.accountId));
- }
- }, 300, { leading: true });
-
- render() {
- const { intl, accountIds, hasMore, isAccount, diffCount, accountId, unavailable } = this.props;
-
- if (!isAccount && accountId !== -1) {
- return (
-
- );
- }
-
- if (accountId === -1 || (!accountIds)) {
- return (
-
- );
- }
-
- if (unavailable) {
- return (
-
-
-
- );
- }
-
- return (
-
- }
- itemClassName='pb-4'
- >
- {accountIds.map(id =>
- ,
- )}
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/following/index.tsx b/app/soapbox/features/following/index.tsx
new file mode 100644
index 000000000..cd0a10351
--- /dev/null
+++ b/app/soapbox/features/following/index.tsx
@@ -0,0 +1,115 @@
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import debounce from 'lodash/debounce';
+import React, { useCallback, useEffect, useState } from 'react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import {
+ fetchAccount,
+ fetchFollowing,
+ expandFollowing,
+ fetchAccountByUsername,
+} from 'soapbox/actions/accounts';
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import { Spinner } from 'soapbox/components/ui';
+import AccountContainer from 'soapbox/containers/account_container';
+import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
+import { findAccountByUsername } from 'soapbox/selectors';
+
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.following', defaultMessage: 'Following' },
+});
+
+interface IFollowing {
+ params?: {
+ username?: string,
+ }
+}
+
+/** Displays a list of accounts the given user is following. */
+const Following: React.FC = (props) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const features = useFeatures();
+ const ownAccount = useOwnAccount();
+
+ const [loading, setLoading] = useState(true);
+
+ const username = props.params?.username || '';
+ const account = useAppSelector(state => findAccountByUsername(state, username));
+ const isOwnAccount = username.toLowerCase() === ownAccount?.username?.toLowerCase();
+
+ const accountIds = useAppSelector(state => state.user_lists.following.get(account!?.id)?.items || ImmutableOrderedSet());
+ const hasMore = useAppSelector(state => !!state.user_lists.following.get(account!?.id)?.next);
+
+ const isUnavailable = useAppSelector(state => {
+ const blockedBy = state.relationships.getIn([account?.id, 'blocked_by']) === true;
+ return isOwnAccount ? false : (blockedBy && !features.blockersVisible);
+ });
+
+ const handleLoadMore = useCallback(debounce(() => {
+ if (account) {
+ dispatch(expandFollowing(account.id));
+ }
+ }, 300, { leading: true }), [account?.id]);
+
+ useEffect(() => {
+ let promises = [];
+
+ if (account) {
+ promises = [
+ dispatch(fetchAccount(account.id)),
+ dispatch(fetchFollowing(account.id)),
+ ];
+ } else {
+ promises = [
+ dispatch(fetchAccountByUsername(username)),
+ ];
+ }
+
+ Promise.all(promises)
+ .then(() => setLoading(false))
+ .catch(() => setLoading(false));
+
+ }, [account?.id, username]);
+
+ if (loading && accountIds.isEmpty()) {
+ return (
+
+ );
+ }
+
+ if (!account) {
+ return (
+
+ );
+ }
+
+ if (isUnavailable) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ }
+ itemClassName='pb-4'
+ >
+ {accountIds.map(id =>
+ ,
+ )}
+
+
+ );
+};
+
+export default Following;
\ No newline at end of file
diff --git a/app/soapbox/reducers/user_lists.ts b/app/soapbox/reducers/user_lists.ts
index fc86cceb2..38017f0bb 100644
--- a/app/soapbox/reducers/user_lists.ts
+++ b/app/soapbox/reducers/user_lists.ts
@@ -98,7 +98,6 @@ type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_
type ListPath = ['follow_requests' | 'blocks' | 'mutes' | 'directory'];
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: string | null) => {
-
return state.setIn(path, ListRecord({
next,
items: ImmutableOrderedSet(accounts.map(item => item.id)),
diff --git a/app/soapbox/utils/accounts.ts b/app/soapbox/utils/accounts.ts
index b2ed45d39..3a1cadbe9 100644
--- a/app/soapbox/utils/accounts.ts
+++ b/app/soapbox/utils/accounts.ts
@@ -1,5 +1,3 @@
-import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
-
import type { Account } from 'soapbox/types/entities';
const getDomainFromURL = (account: Account): string => {
@@ -28,12 +26,6 @@ export const getAcct = (account: Account, displayFqn: boolean): string => (
displayFqn === true ? account.fqn : account.acct
);
-export const getFollowDifference = (state: ImmutableMap, accountId: string, type: string): number => {
- const items: any = state.getIn(['user_lists', type, accountId, 'items'], ImmutableOrderedSet());
- const counter: number = Number(state.getIn(['accounts_counters', accountId, `${type}_count`], 0));
- return Math.max(counter - items.size, 0);
-};
-
export const isLocal = (account: Account): boolean => {
const domain: string = account.acct.split('@')[1];
return domain === undefined ? true : false;