From f70d44f67cce1573fbebbc63da498b19d1432ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 13 May 2022 23:14:55 +0200 Subject: [PATCH 1/3] Display familiar followers on Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/familiar_followers.ts | 59 +++++++++++++ .../components/status_reply_mentions.js | 2 +- .../ui/components/birthdays_modal.tsx | 4 +- .../components/familiar_followers_modal.tsx | 57 +++++++++++++ .../features/ui/components/modal_root.js | 2 + .../components/profile_familiar_followers.tsx | 83 +++++++++++++++++++ .../ui/components/profile_info_panel.tsx | 3 + .../features/ui/util/async-components.ts | 4 + app/soapbox/locales/pl.json | 5 ++ .../reducers/__tests__/user_lists-test.js | 1 + app/soapbox/reducers/user_lists.js | 6 ++ app/soapbox/utils/features.ts | 6 ++ 12 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/actions/familiar_followers.ts create mode 100644 app/soapbox/features/ui/components/familiar_followers_modal.tsx create mode 100644 app/soapbox/features/ui/components/profile_familiar_followers.tsx diff --git a/app/soapbox/actions/familiar_followers.ts b/app/soapbox/actions/familiar_followers.ts new file mode 100644 index 000000000..ec6eca6d8 --- /dev/null +++ b/app/soapbox/actions/familiar_followers.ts @@ -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, +} + +type FamiliarFollowersFetchRequestFailAction = { + type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL, + id: string, + error: any, +} + +type AccountsImportAction = { + type: typeof ACCOUNTS_IMPORT, + accounts: Array, +} + +export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction + +export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: React.Dispatch, 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, + })); +}; diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js index 76a48d5df..21b68ff21 100644 --- a/app/soapbox/components/status_reply_mentions.js +++ b/app/soapbox/components/status_reply_mentions.js @@ -64,7 +64,7 @@ class StatusReplyMentions extends ImmutablePureComponent { id='reply_mentions.reply' defaultMessage='Replying to {accounts}{more}' values={{ - accounts: to.slice(0, 2).map(account => (<> + accounts: to.slice(0, 2).map((account) => (<> @{account.get('username')} diff --git a/app/soapbox/features/ui/components/birthdays_modal.tsx b/app/soapbox/features/ui/components/birthdays_modal.tsx index 6c43bd7b4..f0b25f5f4 100644 --- a/app/soapbox/features/ui/components/birthdays_modal.tsx +++ b/app/soapbox/features/ui/components/birthdays_modal.tsx @@ -22,11 +22,11 @@ const BirthdaysModal = ({ onClose }: IBirthdaysModal) => { if (!accountIds) { body = ; } else { - const emptyMessage = ; + const emptyMessage = ; body = ( diff --git a/app/soapbox/features/ui/components/familiar_followers_modal.tsx b/app/soapbox/features/ui/components/familiar_followers_modal.tsx new file mode 100644 index 000000000..0ec081d58 --- /dev/null +++ b/app/soapbox/features/ui/components/familiar_followers_modal.tsx @@ -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 = useAppSelector(state => state.user_lists.getIn(['familiar_followers', accountId])); + + const onClickClose = () => { + onClose('FAMILIAR_FOLLOWERS'); + }; + + let body; + + if (!account || !familiarFollowerIds) { + body = ; + } else { + const emptyMessage = }} />; + + body = ( + + {familiarFollowerIds.map(id => + , + )} + + ); + } + + + return ( + }} />} + onClose={onClickClose} + > + {body} + + ); +}; + +export default FamiliarFollowersModal; diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 78f9ee2a4..98daaa78d 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -30,6 +30,7 @@ import { BirthdaysModal, AccountNoteModal, CompareHistoryModal, + FamiliarFollowersModal, } from 'soapbox/features/ui/util/async-components'; import BundleContainer from '../containers/bundle_container'; @@ -66,6 +67,7 @@ const MODAL_COMPONENTS = { 'BIRTHDAYS': BirthdaysModal, 'ACCOUNT_NOTE': AccountNoteModal, 'COMPARE_HISTORY': CompareHistoryModal, + 'FAMILIAR_FOLLOWERS': FamiliarFollowersModal, }; export default class ModalRoot extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/profile_familiar_followers.tsx b/app/soapbox/features/ui/components/profile_familiar_followers.tsx new file mode 100644 index 000000000..a0dcef2c9 --- /dev/null +++ b/app/soapbox/features/ui/components/profile_familiar_followers.tsx @@ -0,0 +1,83 @@ +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 } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; +import { getFeatures } from 'soapbox/utils/features'; + +import type { Account } from 'soapbox/types/entities'; + +const getAccount = makeGetAccount(); + +interface IProfileFamiliarFollowers { + account: Account, +} + +const ProfileFamiliarFollowers: React.FC = ({ account }) => { + const dispatch = useDispatch(); + const me = useAppSelector((state) => state.me); + const features = useAppSelector((state) => getFeatures(state.instance)); + const familiarFollowerIds: ImmutableOrderedSet = useAppSelector(state => state.user_lists.getIn(['familiar_followers', account.id], ImmutableOrderedSet())); + const familiarFollowers: ImmutableOrderedSet = 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 = familiarFollowers.map(account => !!account && ( + + + + + {account.verified && } + + + )).toArray(); + + if (familiarFollowerIds.size > 2) { + accounts.push( + + + , + ); + } + + return ( + + , + }} + /> + + ); +}; + +export default ProfileFamiliarFollowers; \ No newline at end of file diff --git a/app/soapbox/features/ui/components/profile_info_panel.tsx b/app/soapbox/features/ui/components/profile_info_panel.tsx index 7fe628ddb..e3d9ac8f4 100644 --- a/app/soapbox/features/ui/components/profile_info_panel.tsx +++ b/app/soapbox/features/ui/components/profile_info_panel.tsx @@ -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 = ({ account, username }) => {renderBirthday()} + + ); diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index aab5bf76d..ed1abd55e 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -501,3 +501,7 @@ export function CompareHistoryModal() { export function AuthTokenList() { return import(/* webpackChunkName: "features/auth_token_list" */'../../auth_token_list'); } + +export function FamiliarFollowersModal() { + return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/familiar_followers_modal'); +} diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index e9a38c69c..7a19661ae 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -16,6 +16,9 @@ "account.direct": "Wyślij wiadomość bezpośrednią do @{name}", "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.", @@ -155,6 +158,7 @@ "backups.empty_message.action": "Chcesz utworzyć?", "backups.pending": "Oczekująca", "beta.also_available": "Dostępne w językach:", + "birthdays_modal.empty": "Nikt kogo znasz nie ma dziś urodzin.", "birthday_panel.title": "Birthdays", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", @@ -203,6 +207,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", diff --git a/app/soapbox/reducers/__tests__/user_lists-test.js b/app/soapbox/reducers/__tests__/user_lists-test.js index a168f3cdc..7c5ec7e20 100644 --- a/app/soapbox/reducers/__tests__/user_lists-test.js +++ b/app/soapbox/reducers/__tests__/user_lists-test.js @@ -17,6 +17,7 @@ describe('user_lists reducer', () => { groups_removed_accounts: ImmutableMap(), pinned: ImmutableMap(), birthday_reminders: ImmutableMap(), + familiar_followers: ImmutableMap(), })); }); }); diff --git a/app/soapbox/reducers/user_lists.js b/app/soapbox/reducers/user_lists.js index 992f930cc..69e8c181c 100644 --- a/app/soapbox/reducers/user_lists.js +++ b/app/soapbox/reducers/user_lists.js @@ -27,6 +27,9 @@ import { DIRECTORY_EXPAND_SUCCESS, DIRECTORY_EXPAND_FAIL, } from '../actions/directory'; +import { + FAMILIAR_FOLLOWERS_FETCH_SUCCESS, +} from '../actions/familiar_followers'; import { GROUP_MEMBERS_FETCH_SUCCESS, GROUP_MEMBERS_EXPAND_SUCCESS, @@ -60,6 +63,7 @@ const initialState = ImmutableMap({ groups_removed_accounts: ImmutableMap(), pinned: ImmutableMap(), birthday_reminders: ImmutableMap(), + familiar_followers: ImmutableMap(), }); const normalizeList = (state, type, id, accounts, next) => { @@ -138,6 +142,8 @@ export default function userLists(state = initialState, action) { return normalizeList(state, 'pinned', action.id, action.accounts, action.next); case BIRTHDAY_REMINDERS_FETCH_SUCCESS: return state.setIn(['birthday_reminders', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id))); + case FAMILIAR_FOLLOWERS_FETCH_SUCCESS: + return state.setIn(['familiar_followers', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id))); default: return state; } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 9f60b5a05..45eed8936 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -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 From e45cd2d97d0290bcbcbd9e0fc2ef9a68c70ca7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 16 May 2022 19:37:10 +0200 Subject: [PATCH 2/3] Update Polish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/locales/pl.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 7a19661ae..69b0b6bf9 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -10,8 +10,8 @@ "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.edit_profile": "Edytuj profil", @@ -107,14 +107,14 @@ "admin.users.actions.promote_to_admin_message": "Mianowano @{acct} administratorem", "admin.users.actions.promote_to_moderator": "Mianuj @{name} moderatorem", "admin.users.actions.promote_to_moderator_message": "Mianowano @{acct} moderatorem", - "admin.users.actions.remove_donor": "Remove @{name} as a donor", - "admin.users.actions.set_donor": "Set @{name} as a donor", + "admin.users.actions.remove_donor": "Usuń @{name} ze wspierających", + "admin.users.actions.set_donor": "Ustaw @{name} jako wspierającego", "admin.users.actions.suggest_user": "Polecaj @{name}", "admin.users.actions.unsuggest_user": "Przestań polecać @{name}", "admin.users.actions.unverify_user": "Cofnij weryfikację @{name}", "admin.users.actions.verify_user": "Weryfikuj @{name}", - "admin.users.remove_donor_message": "@{acct} was removed as a donor", - "admin.users.set_donor_message": "@{acct} was set as a donor", + "admin.users.remove_donor_message": "Usunięto @{acct} ze wspierających", + "admin.users.set_donor_message": "Ustawiono @{acct} jako wspierającego", "admin.users.user_deactivated_message": "Zdezaktywowano @{acct}", "admin.users.user_deleted_message": "Usunięto @{acct}", "admin.users.user_suggested_message": "Zaczęto polecać @{acct}", @@ -124,12 +124,12 @@ "admin_nav.awaiting_approval": "Oczekujące zgłoszenia", "admin_nav.dashboard": "Panel administracyjny", "admin_nav.reports": "Zgłoszenia", - "alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out).", - "alert.unexpected.browser": "Browser", + "alert.unexpected.body": "Przepraszamy za niedogodność. Jeśli problem nie zniknie, skontaktuj się z naszym wsparciem technicznym. Możesz też spróbować {clearCookies} (zostaniesz wylogowany(-a) z konta).", + "alert.unexpected.browser": "Przeglądarka", "alert.unexpected.clear_cookies": "wyczyścić pliki cookies i dane przeglądarki", - "alert.unexpected.links.help": "Help Center", - "alert.unexpected.links.status": "Status", - "alert.unexpected.links.support": "Support", + "alert.unexpected.links.help": "Centrum pomocy", + "alert.unexpected.links.status": "Stan", + "alert.unexpected.links.support": "Wsparcie techniczne", "alert.unexpected.message": "Wystąpił nieoczekiwany błąd.", "alert.unexpected.return_home": "Wróć na stronę główną", "alert.unexpected.title": "O nie!", @@ -159,7 +159,7 @@ "backups.pending": "Oczekująca", "beta.also_available": "Dostępne w językach:", "birthdays_modal.empty": "Nikt kogo znasz nie ma dziś urodzin.", - "birthday_panel.title": "Birthdays", + "birthday_panel.title": "Urodziny", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", "bundle_column_error.retry": "Spróbuj ponownie", @@ -747,7 +747,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:", @@ -773,9 +773,9 @@ "onboarding.avatar.title": "Wybierz zdjęcie profilowe", "onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.", "onboarding.display_name.title": "Wybierz wyświetlaną nazwę", - "onboarding.done": "Done", + "onboarding.done": "Gotowe", "onboarding.finished.message": "Cieszymy się, że możemy powitać Cię w naszej społeczności! Naciśnij poniższy przycisk, aby rozpocząć.", - "onboarding.finished.title": "Onboarding complete", + "onboarding.finished.title": "Wprowadzenie", "onboarding.header.subtitle": "Będzie widoczny w górnej części Twojego profilu", "onboarding.header.title": "Wybierz obraz tła", "onboarding.next": "Dalej", @@ -789,8 +789,8 @@ "password_reset.confirmation": "Sprawdź swoją pocztę e-mail, aby potwierdzić.", "password_reset.fields.username_placeholder": "Adres e-mail lub nazwa użytkownika", "password_reset.reset": "Resetuj hasło", - "patron.donate": "Donate", - "patron.title": "Funding Goal", + "patron.donate": "Wesprzyj", + "patron.title": "Cel wsparcia", "pinned_accounts.title": "Polecani przez {name}", "pinned_statuses.none": "Brak przypięć do pokazania.", "poll.closed": "Zamknięte", @@ -827,7 +827,7 @@ "profile_dropdown.add_account": "Dodaj istniejące konto", "profile_dropdown.logout": "Wyloguj @{acct}", "profile_dropdown.theme": "Motyw", - "profile_fields_panel.title": "Profile fields", + "profile_fields_panel.title": "Pola konta", "public.column_settings.title": "Ustawienia osi czasu Fediwersum", "reactions.all": "Wszystkie", "regeneration_indicator.label": "Ładuję…", From 3cd8a4b96634fffb6c7792f41331034dc9935baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 20 May 2022 20:54:24 +0200 Subject: [PATCH 3/3] Use useFeatures hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/ui/components/profile_familiar_followers.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/ui/components/profile_familiar_followers.tsx b/app/soapbox/features/ui/components/profile_familiar_followers.tsx index a0dcef2c9..f2e0c9b61 100644 --- a/app/soapbox/features/ui/components/profile_familiar_followers.tsx +++ b/app/soapbox/features/ui/components/profile_familiar_followers.tsx @@ -10,9 +10,8 @@ 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 } from 'soapbox/hooks'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; -import { getFeatures } from 'soapbox/utils/features'; import type { Account } from 'soapbox/types/entities'; @@ -25,7 +24,7 @@ interface IProfileFamiliarFollowers { const ProfileFamiliarFollowers: React.FC = ({ account }) => { const dispatch = useDispatch(); const me = useAppSelector((state) => state.me); - const features = useAppSelector((state) => getFeatures(state.instance)); + const features = useFeatures(); const familiarFollowerIds: ImmutableOrderedSet = useAppSelector(state => state.user_lists.getIn(['familiar_followers', account.id], ImmutableOrderedSet())); const familiarFollowers: ImmutableOrderedSet = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId)));