[Glitch] Profile redesign: Show yourself in follower list

Port bbd88d356d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo
2026-02-11 14:19:18 +01:00
committed by Claire
parent 5272a18855
commit 02a7f74976
15 changed files with 522 additions and 423 deletions

View File

@@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import ColumnHeader from '../../../components/column_header';
const messages = defineMessages({
profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
});
class ProfileColumnHeader extends PureComponent {
static propTypes = {
onClick: PropTypes.func,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
render() {
const { onClick, intl, multiColumn } = this.props;
return (
<ColumnHeader
icon='user-circle'
iconComponent={PersonIcon}
title={intl.formatMessage(messages.profile)}
onClick={onClick}
showBackButton
multiColumn={multiColumn}
/>
);
}
}
export default injectIntl(ProfileColumnHeader);

View File

@@ -0,0 +1,34 @@
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { ColumnHeader } from '../../../components/column_header';
const messages = defineMessages({
profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
});
interface ProfileColumnHeaderProps {
onClick: () => void;
multiColumn: boolean;
}
export const ProfileColumnHeader: FC<ProfileColumnHeaderProps> = ({
onClick,
multiColumn,
}) => {
const intl = useIntl();
return (
<ColumnHeader
icon='user-circle'
iconComponent={PersonIcon}
title={intl.formatMessage(messages.profile)}
onClick={onClick}
showBackButton
multiColumn={multiColumn}
/>
);
};

View File

@@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
import { ProfileColumnHeader } from 'flavours/glitch/features/account/components/profile_column_header';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';

View File

@@ -0,0 +1,64 @@
import type { FC, ReactNode } from 'react';
import { FormattedMessage } from 'react-intl';
import { LimitedAccountHint } from '@/flavours/glitch/features/account_timeline/components/limited_account_hint';
import { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibility';
import type { Account } from '@/flavours/glitch/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,113 @@
import { useCallback, useMemo, useRef } from 'react';
import type { FC, ReactNode } from 'react';
import { Account } from '@/flavours/glitch/components/account';
import type { ColumnRef } from '@/flavours/glitch/components/column';
import { Column } from '@/flavours/glitch/components/column';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import ScrollableList from '@/flavours/glitch/components/scrollable_list';
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibility';
import { useLayout } from '@/flavours/glitch/hooks/useLayout';
import { ProfileColumnHeader } from '../../account/components/profile_column_header';
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 columnRef = useRef<ColumnRef>(null);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
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 ref={columnRef}>
<ProfileColumnHeader
onClick={handleHeaderClick}
multiColumn={multiColumn}
/>
<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 '@/flavours/glitch/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,191 +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 'flavours/glitch/components/account';
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
import { useAppSelector } from 'flavours/glitch/store';
import {
lookupAccount,
fetchAccount,
fetchFollowers,
expandFollowers,
} from '../../actions/accounts';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import ProfileColumnHeader from '../account/components/profile_column_header';
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),
};
};
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,
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 });
setRef = c => {
this.column = c;
};
handleHeaderClick = () => {
this.column.scrollTop();
};
render () {
const { accountId, accountIds, hasMore, 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 = suspended || hidden;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} 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 ref={this.setRef}>
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
<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}
>
{accountIds.map(id =>
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>
);
}
}
export default connect(mapStateToProps)(Followers);

View File

@@ -0,0 +1,93 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDebouncedCallback } from 'use-debounce';
import {
expandFollowers,
fetchFollowers,
} from '@/flavours/glitch/actions/accounts';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
import { selectUserListWithoutMe } from '@/flavours/glitch/selectors/user_lists';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/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 '@/flavours/glitch/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,191 +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 'flavours/glitch/components/account';
import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
import { useAppSelector } from 'flavours/glitch/store';
import {
lookupAccount,
fetchAccount,
fetchFollowing,
expandFollowing,
} from '../../actions/accounts';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import ProfileColumnHeader from '../account/components/profile_column_header';
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),
};
};
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,
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 });
setRef = c => {
this.column = c;
};
handleHeaderClick = () => {
this.column.scrollTop();
};
render () {
const { accountId, accountIds, hasMore, 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 = suspended || hidden;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} 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 ref={this.setRef}>
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
<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}
>
{accountIds.map(id =>
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>
);
}
}
export default connect(mapStateToProps)(Following);

View File

@@ -0,0 +1,97 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDebouncedCallback } from 'use-debounce';
import {
expandFollowing,
fetchFollowing,
} from '@/flavours/glitch/actions/accounts';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
import { selectUserListWithoutMe } from '@/flavours/glitch/selectors/user_lists';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/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

@@ -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),
};
},
);