From 961eb53915b9444fed5b0825202b63304eb697bf Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 30 Nov 2018 12:16:08 +1100 Subject: [PATCH] Auto-generate profile images on conversations. --- app/profile_images.js | 67 +++++++++++++++++++++++++++++++++++ js/conversation_controller.js | 16 +++++---- js/models/conversations.js | 53 +++++++++++---------------- js/views/inbox_view.js | 1 + preload.js | 1 + stylesheets/_modules.scss | 4 +++ 6 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 app/profile_images.js diff --git a/app/profile_images.js b/app/profile_images.js new file mode 100644 index 000000000..bbb0b6cdb --- /dev/null +++ b/app/profile_images.js @@ -0,0 +1,67 @@ +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const path = require('path'); +const jdenticon = require('jdenticon'); + +// Icon config +jdenticon.config = { + lightness: { + color: [0.40, 0.80], + grayscale: [0.30, 0.90], + }, + saturation: { + color: 0.50, + grayscale: 0.00, + }, + backColor: '#86444400', +}; + +const { app } = require('electron').remote; + +const userDataPath = app.getPath('userData'); +const PATH = path.join(userDataPath, 'profileImages'); +mkdirp.sync(PATH); + +function hashCode(s) { + let h = 0; + for(let i = 0; i < s.length; i += 1) + h = Math.imul(31, h) + s.charCodeAt(i) | 0; + + return h; +} + +const hasImage = pubKey => fs.existsSync(getImagePath(pubKey)); + +const getImagePath = pubKey => `${PATH}/${pubKey}.png`; +const getOrCreateImagePath = pubKey => { + const imagePath = getImagePath(pubKey); + + // If the image doesn't exist then create it + if (!hasImage(pubKey)) { + /* + We hash the pubKey and then pass that into jdenticon + This is because if we pass pubKey directly, + jdenticon trims the string and then generates a hash + meaning public keys with the same characters at the beginning + will get the same images + */ + const png = jdenticon.toPng(hashCode(pubKey), 50, 0.12); + fs.writeFileSync(imagePath, png); + } + + return imagePath; +}; + +const removeImage = pubKey => { + if (hasImage(pubKey)) { + fs.unlinkSync(getImagePath(pubKey)); + } +} + +module.exports = { + getOrCreateImagePath, + getImagePath, + hasImage, + removeImage, + hashCode, +}; \ No newline at end of file diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 3f1948c3e..c0182a696 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -192,7 +192,7 @@ return conversation; }; - conversation.initialPromise = create(); + conversation.initialPromise = create().then(() => conversation.updateProfileAvatar()); return conversation; }, @@ -251,12 +251,16 @@ conversations.add(collection.models); this._initialFetchComplete = true; - await Promise.all( - conversations.map(conversation => conversation.updateLastMessage()) - ); + const promises = []; + conversations.forEach(conversation => { + promises.concat([ + conversation.updateLastMessage(), + conversation.updateProfile(), + conversation.updateProfileAvatar(), + ]); + }); + await Promise.all(promises); - // Update profiles - conversations.map(conversation => conversation.updateProfile()); window.log.info('ConversationController: done with initial fetch'); } catch (error) { window.log.error( diff --git a/js/models/conversations.js b/js/models/conversations.js index d1fe5c733..ea54d7022 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -3,7 +3,7 @@ /* global BlockedNumberController: false */ /* global ConversationController: false */ /* global i18n: false */ -/* global libsignal: false */ +/* global profileImages: false */ /* global storage: false */ /* global textsecure: false */ /* global Whisper: false */ @@ -174,6 +174,12 @@ deleteAttachmentData, } ); + profileImages.removeImage(this.id); + }, + + async updateProfileAvatar() { + const path = profileImages.getOrCreateImagePath(this.id); + await this.setProfileAvatar(path); }, async updateAndMerge(message) { @@ -1730,34 +1736,12 @@ } }, async setProfileAvatar(avatarPath) { - if (!avatarPath) { - return; - } - - const avatar = await textsecure.messaging.getAvatar(avatarPath); - const key = this.get('profileKey'); - if (!key) { - return; - } - const keyBuffer = window.Signal.Crypto.base64ToArrayBuffer(key); - - // decrypt - const decrypted = await textsecure.crypto.decryptProfile( - avatar, - keyBuffer - ); - - // update the conversation avatar only if hash differs - if (decrypted) { - const newAttributes = await window.Signal.Types.Conversation.maybeUpdateProfileAvatar( - this.attributes, - decrypted, - { - writeNewAttachmentData, - deleteAttachmentData, - } - ); - this.set(newAttributes); + const profileAvatar = this.get('profileAvatar'); + if (profileAvatar !== avatarPath) { + this.set({ profileAvatar: avatarPath }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); } }, async setProfileKey(profileKey) { @@ -1959,8 +1943,9 @@ getAvatarPath() { const avatar = this.get('avatar') || this.get('profileAvatar'); - if (avatar && avatar.path) { - return getAbsoluteAttachmentPath(avatar.path); + if (avatar) { + if (avatar.path) return getAbsoluteAttachmentPath(avatar.path); + return avatar; } return null; @@ -1970,8 +1955,10 @@ const color = this.getColor(); const avatar = this.get('avatar') || this.get('profileAvatar'); - if (avatar && avatar.path) { - return { url: getAbsoluteAttachmentPath(avatar.path), color }; + const url = avatar && avatar.path ? getAbsoluteAttachmentPath(avatar.path) : avatar; + + if (url) { + return { url, color }; } else if (this.isPrivate()) { const symbol = this.isValid() ? '#' : '!'; return { diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 9e0f8e17b..74980bae6 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -334,6 +334,7 @@ openConversation(conversation) { this.searchView.hideHints(); if (conversation) { + conversation.updateProfile(); ConversationController.markAsSelected(conversation); this.conversation_stack.open( ConversationController.get(conversation.id) diff --git a/preload.js b/preload.js index 6fe8ecc96..f868614a2 100644 --- a/preload.js +++ b/preload.js @@ -271,6 +271,7 @@ window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInst window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat; window.loadImage = require('blueimp-load-image'); window.getGuid = require('uuid/v4'); +window.profileImages = require('./app/profile_images'); window.React = require('react'); window.ReactDOM = require('react-dom'); diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 076220432..a2ecc0b96 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1835,6 +1835,10 @@ &:hover { background-color: $color-dark-70; } + + .module-avatar { + background-color: $color-dark-85; + } } .module-conversation-list-item--has-unread {