diff --git a/src/components/elements/Userpic.jsx b/src/components/elements/Userpic.jsx
index 32b410f9..a685b22f 100644
--- a/src/components/elements/Userpic.jsx
+++ b/src/components/elements/Userpic.jsx
@@ -6,6 +6,7 @@ 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 = {
@@ -49,6 +50,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 +60,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 +95,19 @@ class Userpic extends Component {
}
render() {
- const { title, width, height, votingPower, reputation, hideReputationForSmall, showProgress, onClick } = this.props
+ const { account, 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 (true) {
+ lettered =
}
if (votingPower) {
@@ -114,7 +125,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..70f2e1f2
--- /dev/null
+++ b/src/components/elements/messages/LetteredAvatar/index.jsx
@@ -0,0 +1,96 @@
+// 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)) {
+ let index = initials.charCodeAt() - 65;
+
+ if (backgroundColor) {
+ defaultBackground = backgroundColor;
+ } else if (backgroundColors && backgroundColors.length) {
+ let i = sumChars(name) % backgroundColors.length;
+ defaultBackground = backgroundColors[i];
+ } else {
+ defaultBackground = defaultColors[index];
+ }
+ } 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 (
+
+ );
+ }
+}
+
+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..d3359e38 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,29 @@ 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 +164,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
},
},