Added JazzIcon
This commit is contained in:
parent
500a88dbab
commit
a9189979e1
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import { JazzIcon } from './JazzIcon';
|
||||
export { JazzIcon };
|
Loading…
Reference in New Issue