Add endorsed accounts to profiles in web UI (#34568)

This commit is contained in:
Eugen Rochko 2025-04-29 14:14:22 +02:00 committed by GitHub
parent b81c28e7dc
commit 79013c730d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 157 additions and 113 deletions

View File

@ -1,18 +1,18 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import { apiRemoveAccountFromFollowers } from 'mastodon/api/accounts'; import {
import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; apiRemoveAccountFromFollowers,
apiGetEndorsedAccounts,
} from 'mastodon/api/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedAccounts } from './importer';
export const revealAccount = createAction<{ export const revealAccount = createAction<{
id: string; id: string;
}>('accounts/revealAccount'); }>('accounts/revealAccount');
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);
function actionWithSkipLoadingTrue<Args extends object>(args: Args) { function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
return { return {
payload: { payload: {
@ -104,3 +104,12 @@ export const removeAccountFromFollowers = createDataLoadingThunk(
apiRemoveAccountFromFollowers(accountId), apiRemoveAccountFromFollowers(accountId),
(relationship) => ({ relationship }), (relationship) => ({ relationship }),
); );
export const fetchEndorsedAccounts = createDataLoadingThunk(
'accounts/endorsements',
({ accountId }: { accountId: string }) => apiGetEndorsedAccounts(accountId),
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
return data;
},
);

View File

@ -1,34 +0,0 @@
import api from '../api';
export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL';
export const fetchFeaturedTags = (id) => (dispatch, getState) => {
if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
return;
}
dispatch(fetchFeaturedTagsRequest(id));
api().get(`/api/v1/accounts/${id}/featured_tags`)
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
};
export const fetchFeaturedTagsRequest = (id) => ({
type: FEATURED_TAGS_FETCH_REQUEST,
id,
});
export const fetchFeaturedTagsSuccess = (id, tags) => ({
type: FEATURED_TAGS_FETCH_SUCCESS,
id,
tags,
});
export const fetchFeaturedTagsFail = (id, error) => ({
type: FEATURED_TAGS_FETCH_FAIL,
id,
error,
});

View File

@ -0,0 +1,7 @@
import { apiGetFeaturedTags } from 'mastodon/api/accounts';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
export const fetchFeaturedTags = createDataLoadingThunk(
'accounts/featured_tags',
({ accountId }: { accountId: string }) => apiGetFeaturedTags(accountId),
);

View File

@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);

View File

@ -1,7 +1,6 @@
import { createPollFromServerJSON } from 'mastodon/models/poll'; import { createPollFromServerJSON } from 'mastodon/models/poll';
import { importAccounts } from '../accounts_typed'; import { importAccounts } from './accounts';
import { normalizeStatus } from './normalizer'; import { normalizeStatus } from './normalizer';
import { importPolls } from './polls'; import { importPolls } from './polls';

View File

@ -1,5 +1,7 @@
import { apiRequestPost } from 'mastodon/api'; import { apiRequestPost, apiRequestGet } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
export const apiSubmitAccountNote = (id: string, value: string) => export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, { apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
@ -23,3 +25,9 @@ export const apiRemoveAccountFromFollowers = (id: string) =>
apiRequestPost<ApiRelationshipJSON>( apiRequestPost<ApiRelationshipJSON>(
`v1/accounts/${id}/remove_from_followers`, `v1/accounts/${id}/remove_from_followers`,
); );
export const apiGetFeaturedTags = (id: string) =>
apiRequestGet<ApiHashtagJSON>(`v1/accounts/${id}/featured_tags`);
export const apiGetEndorsedAccounts = (id: string) =>
apiRequestGet<ApiAccountJSON>(`v1/accounts/${id}/endorsements`);

View File

@ -7,19 +7,21 @@ import { useParams } from 'react-router';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines'; import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
import { Account } from 'mastodon/components/account';
import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint'; import { RemoteHint } from 'mastodon/components/remote_hint';
import StatusContainer from 'mastodon/containers/status_container'; import StatusContainer from 'mastodon/containers/status_container';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import { useAccountId } from 'mastodon/hooks/useAccountId'; import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility'; import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { AccountHeader } from '../account_timeline/components/account_header';
import Column from '../ui/components/column';
import { EmptyMessage } from './components/empty_message'; import { EmptyMessage } from './components/empty_message';
import { FeaturedTag } from './components/featured_tag'; import { FeaturedTag } from './components/featured_tag';
import type { TagMap } from './components/featured_tag'; import type { TagMap } from './components/featured_tag';
@ -29,7 +31,9 @@ interface Params {
id?: string; id?: string;
} }
const AccountFeatured = () => { const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
multiColumn,
}) => {
const accountId = useAccountId(); const accountId = useAccountId();
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const forceEmptyState = suspended || blockedBy || hidden; const forceEmptyState = suspended || blockedBy || hidden;
@ -40,7 +44,8 @@ const AccountFeatured = () => {
useEffect(() => { useEffect(() => {
if (accountId) { if (accountId) {
void dispatch(expandAccountFeaturedTimeline(accountId)); void dispatch(expandAccountFeaturedTimeline(accountId));
dispatch(fetchFeaturedTags(accountId)); void dispatch(fetchFeaturedTags({ accountId }));
void dispatch(fetchEndorsedAccounts({ accountId }));
} }
}, [accountId, dispatch]); }, [accountId, dispatch]);
@ -67,6 +72,17 @@ const AccountFeatured = () => {
ImmutableList(), ImmutableList(),
) as ImmutableList<string>, ) as ImmutableList<string>,
); );
const featuredAccountIds = useAppSelector(
(state) =>
state.user_lists.getIn(
['featured_accounts', accountId, 'items'],
ImmutableList(),
) as ImmutableList<string>,
);
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (isLoading) { if (isLoading) {
return ( return (
@ -78,7 +94,11 @@ const AccountFeatured = () => {
); );
} }
if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) { if (
featuredStatusIds.isEmpty() &&
featuredTags.isEmpty() &&
featuredAccountIds.isEmpty()
) {
return ( return (
<AccountFeaturedWrapper accountId={accountId}> <AccountFeaturedWrapper accountId={accountId}>
<EmptyMessage <EmptyMessage
@ -131,6 +151,19 @@ const AccountFeatured = () => {
))} ))}
</> </>
)} )}
{!featuredAccountIds.isEmpty() && (
<>
<h4 className='column-subheading'>
<FormattedMessage
id='account.featured.accounts'
defaultMessage='Profiles'
/>
</h4>
{featuredAccountIds.map((featuredAccountId) => (
<Account key={featuredAccountId} id={featuredAccountId} />
))}
</>
)}
<RemoteHint accountId={accountId} /> <RemoteHint accountId={accountId} />
</div> </div>
</Column> </Column>

View File

@ -147,7 +147,7 @@ export const AccountGallery: React.FC<{
[dispatch], [dispatch],
); );
if (accountId && !isAccount) { if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />; return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
} }

View File

@ -107,7 +107,6 @@ const messages = defineMessages({
id: 'account.disable_notifications', id: 'account.disable_notifications',
defaultMessage: 'Stop notifying me when @{name} posts', defaultMessage: 'Stop notifying me when @{name} posts',
}, },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { preferences: {
id: 'navigation_bar.preferences', id: 'navigation_bar.preferences',
defaultMessage: 'Preferences', defaultMessage: 'Preferences',
@ -451,7 +450,6 @@ export const AccountHeader: React.FC<{
text: intl.formatMessage(messages.preferences), text: intl.formatMessage(messages.preferences),
href: '/settings/preferences', href: '/settings/preferences',
}); });
arr.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
arr.push(null); arr.push(null);
arr.push({ arr.push({
text: intl.formatMessage(messages.follow_requests), text: intl.formatMessage(messages.follow_requests),

View File

@ -13,7 +13,6 @@ import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import { ColumnBackButton } from '../../components/column_back_button'; import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
@ -27,7 +26,7 @@ import { LimitedAccountHint } from './components/limited_account_hint';
const emptyList = ImmutableList(); const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => { const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (accountId === null) { if (accountId === null) {
return { return {
@ -86,7 +85,6 @@ class AccountTimeline extends ImmutablePureComponent {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged })); dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
} }
dispatch(fetchFeaturedTags(accountId));
dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
if (accountId === me) { if (accountId === me) {

View File

@ -9,7 +9,6 @@ import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { preferences: {
id: 'navigation_bar.preferences', id: 'navigation_bar.preferences',
defaultMessage: 'Preferences', defaultMessage: 'Preferences',
@ -53,7 +52,6 @@ export const ActionBar: React.FC = () => {
text: intl.formatMessage(messages.preferences), text: intl.formatMessage(messages.preferences),
href: '/settings/preferences', href: '/settings/preferences',
}, },
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
null, null,
{ {
text: intl.formatMessage(messages.follow_requests), text: intl.formatMessage(messages.follow_requests),

View File

@ -29,7 +29,7 @@ import { LimitedAccountHint } from '../account_timeline/components/limited_accou
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => { const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (!accountId) { if (!accountId) {
return { return {

View File

@ -29,7 +29,7 @@ import { LimitedAccountHint } from '../account_timeline/components/limited_accou
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => { const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (!accountId) { if (!accountId) {
return { return {

View File

@ -11,27 +11,25 @@ interface Params {
id?: string; id?: string;
} }
export function useAccountId() { export const useAccountId = () => {
const { acct, id } = useParams<Params>(); const { acct, id } = useParams<Params>();
const dispatch = useAppDispatch();
const accountId = useAppSelector( const accountId = useAppSelector(
(state) => (state) =>
id ?? id ?? (acct ? state.accounts_map[normalizeForLookup(acct)] : undefined),
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
); );
const account = useAppSelector((state) => const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined, accountId ? state.accounts.get(accountId) : undefined,
); );
const isAccount = !!account; const accountInStore = !!account;
const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
if (!accountId) { if (typeof accountId === 'undefined' && acct) {
dispatch(lookupAccount(acct)); dispatch(lookupAccount(acct));
} else if (!isAccount) { } else if (accountId && !accountInStore) {
dispatch(fetchAccount(accountId)); dispatch(fetchAccount(accountId));
} }
}, [dispatch, accountId, acct, isAccount]); }, [dispatch, accountId, acct, accountInStore]);
return accountId; return accountId;
} };

View File

@ -1,12 +1,14 @@
import { getAccountHidden } from 'mastodon/selectors/accounts'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
export function useAccountVisibility(accountId?: string) { export function useAccountVisibility(accountId?: string | null) {
const blockedBy = useAppSelector( const blockedBy = useAppSelector((state) =>
(state) => !!state.relationships.getIn([accountId, 'blocked_by'], false), accountId
? !!state.relationships.getIn([accountId, 'blocked_by'], false)
: false,
); );
const suspended = useAppSelector( const suspended = useAppSelector((state) =>
(state) => !!state.accounts.getIn([accountId, 'suspended'], false), accountId ? !!state.accounts.getIn([accountId, 'suspended'], false) : false,
); );
const hidden = useAppSelector((state) => const hidden = useAppSelector((state) =>
accountId ? Boolean(getAccountHidden(state, accountId)) : false, accountId ? Boolean(getAccountHidden(state, accountId)) : false,

View File

@ -29,6 +29,7 @@
"account.enable_notifications": "Notify me when @{name} posts", "account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile", "account.endorse": "Feature on profile",
"account.featured": "Featured", "account.featured": "Featured",
"account.featured.accounts": "Profiles",
"account.featured.hashtags": "Hashtags", "account.featured.hashtags": "Hashtags",
"account.featured.posts": "Posts", "account.featured.posts": "Posts",
"account.featured_tags.last_status_at": "Last post on {date}", "account.featured_tags.last_status_at": "Last post on {date}",

View File

@ -4,9 +4,9 @@ import { Map as ImmutableMap } from 'immutable';
import { import {
followAccountSuccess, followAccountSuccess,
unfollowAccountSuccess, unfollowAccountSuccess,
importAccounts,
revealAccount, revealAccount,
} from 'mastodon/actions/accounts_typed'; } from 'mastodon/actions/accounts_typed';
import { importAccounts } from 'mastodon/actions/importer/accounts';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account'; import type { Account } from 'mastodon/models/account';

View File

@ -1,23 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts';
import { importAccounts } from '../actions/accounts_typed';
import { domain } from '../initial_state';
const pattern = new RegExp(`@${domain}$`, 'gi');
export const normalizeForLookup = str =>
str.toLowerCase().replace(pattern, '');
const initialState = ImmutableMap();
export default function accountsMap(state = initialState, action) {
switch(action.type) {
case ACCOUNT_LOOKUP_FAIL:
return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state;
case importAccounts.type:
return state.withMutations(map => action.payload.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
default:
return state;
}
}

View File

@ -0,0 +1,38 @@
import { createReducer } from '@reduxjs/toolkit';
import type { UnknownAction } from '@reduxjs/toolkit';
import type { AxiosError } from 'axios';
import { ACCOUNT_LOOKUP_FAIL } from 'mastodon/actions/accounts';
import { importAccounts } from 'mastodon/actions/importer/accounts';
import { domain } from 'mastodon/initial_state';
interface AccountLookupFailAction extends UnknownAction {
acct: string;
error?: AxiosError;
}
const pattern = new RegExp(`@${domain}$`, 'gi');
export const normalizeForLookup = (str: string) =>
str.toLowerCase().replace(pattern, '');
const initialState: Record<string, string | null> = {};
export const accountsMapReducer = createReducer(initialState, (builder) => {
builder
.addCase(importAccounts, (state, action) => {
action.payload.accounts.forEach((account) => {
state[normalizeForLookup(account.acct)] = account.id;
});
})
.addMatcher(
(action: UnknownAction): action is AccountLookupFailAction =>
action.type === ACCOUNT_LOOKUP_FAIL,
(state, action) => {
if (action.error?.response?.status === 404) {
state[normalizeForLookup(action.acct)] = null;
}
},
);
});

View File

@ -4,7 +4,7 @@ import { loadingBarReducer } from 'react-redux-loading-bar';
import { combineReducers } from 'redux-immutable'; import { combineReducers } from 'redux-immutable';
import { accountsReducer } from './accounts'; import { accountsReducer } from './accounts';
import accounts_map from './accounts_map'; import { accountsMapReducer } from './accounts_map';
import { alertsReducer } from './alerts'; import { alertsReducer } from './alerts';
import announcements from './announcements'; import announcements from './announcements';
import { composeReducer } from './compose'; import { composeReducer } from './compose';
@ -49,7 +49,7 @@ const reducers = {
user_lists, user_lists,
status_lists, status_lists,
accounts: accountsReducer, accounts: accountsReducer,
accounts_map, accounts_map: accountsMapReducer,
statuses, statuses,
relationships: relationshipsReducer, relationships: relationshipsReducer,
settings, settings,

View File

@ -5,9 +5,7 @@ import {
fetchDirectory fetchDirectory
} from 'mastodon/actions/directory'; } from 'mastodon/actions/directory';
import { import {
FEATURED_TAGS_FETCH_REQUEST, fetchFeaturedTags
FEATURED_TAGS_FETCH_SUCCESS,
FEATURED_TAGS_FETCH_FAIL,
} from 'mastodon/actions/featured_tags'; } from 'mastodon/actions/featured_tags';
import { import {
@ -31,6 +29,7 @@ import {
FOLLOW_REQUESTS_EXPAND_FAIL, FOLLOW_REQUESTS_EXPAND_FAIL,
authorizeFollowRequestSuccess, authorizeFollowRequestSuccess,
rejectFollowRequestSuccess, rejectFollowRequestSuccess,
fetchEndorsedAccounts,
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
BLOCKS_FETCH_REQUEST, BLOCKS_FETCH_REQUEST,
@ -191,21 +190,27 @@ export default function userLists(state = initialState, action) {
case MUTES_FETCH_FAIL: case MUTES_FETCH_FAIL:
case MUTES_EXPAND_FAIL: case MUTES_EXPAND_FAIL:
return state.setIn(['mutes', 'isLoading'], false); return state.setIn(['mutes', 'isLoading'], false);
case FEATURED_TAGS_FETCH_SUCCESS:
return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id);
case FEATURED_TAGS_FETCH_REQUEST:
return state.setIn(['featured_tags', action.id, 'isLoading'], true);
case FEATURED_TAGS_FETCH_FAIL:
return state.setIn(['featured_tags', action.id, 'isLoading'], false);
default: default:
if(fetchDirectory.fulfilled.match(action)) if (fetchEndorsedAccounts.fulfilled.match(action))
return normalizeList(state, ['featured_accounts', action.meta.arg.accountId], action.payload, undefined);
else if (fetchEndorsedAccounts.pending.match(action))
return state.setIn(['featured_accounts', action.meta.arg.accountId, 'isLoading'], true);
else if (fetchEndorsedAccounts.rejected.match(action))
return state.setIn(['featured_accounts', action.meta.arg.accountId, 'isLoading'], false);
else if (fetchFeaturedTags.fulfilled.match(action))
return normalizeFeaturedTags(state, ['featured_tags', action.meta.arg.accountId], action.payload, action.meta.arg.accountId);
else if (fetchFeaturedTags.pending.match(action))
return state.setIn(['featured_tags', action.meta.arg.accountId, 'isLoading'], true);
else if (fetchFeaturedTags.rejected.match(action))
return state.setIn(['featured_tags', action.meta.arg.accountId, 'isLoading'], false);
else if (fetchDirectory.fulfilled.match(action))
return normalizeList(state, ['directory'], action.payload.accounts, undefined); return normalizeList(state, ['directory'], action.payload.accounts, undefined);
else if( expandDirectory.fulfilled.match(action)) else if (expandDirectory.fulfilled.match(action))
return appendToList(state, ['directory'], action.payload.accounts, undefined); return appendToList(state, ['directory'], action.payload.accounts, undefined);
else if(fetchDirectory.pending.match(action) || else if (fetchDirectory.pending.match(action) ||
expandDirectory.pending.match(action)) expandDirectory.pending.match(action))
return state.setIn(['directory', 'isLoading'], true); return state.setIn(['directory', 'isLoading'], true);
else if(fetchDirectory.rejected.match(action) || else if (fetchDirectory.rejected.match(action) ||
expandDirectory.rejected.match(action)) expandDirectory.rejected.match(action))
return state.setIn(['directory', 'isLoading'], false); return state.setIn(['directory', 'isLoading'], false);
else else