From 0e1302587a2e2f0d528cd205151a94a8c037b405 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 13 Jun 2022 11:35:54 -0400 Subject: [PATCH 1/4] Improve subscription button on header --- .../actions/__tests__/accounts.test.ts | 10 +- app/soapbox/actions/accounts.js | 5 +- .../features/account/components/header.js | 26 +---- .../ui/components/subscription-button.tsx | 105 ++++++++++++++++++ .../ui/components/subscription_button.js | 83 -------------- app/soapbox/utils/features.ts | 1 + 6 files changed, 121 insertions(+), 109 deletions(-) create mode 100644 app/soapbox/features/ui/components/subscription-button.tsx delete mode 100644 app/soapbox/features/ui/components/subscription_button.js 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..d90fd0f39 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, @@ -250,22 +251,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 +461,7 @@ class Header extends ImmutablePureComponent { } + title={} />, ); } @@ -578,11 +563,6 @@ class Header extends ImmutablePureComponent { const menu = this.makeMenu(); const header = account.get('header', ''); - // NOTE: Removing Subscription element - // {features.accountSubscriptions &&
- // - //
} - return (
@@ -618,6 +598,8 @@ class Header extends ImmutablePureComponent {
+ + {me && ( { + const dispatch = useAppDispatch(); + const features = useFeatures(); + const intl = useIntl(); + + const following = account.relationship?.following; + const requested = 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) { + return null; + } + + if (requested || following) { + 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..8c3f3636c 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, ]), /** From 5fa875ef64d7b7064feae667230369b0c7669760 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 14 Jun 2022 11:58:42 -0400 Subject: [PATCH 2/4] Add tests --- .../__tests__/subscribe-button.test.tsx | 178 ++++++++++++++++++ .../ui/components/subscription-button.tsx | 8 +- 2 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx diff --git a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx new file mode 100644 index 000000000..4b0efc88e --- /dev/null +++ b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx @@ -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('', () => { + 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 index a0c3e2312..bbe01f3ba 100644 --- a/app/soapbox/features/ui/components/subscription-button.tsx +++ b/app/soapbox/features/ui/components/subscription-button.tsx @@ -30,8 +30,8 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => { const features = useFeatures(); const intl = useIntl(); - const following = account.relationship?.following; - const requested = account.relationship?.requested; + const isFollowing = account.relationship?.following; + const isRequested = account.relationship?.requested; const isSubscribed = features.accountNotifies ? account.relationship?.notifying : account.relationship?.subscribing; @@ -83,11 +83,11 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => { } }; - if (!features.accountSubscriptions) { + if (!features.accountSubscriptions && !features.accountNotifies) { return null; } - if (requested || following) { + if (isRequested || isFollowing) { return ( Date: Tue, 14 Jun 2022 12:01:37 -0400 Subject: [PATCH 3/4] Remove unused translations --- app/soapbox/features/account/components/header.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index d90fd0f39..a29e8270f 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -62,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}' }, }); From 16c06e1d607f2614e13b26510f15462f9a954cfc Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 17 Jun 2022 07:47:21 -0400 Subject: [PATCH 4/4] Temporarily disable for non-PLEROMA/MASTODON --- .../__tests__/subscribe-button.test.tsx | 258 +++++++++--------- app/soapbox/utils/features.ts | 2 +- 2 files changed, 130 insertions(+), 130 deletions(-) diff --git a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx index 4b0efc88e..3efa4d617 100644 --- a/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx +++ b/app/soapbox/features/ui/components/__tests__/subscribe-button.test.tsx @@ -24,155 +24,155 @@ describe('', () => { }); }); - describe('with "accountNotifies" enabled', () => { - beforeEach(() => { - store = { - ...store, - instance: normalizeInstance({ - version: '3.4.1 (compatible; TruthSocial 1.0.0)', - software: 'TRUTHSOCIAL', - pleroma: ImmutableMap({}), - }), - }; - }); + // 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 }) }); + // describe('when the relationship is requested', () => { + // beforeEach(() => { + // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ requested: true }) }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); - it('renders the button', () => { - render(, null, store); - expect(screen.getByTestId('icon-button')).toBeInTheDocument(); - }); + // 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 }), - }); + // describe('when the user "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: true }), + // }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // 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}`); - }); - }); + // 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 }), - }); + // describe('when the user is not "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: false }), + // }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // 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}`); - }); - }); - }); + // 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 }) }); + // describe('when the user is not following the account', () => { + // beforeEach(() => { + // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: false }) }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); - it('renders nothing', () => { - render(, null, store); - expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); - }); - }); + // 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 }) }); + // describe('when the user is following the account', () => { + // beforeEach(() => { + // account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // store = { + // ...store, + // accounts: ImmutableMap({ + // '1': account, + // }), + // }; + // }); - it('renders the button', () => { - render(, null, store); - expect(screen.getByTestId('icon-button')).toBeInTheDocument(); - }); + // 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 }), - }); + // describe('when the user "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: true }), + // }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // 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}`); - }); - }); + // 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 }), - }); + // describe('when the user is not "isSubscribed"', () => { + // beforeEach(() => { + // account = normalizeAccount({ + // ...account, + // relationship: normalizeRelationship({ requested: true, notifying: false }), + // }); - store = { - ...store, - accounts: ImmutableMap({ - '1': account, - }), - }; - }); + // 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}`); - }); - }); - }); - }); + // 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/utils/features.ts b/app/soapbox/utils/features.ts index 8c3f3636c..0d9d8e9b7 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -126,7 +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, + // v.software === TRUTHSOCIAL, ]), /**