remove JazzIcon and use instead a colored circle with Initial

This commit is contained in:
Audric Ackermann 2020-09-10 15:53:36 +10:00
parent 1da4ae69c2
commit 28eec84783
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
7 changed files with 122 additions and 267 deletions

View File

@ -1,9 +1,9 @@
import React from 'react';
import classNames from 'classnames';
import { JazzIcon } from './JazzIcon';
import { getInitials } from '../util/getInitials';
import { LocalizerType } from '../types/Util';
import { AvatarPlaceHolder } from './AvatarPlaceHolder';
interface Props {
avatarPath?: string;
@ -48,30 +48,25 @@ export class Avatar extends React.PureComponent<Props, State> {
}
public renderIdenticon() {
const { phoneNumber, borderColor, borderWidth, size } = this.props;
const { phoneNumber, size, name, profileName } = this.props;
if (!phoneNumber) {
return this.renderNoImage();
throw new Error('Empty phoneNumber for identifcon');
}
const borderStyle = this.getBorderStyle(borderColor, borderWidth);
// Generate the seed
const hash = phoneNumber.substring(0, 12);
const seed = parseInt(hash, 16) || 1234;
return <JazzIcon seed={seed} diameter={size} paperStyles={borderStyle} />;
const userName = profileName || name;
return (
<AvatarPlaceHolder
phoneNumber={phoneNumber}
diameter={size}
name={userName}
colors={this.getAvatarColors()}
/>
);
}
public renderImage() {
const {
avatarPath,
name,
phoneNumber,
profileName,
borderColor,
borderWidth,
} = this.props;
const { avatarPath, name, phoneNumber, profileName } = this.props;
const { imageBroken } = this.state;
if (!avatarPath || imageBroken) {
@ -82,11 +77,8 @@ export class Avatar extends React.PureComponent<Props, State> {
!name && profileName ? ` ~${profileName}` : ''
}`;
const borderStyle = this.getBorderStyle(borderColor, borderWidth);
return (
<img
style={borderStyle}
onError={this.handleImageErrorBound}
alt={window.i18n('contactAvatarAlt', [title])}
src={avatarPath}
@ -95,14 +87,7 @@ export class Avatar extends React.PureComponent<Props, State> {
}
public renderNoImage() {
const {
conversationType,
name,
noteToSelf,
size,
borderColor,
borderWidth,
} = this.props;
const { conversationType, name, noteToSelf, size } = this.props;
const initials = getInitials(name);
const isGroup = conversationType === 'group';
@ -119,8 +104,6 @@ export class Avatar extends React.PureComponent<Props, State> {
);
}
const borderStyle = this.getBorderStyle(borderColor, borderWidth);
if (!isGroup && initials) {
return (
<div
@ -128,7 +111,6 @@ export class Avatar extends React.PureComponent<Props, State> {
'module-avatar__label',
`module-avatar__label--${size}`
)}
style={borderStyle}
>
{initials}
</div>
@ -142,24 +124,17 @@ export class Avatar extends React.PureComponent<Props, State> {
`module-avatar__icon--${conversationType}`,
`module-avatar__icon--${size}`
)}
style={borderStyle}
/>
);
}
public render() {
const {
avatarPath,
color,
size,
noteToSelf,
conversationType,
} = this.props;
const { avatarPath, color, size, conversationType } = this.props;
const { imageBroken } = this.state;
// If it's a direct conversation then we must have an identicon
const hasAvatar = avatarPath || conversationType === 'direct';
const hasImage = !noteToSelf && hasAvatar && !imageBroken;
const hasImage = hasAvatar && !imageBroken;
if (
size !== 28 &&
@ -207,17 +182,9 @@ export class Avatar extends React.PureComponent<Props, State> {
: this.renderIdenticon();
}
private getBorderStyle(_color?: string, _width?: number) {
//const borderWidth = typeof width === 'number' ? width : 3;
// no border at all for now
return undefined;
/* return color
? {
borderColor: color,
borderStyle: 'solid',
borderWidth: borderWidth,
}
: undefined; */
private getAvatarColors(): Array<string> {
// const theme = window.Events.getThemedSettings();
// defined in session-android as `profile_picture_placeholder_colors`
return ['#5ff8b0', '#26cdb9', '#f3c615', '#fcac5a'];
}
}

View File

@ -0,0 +1,101 @@
import React from 'react';
import { getInitials } from '../../util/getInitials';
interface Props {
diameter: number;
phoneNumber: string;
colors: Array<string>;
name?: string;
}
interface State {
sha512Seed?: string;
}
export class AvatarPlaceHolder extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
sha512Seed: undefined,
};
}
public componentDidMount() {
void this.sha512(this.props.phoneNumber).then((sha512Seed: string) => {
this.setState({ sha512Seed });
});
}
public componentDidUpdate(prevProps: Props, prevState: State) {
if (this.props.phoneNumber === prevProps.phoneNumber) {
return;
}
void this.sha512(this.props.phoneNumber).then((sha512Seed: string) => {
this.setState({ sha512Seed });
});
}
public render() {
if (!this.state.sha512Seed) {
return <></>;
}
const { colors, diameter, phoneNumber, name } = this.props;
const r = diameter / 2;
const initial =
getInitials(name)?.toLocaleUpperCase() ||
getInitials(phoneNumber)?.toLocaleUpperCase() ||
'0';
const viewBox = `0 0 ${diameter} ${diameter}`;
const fontSize = diameter * 0.5;
// Generate the seed simulate the .hashCode as Java
const hash = parseInt(this.state.sha512Seed.substring(0, 12), 16) || 0;
const bgColorIndex = hash % colors.length;
const bgColor = colors[bgColorIndex];
return (
<svg viewBox={viewBox}>
<g id="UrTavla">
<circle
cx={r}
cy={r}
r={r}
fill={bgColor}
shape-rendering="geometricPrecision"
// stroke="black"
// stroke-width="1"
/>
<text
font-size={fontSize}
x="50%"
y="50%"
fill="white"
text-anchor="middle"
stroke="white"
stroke-width={1}
alignment-baseline="central"
>
{initial}
</text>
</g>
</svg>
);
}
private async sha512(str: string) {
// tslint:disable-next-line: await-promise
const buf = await crypto.subtle.digest(
'SHA-512',
new TextEncoder().encode(str)
);
// tslint:disable: prefer-template restrict-plus-operands
return Array.prototype.map
.call(new Uint8Array(buf), (x: any) => ('00' + x.toString(16)).slice(-2))
.join('');
}
}

View File

@ -0,0 +1 @@
export { AvatarPlaceHolder } from './AvatarPlaceHolder';

View File

@ -1,166 +0,0 @@
// Modified from https://github.com/redlanta/react-jazzicon
import React from 'react';
import Color from 'color';
import { Paper } from './Paper';
import { RNG } from './RNG';
const defaultColors = [
'#01888c', // teal
'#fc7500', // bright orange
'#034f5d', // dark teal
'#E784BA', // light pink
'#81C8B6', // bright green
'#c7144c', // raspberry
'#f3c100', // goldenrod
'#1598f2', // lightning blue
'#2465e1', // sail blue
'#f19e02', // gold
];
const isColor = (str: string) => /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(str);
const isColors = (arr: Array<string>) => {
if (!Array.isArray(arr)) {
return false;
}
if (arr.every(value => typeof value === 'string' && isColor(value))) {
return true;
}
return false;
};
interface Props {
diameter: number;
seed: number;
paperStyles?: Object;
svgStyles?: Object;
shapeCount?: number;
wobble?: number;
colors?: Array<string>;
}
// tslint:disable-next-line no-http-string
const svgns = 'http://www.w3.org/2000/svg';
const shapeCount = 4;
const wobble = 30;
export class JazzIcon extends React.PureComponent<Props> {
public render() {
const {
colors: customColors,
diameter,
paperStyles,
seed,
svgStyles,
} = this.props;
const generator = new RNG(seed);
const colors = customColors || defaultColors;
const newColours = this.hueShift(
this.colorsForIcon(colors).slice(),
generator
);
const shapesArr = Array(shapeCount).fill(null);
const shuffledColours = this.shuffleArray(newColours, generator);
return (
<Paper color={shuffledColours[0]} diameter={diameter} style={paperStyles}>
<svg
xmlns={svgns}
x="0"
y="0"
height={diameter}
width={diameter}
style={svgStyles}
>
{shapesArr.map((_, i) =>
this.genShape(
shuffledColours[i + 1],
diameter,
i,
shapeCount - 1,
generator
)
)}
</svg>
</Paper>
);
}
private hueShift(colors: Array<string>, generator: RNG) {
const amount = generator.random() * 30 - wobble / 2;
return colors.map(hex =>
Color(hex)
.rotate(amount)
.hex()
);
}
private genShape(
colour: string,
diameter: number,
i: number,
total: number,
generator: RNG
) {
const center = diameter / 2;
const firstRot = generator.random();
const angle = Math.PI * 2 * firstRot;
const velocity =
(diameter / total) * generator.random() + (i * diameter) / total;
const tx = Math.cos(angle) * velocity;
const ty = Math.sin(angle) * velocity;
const translate = `translate(${tx} ${ty})`;
// Third random is a shape rotation on top of all of that.
const secondRot = generator.random();
const rot = firstRot * 360 + secondRot * 180;
const rotate = `rotate(${rot.toFixed(1)} ${center} ${center})`;
const transform = `${translate} ${rotate}`;
return (
<rect
key={i}
x="0"
y="0"
rx="0"
ry="0"
height={diameter}
width={diameter}
transform={transform}
fill={colour}
/>
);
}
private colorsForIcon(arr: Array<string>) {
if (isColors(arr)) {
return arr;
}
return defaultColors;
}
private shuffleArray<T>(array: Array<T>, generator: RNG) {
let currentIndex = array.length;
const newArray = [...array];
// While there remain elements to shuffle...
while (currentIndex > 0) {
// Pick a remaining element...
const randomIndex = generator.next() % currentIndex;
currentIndex -= 1;
// And swap it with the current element.
const temporaryValue = newArray[currentIndex];
newArray[currentIndex] = newArray[randomIndex];
newArray[randomIndex] = temporaryValue;
}
return newArray;
}
}

View File

@ -1,25 +0,0 @@
import React from 'react';
const styles = {
borderRadius: '50%',
display: 'inline-block',
margin: 0,
overflow: 'hidden',
padding: 0,
};
// @ts-ignore
export const Paper = ({ children, color, diameter, style: styleOverrides }) => (
<div
className="paper"
style={{
...styles,
backgroundColor: color,
height: diameter,
width: diameter,
...(styleOverrides || {}),
}}
>
{children}
</div>
);

View File

@ -1,21 +0,0 @@
export class RNG {
private _seed: number;
constructor(seed: number) {
this._seed = seed % 2147483647;
if (this._seed <= 0) {
this._seed += 2147483646;
}
}
public next() {
return (this._seed = (this._seed * 16807) % 2147483647);
}
public nextFloat() {
return (this.next() - 1) / 2147483646;
}
public random() {
return this.nextFloat();
}
}

View File

@ -1,2 +0,0 @@
import { JazzIcon } from './JazzIcon';
export { JazzIcon };