User endorsements

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-01-10 21:19:38 +01:00
parent e1475e0ba5
commit 0a160f4422
12 changed files with 154 additions and 16 deletions

View File

@ -55,6 +55,10 @@ export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
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_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS';
export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL';
@ -960,6 +964,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) {
return (dispatch, getState) => {
dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });

View File

@ -280,7 +280,10 @@ 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 });
}
menu.push(null);
} else if (features.lists && features.unrestrictedLists) {
menu.push({

View File

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

View File

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

View File

@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { List as ImmutableList } from 'immutable';
import Icon from 'soapbox/components/icon';
import AccountContainer from '../../../containers/account_container';
import { fetchPinnedAccounts } from '../../../actions/accounts';
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');
}
export function PinnedAccountsPanel() {
return import(/* webpackChunkName: "features/pinned_accounts]" */'../components/pinned_accounts_panel');
}
export function InstanceInfoPanel() {
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.fields.username_placeholder": "Adres e-mail lub nazwa użytkownika",
"password_reset.reset": "Resetuj hasło",
"pinned_accounts.title": "Polecani przez {name}",
"pinned_statuses.none": "Brak przypięć do pokazania.",
"poll.closed": "Zamknięte",
"poll.refresh": "Odśwież",

View File

@ -12,9 +12,10 @@ import {
SignUpPanel,
ProfileInfoPanel,
ProfileMediaPanel,
PinnedAccountsPanel,
} from 'soapbox/features/ui/util/async-components';
import LinkFooter from '../features/ui/components/link_footer';
import { getAcct } from 'soapbox/utils/accounts';
import { getAcct, isLocal } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import { getFeatures } from 'soapbox/utils/features';
import { makeGetAccount } from '../selectors';
@ -116,7 +117,11 @@ class ProfilePage extends ImmutablePureComponent {
{Component => <Component account={account} />}
</BundleContainer>
)}
{features.suggestions && (
{account && features.accountEndorsements && isLocal(account) ? (
<BundleContainer fetchComponent={PinnedAccountsPanel}>
{Component => <Component account={account} />}
</BundleContainer>
) : features.suggestions && (
<BundleContainer fetchComponent={WhoToFollowPanel}>
{Component => <Component />}
</BundleContainer>

View File

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

View File

@ -10,6 +10,7 @@ import {
FOLLOW_REQUESTS_EXPAND_SUCCESS,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
PINNED_ACCOUNTS_FETCH_SUCCESS,
} from '../actions/accounts';
import {
REBLOGS_FETCH_SUCCESS,
@ -52,6 +53,7 @@ const initialState = ImmutableMap({
mutes: ImmutableMap(),
groups: ImmutableMap(),
groups_removed_accounts: ImmutableMap(),
pinned: ImmutableMap(),
});
const normalizeList = (state, type, id, accounts, next) => {
@ -126,6 +128,8 @@ export default function userLists(state = initialState, action) {
return appendToList(state, 'groups_removed_accounts', action.id, action.accounts, action.next);
case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS:
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:
return state;
}

View File

@ -76,6 +76,7 @@ export const getFeatures = createSelector([
]),
remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
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 {
width: 20px;
min-width: 20px;
height: 20px;
}
}