Merge branch 'familiar-followers' into 'develop'
Display familiar followers on Mastodon See merge request soapbox-pub/soapbox-fe!1378
This commit is contained in:
commit
3698c5a026
11 changed files with 220 additions and 5 deletions
59
app/soapbox/actions/familiar_followers.ts
Normal file
59
app/soapbox/actions/familiar_followers.ts
Normal 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,
|
||||
}));
|
||||
};
|
|
@ -22,11 +22,11 @@ const BirthdaysModal = ({ onClose }: IBirthdaysModal) => {
|
|||
if (!accountIds) {
|
||||
body = <Spinner />;
|
||||
} 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 = (
|
||||
<ScrollableList
|
||||
scrollKey='reblogs'
|
||||
scrollKey='birthdays'
|
||||
emptyMessage={emptyMessage}
|
||||
itemClassName='pb-3'
|
||||
>
|
||||
|
|
|
@ -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;
|
Binary file not shown.
|
@ -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;
|
|
@ -9,6 +9,7 @@ import VerificationBadge from 'soapbox/components/verification_badge';
|
|||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
||||
import ProfileFamiliarFollowers from './profile_familiar_followers';
|
||||
import ProfileStats from './profile_stats';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
@ -222,6 +223,8 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
|
||||
{renderBirthday()}
|
||||
</div>
|
||||
|
||||
<ProfileFamiliarFollowers account={account} />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -505,3 +505,7 @@ export function AuthTokenList() {
|
|||
export function VerifySmsModal() {
|
||||
return import(/* webpackChunkName: "features/ui" */'../components/modals/verify-sms-modal');
|
||||
}
|
||||
|
||||
export function FamiliarFollowersModal() {
|
||||
return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/familiar_followers_modal');
|
||||
}
|
||||
|
|
|
@ -10,13 +10,16 @@
|
|||
"account.block_domain": "Blokuj wszystko z {domain}",
|
||||
"account.blocked": "Zablokowany(-a)",
|
||||
"account.chat": "Napisz do @{name}",
|
||||
"account.column_settings.description": "These settings apply to all account timelines.",
|
||||
"account.column_settings.title": "Account timeline settings",
|
||||
"account.column_settings.description": "Te ustawienia dotyczą wszystkich osi czasu konta.",
|
||||
"account.column_settings.title": "Ustawienia osi czasu",
|
||||
"account.deactivated": "Dezaktywowany(-a)",
|
||||
"account.direct": "Wyślij wiadomość bezpośrednią do @{name}",
|
||||
"account.domain_blocked": "Wyciszono domenę",
|
||||
"account.edit_profile": "Edytuj profil",
|
||||
"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.followers": "Śledzący",
|
||||
"account.followers.empty": "Nikt jeszcze nie śledzi tego użytkownika.",
|
||||
|
@ -209,6 +212,7 @@
|
|||
"column.domain_blocks": "Ukryte domeny",
|
||||
"column.edit_profile": "Edytuj profil",
|
||||
"column.export_data": "Eksportuj dane",
|
||||
"column.familiar_followers": "Obserwujący {name} których znasz",
|
||||
"column.favourited_statuses": "Polubione wpisy",
|
||||
"column.favourites": "Polubienia",
|
||||
"column.federation_restrictions": "Ograniczenia federacji",
|
||||
|
@ -765,7 +769,7 @@
|
|||
"notifications.column_settings.filter_bar.category": "Szybkie filtrowanie",
|
||||
"notifications.column_settings.filter_bar.show": "Pokaż",
|
||||
"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.move": "Przenoszone konta:",
|
||||
"notifications.column_settings.poll": "Wyniki głosowania:",
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -256,6 +256,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
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. */
|
||||
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
|
||||
|
||||
|
|
Loading…
Reference in a new issue