diff --git a/src/components/elements/Userpic.jsx b/src/components/elements/Userpic.jsx index 32b410f9..3f6e089c 100644 --- a/src/components/elements/Userpic.jsx +++ b/src/components/elements/Userpic.jsx @@ -6,10 +6,12 @@ import tt from 'counterpart' import CircularProgress from './CircularProgress' import { proxifyImageUrlWithStrip } from 'app/utils/ProxifyUrl'; +import LetteredAvatar from 'app/components/elements/messages/LetteredAvatar' class Userpic extends Component { static propTypes = { account: PropTypes.string, + disabled: PropTypes.bool, votingPower: PropTypes.number, showProgress: PropTypes.bool, progressClass: PropTypes.string, @@ -21,6 +23,7 @@ class Userpic extends Component { static defaultProps = { width: 48, height: 48, + disabled: false, hideIfDefault: false, showProgress: false } @@ -49,6 +52,8 @@ class Userpic extends Component { } } + let isDefault = false + if (url && /^(https?:)\/\//.test(url)) { const size = width && width > 75 ? '200x200' : '75x75'; url = proxifyImageUrlWithStrip(url, size); @@ -57,9 +62,10 @@ class Userpic extends Component { return null; } url = require('app/assets/images/user.png'); + isDefault = true } - return url + return { url, isDefault } } votingPowerToPercents = power => power / 100 @@ -91,12 +97,20 @@ class Userpic extends Component { } render() { - const { title, width, height, votingPower, reputation, hideReputationForSmall, showProgress, onClick } = this.props + const { account, disabled, title, width, height, votingPower, reputation, hideReputationForSmall, showProgress, onClick } = this.props + + const { url, isDefault } = this.extractUrl() const style = { width: `${width}px`, height: `${height}px`, - backgroundImage: `url(${this.extractUrl()})` + backgroundImage: `url(${url})` + } + + let lettered + if (isDefault) { + lettered = } if (votingPower) { @@ -114,7 +128,9 @@ class Userpic extends Component {
{reputation}
} else { - return
+ return
+ {lettered} +
} } } diff --git a/src/components/elements/messages/AuthorDropdown/AuthorDropdown.scss b/src/components/elements/messages/AuthorDropdown/AuthorDropdown.scss new file mode 100644 index 00000000..f5437490 --- /dev/null +++ b/src/components/elements/messages/AuthorDropdown/AuthorDropdown.scss @@ -0,0 +1,28 @@ +.AuthorDropdown { + padding: 0.5rem; + + .link { + font-weight: bold; + } + + .last-seen { + font-size: 95%; + } + + .btns { + min-width: 250px; + width: 100%; + } + .btn { + float: right; + margin-right: 0.5rem !important; + margin-top: 0.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + + .title { + vertical-align: middle; + margin-left: 5px; + } + } +} diff --git a/src/components/elements/messages/AuthorDropdown/index.jsx b/src/components/elements/messages/AuthorDropdown/index.jsx index 090a7422..3456bd56 100644 --- a/src/components/elements/messages/AuthorDropdown/index.jsx +++ b/src/components/elements/messages/AuthorDropdown/index.jsx @@ -3,6 +3,15 @@ import {connect} from 'react-redux' import { withRouter } from 'react-router' import { Link } from 'react-router-dom' import tt from 'counterpart' +import cn from 'classnames' + +import ExtLink from 'app/components/elements/ExtLink' +import Icon from 'app/components/elements/Icon' +import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper' +import transaction from 'app/redux/TransactionReducer' +import { getRoleInGroup } from 'app/utils/groups' + +import './AuthorDropdown.scss' class AuthorDropdown extends React.Component { constructor(props) { @@ -11,20 +20,121 @@ class AuthorDropdown extends React.Component { } } + btnClick = (e, isBanned) => { + e.preventDefault() + const { author, username, the_group } = this.props + if (!the_group) { + return + } + this.setState({submitting: true}) + + const member_type = isBanned ? 'member' : 'banned' + this.props.groupMember({ + requester: username, group: the_group.name, + member: author, + member_type, + onSuccess: () => { + this.setState({submitting: false}) + document.body.click() + }, + onError: (err, errStr) => { + this.setState({submitting: false}) + alert(errStr) + } + }) + } + render() { - const { author } = this.props - return
{author}
+ const { author, authorAcc, the_group, account } = this.props + + let lastSeen + if (authorAcc && authorAcc.last_seen) { + lastSeen = authorAcc.last_seen + } + + let isModer + if (the_group && account) { + const { amModer } = getRoleInGroup(the_group, account.name) + isModer = amModer + } + + let banBtn + if (isModer) { + const isBanned = authorAcc && authorAcc.member_type === 'banned' + banBtn = + } + + return
+
+ {'@' + author} +
+ {lastSeen ?
+ {tt('messages.last_seen')} + +
: lastSeen} +
+ {banBtn} +
+
} } export default withRouter(connect( (state, ownProps) => { + const currentUser = state.user.get('current') + const accounts = state.global.get('accounts') + + let authorAcc = accounts.get(ownProps.author) + authorAcc = authorAcc ? authorAcc.toJS() : null + + let the_group = state.global.get('the_group') + if (the_group && the_group.toJS) the_group = the_group.toJS() + + const username = state.user.getIn(['current', 'username']) return { + username, + authorAcc, + the_group, + account: currentUser && accounts && accounts.toJS()[currentUser.get('username')], } }, dispatch => ({ - deleteGroup: ({ owner, name, password, }) => { - } + 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) + }, + })); + }, }), )(AuthorDropdown)) diff --git a/src/components/elements/messages/LetteredAvatar/LetteredAvatar.css b/src/components/elements/messages/LetteredAvatar/LetteredAvatar.css new file mode 100644 index 00000000..76433114 --- /dev/null +++ b/src/components/elements/messages/LetteredAvatar/LetteredAvatar.css @@ -0,0 +1,17 @@ +.lettered-avatar-wrapper { + text-align: center; +} + +.lettered-avatar-wrapper.light { + color: #000; +} + +.lettered-avatar-wrapper.dark { + color: #fff; +} + +.lettered-avatar { + white-space: nowrap; + overflow: hidden; + font-size: 24px; +} diff --git a/src/components/elements/messages/LetteredAvatar/colors.js b/src/components/elements/messages/LetteredAvatar/colors.js new file mode 100644 index 00000000..05f85bd1 --- /dev/null +++ b/src/components/elements/messages/LetteredAvatar/colors.js @@ -0,0 +1,28 @@ +export const defaultColors = [ + '#e25f51',// A + '#f26091',// B + '#bb65ca',// C + '#9572cf',// D + '#7884cd',// E + '#5b95f9',// F + '#48c2f9',// G + '#45d0e2',// H + '#48b6ac',// I + '#52bc89',// J + '#9bce5f',// K + '#d4e34a',// L + '#feda10',// M + '#f7c000',// N + '#ffa800',// O + '#ff8a60',// P + '#c2c2c2',// Q + '#8fa4af',// R + '#a2887e',// S + '#a3a3a3',// T + '#afb5e2',// U + '#b39bdd',// V + '#c2c2c2',// W + '#7cdeeb',// X + '#bcaaa4',// Y + '#add67d'// Z +] diff --git a/src/components/elements/messages/LetteredAvatar/index.jsx b/src/components/elements/messages/LetteredAvatar/index.jsx new file mode 100644 index 00000000..909cdda3 --- /dev/null +++ b/src/components/elements/messages/LetteredAvatar/index.jsx @@ -0,0 +1,98 @@ +// Copyright (c) https://github.com/ipavlyukov/react-lettered-avatar +// The MIT License +import React, { Component } from "react"; +import PropTypes from "prop-types"; + +import { defaultColors } from "./colors"; + +import "./LetteredAvatar.css"; + +class LetteredAvatar extends Component { + render() { + const { + name, + color, + backgroundColors, + backgroundColor, + radius, + size, + } = this.props; + let initials = "", + defaultBackground = ""; + + const sumChars = (str) => { + let sum = 0; + for (let i = 0; i < str.length; i++) { + sum += str.charCodeAt(i); + } + return sum; + }; + + // GET AND SET INITIALS + const names = name.split(" "); + if (names.length === 1) { + initials = names[0].substring(0, 1).toUpperCase(); + } else if (names.length > 1) { + names.forEach((n, i) => { + initials += names[i].substring(0, 1).toUpperCase(); + }); + } + + // SET BACKGROUND COLOR + if (/[A-Z]/.test(initials)) { + if (backgroundColor) { + defaultBackground = backgroundColor; + } else { + let colors = backgroundColors + if (backgroundColors && backgroundColors.length) { + colors = backgroundColors + } else { + colors = defaultColors + } + let i = sumChars(name) % colors.length + defaultBackground = colors[i] + } + } else if (/[\d]/.test(initials)) { + defaultBackground = defaultColors[parseInt(initials)]; + } else { + defaultBackground = "#051923"; + } + + const fontSize = size / 2 + + const styles = { + color, + backgroundColor: `${defaultBackground}`, + width: size, + height: size, + lineHeight: `${size}px`, + borderRadius: `${radius || radius === 0 ? radius : size}px`, + fontSize: `100%`, + }; + return ( +
+
{initials}
+
+ ); + } +} + +LetteredAvatar.propTypes = { + name: PropTypes.string.isRequired, + color: PropTypes.string, + backgroundColor: PropTypes.string, + radius: PropTypes.number, + size: PropTypes.number, +}; + +LetteredAvatar.defaultProps = { + name: "Lettered Avatar", + color: "", + size: 48, +}; + +export default LetteredAvatar; diff --git a/src/components/elements/messages/Message/Message.css b/src/components/elements/messages/Message/Message.css index abde29ef..a874a7d8 100644 --- a/src/components/elements/messages/Message/Message.css +++ b/src/components/elements/messages/Message/Message.css @@ -26,6 +26,10 @@ color: #0078C4; padding-bottom: 3px; } +.msgs-message .bubble-container .author.banned { + text-decoration: line-through; + color: #999; +} .msgs-message .bubble-container .avatar { width: 42px; margin-top: 14px; diff --git a/src/components/elements/messages/Message/index.jsx b/src/components/elements/messages/Message/index.jsx index 392f081f..692b7bf3 100644 --- a/src/components/elements/messages/Message/index.jsx +++ b/src/components/elements/messages/Message/index.jsx @@ -1,7 +1,9 @@ import React from 'react'; +import {connect} from 'react-redux' import { Fade } from 'react-foundation-components/lib/global/fade' import { LinkWithDropdown } from 'react-foundation-components/lib/global/dropdown' import tt from 'counterpart'; +import cn from 'classnames' import { Asset } from 'golos-lib-js/lib/utils' import AuthorDropdown from 'app/components/elements/messages/AuthorDropdown' @@ -11,7 +13,7 @@ import { displayQuoteMsg } from 'app/utils/MessageUtils'; import { proxifyImageUrl } from 'app/utils/ProxifyUrl'; import './Message.css'; -export default class Message extends React.Component { +class Message extends React.Component { onMessageSelect = (idx, event) => { if (this.props.onMessageSelect) { const { data, selected } = this.props; @@ -107,23 +109,30 @@ export default class Message extends React.Component { let author let avatar if (!isMine && group) { + const { authorAcc } = this.props + const isBanned = authorAcc && authorAcc.member_type === 'banned' + if (startsSequence) { - author =
+ author =
{from}
avatar = } transition={Fade} > - + } - avatar =
+ + avatar =
{avatar}
} @@ -156,3 +165,18 @@ export default class Message extends React.Component { ); } } + +export default connect( + (state, ownProps) => { + const accounts = state.global.get('accounts') + + let authorAcc = ownProps.data && accounts.get(ownProps.data.from) + authorAcc = authorAcc ? authorAcc.toJS() : null + + return { + authorAcc, + } + }, + dispatch => ({ + }), +)(Message) diff --git a/src/redux/FetchDataSaga.js b/src/redux/FetchDataSaga.js index d931dbf7..13def8b3 100644 --- a/src/redux/FetchDataSaga.js +++ b/src/redux/FetchDataSaga.js @@ -119,6 +119,7 @@ export function* fetchState(location_change_action) { let query = { group: path, cache: Object.keys(space), + accounts: true, contacts: { owner: account, limit: 100, cache: Object.keys(conCache), @@ -137,6 +138,13 @@ export function* fetchState(location_change_action) { } else { thRes = yield call(getThread) } + + if (thRes.accounts) { + for (const [n, acc] of Object.entries(thRes.accounts)) { + state.accounts[n] = acc + } + } + console.log('proc:' + thRes._dec_processed) if (the_group && thRes.error) { the_group.error = thRes.error diff --git a/src/redux/GlobalReducer.js b/src/redux/GlobalReducer.js index 8f9cca60..c9b2db3e 100644 --- a/src/redux/GlobalReducer.js +++ b/src/redux/GlobalReducer.js @@ -469,6 +469,12 @@ export default createModule({ } new_state = updateInMyGroups(new_state, group, groupUpdater) new_state = updateTheGroup(new_state, group, groupUpdater) + new_state = new_state.updateIn(['accounts', member], + Map(), + acc => { + acc = acc.set('member_type', member_type) + return acc + }) return new_state }, },