From 6d9fada37a57e4e791bc927457ada84dd27b9f35 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Wed, 12 Jun 2024 13:56:49 +0300 Subject: [PATCH] HF 30 - Private message groups --- package.json | 2 +- .../messages/StartPanel/StartPanel.scss | 1 + .../elements/messages/Stepper/Stepper.scss | 4 + .../elements/messages/Stepper/index.jsx | 24 +++- src/components/modules/CreateGroup.jsx | 43 +++++-- src/components/modules/CreateGroup.scss | 12 ++ src/components/modules/Modals.jsx | 8 +- src/components/modules/groups/GroupAdmin.jsx | 104 +++++++++++++++ src/components/modules/groups/GroupFinal.jsx | 30 +++++ src/components/modules/groups/GroupLogo.jsx | 119 ++++++++++++++---- src/components/modules/groups/GroupName.jsx | 32 ++--- src/locales/en.json | 18 ++- src/locales/ru-RU.json | 18 ++- src/utils/misc.js | 5 +- yarn.lock | 4 +- 15 files changed, 361 insertions(+), 63 deletions(-) create mode 100644 src/components/modules/groups/GroupAdmin.jsx create mode 100644 src/components/modules/groups/GroupFinal.jsx diff --git a/package.json b/package.json index 5683ff3d6..92aff8533 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "connected-react-router": "^6.9.2", "counterpart": "^0.18.6", "emoji-picker-element": "^1.10.1", - "formik": "https://gitpkg.now.sh/golos-blockchain/formik/packages/formik?b697b6ef3f13c795bb862b35589fffde442ab465", + "formik": "https://gitpkg.now.sh/golos-blockchain/formik/packages/formik?3b21166c33ade760d562091e1fa0b71d172a7aaf", "git-rev-sync": "^3.0.2", "golos-lib-js": "^0.9.69", "history": "4.10.1", diff --git a/src/components/elements/messages/StartPanel/StartPanel.scss b/src/components/elements/messages/StartPanel/StartPanel.scss index 63dac7e5a..3e991bd09 100644 --- a/src/components/elements/messages/StartPanel/StartPanel.scss +++ b/src/components/elements/messages/StartPanel/StartPanel.scss @@ -1,5 +1,6 @@ .msgs-start-panel { .button { display: block; + width: 100%; } } diff --git a/src/components/elements/messages/Stepper/Stepper.scss b/src/components/elements/messages/Stepper/Stepper.scss index fef8b68a6..ea322122e 100644 --- a/src/components/elements/messages/Stepper/Stepper.scss +++ b/src/components/elements/messages/Stepper/Stepper.scss @@ -17,6 +17,10 @@ .bar { background-color: #0078C4; } + cursor: pointer; + &:hover { + color: #0078C4; + } } &.current { color: #0078C4; diff --git a/src/components/elements/messages/Stepper/index.jsx b/src/components/elements/messages/Stepper/index.jsx index 39186556f..eced88131 100644 --- a/src/components/elements/messages/Stepper/index.jsx +++ b/src/components/elements/messages/Stepper/index.jsx @@ -12,6 +12,17 @@ class Stepper extends React.Component { } } + _goToStep = (step) => { // TODO: private, if make public - check step exists + this.setState({ + currentStep: step + }, () => { + const { onStep } = this.props + if (onStep) { + onStep({ step }) + } + }) + } + nextStep = () => { const { steps } = this.props const entr = Object.entries(steps) @@ -19,9 +30,7 @@ class Stepper extends React.Component { let found for (const [key, content] of entr) { if (found) { - this.setState({ - currentStep: key - }) + this._goToStep(key) return key } found = key === currentStep @@ -42,7 +51,14 @@ class Stepper extends React.Component { const isCurrent = key === currentStep foundCurrent = foundCurrent || isCurrent const cn = foundCurrent ? (isCurrent ? 'current' : '') : 'left' - stepObjs.push(
+ let onClick + if (!foundCurrent) { + onClick = (e) => { + e.preventDefault() + this._goToStep(key) + } + } + stepObjs.push(
{content}
) diff --git a/src/components/modules/CreateGroup.jsx b/src/components/modules/CreateGroup.jsx index 16c33654c..b995a6b24 100644 --- a/src/components/modules/CreateGroup.jsx +++ b/src/components/modules/CreateGroup.jsx @@ -15,13 +15,15 @@ import LoadingIndicator from 'app/components/elements/LoadingIndicator' 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 from 'app/components/modules/groups/GroupLogo' +import GroupLogo, { validateLogoStep } from 'app/components/modules/groups/GroupLogo' +import GroupAdmin, { validateAdminStep } from 'app/components/modules/groups/GroupAdmin' +import GroupFinal from 'app/components/modules/groups/GroupFinal' const STEPS = () => { return { name: tt('create_group_jsx.step_name'), logo: tt('create_group_jsx.step_logo'), admin: tt('create_group_jsx.step_admin'), - create: tt('create_group_jsx.step_create') + final: tt('create_group_jsx.step_create') } } class CreateGroup extends React.Component { @@ -29,6 +31,7 @@ class CreateGroup extends React.Component { super(props) this.state = { step: 'name', + validators: 0, initialValues: { title: '', name: '', @@ -36,6 +39,8 @@ class CreateGroup extends React.Component { privacy: 'public_group', logo: '', + + admin: '', } } this.stepperRef = React.createRef() @@ -81,12 +86,24 @@ class CreateGroup extends React.Component { } } + setValidating = (validating) => { + this.setState({ + validators: this.state.validators + (validating ? 1 : -1), + }) + } + validate = async (values) => { const errors = {} const { step } = this.state + this.setValidating(true) if (step === 'name') { - await validateNameStep(values, errors, (validating) => this.setState({ validating })) + await validateNameStep(values, errors) + } else if (step === 'logo') { + await validateLogoStep(values, errors) + } else if (step === 'admin') { + await validateAdminStep(values, errors) } + this.setValidating(false) return errors } @@ -96,13 +113,16 @@ class CreateGroup extends React.Component { goNext = (e, setFieldValue) => { e.preventDefault() const step = this.stepperRef.current.nextStep() + } + + onStep = ({ step }) => { this.setState({ step }) } render() { - const { step, loaded, createError, validating } = this.state + const { step, loaded, createError, validators } = this.state let form if (!loaded) { @@ -134,20 +154,23 @@ class CreateGroup extends React.Component { onSubmit={this._onSubmit} > {({ - handleSubmit, isSubmitting, isValid, values, setFieldValue, setFieldTouched, handleChange, + handleSubmit, isSubmitting, isValid, values, errors, setFieldValue, applyFieldValue, setFieldTouched, handleChange, }) => { - const disabled = !isValid || validating + const disabled = !isValid || !!validators return (
- {step === 'name' ? : - step === 'logo' ? : + {step === 'name' ? : + step === 'logo' ? : + step === 'admin' ? : + step === 'final' ? : } - + {isSubmitting ?
: - diff --git a/src/components/modules/CreateGroup.scss b/src/components/modules/CreateGroup.scss index 3780db74f..c647e4164 100644 --- a/src/components/modules/CreateGroup.scss +++ b/src/components/modules/CreateGroup.scss @@ -24,5 +24,17 @@ } .error { margin-bottom: 0px !important; + margin-top: 0.5rem; } + + .image-loader { + margin-top: 14px; + } + + .image-preview { + max-width: 75px; + max-height: 75px; + margin-top: 0.75rem; + border: none; + } } diff --git a/src/components/modules/Modals.jsx b/src/components/modules/Modals.jsx index c3643426d..a20d76004 100644 --- a/src/components/modules/Modals.jsx +++ b/src/components/modules/Modals.jsx @@ -29,9 +29,13 @@ class Modals extends React.Component { onLoginBackdropClick = (e) => { const { loginUnclosable } = this.props; if (loginUnclosable) - throw new Error('Closing login modal is forbidden here'); + this.onUnclosableClick(e) }; + onUnclosableClick = (e) => { + throw new Error('Closing modal is forbidden here') + } + render() { const { show_login_modal, @@ -64,7 +68,7 @@ class Modals extends React.Component { } - {show_create_group_modal && + {show_create_group_modal && } diff --git a/src/components/modules/groups/GroupAdmin.jsx b/src/components/modules/groups/GroupAdmin.jsx new file mode 100644 index 000000000..56f8b3a6f --- /dev/null +++ b/src/components/modules/groups/GroupAdmin.jsx @@ -0,0 +1,104 @@ +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 new file mode 100644 index 000000000..e9baba97b --- /dev/null +++ b/src/components/modules/groups/GroupFinal.jsx @@ -0,0 +1,30 @@ +import React from 'react' +import { connect } from 'react-redux' +import { Field, ErrorMessage, } from 'formik' +import tt from 'counterpart' + +class GroupFinal extends React.Component { + state = {} + + constructor(props) { + super(props) + } + + render() { + return +
+
+ {tt('create_group_jsx.final_desc')} +
+
+
+ } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + }, + dispatch => ({ + }) +)(GroupFinal) diff --git a/src/components/modules/groups/GroupLogo.jsx b/src/components/modules/groups/GroupLogo.jsx index 9c2290e74..145f6e150 100644 --- a/src/components/modules/groups/GroupLogo.jsx +++ b/src/components/modules/groups/GroupLogo.jsx @@ -7,6 +7,60 @@ import tt from 'counterpart' import Input from 'app/components/elements/common/Input'; import PictureSvg from 'app/assets/icons/editor-toolbar/picture.svg'; import DialogManager from 'app/components/elements/common/DialogManager' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import { delay } from 'app/utils/misc' +import { proxifyImageUrlWithStrip } from 'app/utils/ProxifyUrl'; + +export async function validateLogoStep(values, errors) { + if (values.logo) { + try { + let previewRes + const img = document.createElement('img') + const uniq = Math.random() + window._logoValidator = uniq + + img.onload = () => { + console.log('img onload') + previewRes = { ok: true } + } + img.onerror = () => { + console.log('img onerror') + previewRes = { err: 'wrong' } + } + img.src = values.logo + + const started = Date.now() + const checkTimeout = 7000 + while (true) { + const elapsed = Date.now() - started + + if (window._logoValidator !== uniq) { // Another loop started + console.log('ret', uniq) + return + } + + if (!previewRes && elapsed > checkTimeout) { + errors.logo = tt('create_group_jsx.image_timeout') + return + } + + if (previewRes) { + if (previewRes.err) { + errors.logo = tt('create_group_jsx.image_wrong') + return + } else { + return + } + } + + await delay(100) + } + } catch (err) { + console.error(err) + errors.logo = 'Unknown validation error' + } + } +} class GroupLogo extends React.Component { state = {} @@ -15,13 +69,12 @@ class GroupLogo extends React.Component { super(props) } - uploadLogo = (file, name, setFieldValue) => { - const { notify } = this.props - const { uploadImage } = this.props + uploadLogo = (file, name) => { + const { notify, uploadImage, applyFieldValue } = this.props this.setState({ uploading: true }) uploadImage(file, progress => { if (progress.url) { - alert(progress.url) + applyFieldValue('logo', progress.url) } if (progress.error) { const { error } = progress; @@ -31,7 +84,7 @@ class GroupLogo extends React.Component { }) } - _onDrop = (acceptedFiles, rejectedFiles, setFieldValue) => { + onDrop = (acceptedFiles, rejectedFiles) => { const file = acceptedFiles[0] if (!file) { @@ -43,10 +96,10 @@ class GroupLogo extends React.Component { return } - this.uploadLogo(file, file.name, setFieldValue) + this.uploadLogo(file, file.name) }; - _onInputKeyDown = e => { + onInputKeyDown = e => { if (e.which === keyCodes.ENTER) { e.preventDefault(); //this.props.onClose({ @@ -55,10 +108,29 @@ class GroupLogo extends React.Component { } }; + onChange = (e) => { + const { applyFieldValue} = this.props + applyFieldValue('logo', e.target.value) + } + + _renderPreview = () => { + const { values, errors, isValidating } = this.props + let { logo } = values + if (isValidating) { + return
+ +
+ } else if (logo && !errors.logo) { + const size = '75x75' // main size of Userpic + logo = proxifyImageUrlWithStrip(logo, size); + return + } + return null + } render() { - const { values, setFieldValue, setFieldTouched } = this.props - const { uploading } = this.state + const { isValidating } = this.props + const { uploading, } = this.state const selectorStyleCover = uploading ? { @@ -86,36 +158,37 @@ class GroupLogo extends React.Component {
this.onDrop(af, rf)} > - {({getRootProps, getInputProps}) => (
+ {({getRootProps, getInputProps}) => (
- + {tt('create_group_jsx.logo_upload')}
)} -
+
-
+
{tt('create_group_jsx.logo_link')}:
- this.onChange(e)} > - {({ field, form }) => } + {!isValidating && }
+ {this._renderPreview()}
diff --git a/src/components/modules/groups/GroupName.jsx b/src/components/modules/groups/GroupName.jsx index 4ad41173a..81dfc990f 100644 --- a/src/components/modules/groups/GroupName.jsx +++ b/src/components/modules/groups/GroupName.jsx @@ -6,7 +6,7 @@ import { api } from 'golos-lib-js' import Icon from 'app/components/elements/Icon' -export async function validateNameStep(values, errors, setValidating) { +export async function validateNameStep(values, errors) { if (!values.title) { errors.title = tt('g.required') } @@ -14,7 +14,6 @@ export async function validateNameStep(values, errors, setValidating) { if (values.name.length < 3) { errors.name = tt('create_group_jsx.group_min_length') } else { - setValidating(true) let group for (let i = 0; i < 3; ++i) { try { @@ -33,7 +32,6 @@ export async function validateNameStep(values, errors, setValidating) { if (group && group[0]) { errors.name = tt('create_group_jsx.group_already_exists') } - setValidating(false) } } } @@ -45,21 +43,21 @@ export default class GroupName extends React.Component { super(props) } - onTitleChange = (e, setFieldValue, setFieldTouched) => { + onTitleChange = (e) => { const { value } = e.target if (value.trimLeft() !== value) { return } - setFieldValue('title', value) + const { applyFieldValue } = this.props + applyFieldValue('title', value) let link = getSlug(value) - setFieldValue('name', link) - setFieldTouched('name', true) + applyFieldValue('name', link) this.setState({ showName: true }) } - onNameChange = (e, setFieldValue) => { + onNameChange = (e) => { const { value } = e.target for (let i = 0; i < value.length; ++i) { const c = value[i] @@ -73,16 +71,18 @@ export default class GroupName extends React.Component { if (!is_alpha && !is_digit && !is_dash && !is_ul) return; } } - setFieldValue('name', value) + const { applyFieldValue } = this.props + applyFieldValue('name', value) } - onPrivacyChange = (e, setFieldValue) => { - setFieldValue('privacy', e.target.value) - setFieldValue('is_encrypted', true) + onPrivacyChange = (e) => { + const { applyFieldValue } = this.props + applyFieldValue('privacy', e.target.value) + applyFieldValue('is_encrypted', true) } render() { - const { values, setFieldValue, setFieldTouched } = this.props + const { values } = this.props const { showName } = this.state return
@@ -94,7 +94,7 @@ export default class GroupName extends React.Component { type='text' name='title' maxLength='48' - onChange={e => this.onTitleChange(e, setFieldValue, setFieldTouched)} + onChange={e => this.onTitleChange(e)} autoFocus /> @@ -110,7 +110,7 @@ export default class GroupName extends React.Component { type='text' name='name' maxLength='32' - onChange={e => this.onNameChange(e, setFieldValue)} + onChange={e => this.onNameChange(e)} />
@@ -125,7 +125,7 @@ export default class GroupName extends React.Component { this.onPrivacyChange(e, setFieldValue)} + onChange={e => this.onPrivacyChange(e)} > diff --git a/src/locales/en.json b/src/locales/en.json index 347048d48..d77869cc6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,4 +1,14 @@ { + "account_name": { + "account_name_should_be_shorter": "Account name should be shorter.", + "account_name_should_be_longer": "Account name should be longer.", + "account_name_should_not_be_empty": "Account name should not be empty.", + "each_account_segment_should_start_with_a_letter": "Each account name segment should start with a letter.", + "each_account_segment_should_have_only_letters_digits_or_dashes": "Each account name segment should have only letters, digits or dashes.", + "each_account_segment_should_have_only_one_dash_in_a_row": "Each account name segment should have only one dash in a row.", + "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." + }, "chainvalidation_js": { "account_name_should": "Account name should ", "not_be_empty": "not be empty.", @@ -106,9 +116,13 @@ "golos_power_too_low2": "That is not enough ", "golos_power_too_low3": "Your Golos Power is ", "deposit_gp": "Increase Golos Power", - "logo_desc": "The group logo is like a user’s avatar...", + "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" + "logo_link": "Add logo from the URL", + "admin_desc": "Admin of group will be...", + "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..." }, "emoji_i18n": { "categoriesLabel": "Категории", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index d9d60424c..80116c47a 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -1,4 +1,14 @@ { + "account_name": { + "account_name_should_be_shorter": "Имя аккаунта должно быть короче.", + "account_name_should_be_longer": "Имя аккаунта должно быть длиннее.", + "account_name_should_not_be_empty": "Имя аккаунта не должно быть пустым.", + "each_account_segment_should_start_with_a_letter": "Каждый сегмент имени аккаунта должен начинаться с буквы.", + "each_account_segment_should_have_only_letters_digits_or_dashes": "Сегмент имени аккаунта может содержать только буквы, цифры и дефисы.", + "each_account_segment_should_have_only_one_dash_in_a_row": "Каждый сегмент имени аккаунта может содержать только один дефис.", + "each_account_segment_should_end_with_a_letter_or_digit": "Каждый сегмент имени аккаунта должен заканчиваться буквой или цифрой.", + "each_account_segment_should_be_longer": "Сегмент имени аккаунта должен быть длиннее." + }, "chainvalidation_js": { "account_name_should": "Имя аккаунта должно ", "not_be_empty": "не может быть пустым.", @@ -108,9 +118,13 @@ "golos_power_too_low2": "Вам не хватает ", "golos_power_too_low3": "Ваша Сила Голоса - ", "deposit_gp": "Пополнить Силу Голоса", - "logo_desc": "Логотип группы - это как аватарка у пользователя...", + "logo_desc": "Логотип группы - это как аватарка у пользователя... Необязательно, но \"must have\"...", "logo_upload": "Загрузить логотип", - "logo_link": "Добавить логотип ссылкой" + "logo_link": "Добавить логотип ссылкой", + "admin_desc": "Администратором группы будeт...", + "final_desc": "Теперь все готово к созданию группы!", + "image_wrong": "Не удается загрузить картинку.", + "image_timeout": "Не удается загрузить картинку, она загружается слишком долго..." }, "emoji_i18n": { "categoriesLabel": "Категории", diff --git a/src/utils/misc.js b/src/utils/misc.js index 23688d782..3d39bc97c 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -5,6 +5,9 @@ const renderPart = (part, params) => { return part } +const delay = (msec) => new Promise(resolve => setTimeout(resolve, msec)) + export { - renderPart + renderPart, + delay } diff --git a/yarn.lock b/yarn.lock index cd4d7610b..840f5b117 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4891,9 +4891,9 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -"formik@https://gitpkg.now.sh/golos-blockchain/formik/packages/formik?b697b6ef3f13c795bb862b35589fffde442ab465": +"formik@https://gitpkg.now.sh/golos-blockchain/formik/packages/formik?3b21166c33ade760d562091e1fa0b71d172a7aaf": version "2.2.9" - resolved "https://gitpkg.now.sh/golos-blockchain/formik/packages/formik?b697b6ef3f13c795bb862b35589fffde442ab465#a696d8404c7b8751188a426f347589fdc24f4ba7" + resolved "https://gitpkg.now.sh/golos-blockchain/formik/packages/formik?3b21166c33ade760d562091e1fa0b71d172a7aaf#0f72ba16b0610fc14a6d42dfe2556600df5a3132" dependencies: deepmerge "^2.1.1" hoist-non-react-statics "^3.3.0"