Merge branch 'familiar-followers' into 'develop'

Display familiar followers on Mastodon

See merge request soapbox-pub/soapbox-fe!1378
This commit is contained in:
marcin mikołajczak 2022-05-20 19:25:56 +00:00
commit 3698c5a026
11 changed files with 220 additions and 5 deletions

View file

@ -0,0 +1,59 @@
import { RootState } from 'soapbox/store';
import api from '../api';
import { ACCOUNTS_IMPORT, importFetchedAccounts } from './importer';
import type { APIEntity } from 'soapbox/types/entities';
export const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST';
export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS';
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
type FamiliarFollowersFetchRequestAction = {
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
id: string,
}
type FamiliarFollowersFetchRequestSuccessAction = {
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
id: string,
accounts: Array<APIEntity>,
}
type FamiliarFollowersFetchRequestFailAction = {
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
id: string,
error: any,
}
type AccountsImportAction = {
type: typeof ACCOUNTS_IMPORT,
accounts: Array<APIEntity>,
}
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: React.Dispatch<FamiliarFollowersActions>, getState: () => RootState) => {
dispatch({
type: FAMILIAR_FOLLOWERS_FETCH_REQUEST,
id: accountId,
});
api(getState).get(`/api/v1/accounts/familiar_followers?id=${accountId}`)
.then(({ data }) => {
const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts;
dispatch(importFetchedAccounts(accounts) as AccountsImportAction);
dispatch({
type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
id: accountId,
accounts,
});
})
.catch(error => dispatch({
type: FAMILIAR_FOLLOWERS_FETCH_FAIL,
id: accountId,
error,
}));
};

View file

@ -22,11 +22,11 @@ const BirthdaysModal = ({ onClose }: IBirthdaysModal) => {
if (!accountIds) { if (!accountIds) {
body = <Spinner />; body = <Spinner />;
} else { } else {
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this post yet. When someone does, they will show up here.' />; const emptyMessage = <FormattedMessage id='birthdays_modal.empty' defaultMessage='None of your friends have birthday today.' />;
body = ( body = (
<ScrollableList <ScrollableList
scrollKey='reblogs' scrollKey='birthdays'
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
itemClassName='pb-3' itemClassName='pb-3'
> >

View file

@ -0,0 +1,57 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
const getAccount = makeGetAccount();
interface IFamiliarFollowersModal {
accountId: string,
onClose: (string: string) => void,
}
const FamiliarFollowersModal = ({ accountId, onClose }: IFamiliarFollowersModal) => {
const account = useAppSelector(state => getAccount(state, accountId));
const familiarFollowerIds: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.getIn(['familiar_followers', accountId]));
const onClickClose = () => {
onClose('FAMILIAR_FOLLOWERS');
};
let body;
if (!account || !familiarFollowerIds) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='account.familiar_followers.empty' defaultMessage='No one you know follows {name}.' values={{ name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} /> }} />;
body = (
<ScrollableList
scrollKey='familiar_followers'
emptyMessage={emptyMessage}
itemClassName='pb-3'
>
{familiarFollowerIds.map(id =>
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.familiar_followers' defaultMessage='People you know following {name}' values={{ name: <span dangerouslySetInnerHTML={{ __html: account?.display_name_html || '' }} /> }} />}
onClose={onClickClose}
>
{body}
</Modal>
);
};
export default FamiliarFollowersModal;

View file

@ -0,0 +1,82 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react';
import { useEffect } from 'react';
import { FormattedList, FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { fetchAccountFamiliarFollowers } from 'soapbox/actions/familiar_followers';
import { openModal } from 'soapbox/actions/modals';
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
import { Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification_badge';
import { useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import type { Account } from 'soapbox/types/entities';
const getAccount = makeGetAccount();
interface IProfileFamiliarFollowers {
account: Account,
}
const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account }) => {
const dispatch = useDispatch();
const me = useAppSelector((state) => state.me);
const features = useFeatures();
const familiarFollowerIds: ImmutableOrderedSet<string> = useAppSelector(state => state.user_lists.getIn(['familiar_followers', account.id], ImmutableOrderedSet()));
const familiarFollowers: ImmutableOrderedSet<Account | null> = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId)));
useEffect(() => {
if (me && features.familiarFollowers) {
dispatch(fetchAccountFamiliarFollowers(account.id));
}
}, []);
const openFamiliarFollowersModal = () => {
dispatch(openModal('FAMILIAR_FOLLOWERS', {
accountId: account.id,
}));
};
if (familiarFollowerIds.size === 0) {
return null;
}
const accounts: Array<React.ReactNode> = familiarFollowers.map(account => !!account && (
<HoverRefWrapper accountId={account.id} inline>
<Link className='mention' to={`/@${account.acct}`}>
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
{account.verified && <VerificationBadge />}
</Link>
</HoverRefWrapper>
)).toArray();
if (familiarFollowerIds.size > 2) {
accounts.push(
<span className='hover:underline cursor-pointer' role='presentation' onClick={openFamiliarFollowersModal}>
<FormattedMessage
id='account.familiar_followers.more'
defaultMessage='{count} {count, plural, one {other} other {others}} you follow'
values={{ count: familiarFollowerIds.size - familiarFollowers.size }}
/>
</span>,
);
}
return (
<Text theme='muted' size='sm'>
<FormattedMessage
id='account.familiar_followers'
defaultMessage='Followed by {accounts}'
values={{
accounts: <FormattedList type='conjunction' value={accounts} />,
}}
/>
</Text>
);
};
export default ProfileFamiliarFollowers;

View file

@ -9,6 +9,7 @@ import VerificationBadge from 'soapbox/components/verification_badge';
import { useSoapboxConfig } from 'soapbox/hooks'; import { useSoapboxConfig } from 'soapbox/hooks';
import { isLocal } from 'soapbox/utils/accounts'; import { isLocal } from 'soapbox/utils/accounts';
import ProfileFamiliarFollowers from './profile_familiar_followers';
import ProfileStats from './profile_stats'; import ProfileStats from './profile_stats';
import type { Account } from 'soapbox/types/entities'; import type { Account } from 'soapbox/types/entities';
@ -222,6 +223,8 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
{renderBirthday()} {renderBirthday()}
</div> </div>
<ProfileFamiliarFollowers account={account} />
</Stack> </Stack>
</div> </div>
); );

View file

@ -505,3 +505,7 @@ export function AuthTokenList() {
export function VerifySmsModal() { export function VerifySmsModal() {
return import(/* webpackChunkName: "features/ui" */'../components/modals/verify-sms-modal'); return import(/* webpackChunkName: "features/ui" */'../components/modals/verify-sms-modal');
} }
export function FamiliarFollowersModal() {
return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/familiar_followers_modal');
}

View file

@ -10,13 +10,16 @@
"account.block_domain": "Blokuj wszystko z {domain}", "account.block_domain": "Blokuj wszystko z {domain}",
"account.blocked": "Zablokowany(-a)", "account.blocked": "Zablokowany(-a)",
"account.chat": "Napisz do @{name}", "account.chat": "Napisz do @{name}",
"account.column_settings.description": "These settings apply to all account timelines.", "account.column_settings.description": "Te ustawienia dotyczą wszystkich osi czasu konta.",
"account.column_settings.title": "Account timeline settings", "account.column_settings.title": "Ustawienia osi czasu",
"account.deactivated": "Dezaktywowany(-a)", "account.deactivated": "Dezaktywowany(-a)",
"account.direct": "Wyślij wiadomość bezpośrednią do @{name}", "account.direct": "Wyślij wiadomość bezpośrednią do @{name}",
"account.domain_blocked": "Wyciszono domenę", "account.domain_blocked": "Wyciszono domenę",
"account.edit_profile": "Edytuj profil", "account.edit_profile": "Edytuj profil",
"account.endorse": "Polecaj na profilu", "account.endorse": "Polecaj na profilu",
"account.familiar_followers": "Obserwowany(-a) przez {accounts}",
"account.familiar_followers.empty": "Nie znasz nikogo obserwującego {name}.",
"account.familiar_followers.more": "{count} {count, plural, one {innego użytkownika, którego obserwujesz} other {innych użytkowników, których obserwujesz}}",
"account.follow": "Śledź", "account.follow": "Śledź",
"account.followers": "Śledzący", "account.followers": "Śledzący",
"account.followers.empty": "Nikt jeszcze nie śledzi tego użytkownika.", "account.followers.empty": "Nikt jeszcze nie śledzi tego użytkownika.",
@ -209,6 +212,7 @@
"column.domain_blocks": "Ukryte domeny", "column.domain_blocks": "Ukryte domeny",
"column.edit_profile": "Edytuj profil", "column.edit_profile": "Edytuj profil",
"column.export_data": "Eksportuj dane", "column.export_data": "Eksportuj dane",
"column.familiar_followers": "Obserwujący {name} których znasz",
"column.favourited_statuses": "Polubione wpisy", "column.favourited_statuses": "Polubione wpisy",
"column.favourites": "Polubienia", "column.favourites": "Polubienia",
"column.federation_restrictions": "Ograniczenia federacji", "column.federation_restrictions": "Ograniczenia federacji",
@ -765,7 +769,7 @@
"notifications.column_settings.filter_bar.category": "Szybkie filtrowanie", "notifications.column_settings.filter_bar.category": "Szybkie filtrowanie",
"notifications.column_settings.filter_bar.show": "Pokaż", "notifications.column_settings.filter_bar.show": "Pokaż",
"notifications.column_settings.follow": "Nowi śledzący:", "notifications.column_settings.follow": "Nowi śledzący:",
"notifications.column_settings.follow_request": "Nowe prośby o możliwość śledzenia:", "notifications.column_settings.follow_request": "Nowe prośby o możliwość śledzenia:",
"notifications.column_settings.mention": "Wspomnienia:", "notifications.column_settings.mention": "Wspomnienia:",
"notifications.column_settings.move": "Przenoszone konta:", "notifications.column_settings.move": "Przenoszone konta:",
"notifications.column_settings.poll": "Wyniki głosowania:", "notifications.column_settings.poll": "Wyniki głosowania:",

Binary file not shown.

View file

@ -256,6 +256,12 @@ const getInstanceFeatures = (instance: Instance) => {
features.includes('exposable_reactions'), features.includes('exposable_reactions'),
]), ]),
/**
* Can see accounts' followers you know
* @see GET /api/v1/accounts/familiar_followers
*/
familiarFollowers: v.software === MASTODON && gte(v.version, '3.5.0'),
/** Whether the instance federates. */ /** Whether the instance federates. */
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false federating: federation.get('enabled', true) === true, // Assume true unless explicitly false