diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d6e7e7e5b..f431e22ce 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." @@ -1346,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": @@ -1637,5 +1648,13 @@ "settingsUnblockHeader": { "message": "Blocked Users", "description": "Shown in the settings page as the heading for the blocked user settings" + }, + "editProfileTitle": { + "message": "Change your own display name", + "description": "The title shown when user edits their own profile" + }, + "editProfileDisplayNameWarning": { + "message": "Note: Your display name will be visible to your contacts", + "description": "Shown to the user as a warning about setting display name" } } diff --git a/background.html b/background.html index a37e90fe1..4a3fa4f39 100644 --- a/background.html +++ b/background.html @@ -63,9 +63,7 @@ -
- Your identity key: {{ identityKey }} -
+
@@ -150,6 +148,22 @@ 0:00 + + @@ -641,6 +656,7 @@ + diff --git a/js/background.js b/js/background.js index 71e5935ee..a107388b2 100644 --- a/js/background.js +++ b/js/background.js @@ -568,6 +568,48 @@ } }); + Whisper.events.on('onEditProfile', () => { + const ourNumber = textsecure.storage.user.getNumber(); + const profile = storage.getLocalProfile(); + const displayName = profile && profile.name && profile.name.displayName; + if (appView) { + appView.showNicknameDialog({ + title: window.i18n('editProfileTitle'), + message: window.i18n('editProfileDisplayNameWarning'), + nickname: displayName, + onOk: async (newName) => { + // Update our profiles accordingly' + const trimmed = newName && newName.trim(); + + // If we get an empty name then unset the name property + // Otherwise update it + const newProfile = profile || {}; + if (_.isEmpty(trimmed)) { + delete newProfile.name; + } else { + newProfile.name = { + displayName: trimmed, + } + } + + await storage.saveLocalProfile(newProfile); + appView.inboxView.trigger('updateProfile'); + + // Update the conversation if we have it + const conversation = ConversationController.get(ourNumber); + if (conversation) + conversation.setProfile(newProfile); + }, + }) + } + }); + + 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/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/models/conversations.js b/js/models/conversations.js index ce16f3960..d1fe5c733 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1654,6 +1654,47 @@ } }, + // 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(); + }, + async setProfile(profile) { + if (_.isEqual(this.get('profile'), profile)) return; + + this.set({ profile }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + + await this.updateProfile(); + }, + async updateProfile() { + // Prioritise nickname over the profile display name + const nickname = this.getNickname(); + const profile = this.getLocalProfile(); + const displayName = profile && profile.name && profile.name.displayName; + + const profileName = nickname || displayName || null; + await this.setProfileName(profileName); + }, + getLocalProfile() { + return this.get('profile'); + }, + getNickname() { + return this.get('nickname'); + }, + + // SIGNAL PROFILES + onChangeProfileKey() { if (this.isPrivate()) { this.getProfiles(); @@ -1671,148 +1712,22 @@ return Promise.all(_.map(ids, this.getProfile)); }, + // This function is wrongly named by signal + // This is basically an `update` function and thus we have overwritten it with such async getProfile(id) { - if (!textsecure.messaging) { - throw new Error( - 'Conversation.getProfile: textsecure.messaging not available' - ); - } - const c = await ConversationController.getOrCreateAndWait(id, 'private'); - // Because we're no longer using Backbone-integrated saves, we need to manually - // clear the changed fields here so our hasChanged() check is useful. - c.changed = {}; - - try { - await c.deriveAccessKeyIfNeeded(); - const numberInfo = c.getNumberInfo({ disableMeCheck: true }) || {}; - const getInfo = numberInfo[c.id] || {}; - - let profile; - if (getInfo.accessKey) { - try { - profile = await textsecure.messaging.getProfile(id, { - accessKey: getInfo.accessKey, - }); - } catch (error) { - if (error.code === 401 || error.code === 403) { - window.log.info( - `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` - ); - c.set({ sealedSender: SEALED_SENDER.DISABLED }); - profile = await textsecure.messaging.getProfile(id); - } else { - throw error; - } - } - } else { - profile = await textsecure.messaging.getProfile(id); - } - - const identityKey = window.Signal.Crypto.base64ToArrayBuffer( - profile.identityKey - ); - const changed = await textsecure.storage.protocol.saveIdentity( - `${id}.1`, - identityKey, - false - ); - if (changed) { - // save identity will close all sessions except for .1, so we - // must close that one manually. - const address = new libsignal.SignalProtocolAddress(id, 1); - window.log.info('closing session for', address.toString()); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - await sessionCipher.closeOpenSessionForDevice(); - } - - const accessKey = c.get('accessKey'); - if ( - profile.unrestrictedUnidentifiedAccess && - profile.unidentifiedAccess - ) { - window.log.info( - `Setting sealedSender to UNRESTRICTED for conversation ${c.idForLogging()}` - ); - c.set({ - sealedSender: SEALED_SENDER.UNRESTRICTED, - }); - } else if (accessKey && profile.unidentifiedAccess) { - const haveCorrectKey = await window.Signal.Crypto.verifyAccessKey( - window.Signal.Crypto.base64ToArrayBuffer(accessKey), - window.Signal.Crypto.base64ToArrayBuffer(profile.unidentifiedAccess) - ); - - if (haveCorrectKey) { - window.log.info( - `Setting sealedSender to ENABLED for conversation ${c.idForLogging()}` - ); - c.set({ - sealedSender: SEALED_SENDER.ENABLED, - }); - } else { - window.log.info( - `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` - ); - c.set({ - sealedSender: SEALED_SENDER.DISABLED, - }); - } - } else { - window.log.info( - `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` - ); - c.set({ - sealedSender: SEALED_SENDER.DISABLED, - }); - } - - await c.setProfileName(profile.name); - - // This might throw if we can't pull the avatar down, so we do it last - await c.setProfileAvatar(profile.avatar); - } catch (error) { - window.log.error( - 'getProfile error:', - id, - error && error.stack ? error.stack : error - ); - } finally { - if (c.hasChanged()) { - await window.Signal.Data.updateConversation(id, c.attributes, { - Conversation: Whisper.Conversation, - }); - } - } + // We only need to update the profile as they are all stored inside the conversation + await c.updateProfile(); }, - async setProfileName(encryptedName) { - if (!encryptedName) { - return; - } - const key = this.get('profileKey'); - if (!key) { - return; + async setProfileName(name) { + const profileName = this.get('profileName'); + if (profileName !== name) { + this.set({ profileName: name }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); } - - // decode - const keyBuffer = window.Signal.Crypto.base64ToArrayBuffer(key); - const data = window.Signal.Crypto.base64ToArrayBuffer(encryptedName); - - // decrypt - const decrypted = await textsecure.crypto.decryptProfileName( - data, - keyBuffer - ); - - // encode - const profileName = window.Signal.Crypto.stringFromBytes(decrypted); - - // set - this.set({ profileName }); }, async setProfileAvatar(avatarPath) { if (!avatarPath) { @@ -1989,7 +1904,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/models/profile.js b/js/models/profile.js new file mode 100644 index 000000000..0fae1ecfb --- /dev/null +++ b/js/models/profile.js @@ -0,0 +1,35 @@ +/* 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 = 'local-profile'; + + storage.getLocalProfile = () => { + const profile = storage.get(PROFILE_ID, null); + return profile; + } + + 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('saving local profile ', profile); + await storage.put(PROFILE_ID, profile); + } + + storage.removeLocalProfile = async () => { + window.log.info('removing local profile'); + await storage.remove(PROFILE_ID); + } +})(); 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 1db6314e3..f21542c24 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -178,5 +178,16 @@ }); } }, + 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, + }); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index cf9b1fc0b..39cdb76d4 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: !!this.model.getNickname(), onSetDisappearingMessages: seconds => this.setDisappearingMessages(seconds), @@ -204,6 +205,16 @@ onUnblockUser: () => { this.model.unblock(); }, + onChangeNickname: () => { + window.Whisper.events.trigger('showNicknameDialog', { + pubKey: this.model.id, + nickname: this.model.getNickname(), + onOk: newName => this.model.setNickname(newName), + }); + }, + onClearNickname: async () => { + this.model.setNickname(null); + }, }; }; this.titleView = new Whisper.ReactWrapperView({ diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 294b33b86..c9d9d02c8 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,26 @@ this.$el.addClass('expired'); } }, - render_attributes() { + _getIdentityKeyViewProps() { const identityKey = textsecure.storage.get('identityKey').pubKey; + const pubKey = StringView.arrayBufferToHex(identityKey); + const profile = storage.getLocalProfile(); + const name = profile && profile.name && profile.name.displayName; + + return { + identityKey: pubKey, + name, + onEditProfile: async () => { + window.Whisper.events.trigger('onEditProfile'); + }, + } + }, + render_attributes() { return { welcomeToSignal: i18n('welcomeToSignal'), selectAContact: i18n('selectAContact'), searchForPeopleOrGroups: i18n('searchForPeopleOrGroups'), settings: i18n('settings'), - identityKey: StringView.arrayBufferToHex(identityKey), }; }, events: { diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js new file mode 100644 index 000000000..7af20cb46 --- /dev/null +++ b/js/views/nickname_dialog_view.js @@ -0,0 +1,97 @@ +/* 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.$input.val(this.name); + this.$input.focus(); + + this.validateNickname(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'ok', + 'click .cancel': 'cancel', + 'click .clear': 'clear', + change: 'validateNickname', + }, + validateNickname() { + const nickname = this.$input.val(); + + if (_.isEmpty(nickname)) { + this.$('.clear').hide(); + } else { + this.$('.clear').show(); + } + }, + render_attributes() { + return { + message: this.message, + showCancel: !this.hideCancel, + cancel: this.cancelText, + ok: this.okText, + clear: this.clearText, + title: this.title, + }; + }, + ok() { + const nickname = this.$input.val().trim(); + + this.remove(); + if (this.resolve) { + this.resolve(nickname); + } + }, + cancel() { + this.remove(); + if (this.reject) { + this.reject(); + } + }, + clear() { + this.$input.val('').trigger('change'); + }, + onKeyup(event) { + 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/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index c4bea25b5..04a04e851 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 */ @@ -918,11 +919,23 @@ MessageReceiver.prototype.extend({ 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 && conversation) { + let profile = null; + if (message.profile) { + profile = JSON.parse(message.profile.encodeJSON()); + } + + // Update the conversation + conversation.setProfile(profile); + } + 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 f92b65a54..9eb2ec626 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 && this.profile.name) { + const contact = new textsecure.protobuf.DataMessage.Contact(); + contact.name = this.profile.name; + proto.profile = contact; + } + this.dataMessage = proto; return proto; }, @@ -656,6 +663,7 @@ MessageSender.prototype = { profileKey, options ) { + const profile = textsecure.storage.impl.getLocalProfile(); return this.sendMessage( { recipients: [number], @@ -666,6 +674,7 @@ MessageSender.prototype = { needsSync: true, expireTimer, profileKey, + profile, }, options ); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 08b2adfc5..82c277716 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -176,6 +176,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 { diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 3d8376aa9..ae91dc376 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -351,6 +351,63 @@ } } +.nickname-dialog { + display: flex; + align-items: center; + justify-content: center; + + .content { + max-width: 75%; + min-width: 60%; + 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; + } + + .message { + font-style: italic; + color: $grey; + font-size: 12px; + } + } +} + .permissions-popup, .debug-log-window { .modal { 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/_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/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/test/index.html b/test/index.html index e03ce96cf..8e5796eb1 100644 --- a/test/index.html +++ b/test/index.html @@ -127,6 +127,17 @@ 0:00 + + diff --git a/ts/components/IdentityKeyHeader.tsx b/ts/components/IdentityKeyHeader.tsx new file mode 100644 index 000000000..a345cef0e --- /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 display name: {name} +
+ } +
+
+
+ ); + } +} 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..490fd541e 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 { @@ -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 { @@ -90,32 +94,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}
); } @@ -198,6 +190,9 @@ export class ConversationHeader extends React.Component { timerOptions, onBlockUser, onUnblockUser, + hasNickname, + onClearNickname, + onChangeNickname, } = this.props; const disappearingTitle = i18n('disappearingMessages') as any; @@ -237,6 +232,12 @@ export class ConversationHeader extends React.Component { {!isMe ? ( {blockTitle} ) : null} + {!isMe ? ( + {i18n('changeNickname')} + ) : null} + {!isMe && hasNickname ? ( + {i18n('clearNickname')} + ) : null} {i18n('deleteMessages')} );