Merge branch 'account-endorsements' into 'develop'

User endorsements

See merge request soapbox-pub/soapbox-fe!982
This commit is contained in:
Alex Gleason 2022-01-14 17:49:57 +00:00
commit fe3e74d71d
12 changed files with 161 additions and 18 deletions

View File

@ -57,6 +57,10 @@ export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL';
export const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST'; export const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST';
export const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS'; export const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS';
export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL'; export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL';
@ -962,6 +966,43 @@ export function unpinAccountFail(error) {
}; };
} }
export function fetchPinnedAccounts(id) {
return (dispatch, getState) => {
dispatch(fetchPinnedAccountsRequest(id));
api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchPinnedAccountsSuccess(id, response.data, null));
}).catch(error => {
dispatch(fetchPinnedAccountsFail(id, error));
});
};
}
export function fetchPinnedAccountsRequest(id) {
return {
type: PINNED_ACCOUNTS_FETCH_REQUEST,
id,
};
}
export function fetchPinnedAccountsSuccess(id, accounts, next) {
return {
type: PINNED_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchPinnedAccountsFail(id, error) {
return {
type: PINNED_ACCOUNTS_FETCH_FAIL,
id,
error,
};
}
export function accountSearch(params, cancelToken) { export function accountSearch(params, cancelToken) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ type: ACCOUNT_SEARCH_REQUEST, params }); dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });

View File

@ -281,7 +281,14 @@ class Header extends ImmutablePureComponent {
}); });
} }
// menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); if (features.accountEndorsements) {
menu.push({
text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse),
action: this.props.onEndorseToggle,
icon: require('@tabler/icons/icons/user-check.svg'),
});
}
menu.push(null); menu.push(null);
} else if (features.lists && features.unrestrictedLists) { } else if (features.lists && features.unrestrictedLists) {
menu.push({ menu.push({

View File

@ -22,7 +22,7 @@ export default class Header extends ImmutablePureComponent {
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired,
// onEndorseToggle: PropTypes.func.isRequired, onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired,
username: PropTypes.string, username: PropTypes.string,
}; };
@ -83,9 +83,9 @@ export default class Header extends ImmutablePureComponent {
this.props.onChat(this.props.account, this.context.router.history); this.props.onChat(this.props.account, this.context.router.history);
} }
// handleEndorseToggle = () => { handleEndorseToggle = () => {
// this.props.onEndorseToggle(this.props.account); this.props.onEndorseToggle(this.props.account);
// } }
handleAddToList = () => { handleAddToList = () => {
this.props.onAddToList(this.props.account); this.props.onAddToList(this.props.account);

View File

@ -24,8 +24,8 @@ import {
blockAccount, blockAccount,
unblockAccount, unblockAccount,
unmuteAccount, unmuteAccount,
// pinAccount, pinAccount,
// unpinAccount, unpinAccount,
subscribeAccount, subscribeAccount,
unsubscribeAccount, unsubscribeAccount,
} from '../../../actions/accounts'; } from '../../../actions/accounts';
@ -130,13 +130,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
// onEndorseToggle(account) { onEndorseToggle(account) {
// if (account.getIn(['relationship', 'endorsed'])) { if (account.getIn(['relationship', 'endorsed'])) {
// dispatch(unpinAccount(account.get('id'))); dispatch(unpinAccount(account.get('id')));
// } else { } else {
// dispatch(pinAccount(account.get('id'))); dispatch(pinAccount(account.get('id')));
// } }
// }, },
onReport(account) { onReport(account) {
dispatch(initReport(account)); dispatch(initReport(account));

View File

@ -0,0 +1,79 @@
import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import Icon from 'soapbox/components/icon';
import { fetchPinnedAccounts } from '../../../actions/accounts';
import AccountContainer from '../../../containers/account_container';
class PinnedAccountsPanel extends ImmutablePureComponent {
static propTypes = {
pinned: ImmutablePropTypes.list.isRequired,
fetchPinned: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
componentDidMount() {
this.props.fetchPinned();
}
render() {
const { account } = this.props;
const pinned = this.props.pinned.slice(0, this.props.limit);
if (pinned.isEmpty()) {
return null;
}
return (
<div className='wtf-panel'>
<div className='wtf-panel-header'>
<Icon src={require('@tabler/icons/icons/users.svg')} className='wtf-panel-header__icon' />
<span className='wtf-panel-header__label'>
<FormattedMessage
id='pinned_accounts.title'
defaultMessage='{name}s choices'
values={{
name: account.get('display_name_html'),
}}
/>
</span>
</div>
<div className='wtf-panel__content'>
<div className='wtf-panel__list'>
{pinned && pinned.map(suggestion => (
<AccountContainer
key={suggestion}
id={suggestion}
withRelationship={false}
/>
))}
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state, { account }) => ({
pinned: state.getIn(['user_lists', 'pinned', account.get('id'), 'items'], ImmutableList()),
});
const mapDispatchToProps = (dispatch, { account }) => {
return {
fetchPinned: () => dispatch(fetchPinnedAccounts(account.get('id'))),
};
};
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
},
)(PinnedAccountsPanel));

View File

@ -338,6 +338,10 @@ export function ProfileMediaPanel() {
return import(/* webpackChunkName: "features/account_gallery" */'../components/profile_media_panel'); return import(/* webpackChunkName: "features/account_gallery" */'../components/profile_media_panel');
} }
export function PinnedAccountsPanel() {
return import(/* webpackChunkName: "features/pinned_accounts]" */'../components/pinned_accounts_panel');
}
export function InstanceInfoPanel() { export function InstanceInfoPanel() {
return import(/* webpackChunkName: "features/remote_timeline" */'../components/instance_info_panel'); return import(/* webpackChunkName: "features/remote_timeline" */'../components/instance_info_panel');
} }

View File

@ -680,6 +680,7 @@
"password_reset.confirmation": "Sprawdź swoją pocztę e-mail, aby potwierdzić.", "password_reset.confirmation": "Sprawdź swoją pocztę e-mail, aby potwierdzić.",
"password_reset.fields.username_placeholder": "Adres e-mail lub nazwa użytkownika", "password_reset.fields.username_placeholder": "Adres e-mail lub nazwa użytkownika",
"password_reset.reset": "Resetuj hasło", "password_reset.reset": "Resetuj hasło",
"pinned_accounts.title": "Polecani przez {name}",
"pinned_statuses.none": "Brak przypięć do pokazania.", "pinned_statuses.none": "Brak przypięć do pokazania.",
"poll.closed": "Zamknięte", "poll.closed": "Zamknięte",
"poll.refresh": "Odśwież", "poll.refresh": "Odśwież",

View File

@ -13,15 +13,15 @@ import {
SignUpPanel, SignUpPanel,
ProfileInfoPanel, ProfileInfoPanel,
ProfileMediaPanel, ProfileMediaPanel,
PinnedAccountsPanel,
} from 'soapbox/features/ui/util/async-components'; } from 'soapbox/features/ui/util/async-components';
import { findAccountByUsername } from 'soapbox/selectors'; import { findAccountByUsername, makeGetAccount } from 'soapbox/selectors';
import { getAcct } from 'soapbox/utils/accounts'; import { getAcct, isLocal } from 'soapbox/utils/accounts';
import { getFeatures } from 'soapbox/utils/features'; import { getFeatures } from 'soapbox/utils/features';
import { displayFqn } from 'soapbox/utils/state'; import { displayFqn } from 'soapbox/utils/state';
import HeaderContainer from '../features/account_timeline/containers/header_container'; import HeaderContainer from '../features/account_timeline/containers/header_container';
import LinkFooter from '../features/ui/components/link_footer'; import LinkFooter from '../features/ui/components/link_footer';
import { makeGetAccount } from '../selectors';
const mapStateToProps = (state, { params, withReplies = false }) => { const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || ''; const username = params.username || '';
@ -118,7 +118,11 @@ class ProfilePage extends ImmutablePureComponent {
{Component => <Component account={account} />} {Component => <Component account={account} />}
</BundleContainer> </BundleContainer>
)} )}
{features.suggestions && ( {account && features.accountEndorsements && isLocal(account) ? (
<BundleContainer fetchComponent={PinnedAccountsPanel}>
{Component => <Component account={account} />}
</BundleContainer>
) : features.suggestions && (
<BundleContainer fetchComponent={WhoToFollowPanel}> <BundleContainer fetchComponent={WhoToFollowPanel}>
{Component => <Component />} {Component => <Component />}
</BundleContainer> </BundleContainer>

View File

@ -15,6 +15,7 @@ describe('user_lists reducer', () => {
mutes: ImmutableMap(), mutes: ImmutableMap(),
groups: ImmutableMap(), groups: ImmutableMap(),
groups_removed_accounts: ImmutableMap(), groups_removed_accounts: ImmutableMap(),
pinned: ImmutableMap(),
})); }));
}); });
}); });

View File

@ -9,6 +9,7 @@ import {
FOLLOW_REQUESTS_EXPAND_SUCCESS, FOLLOW_REQUESTS_EXPAND_SUCCESS,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS, FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS, FOLLOW_REQUEST_REJECT_SUCCESS,
PINNED_ACCOUNTS_FETCH_SUCCESS,
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
BLOCKS_FETCH_SUCCESS, BLOCKS_FETCH_SUCCESS,
@ -53,6 +54,7 @@ const initialState = ImmutableMap({
mutes: ImmutableMap(), mutes: ImmutableMap(),
groups: ImmutableMap(), groups: ImmutableMap(),
groups_removed_accounts: ImmutableMap(), groups_removed_accounts: ImmutableMap(),
pinned: ImmutableMap(),
}); });
const normalizeList = (state, type, id, accounts, next) => { const normalizeList = (state, type, id, accounts, next) => {
@ -127,6 +129,8 @@ export default function userLists(state = initialState, action) {
return appendToList(state, 'groups_removed_accounts', action.id, action.accounts, action.next); return appendToList(state, 'groups_removed_accounts', action.id, action.accounts, action.next);
case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS: case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS:
return state.updateIn(['groups_removed_accounts', action.groupId, 'items'], list => list.filterNot(item => item === action.id)); return state.updateIn(['groups_removed_accounts', action.groupId, 'items'], list => list.filterNot(item => item === action.id));
case PINNED_ACCOUNTS_FETCH_SUCCESS:
return normalizeList(state, 'pinned', action.id, action.accounts, action.next);
default: default:
return state; return state;
} }

View File

@ -76,6 +76,7 @@ export const getFeatures = createSelector([
]), ]),
remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'), remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
explicitAddressing: v.software === PLEROMA && gte(v.version, '1.0.0'), explicitAddressing: v.software === PLEROMA && gte(v.version, '1.0.0'),
accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'),
}; };
}); });

View File

@ -30,6 +30,7 @@
&.svg-icon { &.svg-icon {
width: 20px; width: 20px;
min-width: 20px;
height: 20px; height: 20px;
} }
} }