Merge remote-tracking branch 'origin/develop' into mastodon-pagination
This commit is contained in:
commit
debfcaeb1e
20 changed files with 142 additions and 187 deletions
|
@ -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…
Reference in a new issue