Added JazzIcon

This commit is contained in:
Mikunj 2019-09-05 09:24:24 +10:00
parent 500a88dbab
commit a9189979e1
6 changed files with 254 additions and 7 deletions

View File

@ -1,7 +1,6 @@
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const sha224 = require('js-sha512').sha512_224;
const { app } = require('electron').remote;
@ -36,11 +35,10 @@ const writePNGImage = (base64String, pubKey) => {
const imagePath = getImagePath(pubKey);
fs.writeFileSync(imagePath, base64String, 'base64');
return imagePath;
}
};
module.exports = {
writePNGImage,
getOrCreateImagePath,
getImagePath,
hasImage,
removeImage,

View File

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { JazzIcon } from './JazzIcon';
import { getInitials } from '../util/getInitials';
import { LocalizerType } from '../types/Util';
@ -22,7 +23,7 @@ interface State {
imageBroken: boolean;
}
export class Avatar extends React.Component<Props, State> {
export class Avatar extends React.PureComponent<Props, State> {
public handleImageErrorBound: () => void;
public constructor(props: Props) {
@ -43,6 +44,22 @@ export class Avatar extends React.Component<Props, State> {
});
}
public renderIdenticon() {
const { phoneNumber, borderColor, borderWidth, size } = this.props;
if (!phoneNumber) {
return this.renderNoImage();
}
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} />;
}
public renderImage() {
const {
avatarPath,
@ -129,10 +146,18 @@ export class Avatar extends React.Component<Props, State> {
}
public render() {
const { avatarPath, color, size, noteToSelf } = this.props;
const {
avatarPath,
color,
size,
noteToSelf,
conversationType,
} = this.props;
const { imageBroken } = this.state;
const hasImage = !noteToSelf && avatarPath && !imageBroken;
// If it's a direct conversation then we must have an identicon
const hasAvatar = avatarPath || conversationType === 'direct';
const hasImage = !noteToSelf && hasAvatar && !imageBroken;
if (size !== 28 && size !== 36 && size !== 48 && size !== 80) {
throw new Error(`Size ${size} is not supported!`);
@ -147,11 +172,22 @@ export class Avatar extends React.Component<Props, State> {
!hasImage ? `module-avatar--${color}` : null
)}
>
{hasImage ? this.renderImage() : this.renderNoImage()}
{hasImage ? this.renderAvatarOrIdenticon() : this.renderNoImage()}
</div>
);
}
private renderAvatarOrIdenticon() {
const { avatarPath, conversationType } = this.props;
// If it's a direct conversation then we must have an identicon
const hasAvatar = avatarPath || conversationType === 'direct';
return hasAvatar && avatarPath
? this.renderImage()
: this.renderIdenticon();
}
private getBorderStyle(color?: string, width?: number) {
const borderWidth = typeof width === 'number' ? width : 3;

View File

@ -0,0 +1,165 @@
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 => {
const color = Color(hex);
color.rotate(amount);
return color.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

@ -0,0 +1,25 @@
import React from 'react';
const styles = {
borderRadius: '50px',
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

@ -0,0 +1,21 @@
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

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