Profile redesign: Show yourself in follower list (#37813)

This commit is contained in:
Echo
2026-02-11 14:19:18 +01:00
committed by GitHub
parent 20fedab093
commit bbd88d356d
13 changed files with 474 additions and 375 deletions

View File

@@ -0,0 +1,64 @@
import type { FC, ReactNode } from 'react';
import { FormattedMessage } from 'react-intl';
import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import type { Account } from '@/mastodon/models/account';
import { RemoteHint } from './remote';
interface BaseEmptyMessageProps {
account?: Account;
defaultMessage: ReactNode;
}
export type EmptyMessageProps = Omit<BaseEmptyMessageProps, 'defaultMessage'>;
export const BaseEmptyMessage: FC<BaseEmptyMessageProps> = ({
account,
defaultMessage,
}) => {
const { blockedBy, hidden, suspended } = useAccountVisibility(account?.id);
if (!account) {
return null;
}
if (suspended) {
return (
<FormattedMessage
id='empty_column.account_suspended'
defaultMessage='Account suspended'
/>
);
}
if (hidden) {
return <LimitedAccountHint accountId={account.id} />;
}
if (blockedBy) {
return (
<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>
);
}
if (account.hide_collections) {
return (
<FormattedMessage
id='empty_column.account_hides_collections'
defaultMessage='This user has chosen to not make this information available'
/>
);
}
const domain = account.acct.split('@')[1];
if (domain) {
return <RemoteHint domain={domain} url={account.url} />;
}
return defaultMessage;
};

View File

@@ -0,0 +1,104 @@
import { useMemo } from 'react';
import type { FC, ReactNode } from 'react';
import { Account } from '@/mastodon/components/account';
import { Column } from '@/mastodon/components/column';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import ScrollableList from '@/mastodon/components/scrollable_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import { useLayout } from '@/mastodon/hooks/useLayout';
import { AccountHeader } from '../../account_timeline/components/account_header';
import { RemoteHint } from './remote';
export interface AccountList {
hasMore: boolean;
isLoading: boolean;
items: string[];
}
interface AccountListProps {
accountId?: string | null;
append?: ReactNode;
emptyMessage: ReactNode;
footer?: ReactNode;
list?: AccountList | null;
loadMore: () => void;
prependAccountId?: string | null;
scrollKey: string;
}
export const AccountList: FC<AccountListProps> = ({
accountId,
append,
emptyMessage,
footer,
list,
loadMore,
prependAccountId,
scrollKey,
}) => {
const account = useAccount(accountId);
const { blockedBy, hidden, suspended } = useAccountVisibility(accountId);
const forceEmptyState = blockedBy || hidden || suspended;
const children = useMemo(() => {
if (forceEmptyState) {
return [];
}
const children =
list?.items.map((followerId) => (
<Account key={followerId} id={followerId} />
)) ?? [];
if (prependAccountId) {
children.unshift(
<Account key={prependAccountId} id={prependAccountId} minimal />,
);
}
return children;
}, [prependAccountId, list, forceEmptyState]);
const { multiColumn } = useLayout();
// Null means accountId does not exist (e.g. invalid acct). Undefined means loading.
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (!accountId || !account) {
return (
<Column bindToDocument={!multiColumn}>
<LoadingIndicator />
</Column>
);
}
const domain = account.acct.split('@')[1];
return (
<Column>
<ColumnBackButton />
<ScrollableList
scrollKey={scrollKey}
hasMore={!forceEmptyState && list?.hasMore}
isLoading={list?.isLoading ?? true}
onLoadMore={loadMore}
prepend={<AccountHeader accountId={accountId} hideTabs />}
alwaysPrepend
append={append ?? <RemoteHint domain={domain} url={account.url} />}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
footer={footer}
>
{children}
</ScrollableList>
</Column>
);
};

View File

@@ -0,0 +1,32 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { TimelineHint } from '@/mastodon/components/timeline_hint';
export const RemoteHint: FC<{ domain?: string; url: string }> = ({
domain,
url,
}) => {
if (!domain) {
return null;
}
return (
<TimelineHint
url={url}
message={
<FormattedMessage
id='hints.profiles.followers_may_be_missing'
defaultMessage='Followers for this profile may be missing.'
/>
}
label={
<FormattedMessage
id='hints.profiles.see_more_followers'
defaultMessage='See more followers on {domain}'
values={{ domain: <strong>{domain}</strong> }}
/>
}
/>
);
};

View File

@@ -1,187 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import {
lookupAccount,
fetchAccount,
fetchFollowers,
expandFollowers,
} from '../../actions/accounts';
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (!accountId) {
return {
isLoading: true,
};
}
return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
const RemoteHint = ({ accountId, url }) => {
const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.followers_may_be_missing' defaultMessage='Followers for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_followers' defaultMessage='See more followers on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
};
class Followers extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
};
_load () {
const { accountId, isAccount, dispatch } = this.props;
if (!isAccount) dispatch(fetchAccount(accountId));
dispatch(fetchFollowers(accountId));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
}
componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
}
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowers(this.props.accountId));
}, 300, { leading: true });
render () {
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
if (!accountIds) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
let emptyMessage;
const forceEmptyState = blockedBy || suspended || hidden;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
}
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton />
<ScrollableList
scrollKey='followers'
hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{forceEmptyState ? [] : accountIds.map(id =>
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>
);
}
}
export default connect(mapStateToProps)(Followers);

View File

@@ -0,0 +1,90 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDebouncedCallback } from 'use-debounce';
import { expandFollowers, fetchFollowers } from '@/mastodon/actions/accounts';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { useRelationship } from '@/mastodon/hooks/useRelationship';
import { selectUserListWithoutMe } from '@/mastodon/selectors/user_lists';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import type { EmptyMessageProps } from './components/empty';
import { BaseEmptyMessage } from './components/empty';
import { AccountList } from './components/list';
const Followers: FC = () => {
const accountId = useAccountId();
const account = useAccount(accountId);
const currentAccountId = useAppSelector(
(state) => (state.meta.get('me') as string | null) ?? null,
);
const followerList = useAppSelector((state) =>
selectUserListWithoutMe(state, 'followers', accountId),
);
const dispatch = useAppDispatch();
useEffect(() => {
if (!followerList && accountId) {
dispatch(fetchFollowers(accountId));
}
}, [accountId, dispatch, followerList]);
const loadMore = useDebouncedCallback(
() => {
if (accountId) {
dispatch(expandFollowers(accountId));
}
},
300,
{ leading: true },
);
const relationship = useRelationship(accountId);
const followerId = relationship?.following ? currentAccountId : null;
const followersExceptMeHidden = !!(
account?.hide_collections &&
followerList?.items.length === 0 &&
followerId
);
const footer = followersExceptMeHidden && (
<div className='empty-column-indicator'>
<FormattedMessage
id='followers.hide_other_followers'
defaultMessage='This user has chosen to not make their other followers visible'
/>
</div>
);
return (
<AccountList
accountId={accountId}
footer={footer}
emptyMessage={<EmptyMessage account={account} />}
list={followerList}
loadMore={loadMore}
prependAccountId={followerId}
scrollKey='followers'
/>
);
};
const EmptyMessage: FC<EmptyMessageProps> = (props) => (
<BaseEmptyMessage
{...props}
defaultMessage={
<FormattedMessage
id='account.followers.empty'
defaultMessage='No one follows this user yet.'
/>
}
/>
);
// eslint-disable-next-line import/no-default-export -- Used by async components.
export default Followers;

View File

@@ -0,0 +1,32 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { TimelineHint } from '@/mastodon/components/timeline_hint';
export const RemoteHint: FC<{ domain?: string; url: string }> = ({
domain,
url,
}) => {
if (!domain) {
return null;
}
return (
<TimelineHint
url={url}
message={
<FormattedMessage
id='hints.profiles.follows_may_be_missing'
defaultMessage='Follows for this profile may be missing.'
/>
}
label={
<FormattedMessage
id='hints.profiles.see_more_follows'
defaultMessage='See more follows on {domain}'
values={{ domain: <strong>{domain}</strong> }}
/>
}
/>
);
};

View File

@@ -1,187 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import {
lookupAccount,
fetchAccount,
fetchFollowing,
expandFollowing,
} from '../../actions/accounts';
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (!accountId) {
return {
isLoading: true,
};
}
return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
const RemoteHint = ({ accountId, url }) => {
const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.follows_may_be_missing' defaultMessage='Follows for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_follows' defaultMessage='See more follows on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
};
class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
};
_load () {
const { accountId, isAccount, dispatch } = this.props;
if (!isAccount) dispatch(fetchAccount(accountId));
dispatch(fetchFollowing(accountId));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
}
componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
}
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowing(this.props.accountId));
}, 300, { leading: true });
render () {
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
if (!accountIds) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
let emptyMessage;
const forceEmptyState = blockedBy || suspended || hidden;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
}
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton />
<ScrollableList
scrollKey='following'
hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{forceEmptyState ? [] : accountIds.map(id =>
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>
);
}
}
export default connect(mapStateToProps)(Following);

View File

@@ -0,0 +1,94 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDebouncedCallback } from 'use-debounce';
import { expandFollowing, fetchFollowing } from '@/mastodon/actions/accounts';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { useRelationship } from '@/mastodon/hooks/useRelationship';
import { selectUserListWithoutMe } from '@/mastodon/selectors/user_lists';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import type { EmptyMessageProps } from '../followers/components/empty';
import { BaseEmptyMessage } from '../followers/components/empty';
import { AccountList } from '../followers/components/list';
import { RemoteHint } from './components/remote';
const Followers: FC = () => {
const accountId = useAccountId();
const account = useAccount(accountId);
const currentAccountId = useAppSelector(
(state) => (state.meta.get('me') as string | null) ?? null,
);
const followingList = useAppSelector((state) =>
selectUserListWithoutMe(state, 'following', accountId),
);
const dispatch = useAppDispatch();
useEffect(() => {
if (!followingList && accountId) {
dispatch(fetchFollowing(accountId));
}
}, [accountId, dispatch, followingList]);
const loadMore = useDebouncedCallback(
() => {
if (accountId) {
dispatch(expandFollowing(accountId));
}
},
300,
{ leading: true },
);
const relationship = useRelationship(accountId);
const followedId = relationship?.followed_by ? currentAccountId : null;
const followingExceptMeHidden = !!(
account?.hide_collections &&
followingList?.items.length === 0 &&
followedId
);
const footer = followingExceptMeHidden && (
<div className='empty-column-indicator'>
<FormattedMessage
id='following.hide_other_following'
defaultMessage='This user has chosen to not make the rest of who they follow visible'
/>
</div>
);
const domain = account?.acct.split('@')[1];
return (
<AccountList
accountId={accountId}
append={domain && <RemoteHint domain={domain} url={account.url} />}
emptyMessage={<EmptyMessage account={account} />}
footer={footer}
list={followingList}
loadMore={loadMore}
prependAccountId={currentAccountId}
scrollKey='following'
/>
);
};
const EmptyMessage: FC<EmptyMessageProps> = (props) => (
<BaseEmptyMessage
{...props}
defaultMessage={
<FormattedMessage
id='account.follows.empty'
defaultMessage="This user doesn't follow anyone yet."
/>
}
/>
);
// eslint-disable-next-line import/no-default-export -- Used by async components.
export default Followers;

View File

@@ -8,6 +8,7 @@ export const useLayout = () => {
return {
singleColumn: layout === 'single-column' || layout === 'mobile',
multiColumn: layout === 'multi-column',
layout,
};
};

View File

@@ -0,0 +1,19 @@
import { useEffect } from 'react';
import { fetchRelationships } from '../actions/accounts';
import { useAppDispatch, useAppSelector } from '../store';
export function useRelationship(accountId?: string | null) {
const relationship = useAppSelector((state) =>
accountId ? state.relationships.get(accountId) : null,
);
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && !relationship) {
dispatch(fetchRelationships([accountId]));
}
}, [accountId, dispatch, relationship]);
return relationship;
}

View File

@@ -535,6 +535,8 @@
"follow_suggestions.view_all": "View all",
"follow_suggestions.who_to_follow": "Who to follow",
"followed_tags": "Followed hashtags",
"followers.hide_other_followers": "This user has chosen to not make their other followers visible",
"following.hide_other_following": "This user has chosen to not make the rest of who they follow visible",
"footer.about": "About",
"footer.about_mastodon": "About Mastodon",
"footer.about_server": "About {domain}",

View File

@@ -47,5 +47,5 @@ export function toTypedTimeline(timeline?: ImmutableMap<string, unknown>) {
emptyList,
) as ImmutableList<string>,
items: timeline.get('items', emptyList) as ImmutableList<string>,
} as TimelineShape;
} satisfies TimelineShape;
}

View File

@@ -0,0 +1,35 @@
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { createAppSelector } from '../store';
export const selectUserListWithoutMe = createAppSelector(
[
(state) => state.user_lists,
(state) => (state.meta.get('me') as string | null) ?? null,
(_state, listName: string) => listName,
(_state, _listName, accountId?: string | null) => accountId ?? null,
],
(lists, currentAccountId, listName, accountId) => {
if (!accountId || !listName) {
return null;
}
const list = lists.getIn([listName, accountId]) as
| ImmutableMap<string, unknown>
| undefined;
if (!list) {
return null;
}
// Returns the list except the current account.
return {
items: (
list.get('items', ImmutableList<string>()) as ImmutableList<string>
)
.filter((id) => id !== currentAccountId)
.toArray(),
isLoading: !!list.get('isLoading', true),
hasMore: !!list.get('hasMore', false),
};
},
);