diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index ec010cf7b..aacdcd106 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -57,6 +57,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'; @@ -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) { return (dispatch, getState) => { dispatch({ type: ACCOUNT_SEARCH_REQUEST, params }); diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 356d5a6d1..62dc3236a 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -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); } else if (features.lists && features.unrestrictedLists) { menu.push({ diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index 8438e67a6..8b244dd33 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -22,7 +22,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, }; @@ -83,9 +83,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); diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index a16ac36ac..5050db874 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -24,8 +24,8 @@ import { blockAccount, unblockAccount, unmuteAccount, - // pinAccount, - // unpinAccount, + pinAccount, + unpinAccount, subscribeAccount, unsubscribeAccount, } from '../../../actions/accounts'; @@ -130,13 +130,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)); diff --git a/app/soapbox/features/ui/components/pinned_accounts_panel.js b/app/soapbox/features/ui/components/pinned_accounts_panel.js new file mode 100644 index 000000000..da779b160 --- /dev/null +++ b/app/soapbox/features/ui/components/pinned_accounts_panel.js @@ -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 ( +
+
+ + + + +
+
+
+ {pinned && pinned.map(suggestion => ( + + ))} +
+
+
+ ); + } + +} + +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)); diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index d651dc656..08a223f90 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -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'); } diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 848bcbfe5..431520dcd 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -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ż", diff --git a/app/soapbox/pages/profile_page.js b/app/soapbox/pages/profile_page.js index 25f2ff439..5b62910f2 100644 --- a/app/soapbox/pages/profile_page.js +++ b/app/soapbox/pages/profile_page.js @@ -13,15 +13,15 @@ import { SignUpPanel, ProfileInfoPanel, ProfileMediaPanel, + PinnedAccountsPanel, } from 'soapbox/features/ui/util/async-components'; -import { findAccountByUsername } from 'soapbox/selectors'; -import { getAcct } from 'soapbox/utils/accounts'; +import { findAccountByUsername, makeGetAccount } from 'soapbox/selectors'; +import { getAcct, isLocal } from 'soapbox/utils/accounts'; import { getFeatures } from 'soapbox/utils/features'; import { displayFqn } from 'soapbox/utils/state'; import HeaderContainer from '../features/account_timeline/containers/header_container'; import LinkFooter from '../features/ui/components/link_footer'; -import { makeGetAccount } from '../selectors'; const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; @@ -118,7 +118,11 @@ class ProfilePage extends ImmutablePureComponent { {Component => } )} - {features.suggestions && ( + {account && features.accountEndorsements && isLocal(account) ? ( + + {Component => } + + ) : features.suggestions && ( {Component => } diff --git a/app/soapbox/reducers/__tests__/user_lists-test.js b/app/soapbox/reducers/__tests__/user_lists-test.js index 9ef1928ac..49af79bcc 100644 --- a/app/soapbox/reducers/__tests__/user_lists-test.js +++ b/app/soapbox/reducers/__tests__/user_lists-test.js @@ -15,6 +15,7 @@ describe('user_lists reducer', () => { mutes: ImmutableMap(), groups: ImmutableMap(), groups_removed_accounts: ImmutableMap(), + pinned: ImmutableMap(), })); }); }); diff --git a/app/soapbox/reducers/user_lists.js b/app/soapbox/reducers/user_lists.js index 6936bd4f4..2b9feaaa2 100644 --- a/app/soapbox/reducers/user_lists.js +++ b/app/soapbox/reducers/user_lists.js @@ -9,6 +9,7 @@ import { FOLLOW_REQUESTS_EXPAND_SUCCESS, FOLLOW_REQUEST_AUTHORIZE_SUCCESS, FOLLOW_REQUEST_REJECT_SUCCESS, + PINNED_ACCOUNTS_FETCH_SUCCESS, } from '../actions/accounts'; import { BLOCKS_FETCH_SUCCESS, @@ -53,6 +54,7 @@ const initialState = ImmutableMap({ mutes: ImmutableMap(), groups: ImmutableMap(), groups_removed_accounts: ImmutableMap(), + pinned: ImmutableMap(), }); 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); 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; } diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 7d73a5535..cd9df9c20 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -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'), }; }); diff --git a/app/styles/components/wtf-panel.scss b/app/styles/components/wtf-panel.scss index 120abe338..1e5e13230 100644 --- a/app/styles/components/wtf-panel.scss +++ b/app/styles/components/wtf-panel.scss @@ -30,6 +30,7 @@ &.svg-icon { width: 20px; + min-width: 20px; height: 20px; } }