diff --git a/app/soapbox/actions/__tests__/accounts.test.ts b/app/soapbox/actions/__tests__/accounts.test.ts index b02469527..2ea60bd80 100644 --- a/app/soapbox/actions/__tests__/accounts.test.ts +++ b/app/soapbox/actions/__tests__/accounts.test.ts @@ -435,10 +435,14 @@ describe('followAccount()', () => { skipLoading: true, }, ]; - await store.dispatch(followAccount(id)); - const actions = store.getActions(); - expect(actions).toEqual(expectedActions); + try { + await store.dispatch(followAccount(id)); + } catch (e) { + const actions = store.getActions(); + expect(actions).toEqual(expectedActions); + expect(e).toEqual(new Error('Network Error')); + } }); }); }); diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 5cc0008a4..63314a6b1 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -240,7 +240,10 @@ export function followAccount(id, options = { reblogs: true }) { return api(getState) .post(`/api/v1/accounts/${id}/follow`, options) .then(response => dispatch(followAccountSuccess(response.data, alreadyFollowing))) - .catch(error => dispatch(followAccountFail(error, locked))); + .catch(error => { + dispatch(followAccountFail(error, locked)); + throw error; + }); }; } diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index b76a156a5..a29e8270f 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -17,6 +17,7 @@ import StillImage from 'soapbox/components/still_image'; import { HStack, IconButton, Menu, MenuButton, MenuItem, MenuList, MenuLink, MenuDivider } from 'soapbox/components/ui'; import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; import ActionButton from 'soapbox/features/ui/components/action-button'; +import SubscriptionButton from 'soapbox/features/ui/components/subscription-button'; import { isLocal, isRemote, @@ -61,8 +62,6 @@ const messages = defineMessages({ promoteToModerator: { id: 'admin.users.actions.promote_to_moderator', defaultMessage: 'Promote @{name} to a moderator' }, demoteToModerator: { id: 'admin.users.actions.demote_to_moderator', defaultMessage: 'Demote @{name} to a moderator' }, demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' }, - subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' }, - unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' }, suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' }, unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' }, }); @@ -250,22 +249,6 @@ class Header extends ImmutablePureComponent { }); } - if (features.accountSubscriptions) { - if (account.relationship?.subscribing) { - menu.push({ - text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }), - action: this.props.onSubscriptionToggle, - icon: require('@tabler/icons/icons/bell.svg'), - }); - } else { - menu.push({ - text: intl.formatMessage(messages.subscribe, { name: account.get('username') }), - action: this.props.onSubscriptionToggle, - icon: require('@tabler/icons/icons/bell-off.svg'), - }); - } - } - if (features.lists) { menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), @@ -476,7 +459,7 @@ class Header extends ImmutablePureComponent { } + title={} />, ); } @@ -578,11 +561,6 @@ class Header extends ImmutablePureComponent { const menu = this.makeMenu(); const header = account.get('header', ''); - // NOTE: Removing Subscription element - // {features.accountSubscriptions &&
- // - //
} - return (
@@ -618,6 +596,8 @@ class Header extends ImmutablePureComponent {
+ + {me && ( ', () => { + let store; + + describe('with "accountNotifies" disabled', () => { + it('renders nothing', () => { + account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) }); + + render(, null, store); + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + + // describe('with "accountNotifies" enabled', () => { + // beforeEach(() => { + // store = { + // ...store, + // instance: normalizeInstance({ + // version: '3.4.1 (compatible; TruthSocial 1.0.0)', + // software: 'TRUTHSOCIAL', + // pleroma: ImmutableMap({}), + // }), + // }; + // }); + + // describe('when the relationship is requested', () => { + // beforeEach(() => { + // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ requested: true }) }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders the button', () => { + // render(, null, store); + // expect(screen.getByTestId('icon-button')).toBeInTheDocument(); + // }); + + // describe('when the user "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: true }), + // }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders the unsubscribe button', () => { + // render(, null, store); + // expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`); + // }); + // }); + + // describe('when the user is not "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: false }), + // }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders the unsubscribe button', () => { + // render(, null, store); + // expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`); + // }); + // }); + // }); + + // describe('when the user is not following the account', () => { + // beforeEach(() => { + // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: false }) }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders nothing', () => { + // render(, null, store); + // expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + // }); + // }); + + // describe('when the user is following the account', () => { + // beforeEach(() => { + // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders the button', () => { + // render(, null, store); + // expect(screen.getByTestId('icon-button')).toBeInTheDocument(); + // }); + + // describe('when the user "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: true }), + // }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders the unsubscribe button', () => { + // render(, null, store); + // expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`); + // }); + // }); + + // describe('when the user is not "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: false }), + // }); + + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); + + // it('renders the unsubscribe button', () => { + // render(, null, store); + // expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`); + // }); + // }); + // }); + // }); + +}); diff --git a/app/soapbox/features/ui/components/subscription-button.tsx b/app/soapbox/features/ui/components/subscription-button.tsx new file mode 100644 index 000000000..bbe01f3ba --- /dev/null +++ b/app/soapbox/features/ui/components/subscription-button.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { + followAccount, + subscribeAccount, + unsubscribeAccount, +} from 'soapbox/actions/accounts'; +import snackbar from 'soapbox/actions/snackbar'; +import { IconButton } from 'soapbox/components/ui'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; + +import type { Account as AccountEntity } from 'soapbox/types/entities'; + +const messages = defineMessages({ + subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' }, + unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' }, + subscribeSuccess: { id: 'account.subscribe.success', defaultMessage: 'You have subscribed to this account.' }, + unsubscribeSuccess: { id: 'account.unsubscribe.success', defaultMessage: 'You have unsubscribed from this account.' }, + subscribeFailure: { id: 'account.subscribe.failure', defaultMessage: 'An error occurred trying to subscribed to this account.' }, + unsubscribeFailure: { id: 'account.unsubscribe.failure', defaultMessage: 'An error occurred trying to unsubscribed to this account.' }, +}); + +interface ISubscriptionButton { + account: AccountEntity +} + +const SubscriptionButton = ({ account }: ISubscriptionButton) => { + const dispatch = useAppDispatch(); + const features = useFeatures(); + const intl = useIntl(); + + const isFollowing = account.relationship?.following; + const isRequested = account.relationship?.requested; + const isSubscribed = features.accountNotifies ? + account.relationship?.notifying : + account.relationship?.subscribing; + const title = isSubscribed ? + intl.formatMessage(messages.unsubscribe, { name: account.get('username') }) : + intl.formatMessage(messages.subscribe, { name: account.get('username') }); + + const onSubscribeSuccess = () => + dispatch(snackbar.success(intl.formatMessage(messages.subscribeSuccess))); + + const onSubscribeFailure = () => + dispatch(snackbar.error(intl.formatMessage(messages.subscribeFailure))); + + const onUnsubscribeSuccess = () => + dispatch(snackbar.success(intl.formatMessage(messages.unsubscribeSuccess))); + + const onUnsubscribeFailure = () => + dispatch(snackbar.error(intl.formatMessage(messages.unsubscribeFailure))); + + const onNotifyToggle = () => { + if (account.relationship?.notifying) { + dispatch(followAccount(account.get('id'), { notify: false } as any)) + ?.then(() => onUnsubscribeSuccess()) + .catch(() => onUnsubscribeFailure()); + } else { + dispatch(followAccount(account.get('id'), { notify: true } as any)) + ?.then(() => onSubscribeSuccess()) + .catch(() => onSubscribeFailure()); + } + }; + + const onSubscriptionToggle = () => { + if (account.relationship?.subscribing) { + dispatch(unsubscribeAccount(account.get('id'))) + ?.then(() => onUnsubscribeSuccess()) + .catch(() => onUnsubscribeFailure()); + } else { + dispatch(subscribeAccount(account.get('id'))) + ?.then(() => onSubscribeSuccess()) + .catch(() => onSubscribeFailure()); + } + }; + + const handleToggle = () => { + if (features.accountNotifies) { + onNotifyToggle(); + } else { + onSubscriptionToggle(); + } + }; + + if (!features.accountSubscriptions && !features.accountNotifies) { + return null; + } + + if (isRequested || isFollowing) { + return ( + + ); + } + + return null; +}; + +export default SubscriptionButton; diff --git a/app/soapbox/features/ui/components/subscription_button.js b/app/soapbox/features/ui/components/subscription_button.js deleted file mode 100644 index a908ca335..000000000 --- a/app/soapbox/features/ui/components/subscription_button.js +++ /dev/null @@ -1,83 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { - followAccount, - subscribeAccount, - unsubscribeAccount, -} from 'soapbox/actions/accounts'; -import Icon from 'soapbox/components/icon'; -import { Button } from 'soapbox/components/ui'; - -const messages = defineMessages({ - subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' }, - unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' }, - subscribed: { id: 'account.subscribed', defaultMessage: 'Subscribed' }, -}); - -const mapStateToProps = state => { - const me = state.get('me'); - return { - me, - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - onSubscriptionToggle(account) { - if (account.relationship?.subscribing) { - dispatch(unsubscribeAccount(account.get('id'))); - } else { - dispatch(subscribeAccount(account.get('id'))); - } - }, - onNotifyToggle(account) { - if (account.relationship?.notifying) { - dispatch(followAccount(account.get('id'), { notify: false })); - } else { - dispatch(followAccount(account.get('id'), { notify: true })); - } - }, -}); - -export default @connect(mapStateToProps, mapDispatchToProps) -@injectIntl -class SubscriptionButton extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record, - features: PropTypes.object.isRequired, - }; - - handleSubscriptionToggle = () => { - if (this.props.features.accountNotifies) this.props.onNotifyToggle(this.props.account); - else this.props.onSubscriptionToggle(this.props.account); - } - - render() { - const { account, intl, features } = this.props; - const subscribing = features.accountNotifies ? account.relationship?.notifying : account.relationship?.subscribing; - const following = account.relationship?.following; - const requested = account.relationship?.requested; - - if (requested || following) { - return ( - - ); - } - - return null; - } - -} diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 8f8a65cc1..0d9d8e9b7 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -126,6 +126,7 @@ const getInstanceFeatures = (instance: Instance) => { accountNotifies: any([ v.software === MASTODON && gte(v.compatVersion, '3.3.0'), v.software === PLEROMA && gte(v.version, '2.4.50'), + // v.software === TRUTHSOCIAL, ]), /**