From 0e6c14eb5c3296ae7d36a51eac1c93b0e87146b4 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 26 Nov 2018 12:19:30 +1100 Subject: [PATCH 01/16] Added profile model. --- js/models/profile.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 js/models/profile.js diff --git a/js/models/profile.js b/js/models/profile.js new file mode 100644 index 000000000..be551ca73 --- /dev/null +++ b/js/models/profile.js @@ -0,0 +1,43 @@ +/* global storage */ +/* global storage: false */ + +/* eslint-disable more/no-then */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + const PROFILE_ID = 'profiles'; + + storage.getProfile = number => { + const profiles = storage.get(PROFILE_ID, {}); + return profiles[number] || null; + } + + storage.saveProfile = async (number, profile) => { + const profiles = storage.get(PROFILE_ID, {}); + if (profiles[number]) { + return; + } + + window.log.info('adding profile ', profile, 'for ', number); + await storage.put(PROFILE_ID, { + ...profiles, + number: profile, + }); + } + + storage.removeProfile = async number => { + const profiles = storage.get(PROFILE_ID, {}); + if (!profiles[number]) { + return; + } + + delete profiles[number]; + + window.log.info('removing profile for ', number); + await storage.put(PROFILE_ID, profiles); + } +})(); From 774c52a40793ca669c543886904f2b8a5702b052 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 26 Nov 2018 12:20:45 +1100 Subject: [PATCH 02/16] Added dynamic profile fetching in conversation. Added setting profile when sending DataMessage. --- background.html | 1 + js/models/conversations.js | 15 +++++++++++++++ js/models/profile.js | 9 ++++++--- js/modules/types/contact.js | 21 +++++++++++---------- libtextsecure/sendmessage.js | 10 ++++++++++ 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/background.html b/background.html index a37e90fe1..e7463fcb5 100644 --- a/background.html +++ b/background.html @@ -608,6 +608,7 @@ + diff --git a/js/models/conversations.js b/js/models/conversations.js index 7af61f601..0338278a0 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1736,7 +1736,21 @@ this.getProfiles(); } }, + // Update profile variables dynamically + async updateProfile() { + const profile = await storage.getProfile(this.id); + if (!profile) { + this.set({ profileName: null }); + } else { + this.set({ profileName: profile.name.displayName }); + } + if (this.hasChanged()) { + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } + }, getProfiles() { // request all conversation members' keys let ids = []; @@ -1866,6 +1880,7 @@ } } }, + // Signal profile name async setProfileName(encryptedName) { if (!encryptedName) { return; diff --git a/js/models/profile.js b/js/models/profile.js index be551ca73..94edd1352 100644 --- a/js/models/profile.js +++ b/js/models/profile.js @@ -1,4 +1,4 @@ -/* global storage */ +/* global storage, _ */ /* global storage: false */ /* eslint-disable more/no-then */ @@ -18,14 +18,17 @@ storage.saveProfile = async (number, profile) => { const profiles = storage.get(PROFILE_ID, {}); - if (profiles[number]) { + const storedProfile = profiles[number]; + + // Only store the profile if we have a different object + if (storedProfile && _.isEqual(storedProfile, profile)) { return; } window.log.info('adding profile ', profile, 'for ', number); await storage.put(PROFILE_ID, { ...profiles, - number: profile, + [number]: profile, }); } diff --git a/js/modules/types/contact.js b/js/modules/types/contact.js index 56baa425d..a031c56eb 100644 --- a/js/modules/types/contact.js +++ b/js/modules/types/contact.js @@ -66,7 +66,7 @@ function idForLogging(message) { exports._validate = (contact, options = {}) => { const { messageId } = options; - const { name, number, email, address, organization } = contact; + const { name, organization } = contact; if ((!name || !name.displayName) && !organization) { return new Error( @@ -74,15 +74,16 @@ exports._validate = (contact, options = {}) => { ); } - if ( - (!number || !number.length) && - (!email || !email.length) && - (!address || !address.length) - ) { - return new Error( - `Message ${messageId}: Contact had no included numbers, email or addresses` - ); - } + // Disabled as we don't require the users to provide this + // if ( + // (!number || !number.length) && + // (!email || !email.length) && + // (!address || !address.length) + // ) { + // return new Error( + // `Message ${messageId}: Contact had no included numbers, email or addresses` + // ); + // } return null; }; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index f92b65a54..29d5c86a7 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -25,6 +25,7 @@ function Message(options) { this.needsSync = options.needsSync; this.expireTimer = options.expireTimer; this.profileKey = options.profileKey; + this.profile = options.profile; if (!(this.recipients instanceof Array) || this.recipients.length < 1) { throw new Error('Invalid recipient list'); @@ -132,6 +133,12 @@ Message.prototype = { proto.profileKey = this.profileKey; } + if (this.profile) { + const contact = new textsecure.protobuf.DataMessage.Contact(); + contact.name = this.profile.name; + proto.contact.push(contact); + } + this.dataMessage = proto; return proto; }, @@ -656,6 +663,8 @@ MessageSender.prototype = { profileKey, options ) { + const myNumber = textsecure.storage.user.getNumber(); + const profile = textsecure.storage.impl.getProfile(myNumber); return this.sendMessage( { recipients: [number], @@ -666,6 +675,7 @@ MessageSender.prototype = { needsSync: true, expireTimer, profileKey, + profile, }, options ); From aa57693fceb4b0c83403f2f4e5acc333dc2eebd9 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 26 Nov 2018 13:16:16 +1100 Subject: [PATCH 03/16] Update profile if we get it through a message. --- js/models/conversations.js | 10 ++++------ libtextsecure/message_receiver.js | 19 ++++++++++++++++++- libtextsecure/sendmessage.js | 2 +- protos/SignalService.proto | 1 + 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 0338278a0..5fdad3772 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1738,14 +1738,12 @@ }, // Update profile variables dynamically async updateProfile() { + const profileName = this.get('profileName'); const profile = await storage.getProfile(this.id); - if (!profile) { - this.set({ profileName: null }); - } else { - this.set({ profileName: profile.name.displayName }); - } - if (this.hasChanged()) { + const newProfileName = (profile && profile.name && profile.name.displayName) || null + if (profileName !== newProfileName) { + this.set({ profileName: newProfileName }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 3e255a274..58356a5f8 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1,5 +1,6 @@ /* global window: false */ /* global textsecure: false */ +/* global storage: false */ /* global StringView: false */ /* global libloki: false */ /* global libsignal: false */ @@ -917,15 +918,31 @@ MessageReceiver.prototype.extend({ p = this.handleEndSession(envelope.source); } return p.then(() => - this.processDecrypted(envelope, msg, envelope.source).then(message => { + this.processDecrypted(envelope, msg, envelope.source).then(async message => { const groupId = message.group && message.group.id; const isBlocked = this.isGroupBlocked(groupId); const isMe = envelope.source === textsecure.storage.user.getNumber(); + const conversation = window.ConversationController.get(envelope.source); const isLeavingGroup = Boolean( message.group && message.group.type === textsecure.protobuf.GroupContext.Type.QUIT ); + // Check if we need to update any profile names + if (!isMe) { + if (message.profile) { + const name = JSON.parse(message.profile.name.encodeJSON()); + await storage.saveProfile(envelope.source, { name }); + } else { + await storage.removeProfile(envelope.source); + } + + // Update the conversation profle + if (conversation) { + conversation.updateProfile(); + } + } + if (type === 'friend-request' && isMe) { window.log.info( 'refusing to add a friend request to ourselves' diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 29d5c86a7..83fdfd76b 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -136,7 +136,7 @@ Message.prototype = { if (this.profile) { const contact = new textsecure.protobuf.DataMessage.Contact(); contact.name = this.profile.name; - proto.contact.push(contact); + proto.profile = contact; } this.dataMessage = proto; diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 6e1cce788..4c59b6352 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -184,6 +184,7 @@ message DataMessage { optional uint64 timestamp = 7; optional Quote quote = 8; repeated Contact contact = 9; + optional Contact profile = 101; // Loki: The profile of the current user } message NullMessage { From f1d18219edccfdb12caa10fb9f490d03e372eba1 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 26 Nov 2018 15:50:53 +1100 Subject: [PATCH 04/16] Conversation UI update. --- stylesheets/_modules.scss | 14 +++++++++- ts/components/conversation/ContactName.tsx | 7 ++--- .../conversation/ConversationHeader.tsx | 26 +++++-------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index d4c1dfe56..0415cd4d6 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1,8 +1,20 @@ // Using BEM syntax explained here: https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/ // Module: Contact Name +.module-contact-name { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.module-contact-name span { + text-overflow: ellipsis; + overflow-x: hidden; + width: 100%; + text-align: left; +} -.module-contact-name__profile-name { +.module-contact-name__profile-number { font-style: italic; } diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index 404a80583..dbe17c24a 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -21,15 +21,16 @@ export class ContactName extends React.Component { const shouldShowProfile = Boolean(profileName && !name); const profileElement = shouldShowProfile ? ( - ~ + ) : null; return ( - - {shouldShowProfile ? ' ' : null} {profileElement} + + + ); } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 49a99a6ed..0162243f4 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Emojify } from './Emojify'; +import { ContactName } from './ContactName'; import { Avatar } from '../Avatar'; import { Localizer } from '../../types/Util'; import { @@ -90,32 +90,20 @@ export class ConversationHeader extends React.Component { public renderTitle() { const { - name, phoneNumber, i18n, profileName, - isVerified, isKeysPending, } = this.props; return (
- {name ? : null} - {name && phoneNumber ? ' · ' : null} - {phoneNumber ? phoneNumber : null}{' '} - {profileName && !name ? ( - - ~ - - ) : null} - {isVerified ? ' · ' : null} - {isVerified ? ( - - - {i18n('verified')} - - ) : null} - {isKeysPending ? '(pending)' : null} + + {isKeysPending ? ' (pending)' : null}
); } From cf24e42a0e738ad7b6423fe80696822807c171ef Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 27 Nov 2018 09:49:41 +1100 Subject: [PATCH 05/16] Added storing nicknames. --- js/models/conversations.js | 6 +++++- js/models/profile.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 5fdad3772..c83bd0660 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1739,9 +1739,13 @@ // Update profile variables dynamically async updateProfile() { const profileName = this.get('profileName'); + + // Prioritise nickname over the profile display name + const nickname = await storage.getNickname(this.id); const profile = await storage.getProfile(this.id); + const displayName = profile && profile.name && profile.name.displayName; - const newProfileName = (profile && profile.name && profile.name.displayName) || null + const newProfileName = nickname || displayName || null; if (profileName !== newProfileName) { this.set({ profileName: newProfileName }); await window.Signal.Data.updateConversation(this.id, this.attributes, { diff --git a/js/models/profile.js b/js/models/profile.js index 94edd1352..a7a93d45e 100644 --- a/js/models/profile.js +++ b/js/models/profile.js @@ -43,4 +43,41 @@ window.log.info('removing profile for ', number); await storage.put(PROFILE_ID, profiles); } + + // Names that user can set for users + + const NICKNAME_ID = 'nickname'; + + storage.getNickname = number => { + const nicknames = storage.get(NICKNAME_ID, {}); + return nicknames[number] || null; + } + + storage.saveNickname = async (number, name) => { + const nicknames = storage.get(NICKNAME_ID, {}); + const storedName = nicknames[number]; + + // Only store the name if we have a different name + if (storedName === name) { + return; + } + + window.log.info('adding nickname ', name, 'for ', number); + await storage.put(NICKNAME_ID, { + ...nicknames, + [number]: name, + }); + } + + storage.removeNickname = async number => { + const nicknames = storage.get(NICKNAME_ID, {}); + if (!nicknames[number]) { + return; + } + + delete nicknames[number]; + + window.log.info('removing nickname for ', number); + await storage.put(NICKNAME_ID, nicknames); + } })(); From 6ce9d6a08cb51324331a02bfafd69438e752fc25 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 27 Nov 2018 11:27:34 +1100 Subject: [PATCH 06/16] Added nickname dialog. --- _locales/en/messages.json | 3 + background.html | 15 +++++ js/background.js | 6 ++ js/views/app_view.js | 10 +++ js/views/nickname_dialog_view.js | 101 +++++++++++++++++++++++++++++++ stylesheets/_conversation.scss | 51 ++++++++++++++++ test/index.html | 12 ++++ 7 files changed, 198 insertions(+) create mode 100644 js/views/nickname_dialog_view.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d6e7e7e5b..14915f324 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -781,6 +781,9 @@ "cancel": { "message": "Cancel" }, + "clear": { + "message": "Clear" + }, "failedToSend": { "message": "Failed to send to some recipients. Check your network connection." diff --git a/background.html b/background.html index e7463fcb5..40dc5bb73 100644 --- a/background.html +++ b/background.html @@ -150,6 +150,20 @@ 0:00 + + diff --git a/js/background.js b/js/background.js index 71e5935ee..03f49ba4a 100644 --- a/js/background.js +++ b/js/background.js @@ -568,6 +568,12 @@ } }); + Whisper.events.on('showNicknameDialog', options => { + if (appView) { + appView.showNicknameDialog(options); + } + }); + Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => { try { const conversation = ConversationController.get(pubKey); diff --git a/js/views/app_view.js b/js/views/app_view.js index 1db6314e3..acd6a8329 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -178,5 +178,15 @@ }); } }, + showNicknameDialog({ pubKey, title, nickname, onOk, onCancel }) { + const _title = title || `Change nickname for ${pubKey}`; + const dialog = new Whisper.NicknameDialogView({ + title: _title, + name: nickname, + resolve: onOk, + reject: onCancel, + }); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js new file mode 100644 index 000000000..8d4d092eb --- /dev/null +++ b/js/views/nickname_dialog_view.js @@ -0,0 +1,101 @@ +/* global Whisper, i18n, _ */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.NicknameDialogView = Whisper.View.extend({ + className: 'nickname-dialog modal', + templateName: 'nickname-dialog', + initialize(options) { + this.message = options.message; + this.name = options.name; + + this.resolve = options.resolve; + this.okText = options.okText || i18n('ok'); + + this.reject = options.reject; + this.cancelText = options.cancelText || i18n('cancel'); + + this.clear = options.clear; + this.clearText = options.clearText || i18n('clear'); + + this.title = options.title; + + this.render(); + + this.$input = this.$('input'); + this.validateNickname(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'ok', + 'click .cancel': 'cancel', + 'click .clear': 'clear', + change: 'validateNickname', + }, + isValidNickname(name) { + return (name || '').length < 20; + }, + validateNickname() { + const nickname = this.$input.val(); + + if (_.isEmpty(nickname)) { + this.$('.clear').hide(); + } else { + this.$('.clear').show(); + } + + if (this.isValidNickname(nickname)) { + this.$('.content').removeClass('invalid'); + this.$('.content').addClass('valid'); + this.$('.ok').show(); + } else { + this.$('.content').removeClass('valid'); + this.$('.ok').hide(); + } + }, + render_attributes() { + return { + name: this.name, + message: this.message, + showCancel: !this.hideCancel, + cancel: this.cancelText, + ok: this.okText, + clear: this.clearText, + title: this.title, + }; + }, + ok() { + const nickname = this.$input.val(); + if (!this.isValidNickname(nickname)) return; + + this.remove(); + if (this.resolve) { + this.resolve(nickname); + } + }, + cancel() { + this.remove(); + if (this.reject) { + this.reject(); + } + }, + clear() { + this.$input.val('').trigger('change'); + }, + onKeyup(event) { + if (event.key === 'Escape' || event.key === 'Esc') { + this.cancel(); + return; + } + + this.validateNickname(); + }, + focusCancel() { + this.$('.cancel').focus(); + }, + }); +})(); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 3d8376aa9..ca468bd9b 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -351,6 +351,57 @@ } } + +.nickname-dialog { + display: flex; + align-items: center; + justify-content: center; + + .content { + max-width: 75%; + padding: 1em; + background: white; + border-radius: $border-radius; + overflow: auto; + box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.3); + + .buttons { + margin-top: 10px; + + button { + float: right; + margin-left: 10px; + background-color: $grey_l; + border-radius: $border-radius; + padding: 5px 8px; + border: 1px solid $grey_l2; + + &:hover { + background-color: $grey_l2; + border-color: $grey_l3; + } + } + } + + input { + width: 100%; + padding: 8px; + margin-bottom: 4px; + } + + h4 { + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP printers */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ + word-wrap: break-word; /* IE */ + word-break: break-all; + } + } +} + .permissions-popup, .debug-log-window { .modal { diff --git a/test/index.html b/test/index.html index e03ce96cf..05d46c1c6 100644 --- a/test/index.html +++ b/test/index.html @@ -127,6 +127,17 @@ 0:00 + + From 449f44cc5a00c84fef2f2917a9cdcfad161b8631 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 27 Nov 2018 11:47:20 +1100 Subject: [PATCH 07/16] Added changing user nicknames. --- _locales/en/messages.json | 8 ++++++++ js/conversation_controller.js | 3 +++ js/views/conversation_view.js | 19 +++++++++++++++++++ js/views/nickname_dialog_view.js | 2 +- .../conversation/ConversationHeader.tsx | 13 +++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 14915f324..9f5dc789c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1349,6 +1349,14 @@ "message": "Disappearing messages", "description": "Conversation menu option to enable disappearing messages" }, + "changeNickname": { + "message": "Change nickname", + "description": "Conversation menu option to change user nickname" + }, + "clearNickname": { + "message": "Clear nickname", + "description": "Conversation menu option to clear user nickname" + }, "timerOption_0_seconds_abbreviated": { "message": "off", "description": diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 035963e3b..dc11eb45e 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -226,6 +226,9 @@ await Promise.all( conversations.map(conversation => conversation.updateLastMessage()) ); + + // Update profiles + conversations.map(conversation => conversation.updateProfile()); window.log.info('ConversationController: done with initial fetch'); } catch (error) { window.log.error( diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 588babf78..e8ff75c5c 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -174,6 +174,7 @@ name: item.getName(), value: item.get('seconds'), })), + hasNickname: !!storage.getNickname(this.model.id), onSetDisappearingMessages: seconds => this.setDisappearingMessages(seconds), @@ -204,6 +205,24 @@ onUnblockUser: () => { this.model.unblock(); }, + onChangeNickname: () => { + window.Whisper.events.trigger('showNicknameDialog', { + pubKey: this.model.id, + nickname: storage.getNickname(this.model.id), + onOk: async (newName) => { + if (_.isEmpty(newName)) { + await storage.removeNickname(this.model.id) + } else { + await storage.saveNickname(this.model.id, newName); + } + this.model.updateProfile(); + }, + }); + }, + onClearNickname: async () => { + await storage.removeNickname(this.model.id); + this.model.updateProfile(); + }, }; }; this.titleView = new Whisper.ReactWrapperView({ diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js index 8d4d092eb..1010068db 100644 --- a/js/views/nickname_dialog_view.js +++ b/js/views/nickname_dialog_view.js @@ -11,7 +11,7 @@ templateName: 'nickname-dialog', initialize(options) { this.message = options.message; - this.name = options.name; + this.name = options.name || ''; this.resolve = options.resolve; this.okText = options.okText || i18n('ok'); diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 0162243f4..490fd541e 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -36,6 +36,7 @@ interface Props { expirationSettingName?: string; showBackButton: boolean; timerOptions: Array; + hasNickname?: boolean; onSetDisappearingMessages: (seconds: number) => void; onDeleteMessages: () => void; @@ -48,6 +49,9 @@ interface Props { onBlockUser: () => void; onUnblockUser: () => void; + + onClearNickname: () => void; + onChangeNickname: () => void; } export class ConversationHeader extends React.Component { @@ -186,6 +190,9 @@ export class ConversationHeader extends React.Component { timerOptions, onBlockUser, onUnblockUser, + hasNickname, + onClearNickname, + onChangeNickname, } = this.props; const disappearingTitle = i18n('disappearingMessages') as any; @@ -225,6 +232,12 @@ export class ConversationHeader extends React.Component { {!isMe ? ( {blockTitle} ) : null} + {!isMe ? ( + {i18n('changeNickname')} + ) : null} + {!isMe && hasNickname ? ( + {i18n('clearNickname')} + ) : null} {i18n('deleteMessages')} ); From 98c4b5d77b8659801fa3d0431ab99aab0861237b Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 27 Nov 2018 12:17:12 +1100 Subject: [PATCH 08/16] Added editing own nickname. Fix dark theme support. Fix notification titles. --- background.html | 10 +++---- js/background.js | 34 +++++++++++++++++++++++ js/models/conversations.js | 15 +++++++++-- js/modules/signal.js | 2 ++ js/views/app_view.js | 3 ++- js/views/inbox_view.js | 34 +++++++++++++++++++++-- js/views/nickname_dialog_view.js | 36 +++++++++++-------------- stylesheets/_conversation.scss | 8 +++++- stylesheets/_index.scss | 42 ++++++++++++++++++++++++----- stylesheets/_theme_dark.scss | 41 ++++++++++++++++++++++++++++ ts/components/IdentityKeyHeader.tsx | 38 ++++++++++++++++++++++++++ 11 files changed, 226 insertions(+), 37 deletions(-) create mode 100644 ts/components/IdentityKeyHeader.tsx diff --git a/background.html b/background.html index 40dc5bb73..60ce85d54 100644 --- a/background.html +++ b/background.html @@ -63,9 +63,7 @@ -
- Your identity key: {{ identityKey }} -
+
@@ -155,8 +153,10 @@ {{ #title }}

{{ title }}

{{ /title }} -
{{ message }}
- + + {{ #message }} +
{{ message }}
+ {{ /message }}
diff --git a/js/background.js b/js/background.js index 03f49ba4a..7cab7890e 100644 --- a/js/background.js +++ b/js/background.js @@ -568,6 +568,40 @@ } }); + Whisper.events.on('onEditProfile', () => { + const ourNumber = textsecure.storage.user.getNumber(); + const profile = storage.getProfile(ourNumber); + const nickname = profile && profile.name && profile.name.displayName; + if (appView) { + appView.showNicknameDialog({ + title: 'Change your own nickname', + message: 'Note: Your nickname will be visible to your contacts.', + nickname, + onOk: async (newNickname) => { + + // Update our profiles accordingly + if (_.isEmpty(newNickname)) { + await storage.removeProfile(ourNumber); + } else { + await storage.saveProfile(ourNumber, { + name: { + displayName: newNickname, + }, + }); + } + + appView.inboxView.trigger('updateProfile'); + + // Update the conversation if we have it + try { + const conversation = ConversationController.get(ourNumber); + conversation.updateProfile(); + } catch (e) {} + }, + }) + } + }); + Whisper.events.on('showNicknameDialog', options => { if (appView) { appView.showNicknameDialog(options); diff --git a/js/models/conversations.js b/js/models/conversations.js index c83bd0660..0cca4c713 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1736,7 +1736,15 @@ this.getProfiles(); } }, - // Update profile variables dynamically + /* + Update profile values from the profile in storage. + + Signal has methods of setting data from a profile it fetches. + It fetches this via a server and they aren't saved anywhere. + + We made our own profile storage system so thus to avoid + any future conflict with upstream, we just use this method to update the values. + */ async updateProfile() { const profileName = this.get('profileName'); @@ -2083,7 +2091,10 @@ getTitle() { if (this.isPrivate()) { - return this.get('name') || this.getNumber(); + const profileName = this.getProfileName(); + const number = this.getNumber(); + const name = profileName ? `${profileName} (${number})` : number; + return this.get('name') || name; } return this.get('name') || 'Unknown group'; }, diff --git a/js/modules/signal.js b/js/modules/signal.js index b7755f25b..4ed80eba7 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -43,6 +43,7 @@ const { MediaGallery, } = require('../../ts/components/conversation/media-gallery/MediaGallery'); const { MainHeader } = require('../../ts/components/MainHeader'); +const { IdentityKeyHeader } = require('../../ts/components/IdentityKeyHeader'); const { Message } = require('../../ts/components/conversation/Message'); const { MessageBody } = require('../../ts/components/conversation/MessageBody'); const { @@ -184,6 +185,7 @@ exports.setup = (options = {}) => { Lightbox, LightboxGallery, MainHeader, + IdentityKeyHeader, MediaGallery, Message, MessageBody, diff --git a/js/views/app_view.js b/js/views/app_view.js index acd6a8329..f21542c24 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -178,10 +178,11 @@ }); } }, - showNicknameDialog({ pubKey, title, nickname, onOk, onCancel }) { + showNicknameDialog({ pubKey, title, message, nickname, onOk, onCancel }) { const _title = title || `Change nickname for ${pubKey}`; const dialog = new Whisper.NicknameDialogView({ title: _title, + message, name: nickname, resolve: onOk, reject: onCancel, diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 294b33b86..67fb8642b 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -6,6 +6,7 @@ /* global textsecure: false */ /* global Signal: false */ /* global StringView: false */ +/* global storage: false */ // eslint-disable-next-line func-names (function() { @@ -42,7 +43,7 @@ if ($el && $el.length > 0) { $el.remove(); } - } + }, }); Whisper.FontSizeView = Whisper.View.extend({ @@ -113,6 +114,16 @@ this.listenTo(me, 'change', update); this.$('.main-header-placeholder').append(this.mainHeaderView.el); + this.identityKeyView = new Whisper.ReactWrapperView({ + className: 'identity-key-wrapper', + Component: Signal.Components.IdentityKeyHeader, + props: this._getIdentityKeyViewProps(), + }); + this.on('updateProfile', () => { + this.identityKeyView.update(this._getIdentityKeyViewProps()); + }) + this.$('.identity-key-placeholder').append(this.identityKeyView.el); + this.conversation_stack = new Whisper.ConversationStack({ el: this.$('.conversation-stack'), model: { window: options.window }, @@ -184,14 +195,33 @@ this.$el.addClass('expired'); } }, + _getIdentityKeyViewProps() { + const identityKey = textsecure.storage.get('identityKey').pubKey; + const pubKey = StringView.arrayBufferToHex(identityKey); + const profile = storage.getProfile(pubKey); + const name = profile && profile.name && profile.name.displayName; + + return { + identityKey: pubKey, + name, + onEditProfile: async () => { + window.Whisper.events.trigger('onEditProfile'); + }, + } + }, render_attributes() { const identityKey = textsecure.storage.get('identityKey').pubKey; + const pubKey = StringView.arrayBufferToHex(identityKey); + const profile = storage.getProfile(pubKey); + const name = profile && profile.name && profile.name.displayName; + return { welcomeToSignal: i18n('welcomeToSignal'), selectAContact: i18n('selectAContact'), searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'), settings: i18n('settings'), - identityKey: StringView.arrayBufferToHex(identityKey), + identityKey: pubKey, + name, }; }, events: { diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js index 1010068db..7af20cb46 100644 --- a/js/views/nickname_dialog_view.js +++ b/js/views/nickname_dialog_view.js @@ -27,6 +27,9 @@ this.render(); this.$input = this.$('input'); + this.$input.val(this.name); + this.$input.focus(); + this.validateNickname(); }, events: { @@ -36,9 +39,6 @@ 'click .clear': 'clear', change: 'validateNickname', }, - isValidNickname(name) { - return (name || '').length < 20; - }, validateNickname() { const nickname = this.$input.val(); @@ -47,19 +47,9 @@ } else { this.$('.clear').show(); } - - if (this.isValidNickname(nickname)) { - this.$('.content').removeClass('invalid'); - this.$('.content').addClass('valid'); - this.$('.ok').show(); - } else { - this.$('.content').removeClass('valid'); - this.$('.ok').hide(); - } }, render_attributes() { return { - name: this.name, message: this.message, showCancel: !this.hideCancel, cancel: this.cancelText, @@ -69,8 +59,7 @@ }; }, ok() { - const nickname = this.$input.val(); - if (!this.isValidNickname(nickname)) return; + const nickname = this.$input.val().trim(); this.remove(); if (this.resolve) { @@ -87,12 +76,19 @@ this.$input.val('').trigger('change'); }, onKeyup(event) { - if (event.key === 'Escape' || event.key === 'Esc') { - this.cancel(); - return; - } - this.validateNickname(); + switch (event.key) { + case 'Enter': + this.ok(); + break; + case 'Escape': + case 'Esc': + this.cancel(); + break; + default: + return; + } + event.preventDefault(); }, focusCancel() { this.$('.cancel').focus(); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index ca468bd9b..ae91dc376 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -351,7 +351,6 @@ } } - .nickname-dialog { display: flex; align-items: center; @@ -359,6 +358,7 @@ .content { max-width: 75%; + min-width: 60%; padding: 1em; background: white; border-radius: $border-radius; @@ -399,6 +399,12 @@ word-wrap: break-word; /* IE */ word-break: break-all; } + + .message { + font-style: italic; + color: $grey; + font-size: 12px; + } } } diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index a500041e0..1bb8b18e6 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -71,21 +71,51 @@ } } -.identityKeyWrapper { +.identity-key-wrapper { background-color: $color-black-008-no-tranparency; - text-align: center; - height: 50px; - line-height: 50px; + display: flex; + flex: 1; + height: 60px; + padding-left: 16px; + padding-right: 16px; +} + +.identity-key-container { + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + justify-content: space-around; white-space: nowrap; + overflow-x: hidden; +} + +.identity-key-text-container { + flex: 1; + text-align: center; + flex-direction: column; } -.identityKey { +.identity-key-container div { + overflow-x: hidden; + text-overflow: ellipsis; +} + +.identity-key_bold { font-weight: bold; } +.identity-key-wrapper__pencil-icon { + @include color-svg('../images/lead-pencil.svg', $color-gray-60); + height: 20px; + width: 20px; + margin-left: 4px; + cursor: pointer; +} + .underneathIdentityWrapper { position: absolute; - top: 50px; + top: 60px; bottom: 0; left: 300px; right: 0; diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 87dc3f85e..5e955ed84 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -6,6 +6,16 @@ body.dark-theme { } .dark-theme { + // identity key + .identity-key-wrapper { + background-color:$color-gray-85; + } + + .identity-key-wrapper__pencil-icon { + @include color-svg('../images/lead-pencil.svg', $color-gray-25); + } + + // _conversation .conversation { @@ -89,6 +99,37 @@ body.dark-theme { } } + .nickname-dialog { + .content { + background: $color-black; + color: $color-dark-05; + + .buttons { + button { + background-color: $color-dark-85; + border-radius: $border-radius; + border: 1px solid $color-dark-60; + color: $color-dark-05; + + &:hover { + background-color: $color-dark-70; + border-color: $color-dark-55; + } + } + } + + input { + color: $color-dark-05; + background-color: $color-dark-70; + border-color: $color-dark-55; + } + + .message { + color: $color-light-35; + } + } + } + .conversation-loading-screen { background-color: $color-gray-95; } diff --git a/ts/components/IdentityKeyHeader.tsx b/ts/components/IdentityKeyHeader.tsx new file mode 100644 index 000000000..b03b60876 --- /dev/null +++ b/ts/components/IdentityKeyHeader.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +interface Props { + identityKey: string; + name?: string; + onEditProfile: () => void; +} + +export class IdentityKeyHeader extends React.Component { + public render() { + const { + name, + identityKey, + onEditProfile, + } = this.props; + + return ( +
+
+
+ Your identity key: {identityKey} +
+ {!!name && +
+ Your nickname: {name} +
+ } +
+
+
+ ); + } +} From 14ee7fec656837332f2aaa7b17204ab755595c80 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 28 Nov 2018 10:07:03 +1100 Subject: [PATCH 09/16] Moved and nicknames into conversations. --- js/background.js | 29 ++++++------- js/models/conversations.js | 50 ++++++++++++++++------ js/models/profile.js | 70 +++++-------------------------- js/views/conversation_view.js | 16 ++----- js/views/inbox_view.js | 9 +--- libtextsecure/message_receiver.js | 13 +++--- libtextsecure/sendmessage.js | 3 +- 7 files changed, 74 insertions(+), 116 deletions(-) diff --git a/js/background.js b/js/background.js index 7cab7890e..8cd1f68d4 100644 --- a/js/background.js +++ b/js/background.js @@ -570,33 +570,34 @@ Whisper.events.on('onEditProfile', () => { const ourNumber = textsecure.storage.user.getNumber(); - const profile = storage.getProfile(ourNumber); - const nickname = profile && profile.name && profile.name.displayName; + const profile = storage.getLocalProfile(); + const displayName = profile && profile.name && profile.name.displayName; if (appView) { appView.showNicknameDialog({ title: 'Change your own nickname', message: 'Note: Your nickname will be visible to your contacts.', - nickname, + nickname: displayName, onOk: async (newNickname) => { - - // Update our profiles accordingly - if (_.isEmpty(newNickname)) { - await storage.removeProfile(ourNumber); + // Update our profiles accordingly' + const trimmed = newNickname && newNickname.trim(); + let newProfile = null; + if (_.isEmpty(trimmed)) { + await storage.removeLocalProfile(); } else { - await storage.saveProfile(ourNumber, { + newProfile = { name: { - displayName: newNickname, + displayName: trimmed, }, - }); + }; + await storage.saveLocalProfile(newProfile); } appView.inboxView.trigger('updateProfile'); // Update the conversation if we have it - try { - const conversation = ConversationController.get(ourNumber); - conversation.updateProfile(); - } catch (e) {} + const conversation = ConversationController.get(ourNumber); + if (conversation) + conversation.setProfile(newProfile); }, }) } diff --git a/js/models/conversations.js b/js/models/conversations.js index 0cca4c713..9bc738bcf 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1731,26 +1731,35 @@ } }, - onChangeProfileKey() { - if (this.isPrivate()) { - this.getProfiles(); - } + // LOKI PROFILES + + async setNickname(nickname) { + const trimmed = nickname && nickname.trim(); + if (this.get('nickname') === trimmed) return; + + this.set({ nickname: trimmed }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + + await this.updateProfile(); }, - /* - Update profile values from the profile in storage. + async setProfile(profile) { + if (_.isEqual(this.get('profile'), profile)) return; - Signal has methods of setting data from a profile it fetches. - It fetches this via a server and they aren't saved anywhere. + this.set({ profile }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); - We made our own profile storage system so thus to avoid - any future conflict with upstream, we just use this method to update the values. - */ + await this.updateProfile(); + }, async updateProfile() { const profileName = this.get('profileName'); // Prioritise nickname over the profile display name - const nickname = await storage.getNickname(this.id); - const profile = await storage.getProfile(this.id); + const nickname = this.getNickname(); + const profile = this.getLocalProfile(); const displayName = profile && profile.name && profile.name.displayName; const newProfileName = nickname || displayName || null; @@ -1761,6 +1770,21 @@ }); } }, + getLocalProfile() { + return this.get('profile'); + }, + getNickname() { + return this.get('nickname'); + }, + + // SIGNAL PROFILES + + onChangeProfileKey() { + if (this.isPrivate()) { + this.getProfiles(); + } + }, + getProfiles() { // request all conversation members' keys let ids = []; diff --git a/js/models/profile.js b/js/models/profile.js index a7a93d45e..0fae1ecfb 100644 --- a/js/models/profile.js +++ b/js/models/profile.js @@ -9,75 +9,27 @@ window.Whisper = window.Whisper || {}; - const PROFILE_ID = 'profiles'; + const PROFILE_ID = 'local-profile'; - storage.getProfile = number => { - const profiles = storage.get(PROFILE_ID, {}); - return profiles[number] || null; + storage.getLocalProfile = () => { + const profile = storage.get(PROFILE_ID, null); + return profile; } - storage.saveProfile = async (number, profile) => { - const profiles = storage.get(PROFILE_ID, {}); - const storedProfile = profiles[number]; + storage.saveLocalProfile = async (profile) => { + const storedProfile = storage.get(PROFILE_ID, null); // Only store the profile if we have a different object if (storedProfile && _.isEqual(storedProfile, profile)) { return; } - window.log.info('adding profile ', profile, 'for ', number); - await storage.put(PROFILE_ID, { - ...profiles, - [number]: profile, - }); + window.log.info('saving local profile ', profile); + await storage.put(PROFILE_ID, profile); } - storage.removeProfile = async number => { - const profiles = storage.get(PROFILE_ID, {}); - if (!profiles[number]) { - return; - } - - delete profiles[number]; - - window.log.info('removing profile for ', number); - await storage.put(PROFILE_ID, profiles); - } - - // Names that user can set for users - - const NICKNAME_ID = 'nickname'; - - storage.getNickname = number => { - const nicknames = storage.get(NICKNAME_ID, {}); - return nicknames[number] || null; - } - - storage.saveNickname = async (number, name) => { - const nicknames = storage.get(NICKNAME_ID, {}); - const storedName = nicknames[number]; - - // Only store the name if we have a different name - if (storedName === name) { - return; - } - - window.log.info('adding nickname ', name, 'for ', number); - await storage.put(NICKNAME_ID, { - ...nicknames, - [number]: name, - }); - } - - storage.removeNickname = async number => { - const nicknames = storage.get(NICKNAME_ID, {}); - if (!nicknames[number]) { - return; - } - - delete nicknames[number]; - - window.log.info('removing nickname for ', number); - await storage.put(NICKNAME_ID, nicknames); + storage.removeLocalProfile = async () => { + window.log.info('removing local profile'); + await storage.remove(PROFILE_ID); } })(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index e8ff75c5c..310e5e1cf 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -174,7 +174,7 @@ name: item.getName(), value: item.get('seconds'), })), - hasNickname: !!storage.getNickname(this.model.id), + hasNickname: !!this.model.getNickname(), onSetDisappearingMessages: seconds => this.setDisappearingMessages(seconds), @@ -208,20 +208,12 @@ onChangeNickname: () => { window.Whisper.events.trigger('showNicknameDialog', { pubKey: this.model.id, - nickname: storage.getNickname(this.model.id), - onOk: async (newName) => { - if (_.isEmpty(newName)) { - await storage.removeNickname(this.model.id) - } else { - await storage.saveNickname(this.model.id, newName); - } - this.model.updateProfile(); - }, + nickname: this.model.getNickname(), + onOk: newName => this.model.setNickname(newName), }); }, onClearNickname: async () => { - await storage.removeNickname(this.model.id); - this.model.updateProfile(); + this.model.setNickname(null); }, }; }; diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 67fb8642b..c9d9d02c8 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -198,7 +198,7 @@ _getIdentityKeyViewProps() { const identityKey = textsecure.storage.get('identityKey').pubKey; const pubKey = StringView.arrayBufferToHex(identityKey); - const profile = storage.getProfile(pubKey); + const profile = storage.getLocalProfile(); const name = profile && profile.name && profile.name.displayName; return { @@ -210,18 +210,11 @@ } }, render_attributes() { - const identityKey = textsecure.storage.get('identityKey').pubKey; - const pubKey = StringView.arrayBufferToHex(identityKey); - const profile = storage.getProfile(pubKey); - const name = profile && profile.name && profile.name.displayName; - return { welcomeToSignal: i18n('welcomeToSignal'), selectAContact: i18n('selectAContact'), searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'), settings: i18n('settings'), - identityKey: pubKey, - name, }; }, events: { diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 58356a5f8..ddd6d4029 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -929,18 +929,15 @@ MessageReceiver.prototype.extend({ ); // Check if we need to update any profile names - if (!isMe) { + if (!isMe && conversation) { + let profile = null; if (message.profile) { const name = JSON.parse(message.profile.name.encodeJSON()); - await storage.saveProfile(envelope.source, { name }); - } else { - await storage.removeProfile(envelope.source); + profile = { name }; } - // Update the conversation profle - if (conversation) { - conversation.updateProfile(); - } + // Update the conversation + conversation.setProfile(profile); } if (type === 'friend-request' && isMe) { diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 83fdfd76b..e3ab2dbcd 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -663,8 +663,7 @@ MessageSender.prototype = { profileKey, options ) { - const myNumber = textsecure.storage.user.getNumber(); - const profile = textsecure.storage.impl.getProfile(myNumber); + const profile = textsecure.storage.impl.getLocalProfile(); return this.sendMessage( { recipients: [number], From ac4e04912c53a754a5749480bee5997f8e0f981d Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 28 Nov 2018 10:44:44 +1100 Subject: [PATCH 10/16] Minor language fixes. --- background.html | 2 +- js/background.js | 8 ++++---- test/index.html | 2 +- ts/components/IdentityKeyHeader.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/background.html b/background.html index 60ce85d54..4a3fa4f39 100644 --- a/background.html +++ b/background.html @@ -153,7 +153,7 @@ {{ #title }}

{{ title }}

{{ /title }} - + {{ #message }}
{{ message }}
{{ /message }} diff --git a/js/background.js b/js/background.js index 8cd1f68d4..e1cb7f6cc 100644 --- a/js/background.js +++ b/js/background.js @@ -574,12 +574,12 @@ const displayName = profile && profile.name && profile.name.displayName; if (appView) { appView.showNicknameDialog({ - title: 'Change your own nickname', - message: 'Note: Your nickname will be visible to your contacts.', + title: 'Change your own display name', + message: 'Note: Your display name will be visible to your contacts.', nickname: displayName, - onOk: async (newNickname) => { + onOk: async (newName) => { // Update our profiles accordingly' - const trimmed = newNickname && newNickname.trim(); + const trimmed = newName && newName.trim(); let newProfile = null; if (_.isEmpty(trimmed)) { await storage.removeLocalProfile(); diff --git a/test/index.html b/test/index.html index 05d46c1c6..9e2db1e7d 100644 --- a/test/index.html +++ b/test/index.html @@ -130,7 +130,7 @@