Merge pull request #61 from Mikunj/feature/profile-nickname

Added profile sharing and setting nicknames.
pull/65/head
sachaaaaa 6 years ago committed by GitHub
commit f900fc496d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"
}
}

@ -63,9 +63,7 @@
</div>
</div>
</div>
<div class='identityKeyWrapper'>
Your identity key: <span class='identityKey'>{{ identityKey }}</span>
</div>
<div class='identity-key-placeholder'></div>
<div class='underneathIdentityWrapper'>
<div class='conversation-stack'>
<div class='conversation placeholder'>
@ -150,6 +148,22 @@
<span class='time'>0:00</span>
<button class='close'><span class='icon'></span></button>
</script>
<script type='text/x-tmpl-mustache' id='nickname-dialog'>
<div class="content">
{{ #title }}
<h4>{{ title }}</h4>
{{ /title }}
<input type='text' name='name' class='name' placeholder='Type a name' autofocus maxlength="25">
{{ #message }}
<div class='message'>{{ message }}</div>
{{ /message }}
<div class='buttons'>
<button class='clear' tabindex='3'>{{ clear }}</button>
<button class='cancel' tabindex='2'>{{ cancel }}</button>
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content">
{{ #title }}
@ -608,6 +622,7 @@
<script type='text/javascript' src='js/models/messages.js'></script>
<script type='text/javascript' src='js/models/conversations.js'></script>
<script type='text/javascript' src='js/models/blockedNumbers.js'></script>
<script type='text/javascript' src='js/models/profile.js'></script>
<script type='text/javascript' src='js/expiring_messages.js'></script>
<script type='text/javascript' src='js/chromium.js'></script>
@ -641,6 +656,7 @@
<script type='text/javascript' src='js/views/inbox_view.js'></script>
<script type='text/javascript' src='js/views/network_status_view.js'></script>
<script type='text/javascript' src='js/views/confirmation_dialog_view.js'></script>
<script type='text/javascript' src='js/views/nickname_dialog_view.js'></script>
<script type='text/javascript' src='js/views/identicon_svg_view.js'></script>
<script type='text/javascript' src='js/views/install_view.js'></script>
<script type='text/javascript' src='js/views/banner_view.js'></script>

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

@ -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(

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

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

@ -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,

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

@ -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({

@ -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: {

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

@ -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'

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

@ -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 {

@ -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 {

@ -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;

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

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

@ -127,6 +127,17 @@
<span class='time'>0:00</span>
<button class='close'><span class='icon'></span></button>
</script>
<script type='text/x-tmpl-mustache' id='nickname-dialog'>
<div class='content'>
<div class='message'>{{ message }}</div>
<input type='text' name='name' class='name' placeholder='Type a name' value='{{ name }}'>
<div class='buttons'>
<button class='clear' tabindex='3'>{{ clear }}</button>
<button class='cancel' tabindex='2'>{{ cancel }}</button>
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content">
<div class='message'>{{ message }}</div>
@ -378,6 +389,7 @@
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/network_status_view.js'></script>
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/nickname_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/last_seen_indicator_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/scroll_down_button_view.js' data-cover></script>

@ -0,0 +1,38 @@
import React from 'react';
interface Props {
identityKey: string;
name?: string;
onEditProfile: () => void;
}
export class IdentityKeyHeader extends React.Component<Props> {
public render() {
const {
name,
identityKey,
onEditProfile,
} = this.props;
return (
<div className='identity-key-container'>
<div className='identity-key-text-container'>
<div>
Your identity key: <span className='identity-key_bold'>{identityKey}</span>
</div>
{!!name &&
<div>
Your display name: <span className='identity-key_bold'>{name}</span>
</div>
}
</div>
<div
id='editProfile'
role="button"
onClick={onEditProfile}
className="identity-key-wrapper__pencil-icon"
/>
</div>
);
}
}

@ -21,15 +21,16 @@ export class ContactName extends React.Component<Props> {
const shouldShowProfile = Boolean(profileName && !name);
const profileElement = shouldShowProfile ? (
<span className={`${prefix}__profile-name`}>
~<Emojify text={profileName || ''} i18n={i18n} />
<Emojify text={profileName || ''} i18n={i18n} />
</span>
) : null;
return (
<span className={prefix}>
<Emojify text={title} i18n={i18n} />
{shouldShowProfile ? ' ' : null}
{profileElement}
<span className={shouldShowProfile ? `${prefix}__profile-number` : ''}>
<Emojify text={title} i18n={i18n} />
</span>
</span>
);
}

@ -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<TimerOption>;
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<Props> {
@ -90,32 +94,20 @@ export class ConversationHeader extends React.Component<Props> {
public renderTitle() {
const {
name,
phoneNumber,
i18n,
profileName,
isVerified,
isKeysPending,
} = this.props;
return (
<div className="module-conversation-header__title">
{name ? <Emojify text={name} i18n={i18n} /> : null}
{name && phoneNumber ? ' · ' : null}
{phoneNumber ? phoneNumber : null}{' '}
{profileName && !name ? (
<span className="module-conversation-header__title__profile-name">
~<Emojify text={profileName} i18n={i18n} />
</span>
) : null}
{isVerified ? ' · ' : null}
{isVerified ? (
<span>
<span className="module-conversation-header__title__verified-icon" />
{i18n('verified')}
</span>
) : null}
{isKeysPending ? '(pending)' : null}
<ContactName
phoneNumber={phoneNumber}
profileName={profileName}
i18n={i18n}
/>
{isKeysPending ? ' (pending)' : null}
</div>
);
}
@ -198,6 +190,9 @@ export class ConversationHeader extends React.Component<Props> {
timerOptions,
onBlockUser,
onUnblockUser,
hasNickname,
onClearNickname,
onChangeNickname,
} = this.props;
const disappearingTitle = i18n('disappearingMessages') as any;
@ -237,6 +232,12 @@ export class ConversationHeader extends React.Component<Props> {
{!isMe ? (
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
) : null}
{!isMe ? (
<MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem>
) : null}
{!isMe && hasNickname ? (
<MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
</ContextMenu>
);

Loading…
Cancel
Save