From a9189979e1f46b272470748fc9c1e28e59306dbb Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 5 Sep 2019 09:24:24 +1000 Subject: [PATCH] Added JazzIcon --- app/profile_images.js | 4 +- ts/components/Avatar.tsx | 44 +++++++- ts/components/JazzIcon/JazzIcon.tsx | 165 ++++++++++++++++++++++++++++ ts/components/JazzIcon/Paper.tsx | 25 +++++ ts/components/JazzIcon/RNG.tsx | 21 ++++ ts/components/JazzIcon/index.tsx | 2 + 6 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 ts/components/JazzIcon/JazzIcon.tsx create mode 100644 ts/components/JazzIcon/Paper.tsx create mode 100644 ts/components/JazzIcon/RNG.tsx create mode 100644 ts/components/JazzIcon/index.tsx diff --git a/app/profile_images.js b/app/profile_images.js index 7cbc9b4c5..f8c8a26f0 100644 --- a/app/profile_images.js +++ b/app/profile_images.js @@ -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, diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 7a4fe36a7..a400af605 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -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 { +export class Avatar extends React.PureComponent { public handleImageErrorBound: () => void; public constructor(props: Props) { @@ -43,6 +44,22 @@ export class Avatar extends React.Component { }); } + 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 ; + } + public renderImage() { const { avatarPath, @@ -129,10 +146,18 @@ export class Avatar extends React.Component { } 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 { !hasImage ? `module-avatar--${color}` : null )} > - {hasImage ? this.renderImage() : this.renderNoImage()} + {hasImage ? this.renderAvatarOrIdenticon() : this.renderNoImage()} ); } + 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; diff --git a/ts/components/JazzIcon/JazzIcon.tsx b/ts/components/JazzIcon/JazzIcon.tsx new file mode 100644 index 000000000..9773c9b5e --- /dev/null +++ b/ts/components/JazzIcon/JazzIcon.tsx @@ -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) => { + 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; +} + +// 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 { + 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 ( + + + {shapesArr.map((_, i) => + this.genShape( + shuffledColours[i + 1], + diameter, + i, + shapeCount - 1, + generator + ) + )} + + + ); + } + + private hueShift(colors: Array, 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 ( + + ); + } + + private colorsForIcon(arr: Array) { + if (isColors(arr)) { + return arr; + } + + return defaultColors; + } + + private shuffleArray(array: Array, 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; + } +} diff --git a/ts/components/JazzIcon/Paper.tsx b/ts/components/JazzIcon/Paper.tsx new file mode 100644 index 000000000..c6d38ab6b --- /dev/null +++ b/ts/components/JazzIcon/Paper.tsx @@ -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 }) => ( +
+ {children} +
+); diff --git a/ts/components/JazzIcon/RNG.tsx b/ts/components/JazzIcon/RNG.tsx new file mode 100644 index 000000000..b6c18ac6b --- /dev/null +++ b/ts/components/JazzIcon/RNG.tsx @@ -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(); + } +} diff --git a/ts/components/JazzIcon/index.tsx b/ts/components/JazzIcon/index.tsx new file mode 100644 index 000000000..204774dc6 --- /dev/null +++ b/ts/components/JazzIcon/index.tsx @@ -0,0 +1,2 @@ +import { JazzIcon } from './JazzIcon'; +export { JazzIcon };