Merge branch 'account-subscriptions' into 'develop'
Improve Account Subscriptions See merge request soapbox-pub/soapbox-fe!1527
This commit is contained in:
commit
b81dd09fc3
|
@ -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'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
|||
<Badge
|
||||
key='blocked'
|
||||
slug='opaque'
|
||||
title={<FormattedMessage id='account.blocked' defaultMessage='Blocked' />}
|
||||
title={<FormattedMessage id='account.blocked' defaultMessage='Blocked' />}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
@ -578,11 +561,6 @@ class Header extends ImmutablePureComponent {
|
|||
const menu = this.makeMenu();
|
||||
const header = account.get('header', '');
|
||||
|
||||
// NOTE: Removing Subscription element
|
||||
// {features.accountSubscriptions && <div className='account__header__subscribe'>
|
||||
// <SubscriptionButton account={account} />
|
||||
// </div>}
|
||||
|
||||
return (
|
||||
<div className='-mt-4 -mx-4'>
|
||||
<div>
|
||||
|
@ -618,6 +596,8 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='mt-6 flex justify-end w-full sm:pb-1'>
|
||||
<div className='mt-10 flex flex-row space-y-0 space-x-2'>
|
||||
<SubscriptionButton account={account} />
|
||||
|
||||
{me && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../../../normalizers';
|
||||
import SubscribeButton from '../subscription-button';
|
||||
|
||||
let account = {
|
||||
id: '1',
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
};
|
||||
|
||||
describe('<SubscribeButton />', () => {
|
||||
let store;
|
||||
|
||||
describe('with "accountNotifies" disabled', () => {
|
||||
it('renders nothing', () => {
|
||||
account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) });
|
||||
|
||||
render(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, 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(<SubscribeButton account={account} />, null, store);
|
||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
|
@ -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 (
|
||||
<IconButton
|
||||
src={isSubscribed ? require('@tabler/icons/icons/bell-ringing.svg') : require('@tabler/icons/icons/bell.svg')}
|
||||
onClick={handleToggle}
|
||||
title={title}
|
||||
className='text-primary-700 bg-primary-100 dark:!bg-slate-700 dark:!text-white hover:bg-primary-200 p-2'
|
||||
iconClassName='w-5 h-5'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SubscriptionButton;
|
|
@ -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 (
|
||||
<Button
|
||||
className={classNames('subscription-button', subscribing && 'button-active')}
|
||||
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe, { name: account.get('username') })}
|
||||
onClick={this.handleSubscriptionToggle}
|
||||
>
|
||||
<Icon src={subscribing ? require('@tabler/icons/icons/bell-ringing.svg') : require('@tabler/icons/icons/bell.svg')} />
|
||||
{subscribing && intl.formatMessage(messages.subscribed)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
]),
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue