From 8dfa1f49f3f07de1ca4d805ce7bce25c3cf29db0 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Thu, 27 Jun 2024 08:56:09 +0300 Subject: [PATCH] HF 30 - Private message groups - members, etc. --- config-overrides.js | 1 + package.json | 1 + src/_themes.scss | 2 + src/assets/icons/ionicons/ban-outline.svg | 1 + src/assets/icons/ionicons/ban.svg | 1 + .../icons/ionicons/checkmark-circle.svg | 2 +- src/assets/icons/ionicons/checkmark-sharp.svg | 2 +- src/assets/icons/ionicons/person-add.svg | 1 + src/assets/icons/ionicons/person.svg | 1 + src/components/all.scss | 2 + src/components/elements/Icon.jsx | 4 + .../elements/common/AccountName/index.jsx | 66 +++++ .../elements/common/AccountName/index.scss | 14 + .../elements/groups/GroupMember.jsx | 121 ++++++++ src/components/modules/CreateGroup.jsx | 88 +++--- src/components/modules/Modals.jsx | 38 ++- src/components/modules/groups/GroupAdmin.jsx | 104 ------- src/components/modules/groups/GroupFinal.jsx | 24 +- .../modules/groups/GroupMembers.jsx | 265 ++++++++++++++++++ .../modules/groups/GroupMembers.scss | 58 ++++ src/components/modules/groups/GroupName.jsx | 6 +- .../modules/groups/GroupSettings.jsx | 33 +-- src/components/modules/groups/MyGroups.jsx | 12 +- src/components/pages/Messages.jsx | 3 + src/locales/en.json | 18 +- src/locales/ru-RU.json | 36 ++- src/redux/FetchDataSaga.js | 43 ++- src/redux/GlobalReducer.js | 72 +++++ src/redux/PollDataSaga.js | 4 + src/redux/TransactionSaga.js | 2 +- src/redux/UserReducer.js | 7 + src/utils/translateError.js | 62 +++- yarn.lock | 191 ++++++++++++- 33 files changed, 1085 insertions(+), 200 deletions(-) create mode 100644 src/assets/icons/ionicons/ban-outline.svg create mode 100644 src/assets/icons/ionicons/ban.svg create mode 100644 src/assets/icons/ionicons/person-add.svg create mode 100644 src/assets/icons/ionicons/person.svg create mode 100644 src/components/elements/common/AccountName/index.jsx create mode 100644 src/components/elements/common/AccountName/index.scss create mode 100644 src/components/elements/groups/GroupMember.jsx delete mode 100644 src/components/modules/groups/GroupAdmin.jsx create mode 100644 src/components/modules/groups/GroupMembers.jsx create mode 100644 src/components/modules/groups/GroupMembers.scss diff --git a/config-overrides.js b/config-overrides.js index 6f7f062b2..bf5136b12 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -33,6 +33,7 @@ module.exports = function override(config, env) { 'process.env.IS_APP': JSON.stringify(!!process.env.IS_APP), 'process.env.DESKTOP_APP': JSON.stringify(!!process.env.DESKTOP_APP), 'process.env.MOBILE_APP': JSON.stringify(!!process.env.MOBILE_APP), + //'process.env.NO_NOTIFY': JSON.stringify(true), }), ) diff --git a/package.json b/package.json index 92aff8533..1f3d4de16 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-notification": "^6.8.5", "react-redux": "^7.2.6", "react-router-dom": "^5.3.0", + "react-select": "^5.8.0", "react-textarea-autosize": "^8.3.3", "redux": "^4.1.2", "redux-logger": "^3.0.6", diff --git a/src/_themes.scss b/src/_themes.scss index ea18d539a..525d58fdf 100644 --- a/src/_themes.scss +++ b/src/_themes.scss @@ -57,6 +57,7 @@ $themes: ( navBackgroundColor: $color-white, highlightBackgroundColor: #f3faf0, tableRowEvenBackgroundColor: #f4f4f4, + borderColor: $light-gray, border: 1px solid $light-gray, borderLight: 1px solid $color-border-light-lightest, borderDark: 1px solid $color-text-gray, @@ -98,6 +99,7 @@ $themes: ( zebra: $black-gray, highlighted: $black-gray, tableRowEvenBackgroundColor: #212C33, + borderColor: $color-border-dark, border: 1px solid $color-border-dark, borderLight: 1px solid $color-border-dark-lightest, textColorPrimaryborderDark: 1px solid $color-text-gray-light, diff --git a/src/assets/icons/ionicons/ban-outline.svg b/src/assets/icons/ionicons/ban-outline.svg new file mode 100644 index 000000000..723ad0f15 --- /dev/null +++ b/src/assets/icons/ionicons/ban-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/ionicons/ban.svg b/src/assets/icons/ionicons/ban.svg new file mode 100644 index 000000000..309cc25cb --- /dev/null +++ b/src/assets/icons/ionicons/ban.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/ionicons/checkmark-circle.svg b/src/assets/icons/ionicons/checkmark-circle.svg index 42d13ecb6..482458739 100644 --- a/src/assets/icons/ionicons/checkmark-circle.svg +++ b/src/assets/icons/ionicons/checkmark-circle.svg @@ -1 +1 @@ -Checkmark Circle \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/icons/ionicons/checkmark-sharp.svg b/src/assets/icons/ionicons/checkmark-sharp.svg index c8bfecc03..e4974ce93 100644 --- a/src/assets/icons/ionicons/checkmark-sharp.svg +++ b/src/assets/icons/ionicons/checkmark-sharp.svg @@ -1 +1 @@ -Checkmark \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/icons/ionicons/person-add.svg b/src/assets/icons/ionicons/person-add.svg new file mode 100644 index 000000000..94e1a1403 --- /dev/null +++ b/src/assets/icons/ionicons/person-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/ionicons/person.svg b/src/assets/icons/ionicons/person.svg new file mode 100644 index 000000000..04ac019df --- /dev/null +++ b/src/assets/icons/ionicons/person.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/all.scss b/src/components/all.scss index 493d1fbe7..f4162c51d 100644 --- a/src/components/all.scss +++ b/src/components/all.scss @@ -8,6 +8,7 @@ @import "./elements/VerticalMenu"; @import "./elements/app/AppReminder"; @import "./elements/app/LoginAppReminder"; +@import "./elements/common/AccountName/index"; @import "./elements/common/DialogManager/index"; @import "./elements/common/Input/index"; @import './elements/donate/PresetSelector'; @@ -23,6 +24,7 @@ @import './modules/LoginForm.scss'; @import './modules/CreateGroup.scss'; @import './modules/groups/MyGroups.scss'; +@import './modules/groups/GroupMembers.scss'; @import './modules/groups/GroupSettings.scss'; @import './modules/Modals.scss'; diff --git a/src/components/elements/Icon.jsx b/src/components/elements/Icon.jsx index aff269505..80fe69eef 100644 --- a/src/components/elements/Icon.jsx +++ b/src/components/elements/Icon.jsx @@ -128,6 +128,8 @@ const icons = new Map([ // ['editor-toolbar/picture', require('app/assets/icons/editor-toolbar/picture.svg')], // ['editor-toolbar/video', require('app/assets/icons/editor-toolbar/video.svg')], // ['editor-toolbar/search', require('app/assets/icons/editor-toolbar/search.svg')], + ['ionicons/ban', require('app/assets/icons/ionicons/ban.svg')], + ['ionicons/ban-outline', require('app/assets/icons/ionicons/ban-outline.svg')], ['ionicons/checkmark-circle', require('app/assets/icons/ionicons/checkmark-circle.svg')], ['ionicons/checkmark-sharp', require('app/assets/icons/ionicons/checkmark-sharp.svg')], ['ionicons/gift', require('app/assets/icons/ionicons/gift.svg')], @@ -137,6 +139,8 @@ const icons = new Map([ ['ionicons/lock-closed-outline', require('app/assets/icons/ionicons/lock-closed-outline.svg')], ['ionicons/lock-open-outline', require('app/assets/icons/ionicons/lock-open-outline.svg')], ['ionicons/trash-outline', require('app/assets/icons/ionicons/trash-outline.svg')], + ['ionicons/person', require('app/assets/icons/ionicons/person.svg')], + ['ionicons/person-add', require('app/assets/icons/ionicons/person-add.svg')], // ['notification/comment', require('app/assets/icons/notification/comment.svg')], // ['notification/donate', require('app/assets/icons/notification/donate.svg')], // ['notification/transfer', require('app/assets/icons/notification/transfer.svg')], diff --git a/src/components/elements/common/AccountName/index.jsx b/src/components/elements/common/AccountName/index.jsx new file mode 100644 index 000000000..11cf7c5ae --- /dev/null +++ b/src/components/elements/common/AccountName/index.jsx @@ -0,0 +1,66 @@ +import React from 'react' +import tt from 'counterpart' +import AsyncSelect from 'react-select/async' +import { api } from 'golos-lib-js' + +import Userpic from 'app/components/elements/Userpic' + +class AccountName extends React.Component { + constructor(props) { + super(props) + } + + lookupAccounts = async (value) => { + try { + const { includeFrozen, filterAccounts } = this.props + const accNames = await api.lookupAccountsAsync(value.toLowerCase(), 6, { + include_frozen: includeFrozen, + filter_accounts: [...filterAccounts], + }) + const accs = await api.lookupAccountNamesAsync(accNames) + return accs + } catch (err) { + console.error(err) + return [] + } + } + + onChange = (acc) => { + const { onChange } = this.props + if (onChange) { + const e = { target: { value: acc.name, account: acc } } + onChange(e, acc) + } + } + + render() { + const { onChange, className, ...rest } = this.props + return tt('account_name_jsx.loading')} + noOptionsMessage={() => tt('account_name_jsx.no_options')} + + loadOptions={this.lookupAccounts} + defaultOptions={true} + cacheOptions={false} + + className={'AccountName ' + (className || ' ')} + getOptionLabel={(option) => { + return + + {`${option.name}`} + + }} + controlShouldRenderValue={false} + onChange={this.onChange} + {...rest} + /> + } +} + +AccountName.defaultProps = { + includeFrozen: false, + filterAccounts: [], +} + +export default AccountName diff --git a/src/components/elements/common/AccountName/index.scss b/src/components/elements/common/AccountName/index.scss new file mode 100644 index 000000000..bd494fc0a --- /dev/null +++ b/src/components/elements/common/AccountName/index.scss @@ -0,0 +1,14 @@ +.AccountName { + input { + box-shadow: none !important; + line-height: 1 !important; + height: 1.8rem !important; + } + .name-item { + margin-top: 0.45rem; + display: inline-block; + .title { + margin-left: 0.25rem; + } + } +} diff --git a/src/components/elements/groups/GroupMember.jsx b/src/components/elements/groups/GroupMember.jsx new file mode 100644 index 000000000..944d77311 --- /dev/null +++ b/src/components/elements/groups/GroupMember.jsx @@ -0,0 +1,121 @@ +import React from 'react' +import tt from 'counterpart' +import cn from 'classnames' + +import Icon from 'app/components/elements/Icon' +import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper' +import Userpic from 'app/components/elements/Userpic' + +class GroupMember extends React.Component { + // shouldComponentUpdate(nextProps) { + // const { member } = this.props + // if (member.member_type !== nextProps.member_type) { + // return true + // } + // return false + // } + + groupMember = (e, member, setMemberType) => { + e.preventDefault() + const { username, currentGroup } = this.props + const { account } = member + const currentMemberType = member.member_type + + if (currentMemberType === setMemberType) return; + if (username === account) return + const group = currentGroup.name + + // Update in UI immediately + this.props.updateGroupMember(group, account, setMemberType) + + const { creatingNew } = currentGroup + if (!creatingNew) { + this.props.groupMember({ + requester: username, group, + member: account, + member_type: setMemberType, + onSuccess: () => { + }, + onError: (err, errStr) => { + // Update it back! + this.props.updateGroupMember(group, account, currentMemberType) + + if (errStr.includes('duplicate transaction')) { + // "Too Many Requests" - just nothing action, not alert + return + } + alert(errStr) + } + }) + } + } + + render() { + const { member, username, currentGroup } = this.props + const { account, member_type, joined } = member + const { creatingNew } = currentGroup + + const isMe = username === account + const isOwner = currentGroup.owner === account + const isMember = !isOwner && member_type === 'member' + const isModer = !isOwner && member_type === 'moder' + const isBanned = !isOwner && member_type === 'banned' + + let memberTitle, moderTitle, banTitle + let memberBtn, moderBtn, banBtn, deleteBtn + let ownerTitle = tt('group_members_jsx.owner') + if (account !== username) { + memberTitle = (isMember && tt('group_members_jsx.member')) || + (isBanned && tt('group_members_jsx.unban')) || + (!isMember && tt('group_members_jsx.make_member')) + moderTitle = (isModer && tt('group_members_jsx.moder')) || + tt('group_members_jsx.make_moder') + banTitle = (isBanned && tt('group_members_jsx.unban')) || + tt('group_members_jsx.ban') + } else { + memberTitle = tt('group_members_jsx.member') + moderTitle = tt('group_members_jsx.moder') + banTitle = tt('group_members_jsx.banned') + } + + if (!creatingNew) { + if (!isMe || isBanned) { + banBtn = this.groupMember(e, member, 'banned')} /> + } + } else { + deleteBtn = this.groupMember(e, member, 'retired')} /> + } + + return + + + + + {account} + + + + + {!creatingNew && } + + + {isOwner && } + {(!isMe || isMember) && this.groupMember(e, member, 'member')} />} + {(!isMe || isModer) && this.groupMember(e, member, 'moder')} />} + {banBtn} + {deleteBtn} + + + } +} + +export default GroupMember diff --git a/src/components/modules/CreateGroup.jsx b/src/components/modules/CreateGroup.jsx index 889e50474..b238b77c3 100644 --- a/src/components/modules/CreateGroup.jsx +++ b/src/components/modules/CreateGroup.jsx @@ -2,7 +2,7 @@ import React from 'react' import {connect} from 'react-redux' import { Formik, Form, Field, ErrorMessage, } from 'formik' import { Map } from 'immutable' -import { api, formatter } from 'golos-lib-js' +import { api } from 'golos-lib-js' import { Asset, Price, AssetEditor } from 'golos-lib-js/lib/utils' import tt from 'counterpart' @@ -16,7 +16,7 @@ import FormikAgent from 'app/components/elements/donate/FormikUtils' import Stepper from 'app/components/elements/messages/Stepper' import GroupName, { validateNameStep } from 'app/components/modules/groups/GroupName' import GroupLogo, { validateLogoStep } from 'app/components/modules/groups/GroupLogo' -import GroupAdmin, { validateAdminStep } from 'app/components/modules/groups/GroupAdmin' +import GroupMembers, { validateMembersStep } from 'app/components/modules/groups/GroupMembers' import GroupFinal from 'app/components/modules/groups/GroupFinal' import DialogManager from 'app/components/elements/common/DialogManager' import { showLoginDialog } from 'app/components/dialogs/LoginDialog' @@ -24,10 +24,27 @@ import { showLoginDialog } from 'app/components/dialogs/LoginDialog' const STEPS = () => { return { name: tt('create_group_jsx.step_name'), logo: tt('create_group_jsx.step_logo'), - admin: tt('create_group_jsx.step_admin'), + members: tt('create_group_jsx.step_members'), final: tt('create_group_jsx.step_create') } } +class ActionOnUnmount extends React.Component { + componentWillUnmount() { + const { values, stripGroupMembers } = this.props + if (!values || !stripGroupMembers) { + console.warn('ActionOnUnmount rendered without req props') + return + } + const { name } = values + if (name) { + this.props.stripGroupMembers(name) + } + } + render () { + return null + } +} + class CreateGroup extends React.Component { constructor(props) { super(props) @@ -35,14 +52,14 @@ class CreateGroup extends React.Component { step: 'name', validators: 0, initialValues: { + creatingNew: true, + title: '', name: '', is_encrypted: true, privacy: 'public_group', logo: '', - - admin: '', } } this.stepperRef = React.createRef() @@ -50,29 +67,27 @@ class CreateGroup extends React.Component { componentDidMount = async () => { try { - const dgp = await api.getDynamicGlobalProperties() - const { min_golos_power_to_emission } = dgp - const minVS = await Asset(min_golos_power_to_emission[0]) + const dgp = await api.getChainPropertiesAsync() + const { private_group_cost } = dgp + const cost = await Asset(private_group_cost) - const acc = this.props.currentAccount.toJS() - let { vesting_shares } = acc - vesting_shares = Asset(vesting_shares) - - if (vesting_shares.gte(minVS)) { + let acc = await api.getAccountsAsync([this.props.currentAccount.get('name')]) + acc = acc[0] + const { sbd_balance } = acc + const gbgBalance = Asset(sbd_balance) + if (gbgBalance.gte(cost)) { this.setState({ loaded: true }) return } - const minGolos = await Asset(min_golos_power_to_emission[1]) - const vsGolos = formatter.vestToGolos(vesting_shares, dgp.total_vesting_shares, dgp.total_vesting_fund_steem) - const delta = minGolos.minus(vsGolos) + const delta = cost.minus(gbgBalance) this.setState({ loaded: true, createError: { - minGolos, - vsGolos, + cost, + gbgBalance, delta, accName: acc.name } @@ -106,8 +121,8 @@ class CreateGroup extends React.Component { await validateNameStep(values, errors) } else if (step === 'logo') { await validateLogoStep(values, errors) - } else if (step === 'admin') { - await validateAdminStep(values, errors) + } else if (step === 'members') { + await validateMembersStep(values, errors) } await this.setValidating(false) return errors @@ -172,18 +187,18 @@ class CreateGroup extends React.Component { } else if (createError) { - const { message, minGolos, delta, vsGolos, accName } = createError + const { message, cost, gbgBalance, delta, accName } = createError if (message) { form =
{message}
} else { - form =
- {tt('create_group_jsx.golos_power_too_low') + minGolos.floatString + '. '}
- {tt('create_group_jsx.golos_power_too_low2')} + form =
+ {tt('create_group_jsx.gbg_too_low') + cost.floatString + '. '}
+ {tt('create_group_jsx.gbg_too_low2')} {delta.floatString}.
- +
} @@ -199,13 +214,13 @@ class CreateGroup extends React.Component { {({ handleSubmit, isSubmitting, isValid, values, errors, setFieldValue, applyFieldValue, setFieldTouched, handleChange, }) => { - const disabled = !isValid || !!validators + const disabled = !isValid || !!validators || !values.name return (
{!isSubmitting ? (step === 'name' ? : step === 'logo' ? : - step === 'admin' ? : + step === 'members' ? : step === 'final' ? : ) : null} @@ -215,18 +230,20 @@ class CreateGroup extends React.Component { {isSubmitting ?
: } + )}}) return
-
-

{tt('msgs_start_panel.create_group')}

-
- {form} +
+

{tt('msgs_start_panel.create_group')}

+
+ {form}
} } @@ -243,7 +260,11 @@ export default connect( } }, dispatch => ({ - privateGroup: ({ password, creator, name, title, logo, admin, is_encrypted, privacy, + stripGroupMembers: (group) => { + dispatch(g.actions.receiveGroupMembers({ + group, members: [], append: false })) + }, + privateGroup: ({ password, creator, name, title, logo, moders, is_encrypted, privacy, onSuccess, onError }) => { let json_metadata = { app: 'golos-messenger', @@ -257,7 +278,6 @@ export default connect( creator, name, json_metadata, - admin: admin, is_encrypted, privacy, extensions: [], diff --git a/src/components/modules/Modals.jsx b/src/components/modules/Modals.jsx index 8ff3ffd22..b29bfd03d 100644 --- a/src/components/modules/Modals.jsx +++ b/src/components/modules/Modals.jsx @@ -7,6 +7,7 @@ import Reveal from 'react-foundation-components/lib/global/reveal'; import CreateGroup from 'app/components/modules/CreateGroup' import GroupSettings from 'app/components/modules/groups/GroupSettings' +import GroupMembers from 'app/components/modules/groups/GroupMembers' import MyGroups from 'app/components/modules/groups/MyGroups' import Donate from 'app/components/modules/Donate' import LoginForm from 'app/components/modules/LoginForm'; @@ -23,6 +24,7 @@ class Modals extends React.Component { show_create_group_modal: PropTypes.bool, show_my_groups_modal: PropTypes.bool, show_group_settings_modal: PropTypes.bool, + show_group_members_modal: PropTypes.bool, show_app_download_modal: PropTypes.bool, hideDonate: PropTypes.func.isRequired, hideAppDownload: PropTypes.func.isRequired, @@ -47,12 +49,14 @@ class Modals extends React.Component { show_create_group_modal, show_my_groups_modal, show_group_settings_modal, + show_group_members_modal, show_app_download_modal, hideLogin, hideDonate, hideCreateGroup, hideMyGroups, hideGroupSettings, + hideGroupMembers, hideAppDownload, notifications, removeNotification, @@ -67,28 +71,45 @@ class Modals extends React.Component { return n; }) : []; + const modalStyle = { + borderRadius: '8px', + boxShadow: '0 0 19px 3px rgba(0,0,0, 0.2)', + overflow: 'hidden', + } + return (
- {show_login_modal && + {show_login_modal && } - {show_donate_modal && + {show_donate_modal && } - {show_create_group_modal && + {show_create_group_modal && } - {show_my_groups_modal && + {show_my_groups_modal && } - {show_group_settings_modal && + {show_group_settings_modal && } - {show_app_download_modal && + {show_group_members_modal && + + + } + {show_app_download_modal && } @@ -112,6 +133,7 @@ export default connect( show_create_group_modal: state.user.get('show_create_group_modal'), show_my_groups_modal: state.user.get('show_my_groups_modal'), show_group_settings_modal: state.user.get('show_group_settings_modal'), + show_group_members_modal: state.user.get('show_group_members_modal'), show_app_download_modal: state.user.get('show_app_download_modal'), loginUnclosable, notifications: state.app.get('notifications'), @@ -138,6 +160,10 @@ export default connect( if (e) e.preventDefault() dispatch(user.actions.hideGroupSettings()) }, + hideGroupMembers: e => { + if (e) e.preventDefault() + dispatch(user.actions.hideGroupMembers()) + }, hideAppDownload: e => { if (e) e.preventDefault() dispatch(user.actions.hideAppDownload()) diff --git a/src/components/modules/groups/GroupAdmin.jsx b/src/components/modules/groups/GroupAdmin.jsx deleted file mode 100644 index 56f8b3a6f..000000000 --- a/src/components/modules/groups/GroupAdmin.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import { Field, ErrorMessage, } from 'formik' -import tt from 'counterpart' -import { api } from 'golos-lib-js' -import { validateAccountName } from 'golos-lib-js/lib/utils' - -import Input from 'app/components/elements/common/Input'; - -export async function validateAdminStep(values, errors) { - if (!values.admin) { - errors.admin = tt('g.required') - } else { - const nameError = validateAccountName(values.admin) - if (nameError.error) { - errors.admin = tt('account_name.' + nameError.error) - } else { - try { - let accs = await api.getAccountsAsync([values.admin]) - accs = accs[0] - if (!accs) { - errors.admin = tt('g.username_does_not_exist') - } - } catch (err) { - console.error(err) - errors.admin = 'Blockchain unavailable :(' - } - } - } -} - -class GroupAdmin extends React.Component { - state = {} - - constructor(props) { - super(props) - } - - componentDidMount() { - this.load() - } - - componentDidUpdate() { - this.load() - } - - load = () => { - const { loaded } = this.state - if (!loaded) { - const { username, applyFieldValue } = this.props - if (username) { - applyFieldValue('admin', username) - this.setState({ - loaded: true - }) - } - } - } - - onChange = async (e) => { - e.preventDefault() - const { applyFieldValue } = this.props - applyFieldValue('admin', e.target.value) - } - - render() { - const { uploading } = this.state - - return -
-
- {tt('create_group_jsx.admin_desc')} -
-
-
-
- this.onChange(e)} - > - - -
-
-
- } -} - -export default connect( - // mapStateToProps - (state, ownProps) => { - const currentUser = state.user.get('current') - const username = currentUser && currentUser.get('username') - return { - ...ownProps, - username, - } - }, - dispatch => ({ - }) -)(GroupAdmin) diff --git a/src/components/modules/groups/GroupFinal.jsx b/src/components/modules/groups/GroupFinal.jsx index a73215dab..1fa2cdeca 100644 --- a/src/components/modules/groups/GroupFinal.jsx +++ b/src/components/modules/groups/GroupFinal.jsx @@ -3,6 +3,8 @@ import { connect } from 'react-redux' import { Field, ErrorMessage, } from 'formik' import tt from 'counterpart' +import ExtLink from 'app/components/elements/ExtLink' + class GroupFinal extends React.Component { state = {} @@ -10,6 +12,20 @@ class GroupFinal extends React.Component { super(props) } + decorateSubmitError = (error) => { + if (error && error.startsWith && error.startsWith(tt('donate_jsx.insufficient_funds'))) { + const { username } = this.props + return + {error} + + + + + } + return error + } + render() { const { submitError } = this.props return @@ -19,7 +35,7 @@ class GroupFinal extends React.Component { {tt('create_group_jsx.final_desc')} {submitError ?
- {submitError} + {this.decorateSubmitError(submitError)}
: null}
@@ -30,8 +46,12 @@ class GroupFinal extends React.Component { export default connect( // mapStateToProps (state, ownProps) => { + const currentUser = state.user.getIn(['current']) + const username = currentUser && currentUser.get('username') + return { - ...ownProps + ...ownProps, + username, } }, dispatch => ({ diff --git a/src/components/modules/groups/GroupMembers.jsx b/src/components/modules/groups/GroupMembers.jsx new file mode 100644 index 000000000..228232dda --- /dev/null +++ b/src/components/modules/groups/GroupMembers.jsx @@ -0,0 +1,265 @@ +import React from 'react' +import { connect } from 'react-redux' +import { Field, ErrorMessage, } from 'formik' +import tt from 'counterpart' +import { api } from 'golos-lib-js' +import { validateAccountName } from 'golos-lib-js/lib/utils' + +import g from 'app/redux/GlobalReducer' +import transaction from 'app/redux/TransactionReducer' +import AccountName from 'app/components/elements/common/AccountName' +import Input from 'app/components/elements/common/Input'; +import GroupMember from 'app/components/elements/groups/GroupMember' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import { getGroupMeta, getGroupTitle } from 'app/utils/groups' + +export async function validateMembersStep(values, errors) { + /*if (!values.admin) { + errors.admin = tt('g.required') + } else { + const nameError = validateAccountName(values.admin) + if (nameError.error) { + errors.admin = tt('account_name.' + nameError.error) + } else { + try { + let accs = await api.getAccountsAsync([values.admin]) + accs = accs[0] + if (!accs) { + errors.admin = tt('g.username_does_not_exist') + } + } catch (err) { + console.error(err) + errors.admin = 'Blockchain unavailable :(' + } + } + }*/ +} + +class GroupMembers extends React.Component { + state = {} + + constructor(props) { + super(props) + } + + componentDidMount() { + this.init() + } + + componentDidUpdate() { + this.init() + } + + isLoading = () => { + const { group } = this.props + if (!group) return true + const members = group.get('members') + if (!members) return true + return members.get('loading') + } + + init = () => { + const { initialized } = this.state + if (!initialized) { + const { currentGroup } = this.props + if (currentGroup) { + const group = currentGroup + this.props.fetchGroupMembers(group) + this.setState({ + initialized: true + }) + } + } + } + + onAddAccount = (e) => { + try { + const { value } = e.target + const member = value + const member_type = 'member' + + const { username, currentGroup } = this.props + const { creatingNew } = currentGroup + const group = currentGroup.name + + if (creatingNew) { + this.props.updateGroupMember(group, member, member_type) + } else { + this.props.groupMember({ + requester: username, group, + member, + member_type, + onSuccess: () => { + this.props.updateGroupMember(group, member, member_type) + }, + onError: (err, errStr) => { + alert(errStr) + } + }) + } + } catch (err) { // TODO: and it is not enough :) if error in groupMember + console.error(err) + } + } + + render() { + const { currentGroup, group, username } = this.props + const loading = this.isLoading() + let members = group && group.get('members') + if (members) members = members.get('data') + if (members) members = members.toJS() + + let mems + if (loading) { + mems =
+
+
+ +
+
+
+ } else { + mems = [] + for (const member of members) { + const { groupMember, updateGroupMember } = this.props + mems.push() + } + + mems = + + {mems} + +
+ + const filterAccs = new Set() + if (username) filterAccs.add(username) + for (const m of members) { + filterAccs.add(m.account) + } + + mems =
+
+
+ +
+
+
+
+ {mems} +
+
+
+ } + + let header + const { creatingNew } = currentGroup + if (creatingNew) { + header =
+
+ {tt('create_group_jsx.members_desc')} +
+
+ } else { + const { name, json_metadata } = currentGroup + + const meta = getGroupMeta(json_metadata) + let title = getGroupTitle(meta, name) + + title = tt('group_members_jsx.title') + title + tt('group_members_jsx.title2') + header =
+
+
+

{title}

+
+
+
+
+ + +
+
+
+ } + + return
+ {header} + {mems} +
+ } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const currentUser = state.user.get('current') + const username = currentUser && currentUser.get('username') + + const { newGroup } = ownProps + let currentGroup + if (newGroup) { + currentGroup = newGroup + } else { + currentGroup = state.user.get('current_group') + if (currentGroup) currentGroup = currentGroup.toJS() + } + const group = currentGroup && state.global.getIn(['groups', currentGroup.name]) + return { + ...ownProps, + username, + currentGroup, + group, + } + }, + dispatch => ({ + fetchGroupMembers: (group) => { + dispatch(g.actions.fetchGroupMembers({ + group: group.name, creatingNew: !!group.creatingNew })) + }, + updateGroupMember: (group, member, member_type) => { + dispatch(g.actions.updateGroupMember({ + group, member, member_type, })) + }, + groupMember: ({ requester, group, member, member_type, + onSuccess, onError }) => { + const opData = { + requester, + name: group, + member, + member_type, + json_metadata: '{}', + extensions: [], + } + + const plugin = 'private_message' + const json = JSON.stringify(['private_group_member', opData]) + + dispatch(transaction.actions.broadcastOperation({ + type: 'custom_json', + operation: { + id: plugin, + required_posting_auths: [requester], + json, + }, + username: requester, + successCallback: onSuccess, + errorCallback: (err, errStr) => { + console.error(err) + if (onError) onError(err, errStr) + }, + })); + } + }) +)(GroupMembers) diff --git a/src/components/modules/groups/GroupMembers.scss b/src/components/modules/groups/GroupMembers.scss new file mode 100644 index 000000000..90a2b4e66 --- /dev/null +++ b/src/components/modules/groups/GroupMembers.scss @@ -0,0 +1,58 @@ +.GroupMembers { + .member-name { + margin-left: 0.5rem; + line-height: 40px; + vertical-align: top; + } + @include themify($themes) { + .member-btns { + float: right; + padding-right: 1rem; + + .member-btn { + cursor: pointer; + padding-top: 0.35rem; + transition: all .1s ease-in; + &.selected { + cursor: auto; + } + } + .owner { + color: green; + } + .member { + color: $medium-gray; + &.selected { + color: #0078C4; + } + &:hover { + color: #0078C4; + } + } + .moder { + margin-left: 0.25rem; + color: $medium-gray; + &.selected { + color: lime; + } + &:hover { + color: lime; + } + } + .ban { + margin-left: 0.35rem; + color: $medium-gray; + &.selected { + color: red; + } + &:hover { + color: red; + } + } + .delete { + margin-left: 0.5rem; + color: red; + } + } + } +} diff --git a/src/components/modules/groups/GroupName.jsx b/src/components/modules/groups/GroupName.jsx index 961686e47..686865cd6 100644 --- a/src/components/modules/groups/GroupName.jsx +++ b/src/components/modules/groups/GroupName.jsx @@ -52,9 +52,6 @@ export default class GroupName extends React.Component { applyFieldValue('title', value) let link = getSlug(value) applyFieldValue('name', link) - this.setState({ - showName: true - }) } onNameChange = (e) => { @@ -83,7 +80,6 @@ export default class GroupName extends React.Component { render() { const { values } = this.props - const { showName } = this.state return
@@ -102,7 +98,7 @@ export default class GroupName extends React.Component {
- {showName ?
+ {(values.title || values.name) ?
{tt('create_group_jsx.name')}
diff --git a/src/components/modules/groups/GroupSettings.jsx b/src/components/modules/groups/GroupSettings.jsx index 410b905ce..27caa38ef 100644 --- a/src/components/modules/groups/GroupSettings.jsx +++ b/src/components/modules/groups/GroupSettings.jsx @@ -17,7 +17,6 @@ import LoadingIndicator from 'app/components/elements/LoadingIndicator' import DialogManager from 'app/components/elements/common/DialogManager' import { showLoginDialog } from 'app/components/dialogs/LoginDialog' import { validateLogoStep } from 'app/components/modules/groups/GroupLogo' -import { validateAdminStep } from 'app/components/modules/groups/GroupAdmin' import { getGroupLogo, getGroupMeta, getGroupTitle } from 'app/utils/groups' import { proxifyImageUrlWithStrip } from 'app/utils/ProxifyUrl' @@ -32,20 +31,12 @@ class GroupSettings extends React.Component { componentDidMount() { const { currentGroup } = this.props const group = currentGroup.toJS() - const { name, member_list, privacy, json_metadata, is_encrypted } = group + const { name, privacy, json_metadata, is_encrypted } = group const meta = getGroupMeta(json_metadata) - let admin - for (const mem of member_list) { - if (mem.member_type === 'admin') { - admin = mem.account - } - break - } const initialValues = { name, title: meta.title, logo: meta.logo, - admin, privacy, is_encrypted, } @@ -67,11 +58,6 @@ class GroupSettings extends React.Component { applyFieldValue('logo', value) } - onAdminChange = (e, { applyFieldValue }) => { - const { value } = e.target - applyFieldValue('admin', value) - } - uploadLogo = (file, name, { applyFieldValue }) => { const { uploadImage } = this.props this.setState({ uploading: true }) @@ -114,7 +100,6 @@ class GroupSettings extends React.Component { errors.title = tt('create_group_jsx.group_min_length') } await validateLogoStep(values, errors) - await validateAdminStep(values, errors) return errors } @@ -241,19 +226,6 @@ class GroupSettings extends React.Component {
-
-
- {tt('create_group_jsx.admin')} - this.onAdminChange(e, { applyFieldValue })} - validateOnBlur={false} - /> - -
-
@@ -321,7 +293,7 @@ export default connect( payload: {file, progress}, }) }, - privateGroup: ({ password, creator, name, title, logo, admin, is_encrypted, privacy, + privateGroup: ({ password, creator, name, title, logo, is_encrypted, privacy, onSuccess, onError }) => { let json_metadata = { app: 'golos-messenger', @@ -335,7 +307,6 @@ export default connect( creator, name, json_metadata, - admin: admin, is_encrypted, privacy, extensions: [], diff --git a/src/components/modules/groups/MyGroups.jsx b/src/components/modules/groups/MyGroups.jsx index b43f78f07..80fceccfd 100644 --- a/src/components/modules/groups/MyGroups.jsx +++ b/src/components/modules/groups/MyGroups.jsx @@ -75,6 +75,11 @@ class MyGroups extends React.Component { this.props.showGroupSettings({ group }) } + showGroupMembers = (e, group) => { + e.preventDefault() + this.props.showGroupMembers({ group }) + } + _renderGroup = (group) => { const { name, json_metadata } = group @@ -101,7 +106,9 @@ class MyGroups extends React.Component { { e.preventDefault() }}> - @@ -192,6 +199,9 @@ export default connect( showGroupSettings({ group }) { dispatch(user.actions.showGroupSettings({ group })) }, + showGroupMembers({ group }) { + dispatch(user.actions.showGroupMembers({ group })) + }, deleteGroup: ({ owner, name, password, onSuccess, onError }) => { const opData = { diff --git a/src/components/pages/Messages.jsx b/src/components/pages/Messages.jsx index b4bc6e77c..6e394dd1f 100644 --- a/src/components/pages/Messages.jsx +++ b/src/components/pages/Messages.jsx @@ -164,6 +164,9 @@ class Messages extends React.Component { } async setCallback(username, removeTaskIds) { + if (process.env.NO_NOTIFY) { // config-overrides.js, yarn run dev + return + } if (this.checkLoggedOut(username)) return if (this.paused) { setTimeout(() => { diff --git a/src/locales/en.json b/src/locales/en.json index c318f2e3e..23c7bdcc6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -9,8 +9,16 @@ "each_account_segment_should_end_with_a_letter_or_digit": "Each account name segment should end with a letter or digit.", "each_account_segment_should_be_longer": "Each account name segment should be longer." }, + "account_name_jsx": { + "loading": "Loading...", + "no_options": "Cannot search..." + }, "chain_errors": { - "exceeded_maximum_allowed_bandwidth": "Insufficient account bandwidth. Replenish the Golos Power or write to info@golos.id" + "exceeded_maximum_allowed_bandwidth": "Insufficient account bandwidth. Replenish the Golos Power or write to info@golos.id", + "insufficient1": "It costs ", + "insufficient2": ", but you have just ", + "insufficient3": ". ", + "insufficient_top_up": "Top Up" }, "chainvalidation_js": { "account_name_should": "Account name should ", @@ -118,18 +126,21 @@ "submit": "Create", "step_name": "Name", "step_logo": "Logo", - "step_admin": "Administrator", + "step_members": "Members", "step_create": "Create!", "group_already_exists": "Group already exists.", "group_min_length": "Min is 3 symbols.", "golos_power_too_low": "To create group you should have Golos Power at least ", "golos_power_too_low2": "That is not enough ", "golos_power_too_low3": "Your Golos Power is ", + "gbg_too_low": "Group creation costs ", + "gbg_too_low2": "That is not enough ", + "gbg_too_low3": "Your GBG balance is ", "deposit_gp": "Increase Golos Power", "logo_desc": "The group logo is like a user’s avatar... Not required, but \"must have\"...", "logo_upload": "Upload logo", "logo_link": "Add logo from the URL", - "admin_desc": "Admin of group will be...", + "members_desc": "Add people, set moderators of the group...", "final_desc": "Now we are ready to create the group!", "image_wrong": "Cannot load this image.", "image_timeout": "Cannot load this image, it is loading too long..." @@ -260,6 +271,7 @@ "login": "Login", "logout": "Logout", "mentions": "Mentions", + "name": "Name", "night_mode": "Night Mode", "ok": "OK", "refresh": "Refresh", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index c2db4afc5..07089a420 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -9,8 +9,16 @@ "each_account_segment_should_end_with_a_letter_or_digit": "Каждый сегмент имени аккаунта должен заканчиваться буквой или цифрой.", "each_account_segment_should_be_longer": "Сегмент имени аккаунта должен быть длиннее." }, + "account_name_jsx": { + "loading": "Загрузка...", + "no_options": "Не удается найти..." + }, "chain_errors": { - "exceeded_maximum_allowed_bandwidth": "Недостаточно пропускной способности аккаунта. Пополните Силу Голоса или напишите на info@golos.id" + "exceeded_maximum_allowed_bandwidth": "Недостаточно пропускной способности аккаунта. Пополните Силу Голоса или напишите на info@golos.id", + "insufficient1": "Нужно ", + "insufficient2": ", есть ", + "insufficient3": ". ", + "insufficient_top_up": "Пополнить" }, "chainvalidation_js": { "account_name_should": "Имя аккаунта должно ", @@ -121,7 +129,7 @@ "submit": "Создать", "step_name": "Имя", "step_logo": "Логотип", - "step_admin": "Администратор", + "step_members": "Участники", "step_create": "Создать!", "group_already_exists": "Такая группа уже существует.", "validating": "Проверка существования группы...", @@ -129,14 +137,18 @@ "golos_power_too_low": "Для создания группы нужна Сила Голоса не менее ", "golos_power_too_low2": "Вам не хватает ", "golos_power_too_low3": "Ваша Сила Голоса - ", + "gbg_too_low": "Создание группы стоит ", + "gbg_too_low2": "Вам не хватает ", + "gbg_too_low3": "На вашем балансе - ", "deposit_gp": "Пополнить Силу Голоса", "logo_desc": "Логотип группы - это как аватарка у пользователя... Необязательно, но \"must have\"...", "logo_upload": "Загрузить логотип", "logo_link": "Добавить логотип ссылкой", - "admin_desc": "Администратором группы будeт...", + "members_desc": "Вы можете добавить в группу людей, назначить модераторов...", "final_desc": "Теперь все готово к созданию группы!", "image_wrong": "Не удается загрузить картинку.", - "image_timeout": "Не удается загрузить картинку, она загружается слишком долго..." + "image_timeout": "Не удается загрузить картинку, она загружается слишком долго...", + "add_member": "+ Добавить участника..." }, "my_groups_jsx": { "title": "Мои группы", @@ -148,6 +160,21 @@ "login_hint_GROUP": "(удаления группы \"%(GROUP)s\")", "members": "Участники" }, + "group_members_jsx": { + "title": "Участники группы ", + "title2": "", + "check_pending": "Заявки", + "check_pending_hint": "Заявки на вступление в группу", + "check_banned": "Забаненные", + "member": "Обычный участник", + "moder": "Модератор", + "owner": "Владелец группы", + "make_member": "Сделать обычным участником", + "make_moder": "Сделать модератором", + "ban": "Заблокировать", + "unban": "Разблокировать", + "banned": "Заблокирован(-а)" + }, "group_settings_jsx": { "title_GROUP": "Группа %(GROUP)s", "submit": "Сохранить", @@ -271,6 +298,7 @@ "login": "Войти", "logout": "Выйти", "mentions": "Упоминания", + "name": "Имя", "night_mode": "Ночной режим", "ok": "OK", "refresh": "Обновить", diff --git a/src/redux/FetchDataSaga.js b/src/redux/FetchDataSaga.js index b1ebcbff2..e19cbd27b 100644 --- a/src/redux/FetchDataSaga.js +++ b/src/redux/FetchDataSaga.js @@ -9,6 +9,7 @@ export function* fetchDataWatches () { yield fork(watchFetchState) yield fork(watchFetchUiaBalances) yield fork(watchFetchMyGroups) + yield fork(watchFetchGroupMembers) } export function* watchLocationChange() { @@ -33,6 +34,7 @@ export function* fetchState(location_change_action) { state.messages_update = '0'; state.accounts = {} state.assets = {} + state.groups = {} let hasErr = false @@ -117,19 +119,54 @@ export function* watchFetchMyGroups() { export function* fetchMyGroups({ payload: { account } }) { try { - const groups = yield call([api, api.getGroupsAsync], { + const groupsOwn = yield call([api, api.getGroupsAsync], { member: account, - member_types: ['pending', 'member', 'moder', 'admin'], + member_types: [], start_group: '', limit: 100, with_members: { accounts: [account] } }) - console.log('LOO', groups) + let groups = yield call([api, api.getGroupsAsync], { + member: account, + member_types: ['pending', 'member', 'moder'], + start_group: '', + limit: 100, + with_members: { + accounts: [account] + } + }) + groups = [...groupsOwn, ...groups] yield put(g.actions.receiveMyGroups({ groups })) } catch (err) { console.error('fetchMyGroups', err) } } + +export function* watchFetchGroupMembers() { + yield takeLatest('global/FETCH_GROUP_MEMBERS', fetchGroupMembers) +} + +export function* fetchGroupMembers({ payload: { group, creatingNew } }) { + try { + if (creatingNew) { + yield put(g.actions.receiveGroupMembers({ group, members: [], append: true })) + return + } + + yield put(g.actions.receiveGroupMembers({ group, loading: true })) + + const members = yield call([api, api.getGroupMembersAsync], { + group, + member_types: ['pending', 'member', 'moder'/*, 'banned'*/], + start_member: '', + limit: 100, + }) + + yield put(g.actions.receiveGroupMembers({ group, members })) + } catch (err) { + console.error('fetchGroupMembers', err) + } +} \ No newline at end of file diff --git a/src/redux/GlobalReducer.js b/src/redux/GlobalReducer.js index 9ab16b7d9..82c2ab783 100644 --- a/src/redux/GlobalReducer.js +++ b/src/redux/GlobalReducer.js @@ -274,5 +274,77 @@ export default createModule({ return state.set('my_groups', fromJS(groups)) }, }, + { + action: 'FETCH_GROUP_MEMBERS', + reducer: state => state + }, + { + action: 'RECEIVE_GROUP_MEMBERS', + reducer: (state, { payload: { group, members, loading, append } }) => { + let new_state = state + new_state = state.updateIn(['groups', group], + Map(), + gro => { + gro = gro.updateIn(['members'], Map(), mems => { + mems = mems.set('loading', loading || false) + if (append) { + // Immutable's: update do not wants to add notSet, if array is empty... + if (!mems.has('data')) { + mems = mems.set('data', List()) + } + mems = mems.update('data', List(), data => { + for (const item of (members || [])) { + data = data.push(fromJS(item)) + } + return data + }) + } else { + mems = mems.set('data', fromJS(members || [])) + } + return mems + }) + return gro + }) + return new_state + }, + }, + { + action: 'UPDATE_GROUP_MEMBER', + reducer: (state, { payload: { group, member, member_type } }) => { + const now = new Date().toISOString().split('.')[0] + let new_state = state + new_state = state.updateIn(['groups', group], + Map(), + gro => { + gro = gro.updateIn(['members', 'data'], List(), mems => { + const retiring = member_type === 'retired' + const idx = mems.findIndex(i => i.get('account') === member) + if (idx !== -1) { + if (retiring) { + mems = mems.remove(idx) + } else { + mems = mems.update(idx, mem => { + mem = mem.set('member_type', member_type) + return mem + }) + } + } else if (!retiring) { + mems = mems.insert(0, fromJS({ + group, + account: member, + json_metadata: '{}', + member_type, + invited: member, + joined: now, + updated: now, + })) + } + return mems + }) + return gro + }) + return new_state + }, + }, ], }) diff --git a/src/redux/PollDataSaga.js b/src/redux/PollDataSaga.js index 379d9dcba..bc087cde0 100644 --- a/src/redux/PollDataSaga.js +++ b/src/redux/PollDataSaga.js @@ -11,6 +11,10 @@ const wait = ms => ( let webpush_params = null; export default function* pollData() { + if (process.env.NO_NOTIFY) { // config-overrides.js, yarn run dev + console.warn('Notifications disabled in environment variables') + return + } while(true) { if (document.visibilityState !== 'hidden') { const username = yield select(state => state.user.getIn(['current', 'username'])); diff --git a/src/redux/TransactionSaga.js b/src/redux/TransactionSaga.js index e178f096e..8ce75f1c7 100644 --- a/src/redux/TransactionSaga.js +++ b/src/redux/TransactionSaga.js @@ -209,7 +209,7 @@ function* broadcastOperation( console.error('Broadcast error', err) if (errorCallback) { let errStr = err.toString() - errStr = translateError(errStr) + errStr = translateError(errStr, err.payload) errStr = errStr.substring(0, 160) errorCallback(err, errStr) } diff --git a/src/redux/UserReducer.js b/src/redux/UserReducer.js index 50e75e0d2..8f50ddf62 100644 --- a/src/redux/UserReducer.js +++ b/src/redux/UserReducer.js @@ -8,6 +8,7 @@ const defaultState = fromJS({ show_create_group_modal: false, show_my_groups_modal: false, show_group_settings_modal: false, + show_group_members_modal: false, show_app_download_modal: false, loginLoading: false, pub_keys_used: null, @@ -145,6 +146,12 @@ export default createModule({ return state }}, { action: 'HIDE_GROUP_SETTINGS', reducer: state => state.set('show_group_settings_modal', false) }, + { action: 'SHOW_GROUP_MEMBERS', reducer: (state, { payload: { group }}) => { + state = state.set('show_group_members_modal', true) + state = state.set('current_group', fromJS(group)) + return state + }}, + { action: 'HIDE_GROUP_MEMBERS', reducer: state => state.set('show_group_members_modal', false) }, { action: 'SHOW_APP_DOWNLOAD', reducer: state => state.set('show_app_download_modal', true) }, { action: 'HIDE_APP_DOWNLOAD', reducer: state => state.set('show_app_download_modal', false) }, { action: 'SET_DONATE_DEFAULTS', reducer: (state, {payload}) => state.set('donate_defaults', fromJS(payload)) }, diff --git a/src/utils/translateError.js b/src/utils/translateError.js index fa70ee050..ddd0c428b 100644 --- a/src/utils/translateError.js +++ b/src/utils/translateError.js @@ -1,6 +1,40 @@ import tt from 'counterpart' +import { Asset } from 'golos-lib-js/lib/utils' -export function translateError(string) { +const getErrorData = (errPayload, errName, depth = 0) => { + //console.error('getErrorData', errPayload) + if (depth > 50) { + throw new Error('getErrorData - infinity loop detected...') + } + if (errPayload === null) { + return null + } + if (errPayload.name === errName) { + let { stack } = errPayload + stack = stack && stack[0] + return stack ? stack.data : null + } + const { error, data } = errPayload + if (error) { + return getErrorData(error, errName, ++depth) + } + if (data) { + if (data.error) { + return getErrorData(data.error, errName, ++depth) + } + if (Array.isArray(data.stack)) { + for (const s of data.stack) { + const res = getErrorData(s, errName, ++depth) + if (res) { + return res + } + } + } + } + return null +} + +export function translateError(string, errPayload) { if (typeof(string) != 'string') return string switch (string) { case 'Account not found': @@ -37,5 +71,31 @@ export function translateError(string) { string = tt('chain_errors.exceeded_maximum_allowed_bandwidth') } + if (string.includes( + 'Account does not have sufficient funds' + )) { + string = tt('donate_jsx.insufficient_funds') + '.' + + let errData + try { + errData = getErrorData(errPayload, 'insufficient_funds') + if (errData && errData.required) { + let { required, exist } = errData + string += ' ' + tt('chain_errors.insufficient1') + string += Asset(required).floatString + exist = Asset(exist) + if (exist.gt(0)) { + string += tt('chain_errors.insufficient2') + string += exist.floatString + string += tt('chain_errors.insufficient3') + } else { + string += '.' + } + } + } catch (err) { + console.error('getErrorData', err) + } + } + return string } diff --git a/yarn.lock b/yarn.lock index 840f5b117..f64113e3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1031,6 +1031,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1109,6 +1116,94 @@ dependencies: postcss-value-parser "^4.2.0" +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== + +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/react@^11.8.1": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" + integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.4.tgz#fc8f6d80c492cfa08801d544a05331d1cc7cd451" + integrity sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + "@eslint/eslintrc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3" @@ -1124,6 +1219,26 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@floating-ui/core@^1.0.0": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.3.tgz#5e7bb92843f47fd1d8dcb9b3cc3c243aaed54f95" + integrity sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg== + dependencies: + "@floating-ui/utils" "^0.2.3" + +"@floating-ui/dom@^1.0.1": + version "1.6.6" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.6.tgz#be54c1ab2d19112ad323e63dbeb08185fed0ffd3" + integrity sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.3" + +"@floating-ui/utils@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.3.tgz#506fcc73f730affd093044cb2956c31ba6431545" + integrity sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww== + "@formatjs/ecma402-abstract@1.11.3": version "1.11.3" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.3.tgz#f25276dfd4ef3dac90da667c3961d8aa9732e384" @@ -1925,6 +2040,13 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-transition-group@^4.4.0": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" + integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@16 || 17": version "17.0.39" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.39.tgz#d0f4cde092502a6db00a1cded6e6bf2abb7633ce" @@ -3313,6 +3435,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -4038,6 +4165,14 @@ dom-helpers@^3.2.1: dependencies: "@babel/runtime" "^7.1.2" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -4801,6 +4936,11 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -5274,7 +5414,7 @@ history@4.10.1, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -6809,6 +6949,11 @@ memfs@^3.1.2, memfs@^3.4.1: dependencies: fs-monkey "1.0.3" +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -8105,7 +8250,7 @@ prop-types-extra@^1.0.1, prop-types-extra@^1.1.1: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -8464,6 +8609,21 @@ react-scripts@^5.0.0: optionalDependencies: fsevents "^2.3.2" +react-select@^5.8.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.0.tgz#bd5c467a4df223f079dd720be9498076a3f085b5" + integrity sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.1.2" + react-textarea-autosize@^8.3.3: version "8.3.3" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8" @@ -8473,6 +8633,16 @@ react-textarea-autosize@^8.3.3: use-composed-ref "^1.0.0" use-latest "^1.0.0" +react-transition-group@^4.3.0: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -8608,6 +8778,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -9161,7 +9336,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.5.0: +source-map@^0.5.0, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -9396,6 +9571,11 @@ stylehacks@^5.0.3: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -9880,6 +10060,11 @@ use-isomorphic-layout-effect@^1.0.0: resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz#7bb6589170cd2987a152042f9084f9effb75c225" integrity sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ== +use-isomorphic-layout-effect@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" + integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== + use-latest@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.0.tgz#a44f6572b8288e0972ec411bdd0840ada366f232"