From 500a88dbab8a7ec2c9cb6f502d4f2641e7e3cfb1 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 5 Sep 2019 08:57:29 +1000 Subject: [PATCH 1/4] Removed identicon.js Updated profile image helper. --- app/profile_images.js | 27 +++-------------- js/models/conversations.js | 10 +++++-- package.json | 3 +- yarn.lock | 61 ++++++++++++++++++++++++++++++++++---- 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/app/profile_images.js b/app/profile_images.js index ee39ee3a9..7cbc9b4c5 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 Identicon = require('identicon.js'); const sha224 = require('js-sha512').sha512_224; const { app } = require('electron').remote; @@ -13,14 +12,6 @@ mkdirp.sync(PATH); const hasImage = pubKey => fs.existsSync(getImagePath(pubKey)); const getImagePath = pubKey => `${PATH}/${pubKey}.png`; -const getOrCreateImagePath = pubKey => { - // If the image doesn't exist then create it - if (!hasImage(pubKey)) { - return generateImage(pubKey); - } - - return getImagePath(pubKey); -}; const removeImage = pubKey => { if (hasImage(pubKey)) { @@ -41,24 +32,14 @@ const removeImagesNotInArray = pubKeyArray => { .forEach(i => removeImage(i)); }; -const generateImage = pubKey => { +const writePNGImage = (base64String, pubKey) => { const imagePath = getImagePath(pubKey); - - /* - We hash the pubKey and then pass that into Identicon. - This is to avoid getting the same image - if 2 public keys start with the same 15 characters. - */ - const png = new Identicon(sha224(pubKey), { - margin: 0.2, - background: [0, 0, 0, 0], - }).toString(); - fs.writeFileSync(imagePath, png, 'base64'); + fs.writeFileSync(imagePath, base64String, 'base64'); return imagePath; -}; +} module.exports = { - generateImage, + writePNGImage, getOrCreateImagePath, getImagePath, hasImage, diff --git a/js/models/conversations.js b/js/models/conversations.js index dff8043fa..2a75f8949 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -309,11 +309,15 @@ }, async updateProfileAvatar() { - if (this.isRss()) { + if (this.isRss() || this.isPublic()) { return; } - const path = profileImages.getOrCreateImagePath(this.id); - await this.setProfileAvatar(path); + + // Remove old identicons + if (profileImages.hasImage(this.id)) { + profileImages.removeImage(this.id); + await this.setProfileAvatar(null); + } }, async updateAndMerge(message) { diff --git a/package.json b/package.json index b055f5039..f3a349e66 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "buffer-crc32": "0.2.13", "bunyan": "1.8.12", "classnames": "2.2.5", + "color": "^3.1.2", "config": "1.28.1", "electron-context-menu": "^0.15.0", "electron-editor-context-menu": "1.1.1", @@ -76,7 +77,6 @@ "google-libphonenumber": "3.2.2", "got": "8.2.0", "he": "1.2.0", - "identicon.js": "2.3.3", "intl-tel-input": "12.1.15", "jquery": "3.3.1", "js-sha512": "0.8.0", @@ -122,6 +122,7 @@ "devDependencies": { "@types/chai": "4.1.2", "@types/classnames": "2.2.3", + "@types/color": "^3.0.0", "@types/config": "0.0.34", "@types/filesize": "3.6.0", "@types/fs-extra": "5.0.5", diff --git a/yarn.lock b/yarn.lock index 20f42cec8..72d1e6646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -89,6 +89,25 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5" integrity sha512-x15/Io+JdzrkM9gnX6SWUs/EmqQzd65TD9tcZIAQ1VIdb93XErNuYmB7Yho8JUCE189ipUSESsWvGvYXRRIvYA== +"@types/color-convert@*": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d" + integrity sha512-OKGEfULrvSL2VRbkl/gnjjgbbF7ycIlpSsX7Nkab4MOWi5XxmgBYvuiQ7lcCFY5cPDz7MUNaKgxte2VRmtr4Fg== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/color@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" + integrity sha512-5qqtNia+m2I0/85+pd2YzAXaTyKO8j+svirO5aN+XaQJ5+eZ8nx0jPtEWZLxCi50xwYsX10xUHetFzfb1WEs4Q== + dependencies: + "@types/color-convert" "*" + "@types/config@0.0.34": version "0.0.34" resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.34.tgz#123f91bdb5afdd702294b9de9ca04d9ea11137b0" @@ -1733,11 +1752,18 @@ color-convert@^1.9.0: dependencies: color-name "^1.1.1" +color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@~0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" -color-name@^1.0.0, color-name@^1.1.1: +color-name@1.1.3, color-name@^1.0.0, color-name@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -1748,6 +1774,14 @@ color-string@^0.3.0: dependencies: color-name "^1.0.0" +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color@^0.11.0: version "0.11.4" resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" @@ -1757,6 +1791,14 @@ color@^0.11.0: color-convert "^1.3.0" color-string "^0.3.0" +color@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" + integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + colormin@^1.0.5: version "1.1.2" resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" @@ -4480,11 +4522,6 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" -identicon.js@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/identicon.js/-/identicon.js-2.3.3.tgz#c505b8d60ecc6ea13bbd991a33964c44c1ad60a1" - integrity sha512-/qgOkXKZ7YbeCYbawJ9uQQ3XJ3uBg9VDpvHjabCAPp6aRMhjLaFAxG90+1TxzrhKaj6AYpVGrx6UXQfQA41UEA== - ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" @@ -4669,6 +4706,11 @@ is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -8640,6 +8682,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + single-line-log@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" From a9189979e1f46b272470748fc9c1e28e59306dbb Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 5 Sep 2019 09:24:24 +1000 Subject: [PATCH 2/4] 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 }; From aa216d7a5ddc811d7439a2ac2324f76bed76f973 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 5 Sep 2019 15:28:00 +1000 Subject: [PATCH 3/4] Added minor comment. --- ts/components/JazzIcon/JazzIcon.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ts/components/JazzIcon/JazzIcon.tsx b/ts/components/JazzIcon/JazzIcon.tsx index 9773c9b5e..e6494dfa1 100644 --- a/ts/components/JazzIcon/JazzIcon.tsx +++ b/ts/components/JazzIcon/JazzIcon.tsx @@ -1,3 +1,5 @@ +// Modified from https://github.com/redlanta/react-jazzicon + import React from 'react'; import Color from 'color'; import { Paper } from './Paper'; From 5dc52f0858af55abe41a4c30a7e602cf322b5f29 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 6 Sep 2019 13:18:26 +1000 Subject: [PATCH 4/4] Fix incorrect hue shifting --- ts/components/JazzIcon/JazzIcon.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ts/components/JazzIcon/JazzIcon.tsx b/ts/components/JazzIcon/JazzIcon.tsx index e6494dfa1..ac082c1af 100644 --- a/ts/components/JazzIcon/JazzIcon.tsx +++ b/ts/components/JazzIcon/JazzIcon.tsx @@ -94,12 +94,7 @@ export class JazzIcon extends React.PureComponent { 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(); - }); + return colors.map(hex => Color(hex).rotate(amount).hex()); } private genShape(