diff --git a/src/components/all.scss b/src/components/all.scss index 7d4ceb35..8bec9535 100644 --- a/src/components/all.scss +++ b/src/components/all.scss @@ -22,6 +22,7 @@ // modules @import './modules/LoginForm.scss'; @import './modules/CreateGroup.scss'; +@import './modules/groups/MyGroups.scss'; @import './modules/Modals.scss'; @import "./pages/Messages"; diff --git a/src/components/dialogs/LoginDialog/index.jsx b/src/components/dialogs/LoginDialog/index.jsx index c184dac5..9a37ddd5 100644 --- a/src/components/dialogs/LoginDialog/index.jsx +++ b/src/components/dialogs/LoginDialog/index.jsx @@ -9,7 +9,7 @@ import Input from 'app/components/elements/common/Input'; import keyCodes from 'app/utils/keyCodes'; import { pageSession } from 'app/redux/UserSaga' -export function showLoginDialog(username, onClose, authType = 'active', saveLogin = false) { +export function showLoginDialog(username, onClose, authType = 'active', saveLogin = false, hint = '') { let dm, oldZ = '' DialogManager.showDialog({ @@ -18,6 +18,7 @@ export function showLoginDialog(username, onClose, authType = 'active', saveLogi props: { username, authType, + hint, }, onClose: (data) => { if (dm) dm.style.zIndex = oldZ @@ -44,7 +45,7 @@ export default class LoginDialog extends React.PureComponent { } componentDidMount() { - let { saveLogin } = this.props + let { saveLogin, hint } = this.props const session = pageSession.load() if (session) { this.setState({ @@ -58,6 +59,11 @@ export default class LoginDialog extends React.PureComponent { const linkInput = document.getElementsByClassName('AddImageDialog__link-input')[0]; if (linkInput) linkInput.focus(); + setTimeout(() => { + this.setState({ + enabled: true + }) + }, hint ? 1500 : 0) } onPasswordChange = (e) => { @@ -122,7 +128,13 @@ export default class LoginDialog extends React.PureComponent { } render() { - const { password, error, saveLogin } = this.state + const { password, error, saveLogin, enabled } = this.state + + let hint + if (this.props.hint) { + hint =  {this.props.hint} + } + return (
{tt('loginform_jsx.is_is_for_operation')} + {hint} + .
-
diff --git a/src/components/modules/groups/MyGroups.jsx b/src/components/modules/groups/MyGroups.jsx new file mode 100644 index 00000000..a87169f4 --- /dev/null +++ b/src/components/modules/groups/MyGroups.jsx @@ -0,0 +1,213 @@ +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 { Asset, Price, AssetEditor } from 'golos-lib-js/lib/utils' +import tt from 'counterpart' + +import g from 'app/redux/GlobalReducer' +import transaction from 'app/redux/TransactionReducer' +import user from 'app/redux/UserReducer' +import { session } from 'app/redux/UserSaga' +import DropdownMenu from 'app/components/elements/DropdownMenu' +import ExtLink from 'app/components/elements/ExtLink' +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import FormikAgent from 'app/components/elements/donate/FormikUtils' +import DialogManager from 'app/components/elements/common/DialogManager' +import { showLoginDialog } from 'app/components/dialogs/LoginDialog' +import { getGroupLogo, getGroupMeta } from 'app/utils/groups' + +class MyGroups extends React.Component { + constructor(props) { + super(props) + this.state = { + loaded: false + } + } + + refetch = () => { + const { currentUser } = this.props + this.props.fetchMyGroups(currentUser) + } + + componentDidMount = async () => { + this.refetch() + } + + createGroup = (e) => { + e.preventDefault() + this.props.showCreateGroup() + } + + _renderGroupLogo = (group, meta) => { + const { json_metadata } = group + + const logo = getGroupLogo(json_metadata) + return + + + } + + deleteGroup = (e, group, title) => { + e.preventDefault() + showLoginDialog(group.owner, (res) => { + const password = res && res.password + if (!password) { + return + } + this.props.deleteGroup({ + owner: group.owner, + name: group.name, + password, + onSuccess: () => { + this.refetch() + }, + onError: (err, errStr) => { + alert(errStr) + } + }) + }, 'active', false, tt('my_groups_jsx.login_hint_GROUP', { + GROUP: title + })) + } + + _renderGroup = (group) => { + const { name, json_metadata } = group + + const meta = getGroupMeta(json_metadata) + + let title = meta.title || name + let titleShr = title + if (titleShr.length > 20) { + titleShr = titleShr.substring(0, 17) + '...' + } + + const kebabItems = [] + + kebabItems.push({ link: '#', onClick: e => { + this.deleteGroup(e, group, titleShr) + }, value: tt('g.delete') }) + + return + + {this._renderGroupLogo(group, meta)} + + {titleShr} + + { + e.preventDefault() + }}> + + + {kebabItems.length ? + + : null} + + + + } + + render() { + let groups, hasGroups + + let { my_groups } = this.props + + if (!my_groups) { + groups = + } else { + my_groups = my_groups.toJS() + + if (!my_groups.length) { + groups =
+ {tt('my_groups_jsx.empty')} + {tt('my_groups_jsx.empty2')} + + {tt('my_groups_jsx.create')} + +
+ } else { + hasGroups = true + groups = [] + for (const g of my_groups) { + groups.push(this._renderGroup(g)) + } + groups = + + {groups} + +
+ } + } + + let button + if (hasGroups) { + button = + } + + return
+
+

{tt('my_groups_jsx.title')}

+
+ {button} + {groups} +
+ } +} + +export default connect( + (state, ownProps) => { + const currentUser = state.user.getIn(['current']) + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]) + const my_groups = state.global.get('my_groups') + + return { ...ownProps, + currentUser, + currentAccount, + my_groups, + } + }, + dispatch => ({ + fetchMyGroups: (currentUser) => { + if (!currentUser) return + const account = currentUser.get('username') + dispatch(g.actions.fetchMyGroups({ account })) + }, + showCreateGroup() { + dispatch(user.actions.showCreateGroup({ redirectAfter: false })) + }, + deleteGroup: ({ owner, name, password, + onSuccess, onError }) => { + const opData = { + owner, + name, + extensions: [], + } + + const json = JSON.stringify(['private_group_delete', opData]) + + dispatch(transaction.actions.broadcastOperation({ + type: 'custom_json', + operation: { + id: 'private_message', + required_auths: [owner], + json, + }, + username: owner, + password, + successCallback: onSuccess, + errorCallback: (err, errStr) => { + console.error(err) + if (onError) onError(err, errStr) + }, + })); + } + }) +)(MyGroups) diff --git a/src/components/modules/groups/MyGroups.scss b/src/components/modules/groups/MyGroups.scss new file mode 100644 index 00000000..2bfb29ea --- /dev/null +++ b/src/components/modules/groups/MyGroups.scss @@ -0,0 +1,27 @@ +.MyGroups { + .group-logo { + width: 67px; + img { + width: 48px; + height: 48px; + } + } + .group-title { + font-weight: bold; + font-size: 110%; + @include themify($themes) { + color: themed('textColorPrimary'); + } + } + .group-buttons { + float: right; + padding-top: 0.8rem; + .button { + vertical-align: middle; + margin-bottom: 0px; + } + .DropdownMenu.show > .VerticalMenu { + transform: translateX(-100%); + } + } +} diff --git a/src/components/pages/Messages.jsx b/src/components/pages/Messages.jsx index b46ef4e0..b4bc6e77 100644 --- a/src/components/pages/Messages.jsx +++ b/src/components/pages/Messages.jsx @@ -801,8 +801,13 @@ class Messages extends React.Component { this.props.logout(username) } + const openMyGroups = (e) => { + e.preventDefault() + this.props.showMyGroups() + } + let user_menu = [ - {link: accountLink, extLink: 'blogs', icon: 'voters', value: tt('g.groups') + (isSmall ? (' @' + username) : ''), addon: }, + {link: '#', onClick: openMyGroups, icon: 'voters', value: tt('g.groups') + (isSmall ? (' @' + username) : '') }, {link: accountLink, extLink: 'blogs', icon: 'new/blogging', value: tt('g.blog'), addon: }, {link: mentionsLink, extLink: 'blogs', icon: 'new/mention', value: tt('g.mentions'), addon: }, {link: donatesLink, extLink: 'wallet', icon: 'editor/coin', value: tt('g.rewards'), addon: }, @@ -1023,6 +1028,9 @@ export default withRouter(connect( } return true; }, + + showMyGroups: () => dispatch(user.actions.showMyGroups()), + fetchState: (to) => { const pathname = '/' + (to ? ('@' + to) : ''); dispatch({type: 'FETCH_STATE', payload: { diff --git a/src/locales/en.json b/src/locales/en.json index 3356a2e2..75f6df4d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -134,6 +134,12 @@ "image_wrong": "Cannot load this image.", "image_timeout": "Cannot load this image, it is loading too long..." }, + "my_groups_jsx": { + "title": "My Groups", + "empty": "You have not any groups yet. ", + "empty2": "You can ", + "create": "create your own group" + }, "emoji_i18n": { "categoriesLabel": "Категории", "emojiUnsupportedMessage": "Ваш браузер не поддерживает эмодзи.", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index 5e9db9e1..3e5003cb 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -48,7 +48,7 @@ "login_to_comment": "Пожалуйста, авторизуйтесь для выполнения следующих действий", "login_to_message": "Пожалуйста, авторизуйтесь с memo-ключом или паролем", "login_active": "Введите пароль (или активный ключ)", - "is_is_for_operation": "Это нужно для отправки операции.", + "is_is_for_operation": "Это нужно для отправки операции", "login_with_active_key_USERNAME": "Пожалуйста, авторизуйтесь Вашим активным ключом\n\nИмя:\n%(USERNAME)s\n\nActive-ключ:\n", "login_to_your_steem_account": "Войти в свой Голос аккаунт", "posting": "Постинг", @@ -136,6 +136,15 @@ "image_wrong": "Не удается загрузить картинку.", "image_timeout": "Не удается загрузить картинку, она загружается слишком долго..." }, + "my_groups_jsx": { + "title": "Мои группы", + "empty": "У вас пока нет групп. ", + "empty2": "Вы можете ", + "create": "создать свою группу", + "create_more": "Создать еще группу", + "edit": "Изменить", + "login_hint_GROUP": "(удаления группы \"%(GROUP)s\")" + }, "emoji_i18n": { "categoriesLabel": "Категории", "emojiUnsupportedMessage": "Ваш браузер не поддерживает эмодзи.", @@ -256,6 +265,7 @@ "settings": "Настройки", "sign_up": "Регистрация", "username_does_not_exist": "Такого имени не существует", - "wallet": "Кошелек" + "wallet": "Кошелек", + "wait": "Ждите..." } } \ No newline at end of file diff --git a/src/redux/FetchDataSaga.js b/src/redux/FetchDataSaga.js index 9a947b96..caa8fb5f 100644 --- a/src/redux/FetchDataSaga.js +++ b/src/redux/FetchDataSaga.js @@ -8,6 +8,7 @@ export function* fetchDataWatches () { yield fork(watchLocationChange) yield fork(watchFetchState) yield fork(watchFetchUiaBalances) + yield fork(watchFetchMyGroups) } export function* watchLocationChange() { @@ -109,3 +110,25 @@ export function* fetchUiaBalances({ payload: { account } }) { console.error('fetchUiaBalances', err) } } + +export function* watchFetchMyGroups() { + yield takeLatest('global/FETCH_MY_GROUPS', fetchMyGroups) +} + +export function* fetchMyGroups({ payload: { account } }) { + try { + const groups = yield call([api, api.getGroupsAsync], { + member: account, + member_types: ['pending', 'member', 'moder', 'admin'], + start_group: '', + limit: 100, + with_members: { + accounts: [account] + } + }) + + yield put(g.actions.receiveMyGroups({ groups })) + } catch (err) { + console.error('fetchMyGroups', err) + } +} diff --git a/src/redux/GlobalReducer.js b/src/redux/GlobalReducer.js index e9845952..9ab16b7d 100644 --- a/src/redux/GlobalReducer.js +++ b/src/redux/GlobalReducer.js @@ -264,5 +264,15 @@ export default createModule({ return state.set('assets', fromJS(assets)) }, }, + { + action: 'FETCH_MY_GROUPS', + reducer: state => state + }, + { + action: 'RECEIVE_MY_GROUPS', + reducer: (state, { payload: { groups } }) => { + return state.set('my_groups', fromJS(groups)) + }, + }, ], }) diff --git a/src/redux/UserReducer.js b/src/redux/UserReducer.js index e60902e3..ead90c9b 100644 --- a/src/redux/UserReducer.js +++ b/src/redux/UserReducer.js @@ -6,6 +6,7 @@ const defaultState = fromJS({ show_login_modal: false, show_donate_modal: false, show_create_group_modal: false, + show_my_groups_modal: false, show_app_download_modal: false, loginLoading: false, pub_keys_used: null, @@ -129,8 +130,14 @@ export default createModule({ { action: 'HIDE_CONNECTION_ERROR_MODAL', reducer: state => state.set('hide_connection_error_modal', true) }, { action: 'SHOW_DONATE', reducer: state => state.set('show_donate_modal', true) }, { action: 'HIDE_DONATE', reducer: state => state.set('show_donate_modal', false) }, - { action: 'SHOW_CREATE_GROUP', reducer: state => state.set('show_create_group_modal', true) }, + { action: 'SHOW_CREATE_GROUP', reducer: (state, { payload: { redirectAfter }}) => { + state = state.set('show_create_group_modal', true) + state = state.set('create_group_redirect_after', redirectAfter) + return state + }}, { action: 'HIDE_CREATE_GROUP', reducer: state => state.set('show_create_group_modal', false) }, + { action: 'SHOW_MY_GROUPS', reducer: state => state.set('show_my_groups_modal', true) }, + { action: 'HIDE_MY_GROUPS', reducer: state => state.set('show_my_groups_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/groups.js b/src/utils/groups.js new file mode 100644 index 00000000..2de29c40 --- /dev/null +++ b/src/utils/groups.js @@ -0,0 +1,28 @@ +import { proxifyImageUrlWithStrip } from 'app/utils/ProxifyUrl' + +const getGroupMeta = (json_metadata) => { + let meta + if (json_metadata) { + meta = JSON.parse(json_metadata) + } + meta = meta || {} // node allows null, object, array... or empty json_metadata + return meta +} + +const getGroupLogo = (json_metadata) => { + const meta = getGroupMeta(json_metadata) + + let { logo } = meta + if (logo && /^(https?:)\/\//.test(logo)) { + const size = '75x75' + logo = proxifyImageUrlWithStrip(logo, size) + } else { + logo = require('app/assets/images/user.png') + } + return logo +} + +export { + getGroupMeta, + getGroupLogo +}