Skip to content

Commit

Permalink
HF 30 - Lettered avatars
Browse files Browse the repository at this point in the history
  • Loading branch information
1aerostorm committed Sep 10, 2024
1 parent 515b8a0 commit 1e2ae41
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 14 deletions.
24 changes: 20 additions & 4 deletions src/components/elements/Userpic.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +23,7 @@ class Userpic extends Component {
static defaultProps = {
width: 48,
height: 48,
disabled: false,
hideIfDefault: false,
showProgress: false
}
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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 = <LetteredAvatar name={account} size={height}
backgroundColor={disabled ? '#999' : undefined} />
}

if (votingPower) {
Expand All @@ -114,7 +128,9 @@ class Userpic extends Component {
<div className="Userpic__badge" title={tt('g.reputation')}>{reputation}</div>
</div>
} else {
return <div className="Userpic" title={title} style={style} onClick={onClick} />
return <div className="Userpic" title={title} style={style} onClick={onClick}>
{lettered}
</div>
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
118 changes: 114 additions & 4 deletions src/components/elements/messages/AuthorDropdown/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 <div>{author}</div>
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 = <button className={cn('button hollow small btn', {
alert: !isBanned,
banned: isBanned,
})} onClick={e => this.btnClick(e, isBanned)} disabled={this.state.submitting}>
<Icon name='ionicons/ban' />
<span className='title'>{isBanned ? tt('group_members_jsx.unban') :
tt('group_members_jsx.ban')}</span>
</button>
}

return <div className='AuthorDropdown'>
<div className='link'>
<ExtLink to={'@' + author} service='blogs'>{'@' + author}</ExtLink>
</div>
{lastSeen ? <div className='last-seen'>
{tt('messages.last_seen')}
<TimeAgoWrapper date={`${lastSeen}`} />
</div> : lastSeen}
<div className='btns'>
{banBtn}
</div>
</div>
}
}

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))
17 changes: 17 additions & 0 deletions src/components/elements/messages/LetteredAvatar/LetteredAvatar.css
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 28 additions & 0 deletions src/components/elements/messages/LetteredAvatar/colors.js
Original file line number Diff line number Diff line change
@@ -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
]
98 changes: 98 additions & 0 deletions src/components/elements/messages/LetteredAvatar/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={`lettered-avatar-wrapper dark`}
style={styles}
aria-label={name}
>
<div className="lettered-avatar" style={{ fontSize }}>{initials}</div>
</div>
);
}
}

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;
Loading

0 comments on commit 1e2ae41

Please sign in to comment.