@@ -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')}
);