Merge remote-tracking branch 'origin/develop' into mastodon-pagination

sidenav-improvements
Alex Gleason 2 years ago
commit debfcaeb1e
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -6,6 +6,9 @@ export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
export function fetchCustomEmojis() {
return (dispatch, getState) => {
const me = getState().get('me');
if (!me) return;
dispatch(fetchCustomEmojisRequest());
api(getState).get('/api/v1/custom_emojis').then(response => {

@ -1,30 +0,0 @@
import api from '../api';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
dispatch(fetchAccountIdentityProofsRequest(accountId));
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
};
export const fetchAccountIdentityProofsRequest = id => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
id,
});
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
accountId,
identity_proofs,
});
export const fetchAccountIdentityProofsFail = (accountId, error) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
accountId,
error,
});

@ -8,13 +8,13 @@ import { importFetchedAccounts } from './importer';
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
export const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST';
export const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS';
export const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
export const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
export function fetchSuggestionsV1(params = {}) {
return (dispatch, getState) => {
@ -48,23 +48,26 @@ export function fetchSuggestionsV2(params = {}) {
export function fetchSuggestions(params = { limit: 50 }) {
return (dispatch, getState) => {
const state = getState();
const me = state.get('me');
const instance = state.get('instance');
const features = getFeatures(instance);
if (!me) return;
if (features.suggestionsV2) {
dispatch(fetchSuggestionsV2(params))
.then(suggestions => {
const accountIds = suggestions.map(({ account }) => account.id);
dispatch(fetchRelationships(accountIds));
})
.catch(() => {});
.catch(() => { });
} else if (features.suggestions) {
dispatch(fetchSuggestionsV1(params))
.then(accounts => {
const accountIds = accounts.map(({ id }) => id);
dispatch(fetchRelationships(accountIds));
})
.catch(() => {});
.catch(() => { });
} else {
// Do nothing
}

@ -1,5 +1,5 @@
import React from 'react';
import { Virtuoso, Components } from 'react-virtuoso';
import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle } from 'react-virtuoso';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { useSettings } from 'soapbox/hooks';
@ -25,7 +25,7 @@ const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
return <div ref={ref} className={context?.listClassName} {...rest} />;
});
interface IScrollableList {
interface IScrollableList extends VirtuosoProps<any, any> {
scrollKey?: string,
onLoadMore?: () => void,
isLoading?: boolean,
@ -45,7 +45,7 @@ interface IScrollableList {
}
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
const ScrollableList: React.FC<IScrollableList> = ({
const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
prepend = null,
alwaysPrepend,
children,
@ -61,7 +61,9 @@ const ScrollableList: React.FC<IScrollableList> = ({
hasMore,
placeholderComponent: Placeholder,
placeholderCount = 0,
}) => {
initialTopMostItemIndex = 0,
scrollerRef,
}, ref) => {
const settings = useSettings();
const autoloadMore = settings.get('autoloadMore');
@ -126,6 +128,7 @@ const ScrollableList: React.FC<IScrollableList> = ({
/** Render the actual Virtuoso list */
const renderFeed = (): JSX.Element => (
<Virtuoso
ref={ref}
useWindowScroll
className={className}
data={data}
@ -133,6 +136,7 @@ const ScrollableList: React.FC<IScrollableList> = ({
endReached={handleEndReached}
isScrolling={isScrolling => isScrolling && onScroll && onScroll()}
itemContent={renderItem}
initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex}
context={{
listClassName: className,
itemClassName,
@ -145,6 +149,7 @@ const ScrollableList: React.FC<IScrollableList> = ({
Item,
Footer: loadMore,
}}
scrollerRef={scrollerRef}
/>
);
@ -162,6 +167,6 @@ const ScrollableList: React.FC<IScrollableList> = ({
{renderBody()}
</PullToRefresh>
);
};
});
export default ScrollableList;

@ -15,6 +15,10 @@ interface IModal {
cancelAction?: () => void,
/** Cancel button text. */
cancelText?: string,
/** URL to an SVG icon for the close button. */
closeIcon?: string,
/** Position of the close button. */
closePosition?: 'left' | 'right',
/** Callback when the modal is confirmed. */
confirmationAction?: () => void,
/** Whether the confirmation button is disabled. */
@ -40,6 +44,8 @@ const Modal: React.FC<IModal> = ({
cancelAction,
cancelText,
children,
closeIcon = require('@tabler/icons/icons/x.svg'),
closePosition = 'right',
confirmationAction,
confirmationDisabled,
confirmationText,
@ -63,14 +69,18 @@ const Modal: React.FC<IModal> = ({
<div data-testid='modal' className='block w-full max-w-xl p-6 mx-auto overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto'>
<div className='sm:flex sm:items-start w-full justify-between'>
<div className='w-full'>
<div className='w-full flex flex-row justify-between items-center'>
<h3 className='text-lg leading-6 font-medium text-gray-900 dark:text-white'>
<div
className={classNames('w-full flex items-center gap-2', {
'flex-row-reverse': closePosition === 'left',
})}
>
<h3 className='flex-grow text-lg leading-6 font-medium text-gray-900 dark:text-white'>
{title}
</h3>
{onClose && (
<IconButton
src={require('@tabler/icons/icons/x.svg')}
src={closeIcon}
title={intl.formatMessage(messages.close)}
onClick={onClose}
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200'

@ -86,7 +86,6 @@ class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
meaccount: ImmutablePropTypes.record,
identity_props: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
username: PropTypes.string,
features: PropTypes.object,

@ -13,7 +13,6 @@ class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
identity_proofs: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@ -143,7 +142,7 @@ class Header extends ImmutablePureComponent {
}
render() {
const { account, identity_proofs } = this.props;
const { account } = this.props;
const moved = (account) ? account.get('moved') : false;
return (
@ -152,7 +151,6 @@ class Header extends ImmutablePureComponent {
<InnerHeader
account={account}
identity_proofs={identity_proofs}
onFollow={this.handleFollow}
onBlock={this.handleBlock}
onMention={this.handleMention}

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
@ -64,7 +63,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
});
return mapStateToProps;

@ -13,7 +13,6 @@ import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
import { fetchPatronAccount } from '../../actions/patron';
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
import StatusList from '../../components/status_list';
@ -84,11 +83,10 @@ class AccountTimeline extends ImmutablePureComponent {
};
componentDidMount() {
const { params: { username }, accountId, accountApId, withReplies, me, patronEnabled } = this.props;
const { params: { username }, accountId, accountApId, withReplies, patronEnabled } = this.props;
if (accountId && accountId !== -1) {
this.props.dispatch(fetchAccount(accountId));
if (me) this.props.dispatch(fetchAccountIdentityProofs(accountId));
if (!withReplies) {
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
@ -105,11 +103,10 @@ class AccountTimeline extends ImmutablePureComponent {
}
componentDidUpdate(prevProps) {
const { params: { username }, me, accountId, withReplies, accountApId, patronEnabled } = this.props;
const { params: { username }, accountId, withReplies, accountApId, patronEnabled } = this.props;
if (accountId && (accountId !== -1) && (accountId !== prevProps.accountId) || withReplies !== prevProps.withReplies) {
this.props.dispatch(fetchAccount(accountId));
if (me) this.props.dispatch(fetchAccountIdentityProofs(accountId));
if (!withReplies) {
this.props.dispatch(expandAccountFeaturedTimeline(accountId));

@ -55,7 +55,7 @@ const AuthToken: React.FC<IAuthToken> = ({ token }) => {
const AuthTokenList: React.FC = () =>{
const dispatch = useAppDispatch();
const intl = useIntl();
const tokens = useAppSelector(state => state.security.get('tokens'));
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
useEffect(() => {
dispatch(fetchOAuthTokens());

@ -6,7 +6,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import Pullable from 'soapbox/components/pullable';
import ScrollableList from 'soapbox/components/scrollable_list';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
@ -151,24 +150,22 @@ class SearchResults extends ImmutablePureComponent {
{this.renderFilterBar()}
{noResultsMessage || (
<Pullable>
<ScrollableList
key={selectedFilter}
scrollKey={`${selectedFilter}:${value}`}
isLoading={submitted && !loaded}
showLoading={submitted && !loaded && results.isEmpty()}
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
placeholderComponent={placeholderComponent}
placeholderCount={20}
className={classNames({
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
'space-y-4': selectedFilter === 'accounts',
})}
>
{searchResults}
</ScrollableList>
</Pullable>
<ScrollableList
key={selectedFilter}
scrollKey={`${selectedFilter}:${value}`}
isLoading={submitted && !loaded}
showLoading={submitted && !loaded && results.isEmpty()}
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
placeholderComponent={placeholderComponent}
placeholderCount={20}
className={classNames({
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
})}
itemClassName={classNames({ 'pb-4': selectedFilter === 'accounts' })}
>
{searchResults}
</ScrollableList>
)}
</>
);

@ -76,24 +76,27 @@ const Header = () => {
<nav className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' aria-label='Header'>
<div className='w-full py-6 flex items-center justify-between border-b border-indigo-500 lg:border-none'>
<div className='flex items-center sm:justify-center relative w-36'>
<div className='hidden sm:block absolute z-0 -top-24 -left-6'>
<div className='hidden md:block absolute z-0 -top-24 -left-6'>
<Sonar />
</div>
<Link to='/' className='z-10'>
<SiteLogo alt='Logo' className='h-6 w-auto cursor-pointer' />
<span className='hidden'>{intl.formatMessage(messages.home)}</span>
</Link>
</div>
<div className='ml-10 flex space-x-6 items-center relative z-10'>
<IconButton
title='Open Menu'
src={require('@tabler/icons/icons/menu-2.svg')}
onClick={open}
className='md:hidden bg-transparent text-gray-400 hover:text-gray-600'
className='md:hidden mr-4 bg-transparent text-gray-400 hover:text-gray-600'
/>
<div className='hidden md:flex items-center space-x-6'>
<HStack space={6} alignItems='center'>
<Link to='/' className='z-10'>
<SiteLogo alt='Logo' className='h-6 w-auto cursor-pointer' />
<span className='hidden'>{intl.formatMessage(messages.home)}</span>
</Link>
</div>
<div className='ml-10 flex space-x-6 items-center relative z-10'>
<HStack alignItems='center'>
<HStack space={6} alignItems='center' className='hidden md:flex md:mr-6'>
{links.get('help') && (
<a
href={links.get('help')}
@ -119,7 +122,7 @@ const Header = () => {
</Button>
)}
</HStack>
</div>
</HStack>
<Form className='hidden xl:flex space-x-2 items-center' onSubmit={handleSubmit}>
<Input

@ -65,6 +65,7 @@ import ThreadStatus from './components/thread-status';
import type { AxiosError } from 'axios';
import type { History } from 'history';
import type { VirtuosoHandle } from 'react-virtuoso';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type { RootState } from 'soapbox/store';
@ -212,6 +213,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
node: HTMLDivElement | null = null;
status: HTMLDivElement | null = null;
scroller: VirtuosoHandle | null = null;
_scrolledIntoView: boolean = false;
fetchData = async() => {
@ -617,11 +619,10 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
}
componentDidUpdate(prevProps: IStatus, prevState: IStatusState) {
const { params, status, displayMedia } = this.props;
const { ancestorsIds } = prevProps;
const { params, status, displayMedia, ancestorsIds } = this.props;
const { isLoaded } = this.state;
if (params.statusId !== prevProps.params.statusId) {
this._scrolledIntoView = false;
this.fetchData();
}
@ -629,17 +630,11 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
this.setState({ showMedia: defaultMediaVisibility(status, displayMedia), loadedStatusId: status.id });
}
if (this._scrolledIntoView) {
return;
}
if (prevProps.status && ancestorsIds && ancestorsIds.size > 0 && this.node) {
const element = this.node.querySelector('.detailed-status');
window.requestAnimationFrame(() => {
element?.scrollIntoView(true);
if (params.statusId !== prevProps.params.statusId || status?.id !== prevProps.status?.id || ancestorsIds.size > prevProps.ancestorsIds.size || isLoaded !== prevState.isLoaded) {
this.scroller?.scrollToIndex({
index: this.props.ancestorsIds.size,
offset: -80,
});
this._scrolledIntoView = true;
}
}
@ -674,6 +669,10 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
}));
}
setScrollerRef = (c: VirtuosoHandle) => {
this.scroller = c;
}
render() {
const { me, status, ancestorsIds, descendantsIds, intl } = this.props;
@ -791,10 +790,12 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
<Stack space={2}>
<div ref={this.setRef} className='thread'>
<ScrollableList
ref={this.setScrollerRef}
onRefresh={this.handleRefresh}
hasMore={!!this.state.next}
onLoadMore={this.handleLoadMore}
placeholderComponent={() => <PlaceholderStatus thread />}
initialTopMostItemIndex={ancestorsIds.size}
>
{children}
</ScrollableList>

@ -30,13 +30,21 @@ const messages = defineMessages({
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
});
interface iActionButton {
interface IActionButton {
/** Target account for the action. */
account: AccountEntity
/** Type of action to prioritize, eg on Blocks and Mutes pages. */
actionType?: 'muting' | 'blocking'
/** Displays shorter text on the "Awaiting approval" button. */
small?: boolean
}
const ActionButton = ({ account, actionType, small }: iActionButton) => {
/**
* Circumstantial action button (usually "Follow") to display on accounts.
* May say "Unblock" or something else, depending on the relationship and
* `actionType` prop.
*/
const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) => {
const dispatch = useDispatch();
const features = useFeatures();
const intl = useIntl();
@ -45,40 +53,41 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
const handleFollow = () => {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(unfollowAccount(account.get('id')));
dispatch(unfollowAccount(account.id));
} else {
dispatch(followAccount(account.get('id')));
dispatch(followAccount(account.id));
}
};
const handleBlock = () => {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
dispatch(unblockAccount(account.id));
} else {
dispatch(blockAccount(account.get('id')));
dispatch(blockAccount(account.id));
}
};
const handleMute = () => {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
dispatch(unmuteAccount(account.id));
} else {
dispatch(muteAccount(account.get('id')));
dispatch(muteAccount(account.id));
}
};
const handleRemoteFollow = () => {
dispatch(openModal('UNAUTHORIZED', {
action: 'FOLLOW',
account: account.get('id'),
ap_id: account.get('url'),
account: account.id,
ap_id: account.url,
}));
};
/** Handles actionType='muting' */
const mutingAction = () => {
const isMuted = account.getIn(['relationship', 'muting']);
const messageKey = isMuted ? messages.unmute : messages.mute;
const text = intl.formatMessage(messageKey, { name: account.get('username') });
const text = intl.formatMessage(messageKey, { name: account.username });
return (
<Button
@ -90,10 +99,11 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
);
};
/** Handles actionType='blocking' */
const blockingAction = () => {
const isBlocked = account.getIn(['relationship', 'blocking']);
const messageKey = isBlocked ? messages.unblock : messages.block;
const text = intl.formatMessage(messageKey, { name: account.get('username') });
const text = intl.formatMessage(messageKey, { name: account.username });
return (
<Button
@ -105,10 +115,9 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
);
};
const empty = <></>;
if (!me) {
// Remote follow
/** Render a remote follow button, depending on features. */
const renderRemoteFollow = () => {
// Remote follow through the API.
if (features.remoteInteractionsAPI) {
return (
<Button
@ -117,18 +126,34 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
text={intl.formatMessage(messages.follow)}
/>
);
// Pleroma's classic remote follow form.
} else if (features.pleromaRemoteFollow) {
return (
<form method='POST' action='/main/ostatus'>
<input type='hidden' name='nickname' value={account.acct} />
<input type='hidden' name='profile' value='' />
<Button text={intl.formatMessage(messages.remote_follow)} type='submit' />
</form>
);
}
return (
<form method='POST' action='/main/ostatus'>
<input type='hidden' name='nickname' value={account.get('acct')} />
<input type='hidden' name='profile' value='' />
<Button text={intl.formatMessage(messages.remote_follow)} type='submit' />
</form>
);
return null;
};
/** Render remote follow if federating, otherwise hide the button. */
const renderLoggedOut = () => {
if (features.federating) {
return renderRemoteFollow();
}
return null;
};
if (!me) {
return renderLoggedOut();
}
if (me !== account.get('id')) {
if (me !== account.id) {
const isFollowing = account.getIn(['relationship', 'following']);
const blockedBy = account.getIn(['relationship', 'blocked_by']) as boolean;
@ -140,9 +165,9 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
}
}
if (!account.get('relationship')) {
if (account.relationship.isEmpty()) {
// Wait until the relationship is loaded
return empty;
return null;
} else if (account.getIn(['relationship', 'requested'])) {
// Awaiting acceptance
return (
@ -176,7 +201,7 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
<Button
theme='danger'
size='sm'
text={intl.formatMessage(messages.unblock, { name: account.get('username') })}
text={intl.formatMessage(messages.unblock, { name: account.username })}
onClick={handleBlock}
/>
);
@ -193,7 +218,7 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => {
);
}
return empty;
return null;
};
export default ActionButton;

@ -29,6 +29,8 @@ const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
<Modal
title={<FormattedMessage id='navigation_bar.in_reply_to' defaultMessage='In reply to' />}
onClose={onClickClose}
closeIcon={require('@tabler/icons/icons/arrow-left.svg')}
closePosition='left'
>
<div className='reply-mentions-modal__accounts'>
{mentions.map(accountId => <Account key={accountId} accountId={accountId} added author={author === accountId} />)}

@ -1,32 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import * as actions from 'soapbox/actions/identity_proofs';
import reducer from '../identity_proofs';
describe('identity_proofs reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap());
});
it('should handle IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST', () => {
const state = ImmutableMap({ isLoading: false });
const action = {
type: actions.IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
};
expect(reducer(state, action).toJS()).toMatchObject({
isLoading: true,
});
});
it('should handle IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL', () => {
const state = ImmutableMap({ isLoading: true });
const action = {
type: actions.IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
};
expect(reducer(state, action).toJS()).toMatchObject({
isLoading: false,
});
});
});

@ -1,26 +0,0 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import {
IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
} from '../actions/identity_proofs';
const initialState = ImmutableMap();
export default function identityProofsReducer(state = initialState, action) {
switch (action.type) {
case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST:
return state.set('isLoading', true);
case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL:
return state.set('isLoading', false);
case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS:
return state.update(identity_proofs => identity_proofs.withMutations(map => {
map.set('isLoading', false);
map.set('loaded', true);
map.set(action.accountId, fromJS(action.identity_proofs));
}));
default:
return state;
}
}

@ -29,7 +29,6 @@ import group_lists from './group_lists';
import group_relationships from './group_relationships';
import groups from './groups';
import history from './history';
import identity_proofs from './identity_proofs';
import instance from './instance';
import listAdder from './list_adder';
import listEditor from './list_editor';
@ -86,7 +85,6 @@ const reducers = {
search,
notifications,
custom_emojis,
identity_proofs,
lists,
listEditor,
listAdder,

@ -356,6 +356,12 @@ const getInstanceFeatures = (instance: Instance) => {
*/
paginatedContext: v.software === TRUTHSOCIAL,
/**
* Displays a form to follow a user when logged out.
* @see POST /main/ostatus
*/
pleromaRemoteFollow: v.software === PLEROMA,
/**
* Can add polls to statuses.
* @see POST /api/v1/statuses

@ -691,8 +691,6 @@ If it's not documented, it's because I inherited it from Mastodon and I don't kn
]
```
- `identity_proofs`
- `lists`
Sample:

Loading…
Cancel
Save